2. Modbus RTU协议:报文结构(地址码、功能码、数据区、CRC校验),03/06/16功能码实战
各位工程师朋友,咱们今天来啃一块硬骨头——Modbus RTU的报文结构。说实话,我刚入行那会儿,看着那一串十六进制数,脑袋都是大的。后来在产线上被折腾了几回,才真正搞明白这玩意儿到底是怎么回事。
Modbus RTU说白了就是一套“问-答”的规矩。主站问,从站答,一问一答之间,数据就传过去了。你想想看,机械手要抓取工件,PLC得知道它现在在什么位置,这就是通过Modbus RTU报文来完成的。
核心要点:Modbus RTU报文 = 地址码 + 功能码 + 数据区 + CRC校验。这四个部分缺一不可,少了任何一个,通讯就会出问题。
2.1 报文结构拆解
咱们先看一个完整的报文例子。假设PLC要读取机械手1号从站的3个寄存器数据:
请求报文:01 03 00 00 00 03 05 CB
响应报文:01 03 06 00 01 00 02 00 03 65 B8
我来逐字节给你拆解一下:
| 字节位置 | 请求报文 | 响应报文 | 说明 |
|---|---|---|---|
| 第1字节 | 01 | 01 | 地址码:从站地址,这里是1号机械手 |
| 第2字节 | 03 | 03 | 功能码:03表示读取保持寄存器 |
| 第3-4字节 | 00 00 | 06 | 请求:起始地址;响应:数据字节数(6字节) |
| 第5-6字节 | 00 03 | 00 01 00 02 00 03 | 请求:寄存器数量;响应:寄存器数据 |
| 最后2字节 | 05 CB | 65 B8 | CRC校验码 |
嗯,这里要注意,地址码的范围是1-247,0是广播地址,248-255是保留地址。我在项目中遇到过有人把地址设为0,结果所有从站都响应了,通讯乱成一锅粥。
2.2 功能码详解:03、06、16
这三个功能码是我用得最多的,几乎覆盖了90%的应用场景。
2.2.1 03功能码:读取保持寄存器
03功能码用来读取从站的保持寄存器数据。保持寄存器是16位的,可以存整数、浮点数或者布尔量。我个人习惯把机械手的位置、速度、状态这些参数都映射到保持寄存器里。
// 读取从站1,起始地址0,读取2个寄存器
请求:01 03 00 00 00 02 C4 0B
响应:01 03 04 00 01 00 02 39 85
响应报文里,04表示数据长度是4个字节,后面跟着两个寄存器的值:00 01(寄存器0的值是1)和00 02(寄存器1的值是2)。
实战技巧:我曾经调试一台六轴机械手,发现读取的位置数据总是跳变。后来用示波器抓波形才发现,是通讯线太长导致信号衰减。解决办法是把波特率从115200降到38400,问题就解决了。有时候,降速比加屏蔽更管用。
2.2.2 06功能码:写单个寄存器
06功能码用来写单个保持寄存器。比如你要让机械手移动到某个位置,就可以用06功能码写入目标位置。
// 写从站1,地址0,写入值0x0100
请求:01 06 00 00 01 00 08 0A
响应:01 06 00 00 01 00 08 0A
注意看,06功能码的响应报文和请求报文是完全一样的,这叫“回显”。如果响应和请求不一致,说明通讯出问题了。
避坑指南:我曾经在调试时,06功能码写成功了,但机械手没反应。查了半天才发现,写入的数值超出了机械手的行程范围,它自动拒绝了。所以,写之前一定要确认数值范围,别让机械手撞到硬限位。
2.2.3 16功能码:写多个寄存器
16功能码用来连续写多个寄存器。比如你要同时设置机械手的X、Y、Z三个轴的位置,用16功能码一次搞定,比用06功能码写三次效率高多了。
// 写从站1,起始地址0,写入3个寄存器
请求:01 10 00 00 00 03 06 00 01 00 02 00 03 65 B8
响应:01 10 00 00 00 03 51 C8
请求报文里,10是16功能码,00 00是起始地址,00 03是寄存器数量,06是数据字节数(3个寄存器×2字节=6字节),后面跟着三个寄存器的值。
响应报文就简单多了,只返回地址、功能码、起始地址和寄存器数量。为什么?因为数据已经写进去了,没必要再传一遍。
2.3 CRC校验:通讯的守门员
CRC校验是Modbus RTU的保命符。没有它,通讯数据被干扰了都不知道。CRC校验算法其实不复杂,就是多项式除法,但手工算起来很麻烦。我一般用现成的计算工具,或者让PLC自己算。
// CRC-16 Modbus算法(C语言实现)
uint16_t crc16_modbus(uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
这个算法我用了十几年,从来没出过问题。你想想看,如果CRC校验不过,从站会直接丢弃报文,不会响应。主站收不到响应,就会重发。重发三次还不行,就该报警了。
经验之谈:我曾经在一个项目里,机械手偶尔会“抽风”,动作不对。排查了三天,最后发现是通讯线被电机电缆干扰了,CRC校验偶尔出错,从站收到了错误数据。后来换了屏蔽双绞线,把通讯线和动力线分开走,问题就再也没出现过。所以,CRC校验不是万能的,但硬件布线做不好,CRC也救不了你。
2.4 实战:机械手位置读取与写入
咱们来一个完整的实战例子。假设有一台机械手,从站地址是2,保持寄存器映射如下:
| 寄存器地址 | 参数 | 数据类型 | 范围 |
|---|---|---|---|
| 0 | X轴位置 | 16位有符号整数 | -32768 ~ 32767 |
| 1 | Y轴位置 | 16位有符号整数 | -32768 ~ 32767 |
| 2 | Z轴位置 | 16位有符号整数 | -32768 ~ 32767 |
| 3 | 状态字 | 16位无符号整数 | 0x0000 ~ 0xFFFF |
场景:PLC需要读取机械手当前位置,然后写入新的目标位置。
第一步:读取当前位置(03功能码)
// 读取从站2,起始地址0,读取4个寄存器
请求:02 03 00 00 00 04 44 39
响应:02 03 08 00 1E 00 32 00 46 00 01 7A 9C
响应数据解析:
- X轴位置:00 1E = 30(单位:mm)
- Y轴位置:00 32 = 50(单位:mm)
- Z轴位置:00 46 = 70(单位:mm)
- 状态字:00 01 = 1(表示机械手空闲)
第二步:写入目标位置(16功能码)
// 写从站2,起始地址0,写入3个寄存器(X=100, Y=200, Z=150)
请求:02 10 00 00 00 03 06 00 64 00 C8 00 96 7A 9C
响应:02 10 00 00 00 03 51 C8
写入数据解析:
- X轴目标位置:00 64 = 100
- Y轴目标位置:00 C8 = 200
- Z轴目标位置:00 96 = 150
写入成功后,机械手会自动移动到目标位置。你想想看,整个过程就是几组报文的事,是不是很简单?
调试小技巧:我建议你在调试时先用Modbus调试工具(比如ModScan、ModSim)模拟主站和从站,确认报文格式正确后再接入PLC。这样能省下不少排查时间。我曾经直接在PLC里写程序调试,结果通讯不通,查了半天才发现是CRC算错了。
2.5 报文结构知识体系
下面这张图是我自己总结的Modbus RTU报文结构知识体系,你看一眼就能明白各个部分之间的关系:
这张图把报文结构的四个部分和它们之间的关系都画清楚了。你调试的时候,对着这张图检查报文,基本不会出错。
最后提醒一句:Modbus RTU是串行通讯,报文之间要有3.5个字符时间的间隔。如果间隔太短,从站会认为报文还没发完,导致通讯失败。我曾经在调试时忽略了这一点,结果报文发出去石沉大海,折腾了半天才发现是波特率设置太快,报文间隔不够。
好了,关于Modbus RTU报文结构和03/06/16功能码的实战,我就讲到这里。这些内容是我多年现场调试经验的总结,希望能帮你少走弯路。记住,报文结构是基础,功能码是工具,CRC校验是保障,三者结合才能实现稳定可靠的通讯。
公众号:蓝海资料掘金营,微信deep3321