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报文结构知识体系 Modbus RTU报文 地址码 (1字节) 功能码 (1字节) 数据区 (N字节) CRC校验 (2字节) 03: 读保持寄存器 06: 写单个寄存器 16: 写多个寄存器 起始地址 (2字节) 寄存器数量 (2字节) 数据内容 (N字节) CRC低字节 CRC高字节 实战要点 地址码范围1-247 | 03读/06写单/16写多 | CRC校验不可省略 | 报文间隔≥3.5字符时间

这张图把报文结构的四个部分和它们之间的关系都画清楚了。你调试的时候,对着这张图检查报文,基本不会出错。

最后提醒一句:Modbus RTU是串行通讯,报文之间要有3.5个字符时间的间隔。如果间隔太短,从站会认为报文还没发完,导致通讯失败。我曾经在调试时忽略了这一点,结果报文发出去石沉大海,折腾了半天才发现是波特率设置太快,报文间隔不够。

好了,关于Modbus RTU报文结构和03/06/16功能码的实战,我就讲到这里。这些内容是我多年现场调试经验的总结,希望能帮你少走弯路。记住,报文结构是基础,功能码是工具,CRC校验是保障,三者结合才能实现稳定可靠的通讯。


公众号:蓝海资料掘金营,微信deep3321