概述:
掌握总线基础知识
了解Modbus通讯协议基本知识
能够进行基于Modbue串行通信协议软件开发
能够搭建RS-485总线并编程实现组网通信
一、Modbus概述
1.什么是Modbus通信协议
Modbus通信协议由Modicon(现为施耐德电气公司的一个品牌)在1979年开发,是全球第一个真正用于工业现场的总线协议。为了更好地普及和推动Modbus在以太网上的分布式应用,目前施耐德公司已将Modbus协议的所有权移交给IDA(Interface for DistributedAutomation,分布式自动化接口)组织,并专门成立了Modbus-IDA组织。该组织的成立为Modbus未来的发展奠定了基础。
Modbus通信协议是应用于电子控制器上的一种通用协议,目前已成为一通用工业标准。通过此协议,控制器之间或者控制器经由网络(例如,以太网)与其他设备之间可以通信。Modbus使不同厂商生产的控制设备可以连成工业网络,进行集中监控。Modbus通信协议定义了一个消息帧结构,并描述了控制器请求访问其他设备的过程,控制器如何响应来自其他设备的请求,以及怎样侦测错误并记录。
在Modbus网络上通信时,每个控制器必须知道它们的设备地址,识别按地址发来的消息,决定要做何种动作。如果需要响应,则控制器将按Modbus消息帧格式生成反馈信息并发出。
2.Modbus通信协议的版本
Modbus通信协议有多个版本:基于串行链路的版本、基于TCP/IP的网络版本以及基于其他互联网协议的网络版本,其中前面两者的实际应用场景较多。
基于串行链路的Modbus通信协议有两种传输模式,分别是Modbus RTU与ModbusASCII,这两种模式在数值数据表示和协议细节方面略有不同。Modbus RTU是一种紧凑的、采用二进制数据表示的方式,而Modbus ASCII的表示方式更加冗长。在数据校验方面.Modbus RTU采用循环冗余校验方式,而Modbus ASCII采用纵向冗余校验方式。另外,配置为Modbus RTU模式的节点无法与Modbus ASCII模式的节点通信。
3.5.2 Modbus通信的请求与响应
Modbus是一种单主/多从的通信协议,即在同一段时间内总线上只能有一个主设备,但可以有一个或多个(最多247个)从设备。主设备是指发起通信的设备,从设备是接收请求并做出响应的设备。在Modbus网络中,通信总是由主设备发起,而从设备没有收到来自主设备的请求时不会主动发送数据。
二、Modbus寄存器
寄存器是Modbus通信协议的一个重要组成部分,它用于存放数据。
Modbus寄存器最初借鉴于PLC(Programmable Logical Controller,可编程控制器)。后来随着Modbus通信协议的发展,寄存器这个概念也不再局限于具体的物理寄存器,而是逐渐拓展到了内存区域范畴。根据存放的数据类型及其读写特性,Modbus寄存器被分为4种类型。
三、Modbus功能码
1.功能码分类
Modbus功能码是Modbus消息帧的一部分,它代表将要执行的动作。以RTU模式为例,见表3-7,RTU消息帧的Modbus功能码占用一个字节,取值范围为1~127。
Modbus标准规定了3类Modbus功能码:公共功能码、用户自定义功能码和保留功能码。公共功能码是经过Modbus协会确认的,被明确定义的功能码,具有唯一性。
四、实验
(1)485主机每隔0.5S查询从传感器数据的Modbus帧。
(2)485网络中从机收到通讯帧后,解析内容,判断是否是发给自己的,然后根据功能码要求采集响应传感数据给主机。
(3)主机收到传感数据后,上报网关
(4)网关通过TCP上报网关
1.定义Modbus帧与Modbus协议管理结构体
在portocol.h中定义:
//类modbus 接收帧定义
__packed typedef struct {
u8 address; //设备地址:0,广播地址;1~255,设备地址。
u8 function; //帧功能,0~255
// u8 count; //帧编号
// u8 datalen; //有效数据长度
u8 *data; //数据存储区
u16 chkval; //校验值
} m_rev_frame_typedef;
//Modbus协议管理结构体
typedef struct {
u8* rxbuf; //接收缓存区
u16 rxlen; //接收数据的长度
u8 frameok; //一帧数据接收完成标记:0,还没完成;1,完成了一帧数据的接收
u8 checkmode; //校验模式:0,校验和;1,异或;2,CRC8;3,CRC16
2.Modbus通讯帧解析函数
//解析一帧数据,解析结果存储在fx里面
//注意:本函数会用到malloc给fx数据指针申请内存,后续用完fx,一定要释放内存!!
//否则会引起内存泄露!!!
//fx:帧指针
//buf:输入数据缓冲区(串口接收到的数据)
//len:输入数据长度
//返回值:解析结果,0,OK,其他,错误代码。
m_result mb_unpack_frame(m_send_frame_typedef *tx,m_rev_frame_typedef *rx)
{
u16 rxchkval=0; //接收到的校验值
u16 calchkval=0; //计算得到的校验值
u8 cmd = 0 ; //计算功能码
u8 datalen=0; //有效数据长度
u8 address=0;
u8 recbyte=0;
u8 res;
DBG_B_INFO("主机解析包程序 ");
// fx->datalen=0; //数据长度清零
if(m_ctrl_dev.rxlen>M_MAX_FRAME_LENGTH||m_ctrl_dev.rxlen<M_MIN_FRAME_LENGTH) {
m_ctrl_dev.rxlen=0; //清除rxlen
m_ctrl_dev.frameok=0; //清除framok标记,以便下次可以正常接收
return MR_FRAME_FORMAT_ERR;//帧格式错误
}
datalen=m_ctrl_dev.rxlen;
DBG_B_INFO("当前数据长度 %d",m_ctrl_dev.rxlen);
switch(m_ctrl_dev.checkmode) {
case M_FRAME_CHECK_SUM: //校验和
calchkval=mc_check_sum(m_ctrl_dev.rxbuf,datalen+4);
rxchkval=m_ctrl_dev.rxbuf[datalen+4];
break;
case M_FRAME_CHECK_XOR: //异或校验
calchkval=mc_check_xor(m_ctrl_dev.rxbuf,datalen+4);
rxchkval=m_ctrl_dev.rxbuf[datalen+4];
break;
case M_FRAME_CHECK_CRC8: //CRC8校验
calchkval=mc_check_crc8(m_ctrl_dev.rxbuf,datalen+4);
rxchkval=m_ctrl_dev.rxbuf[datalen+4];
break;
case M_FRAME_CHECK_CRC16: //CRC16校验
calchkval=mc_check_crc16(m_ctrl_dev.rxbuf,datalen-2);
rxchkval=((u16)m_ctrl_dev.rxbuf[datalen-2]<<8)+m_ctrl_dev.rxbuf[datalen-1];
break;
}
DBG_B_INFO("calchkval = 0x%x 、rxchkval = 0x%x 、datalen = 0x%x",calchkval,rxchkval,datalen);
// DBG_B_INFO("cmd = 0x%x ",cmd);
m_ctrl_dev.rxlen=0; //清除rxlen
m_ctrl_dev.frameok=0; //清除framok标记,以便下次可以正常接收
if(calchkval==rxchkval) { //校验正常
address=m_ctrl_dev.rxbuf[0];
if (address!= tx->address) {
DBG_R_E("返回地址与发送地址不统一");
return MR_FRAME_SLAVE_ADDRESS; //地址错误
}
cmd=m_ctrl_dev.rxbuf[1];
if (cmd!=tx->function) {
DBG_R_E("发送命令与返回命令不统一");
return MR_FRANE_ILLEGAL_FUNCTION; //命令帧错误
}
switch (cmd) {
case 0x02:res=unpack_disc_reg(tx,rx);
break;
case 0x03:res=unpack_readhold_reg(tx,rx);
break;
case 0x04:res=unpack_readinput_reg(tx,rx);
break;
case 0x06:res=unpack_writehold_reg(tx,rx);
break;
default :
break;
}
} else {
return MR_FRAME_CHECK_ERR;
}
return MR_OK;
}
3.编写读取传感器数据并回复响应帧函数
主机发送读取传感器数据命令,从机解析完主机请求帧后,编写响应的函数。
u8 ReadInputRegister(void)
{
u16 regaddress;u16 regcount;
u16 *input_value_p;
u16 iregindex;
//发送缓冲区
u8 sendbuf|20];
u8 send_cnt=0;
//计算得到的校验值
u16 calchkval=0;
//取出主机请求帧中的素统
regaddress=(u16)(m_ctrl_dev.rxbuf|2)<<8);14.
regaddress|=(u16/(m_ctrl_dev.rxbuf|3]);
//取出主机请求帧中的素
regcount=(u16)(m_ctrl_dev.rxbuf(4]<<8);17.
regcount|=(u16)(m_ctrl_dev.rxbuf(5]);
input_value_p=inbuf;
//组建响应帧
if((1<=regcount)&&(regcount<4)){
if((regaddress>=0)&&(regaddress<=3)){
sendbuf[send_cnt]=SLAVE_ADDRESS;
//从机地址
send_cnt++;
sendbuf[send_cnt]=0x04;
//功能码0x04
send_cnt++;
sendbuf[send_cnt]=regcount*2;
//字节长度
send_cnt++;
iregindex=regaddress-0;
//将寄存器内容赋值给响应帧
while(regcount>0){
sendbuf[send_cnt]=(u8)(input_value_pliregindex]>>8);
send_cnt++;
sendbuf[send_cnt]=(u8)(input_value_pliregindex]&0xFF);
send_cnt++;
iregindex++;
regcount--;
}
switch(m_ctrl_dev.checkmode)
{
case M_FRAME_CHECK_SUM:
//校验和
calchkval=mc_check_sum(sendbuf,send_cnt);
break;
calchkval=me_check_xor(sendbuf,send_cnt);
case M_FRAME_CHECK_XOR://异或校验48.49.break;
case M_FRAME_CHECK_CRC8://CRC8校验
calchkval=me_check_crc8(sendbuf,send_cnt);break;
case M_FRAME_CHECK_CRC16:
//CRC16校验
calchkval=mc_check_crc16(sendbuf,send_cnt);
break;
}
if(m_ctrl_dev.checkmode==M_FRAME_CHECK_CRC16) //如果是CRC16,则有2个字节的CRC
{
sendbuf[send_cnt]=(calchkval>>8)&0XFF; //高字节在前
send_cnt++;
sendbuf[send_cnt]=calchkval&0XFF; //低字节在后
}
RS4851_Send_Buffer(sendbuf,send_cnt+1); //发送这一帧数据
}
}
else
{
return 1;
}
return 0;
}
4.程序结构框架