这里分两部分,Modbus和协议,首先什么是协议?百度解释下就是:意思是共同计议,协商;经过谈判、协商而制定的共同承认、共同遵守的文件。比如大学毕业找工作的时候,一般要签一份叫“三方协议”的,三方指自己、校方、企业,这份协议里规定了三方需要遵守的一些事项。
那通信协议又是什么呢?通信就是双方或多方的交流,通信协议就是规定双方或多方需要共同遵守的交流方式。比如现在规定两个人需要用数字来代表文字,目前只定义了1表示“我”,2表示“你”,3表示“他”,然后现在两个人中有个人说了1,另一个立即就知道说的是“我”,但假如有个人不按规定来说话,说了一个4,那另一个人就不明白他在说什么了。这个例子里面,定义的1、2、3的表示,就是一份简单的通信协议。一般的通信协议,如TCP/IP、蓝牙协议等比Modbus复杂,但万变不离其宗。
接下来就是重头戏了,Modbus通信协议。
Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气 Schneider Electric)于1979年为使用可编程逻辑控制器(PLC)通信而发表。Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。--摘自百度百科
Modbus是一种一主一从的一对一通信方式(主机发一帧,从机回一帧的形式),当然也一主多从,但实际也是一对一通信,同一时刻只能有一个从机进行响应。如果需要和多个从机同时通信,这里也支持使用广播,即主机发送指令,所有从机接收指令并执行,但不进行应答。可以参考国际标准(以下简称国标),GBT 19582-2。
当进行一主多从通信时,主机通过从机ID号来区分要通信的从机设备。从机ID范围为1~247,0为广播地址,248~255为用户自定义地址。
目前总共有4种通信形式,RTU、ASCII、TCP、Plus。
RTU是一种远程终端控制系统,这里指的是Modbus的一种通信形式。一般是基于串口进行通信。其报文格式是十六进制的,由Slave ID+数据+CRC校验三部分组成。数据部分详见上面报文解析。剩下的就是数据校验了,这里用的是CRC校验(循环冗余校验,Cyclic Redundancy Check,简称CRC)。要注意的是,数据部分高位数据在前,低位数据在后,而CRC校验则是低位在前,高位在后。
名称 | 从机ID |
数据部分 |
CRC低位 |
CRC高位 |
长度 | 1字节 | n字节 | 1字节 | 1字节 |
CRC校验
CRC(Cyclic Redundancy Checksum)是一种纠错技术,代表循环冗余校验和。CRC的计算原理这里就不讲了,直接上代码。
C语言实现--摘自FreeModbus里的实现
static const UCHAR aucCRCHi[] = {
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40
};
static const UCHAR aucCRCLo[] = {
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7,
0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E,
0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9,
0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC,
0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32,
0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D,
0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38,
0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF,
0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1,
0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4,
0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB,
0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA,
0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0,
0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97,
0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E,
0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89,
0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83,
0x41, 0x81, 0x80, 0x40
};
USHORT
usMBCRC16( UCHAR * pucFrame, USHORT usLen )
{
UCHAR ucCRCHi = 0xFF;
UCHAR ucCRCLo = 0xFF;
int iIndex;
while( usLen-- )
{
iIndex = ucCRCLo ^ *( pucFrame++ );
ucCRCLo = ( UCHAR )( ucCRCHi ^ aucCRCHi[iIndex] );
ucCRCHi = aucCRCLo[iIndex];
}
return ( USHORT )( ucCRCHi << 8 | ucCRCLo );
}
帧的完整性判断
国标里规定:一帧中两个字符之间间隔时间不超过1.5字符,以大于3.5字符间隔时间作为一帧的结束,波特率高于19200时,间隔时间固定为1.7ms。
比如现在串口设置的波特率为9600,1个停止位,无校验位,8个数据位(这种设置以下会简写成9600-1N8的形式),那么代入如下公式:
可以算出,此设置下,断帧时间应该为3.645833……ms。
另外国标还规定,串口配置中,默认应该配置有偶校验。
tips:在9600-1N8的串口设置下,可以快速估算数据发送时间,即1个字节发送时间为1ms。
ASCII是一种字符型的通信方式,一般也是基于串口进行通信。其报文格式是以ASCII码编码的,由帧头(:)+Slave ID+数据+LRC校验+帧尾(/r/n)五部分组成,其中Slave ID、数据部分跟RTU完全一样,只不过是以ASCII编码形式,如Slave ID,RTU是01一个字节的时候,ASCII表示就是30 31两个字节。所以实际工业应用场合很少会用到Modbus/ASCII,因为通信效率太低。
另外跟RTU还有个不同的地方,就是这里使用的校验不是CRC校验,而是LRC校验。
LRC校验
纵向冗余校验,Longitudinal Redundancy Check,简称:LRC。
LRC具体算法如下:
1、对需要校验的数据(2n个字符)两两组成一个16进制的数值求和。
2、将求和结果与256求模。
3、用256减去所得模值得到校验结果(另一种方法:将模值按位取反然后加1)。
C语言实现--摘自FreeModbus里的实现
static UCHAR
prvucMBLRC( UCHAR * pucFrame, USHORT usLen )
{
UCHAR ucLRC = 0; /* LRC char initialized */
while( usLen-- )
{
ucLRC += *pucFrame++; /* Add buffer byte without carry */
}
/* Return twos complement */
ucLRC = ( UCHAR ) ( -( ( CHAR ) ucLRC ) );
return ucLRC;
}
因为整个报文都是ASCII的编码形式,所以如果使用单片机串口通信,可以把串口的数据位设置为7位(ASCII的码表范围就是0~0x7F,只用低7位)。
名称 | 帧头: |
从机ID |
数据部分 |
LRC |
帧尾/r |
帧尾/n |
长度 | 1字节 | 2字节 | n字节 | 2字节 | 1字节 | 1字节 |
TCP是一种网络协议,而Modbus/TCP就是基于网络协议上的一种应用层协议。其报文格式是十六进制的,由报头(2字节的帧序号+2字节的协议类型+2字节的数据长度+1字节的Slave ID)+数据两部分组成。由于该通信方式是基于TCP/IP这种可靠协议上,所以通信不需要有额外的校验机制。
名称 | 帧序号 |
协议类型 |
数据长度 |
从机ID |
数据部分 |
长度 | 2字节 |
2字节 |
2字节 |
1字节 |
n字节 |
Modbus Plus(又称MB+)是一种高速现场总线网络,也是一种典型的令牌总线网。
Modbus功能码表如下:
功能码 |
名称 |
作用 |
01 |
读取线圈状态 |
取得一组逻辑线圈的当前状态(ON/OFF) |
02 |
读取输入状态 |
取得一组开关输入的当前状态(ON/OFF) |
03 |
读取保持寄存器 |
在一个或多个保持寄存器中取得当前的二进制值 |
04 |
读取输入寄存器 |
在一个或多个输入寄存器中取得当前的二进制值 |
05 |
强置单线圈 |
强置一个逻辑线圈的通断状态 |
06 |
预置单寄存器 |
把具体二进值装入一个保持寄存器 |
07 |
读取异常状态 |
取得8个内部线圈的通断状态,这8个线圈的地址由控制器决定,用户逻辑可以将这些线圈定义,以说明从机状态,短报文适宜于迅速读取状态 |
08 |
回送诊断校验 |
把诊断校验报文送从机,以对通信处理进行评鉴 |
09 |
编程(只用于484) |
使主机模拟编程器作用,修改PC从机逻辑 |
10 |
控询(只用于484) |
可使主机与一台正在执行长程序任务从机通信,探询该从机是否已完成其操作任务,仅在含有功能码9的报文发送后,本功能码才发送 |
11 |
读取事件计数 |
可使主机发出单询问,并随即判定操作是否成功,尤其是该命令或其他应答产生通信错误时 |
12 |
读取通信事件记录 |
可是主机检索每台从机的ModBus事务处理通信事件记录。如果某项事务处理完成,记录会给出有关错误 |
13 |
编程(184/384 484 584) |
可使主机模拟编程器功能修改PC从机逻辑 |
14 |
探询(184/384 484 584) |
可使主机与正在执行任务的从机通信,定期控询该从机是否已完成其程序操作,仅在含有功能13的报文发送后,本功能码才得发送 |
15 |
强置多线圈 |
强置一串连续逻辑线圈的通断 |
16 |
预置多寄存器 |
把具体的二进制值装入一串连续的保持寄存器 |
17 |
报告从机标识 |
可使主机判断编址从机的类型及该从机运行指示灯的状态 |
18 |
(884和MICRO 84) |
可使主机模拟编程功能,修改PC状态逻辑 |
19 |
重置通信链路 |
发生非可修改错误后,是从机复位于已知状态,可重置顺序字节 |
20 |
读取通用参数(584L) |
显示扩展存储器文件中的数据信息 |
21 |
写入通用参数(584L) |
把通用参数写入扩展存储文件,或修改之 |
22~64 |
保留作扩展功能备用 |
|
65~72 |
保留以备用户功能所用 |
留作用户功能的扩展编码 |
73~119 |
非法功能 |
|
120~127 |
保留 |
留作内部作用 |
128~255 |
保留 |
用于异常应答 |
其中常用的功能码有8个(01、02、03、04、05、06、15、16),可以分为位操作和字操作两类,其中,位操作的是线圈和离散输入,两者区别在于,线圈是可读可写的,而离散输入是只读。字操作的是保持寄存器和输入寄存器,两者区别在于,保持寄存器是可读可写的,而输入寄存器是只读的。
读线圈功能码01,可读单个或多个
主节点发送帧格式(其中MSB为高字节,LSB为低字节):
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
字段定义 | ADDR | CMD | MSB | LSB | MSB | LSB | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 线圈起始地址 | 线圈个数 | CRC校验 |
从节点正常应答帧格式:
序号 | 0 | 1 | 2 | 3 | 4 | … | L+2 | L+3 | L+4 |
字段定义 | ADDR | CMD | Length | Data1 | Data2 | … | DataN | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 发送字节数L=n/8+(1) | 第一个字节数据值 | 第二个字节数据值 | … | 第N个字节数据值 | CRC校验 |
字节数据值的定义:
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
线圈值 | 首地址+7 | 首地址+6 | 首地址+5 | 首地址+4 | 首地址+3 | 首地址+2 | 首地址+1 | 首地址 |
注:数据从低位开始填充,填充完成后还有剩余的高位则全部补0。
从节点异常应答格式:
序号 | 0 | 1 | 2 | 3 | 4 |
字段定义 | ADDR | CMD+128 | ErrCode | LSB | MSB |
解释 | 从节点地址 | 命令类型+128 | 错误码 | CRC校验 |
写单个线圈功能码05
主节点发送帧格式:
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
字段定义 | ADDR | CMD | MSB | LSB | MSB | LSB | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 线圈地址 | 数据 | CRC校验 |
注:数据为0xFF00表示设置线圈状态为ON,数据为0x0000表示设置线圈状态为OFF。
从节点正常应答帧格式(同发送帧):
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
字段定义 | ADDR | CMD | MSB | LSB | MSB | LSB | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 线圈地址 | 数据 | CRC校验 |
从节点异常应答格式:
序号 | 0 | 1 | 2 | 3 | 4 |
字段定义 | ADDR | CMD+128 | ErrCode | LSB | MSB |
解释 | 从节点地址 | 命令类型+128 | 错误码 | CRC校验 |
写多个线圈功能码15
主节点发送帧格式:
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | … | L+6 | L+7 | L+8 |
字段定义 | ADDR | CMD | MSB | LSB | MSB | LSB | Length | Data1 | … | DataN | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 起始线圈地址 | 线圈数n | 发送字节数L=n/8+(1) | 第一个字节数据值 | … | 第N个字节数据值 | CRC校验 |
字节数据值的定义:
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
线圈值 | 首地址+7 | 首地址+6 | 首地址+5 | 首地址+4 | 首地址+3 | 首地址+2 | 首地址+1 | 首地址 |
注:数据从低位开始填充,填充完成后还有剩余的高位则全部补0。
从节点正常应答帧格式:
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
字段定义 | ADDR | CMD | MSB | LSB | MSB | LSB | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 线圈起始地址 | 线圈个数 | CRC校验 |
从节点异常应答格式:
序号 | 0 | 1 | 2 | 3 | 4 |
字段定义 | ADDR | CMD+128 | ErrCode | LSB | MSB |
解释 | 从节点地址 | 命令类型+128 | 错误码 | CRC校验 |
读保持寄存器功能码03,可读单个或多个
主节点发送帧格式:
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
字段定义 | ADDR | CMD | MSB | LSB | MSB | LSB | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 寄存器起始地址 | 寄存器个数 | CRC校验 |
从节点正常应答帧格式:
序号 | 0 | 1 | 2 | 3 | 4 | … | L+1 | L+2 | L+3 | L+4 |
字段定义 | ADDR | CMD | Length | MSB | LSB | … | MSB | LSB | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 发送字节数L=n*2 | 第一个寄存器值 | … | 第N个寄存器值 | CRC校验 |
从节点异常应答格式:
序号 | 0 | 1 | 2 | 3 | 4 |
字段定义 | ADDR | CMD+128 | ErrCode | LSB | MSB |
解释 | 从节点地址 | 命令类型+128 | 错误码 | CRC校验 |
写单个保持寄存器功能码06
主节点发送帧格式:
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
字段定义 | ADDR | CMD | MSB | LSB | MSB | LSB | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 寄存器地址 | 数据 | CRC校验 |
从节点正常应答帧格式(同发送帧):
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
字段定义 | ADDR | CMD | MSB | LSB | MSB | LSB | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 寄存器地址 | 数据 | CRC校验 |
从节点异常应答格式:
序号 | 0 | 1 | 2 | 3 | 4 |
字段定义 | ADDR | CMD+128 | ErrCode | LSB | MSB |
解释 | 从节点地址 | 命令类型+128 | 错误码 | CRC校验 |
写多个保持寄存器功能码16
主节点发送帧格式:
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | … | L+5 | L+6 | L+7 | L+8 |
字段定义 | ADDR | CMD | MSB | LSB | MSB | LSB | Length | MSB | LSB | … | MSB | LSB | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 起始寄存器地址 | 寄存器数n | 发送字节数L=n*2 | 第一个寄存器值 | … | 第N个寄存器值 | CRC校验 |
从节点正常应答帧格式:
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
字段定义 | ADDR | CMD | MSB | LSB | MSB | LSB | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 寄存器起始地址 | 寄存器个数 | CRC校验 |
从节点异常应答格式:
序号 | 0 | 1 | 2 | 3 | 4 |
字段定义 | ADDR | CMD+128 | ErrCode | LSB | MSB |
解释 | 从节点地址 | 命令类型+128 | 错误码 | CRC校验 |
读离散输入的功能码02,可读单个或多个(协议格式跟01功能码完全一致)
主节点发送帧格式:
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
字段定义 | ADDR | CMD | MSB | LSB | MSB | LSB | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 线圈起始地址 | 线圈个数 | CRC校验 |
从节点正常应答帧格式:
序号 | 0 | 1 | 2 | 3 | 4 | … | L+2 | L+3 | L+4 |
字段定义 | ADDR | CMD | Length | Data1 | Data2 | … | DataN | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 发送字节数L=n/8+(1) | 第一个字节数据值 | 第二个字节数据值 | … | 第N个字节数据值 | CRC校验 |
字节数据值的定义:
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
线圈值 | 首地址+7 | 首地址+6 | 首地址+5 | 首地址+4 | 首地址+3 | 首地址+2 | 首地址+1 | 首地址 |
注:数据从低位开始填充,填充完成后还有剩余的高位则全部补0。
从节点异常应答格式:
序号 | 0 | 1 | 2 | 3 | 4 |
字段定义 | ADDR | CMD+128 | ErrCode | LSB | MSB |
解释 | 从节点地址 | 命令类型+128 | 错误码 | CRC校验 |
读输入寄存器的功能码04,可读单个或多个(协议格式跟03功能码完全一致)
主节点发送帧格式:
序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
字段定义 | ADDR | CMD | MSB | LSB | MSB | LSB | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 寄存器起始地址 | 寄存器个数 | CRC校验 |
从节点正常应答帧格式:
序号 | 0 | 1 | 2 | 3 | 4 | … | L+1 | L+2 | L+3 | L+4 |
字段定义 | ADDR | CMD | Length | MSB | LSB | … | MSB | LSB | LSB | MSB |
解释 | 从节点地址 | 命令类型 | 发送字节数L=n*2 | 第一个寄存器值 | … | 第N个寄存器值 | CRC校验 |
从节点异常应答格式:
序号 | 0 | 1 | 2 | 3 | 4 |
字段定义 | ADDR | CMD+128 | ErrCode | LSB | MSB |
解释 | 从节点地址 | 命令类型+128 | 错误码 | CRC校验 |
功能码 | 说明 |
---|---|
01 | 非法功能。对于服务器(或从站)来说,询问中接收到的功能码是不可允许的操作,可能是因为功能码仅适用于新设备而被选单元中不可实现同时,还指出服务器(或从站)在错误状态中处理这种请求,例如:它是未配置的,且要求返回寄存器值。 |
02 | 非法数据地址。对于服务器(或从站)来说,询问中接收的数据地址是不可允许的地址,特别是参考号和传输长度的组合是无效的。对于带有100个寄存器的控制器来说,偏移量96和长度4的请求会成功,而偏移量96和长度5的请求将产生异常码02。 |
03 | 非法数据值。对于服务器(或从站)来说,询问中包括的值是不可允许的值。该值指示了组合请求剩余结构中的故障。例如:隐含长度是不正确的。modbus协议不知道任何特殊寄存器的任何特殊值的重要意义,寄存器中被提交存储的数据项有一个应用程序期望之外的值。 |
04 | 从站设备故障。当服务器(或从站)正在设法执行请求的操作时,产生不可重新获得的差错。 |
05 | 确认。与编程命令一起使用,服务器(或从站)已经接受请求,并且正在处理这个请求,但是需要长持续时间进行这些操作,返回这个响应防止在客户机(或主站)中发生超时错误,客户机(或主机)可以继续发送轮询程序完成报文来确认是否完成处理。 |
06 | 从属设备忙。与编程命令一起使用。服务器(或从站)正在处理长持续时间的程序命令。当服务器(或从站)空闲时,用户(或主站)应该稍后重新传输报文。 |
08 | 存储奇偶差错。与功能码20和21以及参考类型6一起使用,指示扩展文件区不能通过一致性校验。服务器(或从站)设法读取记录文件,但是在存储器中发现一个奇偶校验错误。客户机(或主方)可以重新发送请求,但可以在服务器(或从站)设备上要求服务。 |
10 | 不可用网关路径。与网关一起使用,指示网关不能为处理请求分配输入端口至输出端口的内部通信路径。通常意味着网关是错误配置的或过载的。 |
11 | 网关目标设备响应失败。与网关一起使用,指示没有从目标设备中获得响应。通常意味着设备未在网络中。 |
Modbus的PLC应用
这是一种基于标准Modbus上的PLC应用协议,目前貌似没有一个绝对的标准,不同的PLC厂家的自己不同的定义,但实际通信链路层走的协议还是按标准的Modbus协议来的。比如某PLC厂家规定,读取保持寄存器的地址是40001~49999,当PLC读取40001地址时,实际链路层读的是0000地址(就是在标准的基准地址前加上一个分段标识符,并且地址偏移1)。这个就不细讲了,具体用到哪家的PLC再去查看对应的资料。
Free Modbus,Github上链接https://github.com/armink/FreeModbus_Slave-Master-RTT-STM32,目前更新至V1.6。原本只有从机是开源的,主机收费,但这个作者自己写了主机部分,实现了带操作系统和裸机的接口。其设计架构也是根据操作的寄存器类型和通信方式进行区分。如从机的保持寄存器的实现,则封装在HoldingReg_S文件中,RTU的通信形式,则在mb_rtu中。架构清晰明了,使用起来也很简单,只需要实现mbport.h里的几个接口就行。
事件类:xMBPortEventInit、xMBPortEventPost、xMBPortEventGet。
串口类:xMBPortSerialInit、vMBPortClose、xMBPortSerialClose、vMBPortSerialEnable、xMBPortSerialGetByte、xMBPortSerialPutByte。
定时类:xMBPortTimersInit、xMBPortTimersClose、vMBPortTimersEnable、vMBPortTimersDisable。
状态回调:pxMBFrameCBByteReceived、pxMBFrameCBTransmitterEmpty、pxMBPortCBTimerExpired。
具体实现就不讲了,github上有例程,如果有需要的小伙伴可以留下言,这边可以单独出一篇详细解析FreeModbus。
Modbus Poll,可模拟主机,跟从机进行通信。
Modbus Slave,可模拟从机,响应主机的通信。
Modbus Scan,更偏向于PLC的操作界面,个人觉得没有Modbus Poll灵活。
串口调试助手,可以完全从指令层面上进行深入了解,而且也没有上述工具一些问题(因为一般集成的功能越多,问题可能越多),但有个缺点就是如果作为从机应答,没办法做到及时响应主机的指令。
串口通信协议、TCP/IP、RS485/RS232/RS422/ttl、STM32单片机、Github、CRC校验、LRC校验