一、MODBUS通讯协议介绍
2、modbus官方组织官网:
https://modbus.org/
3、一个很不错的网站:
https://www.simplymodbus.ca/FC01.htm
modbus通讯协议是由MODICON公司于1979年开发,是一种工业现场总线协议标准。1996年施耐德公司基于以太网TCP/IP协议推出了modbusTCP协议;modbus协议包括三种通讯方式ASCII、RTU和TCP。
无论是基于何种通信方式,在modbus应用协议层,通讯双方都是基于功能码数据包来实现数据交互的:


3、modbus常用功能码数据帧格式详解(
重要)
01H(读取线圈状态)
|
请求数据包格式:ADDRESS(1BYTE) 01H(1BYTE) STARTADDRH(1BYTE) STARTADDRL(1BYTE) NUMH(1BYTE) NUML(1BYTE)
CRCL(1BYTE) CRCH(1BYTE)
解释:ADDRESS=被请求对象的地址,服务器地址或从站地址;STARTADDR=读取的线圈的启示位地址;NUM=连续读取的线圈的个数;CRC=对于RTU来讲是有的,CRC校验。
回应数据包格式:ADDRESS(1BYTE) 01H(1BYTE) BYTECOUNT(1BYTE) DATA1 ... DATAN
CRCL(1BYTE) CRCH(1BYTE)
解释:ADDRESS=被请求对象的地址,也就是服务器或从站地址;BYTECOUNT=发送的数据帧中,数据的字节数,CRC=对于RTU来讲是有的,CRC校验。
注意:
回复数据时,数据帧的DATA顺序是,字节顺序从左往右由低到高,每个字节DATA中的位从右往左是bit0~bit7!
|
02H(读取离散状态)
|
请求数据包格式:ADDRESS(1BYTE) 02H(1BYTE) STARTADDRH(1BYTE) STARTADDRL(1BYTE) NUMH(1BYTE) NUML(1BYTE) CRCL(1BYTE) CRCH(1BYTE) 解释:ADDRESS=被请求对象的地址,服务器地址或从站地址;STARTADDR=读取的离散状态的启示位地址;NUM=连续读取的线圈的个数;CRC=对于RTU来讲是有的,CRC校验。 回应数据包格式:ADDRESS(1BYTE) 01H(1BYTE) BYTECOUNT(1BYTE) DATA1 ... DATAN CRCL(1BYTE) CRCH(1BYTE) 解释:ADDRESS=被请求对象的地址,也就是服务器或从站地址;BYTECOUNT=发送的数据帧中,数据的字节数,CRC=对于RTU来讲是有的,CRC校验。 注意:回复数据时,数据帧的DATA顺序是,字节顺序从左往右由低到高,每个字节DATA中的位从右往左是bit0~bit7! |
03H(读取保持寄存器)
|
请求数据包格式:ADDRESS(1BYTE) 03H(1BYTE) STARTADDRH(1BYTE) STARTADDRL(1BYTE) NUMH(1BYTE) NUML(1BYTE)
CRCL(1BYTE) CRCH(1BYTE)
解释:ADDRESS=被请求对象的地址,服务器地址或从站地址;STARTADDR=读取的线圈的起始字地址;NUM=连续读取的字个数;CRC=对于RTU来讲是有的,CRC校验。
回应数据包格式:ADDRESS(1BYTE) 03H(1BYTE) BYTECOUNT(1BYTE) DATA1 ... DATAN
CRCL(1BYTE) CRCH(1BYTE)
解释:ADDRESS=被请求对象的地址,也就是服务器或从站地址;BYTECOUNT=发送的数据帧中,数据的字节数,CRC=对于RTU来讲是有的,CRC校验。
注意:
1、根据在工业标准,这里的寄存器单位是字(2个byte)或者双字(4byte);2、回复数据时,数据帧中DATA的数据顺序是,以一个字作为单位,从左往右是由低到高,但每个字的字节中,从左往右是从高到低!!!
|
04H(读取输入寄存器)
|
请求数据包格式:ADDRESS(1BYTE) 04H(1BYTE) STARTADDRH(1BYTE) STARTADDRL(1BYTE) NUMH(1BYTE) NUML(1BYTE)
CRCL(1BYTE) CRCH(1BYTE)
解释:ADDRESS=被请求对象的地址,服务器地址或从站地址;STARTADDR=读取的线圈的起始字地址;NUM=连续读取的字个数;CRC=对于RTU来讲是有的,CRC校验。
回应数据包格式:ADDRESS(1BYTE) 04H(1BYTE) BYTECOUNT(1BYTE) DATA1 ... DATAN
CRCL(1BYTE) CRCH(1BYTE)
解释:ADDRESS=被请求对象的地址,也就是服务器或从站地址;BYTECOUNT=发送的数据帧中,数据的字节数,CRC=对于RTU来讲是有的,CRC校验。
注意:
1、根据在工业标准,这里的寄存器单位是字(2个byte)或者双字(4byte);2、回复数据时,数据帧中DATA的数据顺序是,以一个字作为单位,从左往右是由低到高,但每个字的字节中,从左往右是从高到低!!!
|
05H(写入单个线圈指令)
|
请求数据包格式:ADDRESS(1BYTE) 05H(1BYTE) STARTADDRH(1BYTE) STARTADDRL(1BYTE) DATAH(1BYTE) DATAL(1BYTE)
CRCL(1BYTE) CRCH(1BYTE)
解释
:ADDRESS=被请求对象的地址,服务器地址或从站地址;STARTADDR=写入的单个线圈的位地址;DATA=写入线圈的值
(0XFF00代表线圈为TRUE,0X0000代表线圈为FLASE);CRC=对于RTU来讲是有的,CRC校验。
回应数据包格式:ADDRESS(1BYTE) 05H(1BYTE) STARTADDRH(1BYTE) STARTADDRL(1BYTE) DATAH(1BYTE) DATAL(1BYTE)
CRCL(1BYTE) CRCH(1BYTE)
解释:ADDRESS=被请求对象的地址,服务器地址或从站地址;STARTADDR=写入的单个线圈的位地址;DATA=写入线圈的值
(0XFF00代表线圈为TRUE,0X0000代表线圈为FLASE);CRC=对于RTU来讲是有的,CRC校验。
|
06H(写入单个寄存器)
|
请求数据包格式:ADDRESS(1BYTE) 06H(1BYTE) STARTADDRH(1BYTE) STARTADDRL(1BYTE) DATAH(1BYTE) DATAL(1BYTE)
CRCL(1BYTE) CRCH(1BYTE)
解释
:ADDRESS=被请求对象的地址,服务器地址或从站地址;STARTADDR=写入的单个寄存器的字地址;DATA=写入寄存器的值;CRC=对于RTU来讲是有的,CRC校验。
回应数据包格式:ADDRESS(1BYTE) 06H(1BYTE) STARTADDRH(1BYTE) STARTADDRL(1BYTE) DATAH(1BYTE) DATAL(1BYTE)
CRCL(1BYTE) CRCH(1BYTE)
注意:
1、06H功能回复的数据直接是请求的数据;2、根据工业标准,一个寄存器是2个字节,单位是字。
|
0FH(写入多个线圈指令)
|
请求数据包格式:ADDRESS(1BYTE) 0FH(1BYTE) STARTADDRH(1BYTE) STARTADDRL(1BYTE) NUMH(1BYTE) NUML(1BYTE) COUNT(1BYTE) DATAL(1BYTE) ... DATAH(1BYTE)
CRCL(1BYTE) CRCH(1BYTE)
解释
:ADDRESS=被请求对象的地址,服务器地址或从站地址;STARTADDR=写入的线圈的起始地址;NUM=连续写入的线圈的个数(单位是bit);COUNT=DATA的数量(字节数);DATA=写入线圈的值;CRC=对于RTU来讲是有的,CRC校验。
回应数据包格式:ADDRESS(1BYTE) 0FH(1BYTE) STARTADDRH(1BYTE) STARTADDRL(1BYTE) NUMH(1BYTE) NUML(1BYTE)
CRCL(1BYTE) CRCH(1BYTE)
注意:
1、连续写入多个线圈,DATA中,从左往右的字节顺序是从低到高。
|
10H(写入多个寄存器指令)
|
请求数据包格式:ADDRESS(1BYTE) 10H(1BYTE) STARTADDRH(1BYTE) STARTADDRL(1BYTE) NUMH(1BYTE) NUML(1BYTE) COUNT(1BYTE) DATAL(1BYTE) ... DATAH(1BYTE)
CRCL(1BYTE) CRCH(1BYTE)
解释
:ADDRESS=被请求对象的地址,服务器地址或从站地址;STARTADDR=写入的寄存器的起始字地址;NUM=连续写入的字的个数(一个字2个byte);COUNT=DATA的数量(总字节数);DATA=写入寄存器的值;CRC=对于RTU来讲是有的,CRC校验。
回应数据包格式:ADDRESS(1BYTE) 10H(1BYTE) STARTADDRH(1BYTE) STARTADDRL(1BYTE) NUMH(1BYTE) NUML(1BYTE)
CRCL(1BYTE) CRCH(1BYTE)
注意:
1、在连续写入多个寄存器指令中,DATA数据内容,每两个DATA代表一个字,且字的顺序从左到右为由低到高;2、每个字的字节顺序为由高到低!!!
|
关于一个概念的说明:标准的modbus通信协议中;从站是指监听端口,等待主站访问的设备(服务器);主站则是主动访问设备的一端(客户端)。
本人在下面的文章中可能出现描述上的错误,请以此处为准!!!
///
二、MODBUS_RTU协议细节与代码实现
1、modbus_rtu通讯工作方式(
个人的理解方式)
MODBUS_RTU是modbus通讯方式之一,通过收发简单的16进制数据报来通讯。modbus_rtu通讯双方分别称为主机和从机。
主
机,发送相关功能码指令数据包操作主机的内存寄存器;
从
机,可以理解为一个服务器,通过实时监听通讯端口,对主机的指令做出响应。在一个modbu rtu网络中,一般情况下只有一个从机连接最多247个主机;
从
机通过解析从机发送的请求数据包的头部,来确认请求是否是对本从机的请求,从机本身不需要确认请求来自哪个主机,从机以广播的方式将回应数据包发送到端口,所有主机都会收到,但,主机需要根据数据报的内容来判断是否是从机回应自己的数据报。在一个modbus rtu网络中,数据一般被存储在从机中,主机读写主机的内存来操作数据。
2、modbus rtu 数据报的校验
在modbus rtu网络通讯过程中,除了RS485物理通讯层本身的数据奇偶校验外,在modbus rtu应用层协议的数据报中,还包含两个字节的CRC校验。
CRC校验码的产生,
预置1个16位的寄存器为十六进制FFFF(全1),此寄存器为CRC寄存器--->
把第一个8位二进制数据(即通讯信息帧的第一个字节)与16位的CRC寄存器的低八位相异或,吧结果存放于CRC寄存器---->
把CRC寄存器的内容右移一位(朝低位)用0填补最高位,并检测右移后的移出位---->
如果移出位为零,则重复第三步(再次右移一位);如果移出位为1,CRC寄存器与多项式A001进行异或----->
重复步骤3和4,直到右移8次,这样整个8位数据全部进行了处理------>
重复步骤2和5,进行通讯信息帧下一个字节的处理------>
将该通讯信息帧所有字节按上述步骤计算完成后,得到的16位CRC寄存器的高、低字节进行交换。
以下是实现生成CRC校验码的源代码:
static unsigned short Make_CRC(unsigned char *Data_buff,int Data_length) //调用此函数生成CRC校验码,函数返回CRC校验码//
{
if(NULL == Data_buff)
return 0xffff;
unsigned short wcrc = 0xffff; //CRC寄存器预置//
unsigned char temp;
unsigned int i=0,j=0; //计数//
/*循环计算每个数据*/
for(i=0;i<Data_length;++i)
{
temp = *Data_buff & 0x00ff;
Data_buff++; //next data//
wcrc ^= temp;
for(j=0;j<8;++j) //对单笔数据进行具体计算//
{
if(0!= (0x0001 & wcrc)) //判断数据右移出的是否为1,如果是,则与多项式0xa001异或//
{
wcrc >>= 1;
wcrc ^= 0xa001;
}
else
wcrc >>= 1;
}
}
//对计算结果进行高低字节兑换//
unsigned char CRC_L = wcrc & 0xff;
unsigned char CRC_H = wcrc >> 8;
return ((CRC_L << 8) | CRC_H);
}
3、实现一个modbus_rtu主站
这里的主站概念,是站在我个人理解上的;通过定时周期的监听RS485端口,等待从机的指令,并将自己的对应的内存数据发送给从机,或者根据从机的指令,更新自己的内存。
源码中展示的是一个主站的代码框架,实现的是直接
实例化结构体来描述一个主站,通过分别调用提供的
两个函数接口,就可以自动地监听端口,并自动处理从机发送的功能指令。(这里支持处理的功能指令就是笔记开头被详细说明的常用的几个指令,代码基于Linux操作系统实现)
4、利用开放的框架实现的modbus_rtu从站
这里的从站概念依旧是个人理解的从站的概念;从站通过配置自身的从地址与要访问的主站地址,发送功能码操作目标主机的内存,获取或写入数据。
这里提供的是从gitthub上下载下来的代码框架,框架提供了十分丰富的函数接口,可以实现基本的从站搭建。(将.so放入到开发板中的动态依赖库路径中,系统编程中通过调用<modbus/modbus.h>就可以使用对应函数接口了)
///
三、MODBUS_TCP协议细节与代码实现
modbus_tcp本质上是基于modbusRTU功能码数据包演变而来的,遵循TCP/IP网络层协议,是属于TCP/IP四层协议中的应用层。
其数据报文的基本格式是:MBAP+PDU;
MBAP是modbusTCP与mosbusRTU最大的区别,而PDU其实可以理解为modbusRTU
去掉CRC校验字节。
1、modbus tcp协议数据包完整的格式:
MBAP报文头(7个字节):
容
|
解释
|
事务处理标识
|
可以理解为报文的序列号,一般每次通信之后就要加1以区别不同的通信数据报文。计数动作由主站来完成,
从站在回复主站的数据请求时,回复的数据帧要包含回复对应的序列号,主站才可以在接收到从站回复的消息后,知道从站回复的是哪一个请求。
|
协议标识符
|
00 00表示ModbusTCP协议;
这个是固定不可变的。
|
长度
|
表示
接下来的数据长度,单位为字节。
|
单元标识符
|
可以理解为设备地址。就是被访问的设备ID
|
PDU数据内容:这里只展示通用结构,具体参见笔记第一章介绍
功能码(1个字节) + 对应功能码的协议数据内容(N个字节)
2、modbus tcp从站设备的实现:(
理解为网络服务器,监听网络端口,等待从站链接并访问)
原理和第二章介绍的modbusRTU的主站(
这里的主站是个人理解的概念!!!)一样,数据存储在从站中,通过配置指定网络端口和IPV4地址,实时监听网络端口,等待modbusTCP的主站链接,并发送相关的数据报,modbusTCP默认的端口是502,在实际开发中可以自由设定,只要不与设备中其他服务冲突即可。下面是我自己实现的代码框架,该框架基于Linux系统编程,使用C语言编写:(
源码在下载专区上传:
https://download.csdn.net/download/weixin_40639467/12909812)
3、modbus tcp主站设备的实现:(
理解为网络中的客户端,主动连接指定ID的SLAVE,操作服务器内存)
类似于modbusRTU的从站(
这里的从站这个概念是个人的理解!!!);modbusTCP主站根据指定的网路中IP地址和端口以及从站的ID号,主动连接并发送功能码数据包,读写从站的内存数据,并会在发送请求后,等待从站回复数据包。下面是我自己实现的代码框架,基于Linux系统编程,使用C语言编写:(
源码已经在资源处上传了,可以自行下载:
https://download.csdn.net/download/weixin_40639467/12909810)
///
附加说明:本人承接Linux系统的嵌入式软件开发项目,CODESYS的runntime组件开发。欢迎加微:wxk101633(备注:委托开发)