第3章 Modbus协议详解:RTU与TCP帧结构、功能码、寄存器映射、CRC校验

各位工程师朋友,咱们今天来啃一块硬骨头——Modbus协议。说实话,这协议我用了十几年,从早期的RS485串口到现在的以太网,它就像个老黄牛,朴实但可靠。很多刚入行的朋友觉得Modbus太简单,不就是读读写写吗?嗯,真到现场调试的时候,坑可不少。

我个人习惯把Modbus理解成「工业界的通用语言」。不管你是PLC、变频器、传感器还是执行器,只要支持Modbus,大家就能对话。今天咱们重点讲清楚四个核心:帧结构、功能码、寄存器映射、CRC校验。把这四个吃透了,Modbus就算入门了。

Modbus协议知识体系 Modbus协议 帧结构 (RTU/TCP) 功能码 (01~2B) 寄存器映射 CRC校验 线圈/离散输入/保持/输入 位操作/字操作

3.1 帧结构:RTU vs TCP,到底差在哪?

Modbus有两种主流传输模式:RTU(远程终端单元)和TCP(以太网)。说白了,RTU跑在串口上(RS232/RS485),TCP跑在以太网上。帧结构有区别,但核心思想一样——都是「请求-应答」模式。

3.1.1 Modbus RTU帧结构

RTU帧是二进制格式,紧凑高效。我当年在一条老生产线上调试,串口波特率只有9600,一帧数据就那么几个字节,省着用。

字段长度(字节)说明
地址码1从站地址,范围1~247,0为广播
功能码1操作类型,如03读保持寄存器
数据区N寄存器地址、数量、数据内容
CRC校验2循环冗余校验,低字节在前

RTU帧示例(读保持寄存器):

请求:01 03 00 00 00 0A C5 CD
应答:01 03 14 00 01 00 02 ... [20字节数据] ... CRC_H CRC_L
💡 我的经验:RTU帧的字节间间隔不能超过1.5个字符时间,否则从站会认为帧结束。我曾经遇到过因为串口中断优先级设置不当,导致帧被拆成两段,从站死活不响应。后来用示波器一看,间隔超了。

3.1.2 Modbus TCP帧结构

TCP帧去掉了CRC校验(交给TCP/IP协议栈处理),加了一个MBAP头。说白了,TCP/IP已经保证了传输可靠性,Modbus就不需要再自己校验了。

字段长度(字节)说明
事务标识符2用于匹配请求和应答
协议标识符2固定为0x0000
长度2后续字节数(从单元标识符开始)
单元标识符1相当于RTU的地址码
功能码1同RTU
数据区N同RTU

TCP帧示例(读保持寄存器):

请求:00 01 00 00 00 06 01 03 00 00 00 0A
应答:00 01 00 00 00 17 01 03 14 00 01 00 02 ... [20字节数据]
⚠️ 注意:TCP帧的「长度」字段不包括自身和前面的6个字节。很多新手算错长度,导致解析失败。我调试时习惯用Wireshark抓包,一眼就能看出长度对不对。

3.2 功能码:Modbus的指令集

功能码就是Modbus的「动词」。你想读数据?用03。你想写数据?用06或10。你想控制线圈?用05或0F。功能码分两类:位操作和字操作。

3.2.1 常用功能码一览

功能码名称操作对象说明
01读线圈线圈(DO)读取0/1状态
02读离散输入离散输入(DI)只读
03读保持寄存器保持寄存器最常用,读写均可
04读输入寄存器输入寄存器只读
05写单个线圈线圈置位或复位
06写单个寄存器保持寄存器写一个16位数据
0F写多个线圈线圈批量操作
10写多个寄存器保持寄存器批量写,我最常用
2B封装接口扩展功能用于设备识别等
🔑 核心要点:功能码03和06/10是运动控制对接中最常用的。比如你要读取伺服驱动器的当前位置(寄存器地址),就用03;要修改速度指令,就用06或10。

3.2.2 异常响应

如果从站收到非法请求,会返回异常码。异常码 = 功能码 + 0x80。比如你发03读寄存器,从站返回83,说明出错了。

常见异常码:

  • 01:非法功能码——从站不支持这个功能
  • 02:非法数据地址——寄存器地址不存在
  • 03:非法数据值——数据超出范围
  • 04:从站设备故障——硬件出问题了
💡 避坑指南:我曾经调试一台变频器,发03读参数,一直返回83 02。查了半天,发现寄存器地址写错了。变频器的参数地址是十进制,我习惯用十六进制,结果少算了一位。记住:地址从0开始,不是1。

3.3 寄存器映射:把物理量映射到地址空间

寄存器映射是Modbus的灵魂。说白了,就是把温度、速度、位置这些物理量,映射到一个个16位的寄存器地址上。你读地址100,得到的是当前温度;你写地址200,设置的是目标速度。

3.3.1 四种数据对象

对象类型访问方式数据宽度典型用途
线圈(Coil)读写1位启停控制、报警复位
离散输入(DI)只读1位按钮状态、限位开关
输入寄存器(Input Register)只读16位模拟量输入、编码器值
保持寄存器(Holding Register)读写16位参数设置、速度指令

3.3.2 实际案例:伺服驱动器的寄存器映射

我做过一个项目,用Modbus控制台达伺服。它的寄存器映射长这样:

地址(十六进制)  内容              数据类型    读写
0x0000           状态字            uint16     只读
0x0001           控制字            uint16     读写
0x0002           当前位置(低16位)  uint16     只读
0x0003           当前位置(高16位)  uint16     只读
0x0004           目标速度(低16位)  uint16     读写
0x0005           目标速度(高16位)  uint16     读写
0x0006           加速度设定         uint16     读写
...

注意:32位数据(比如位置、速度)需要占用两个连续的寄存器。低16位在前,高16位在后。这是Modbus的默认字节序,但有些设备可能相反。你想想看,如果字节序搞反了,你写进去的速度值可能变成天文数字。

⚠️ 重要提醒:寄存器地址的编号方式有两种:PLC习惯用1-based(从1开始),Modbus协议用0-based(从0开始)。比如PLC手册说「参数P1-01地址是100」,实际Modbus地址是99。我见过有人因为这个差异,调试了整整两天。

3.4 CRC校验:确保数据不出错

CRC校验是Modbus RTU的「安全锁」。串口通信环境嘈杂,电磁干扰、信号衰减都可能导致数据出错。CRC就是用来检测这些错误的。

3.4.1 CRC算法原理

Modbus RTU使用CRC-16,多项式为0x8005。计算过程不复杂,但手算很麻烦。我一般直接用查表法,速度快,代码也简洁。

CRC计算步骤:

  1. 初始化CRC寄存器为0xFFFF
  2. 对每个字节,与CRC低字节异或
  3. 右移一位,如果移出的位为1,则与多项式异或
  4. 重复8次
  5. 处理完所有字节后,取反得到最终CRC

3.4.2 C语言实现(查表法)

// CRC-16 查表法实现
uint16_t crc16_modbus(uint8_t *data, uint16_t len) {
    uint16_t crc = 0xFFFF;
    uint16_t i;
    
    while (len--) {
        crc ^= *data++;
        for (i = 0; i < 8; i++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;  // 0xA001是0x8005的反转
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}
💡 我的习惯:调试时先用Modbus调试工具(比如ModScan)发一帧数据,看它算出的CRC是多少,然后对比自己程序算出来的。如果一致,说明CRC实现没问题。如果不一致,先检查字节序——CRC低字节在前,高字节在后。

3.4.3 常见CRC错误

  • 字节序搞反:CRC低字节在前,高字节在后。很多新手写反了。
  • 漏算地址码:CRC计算范围包括地址码、功能码和数据区,不包括CRC本身。
  • 初始值错误:Modbus RTU的CRC初始值是0xFFFF,不是0x0000。
🔑 总结一下:Modbus协议不复杂,但细节决定成败。帧结构要记牢,功能码要选对,寄存器映射要搞清楚地址偏移,CRC校验要算准确。把这四点掌握了,Modbus通信就稳了。

好了,今天就聊到这儿。下一章咱们讲Modbus在运动控制中的实战应用,包括如何用Modbus控制伺服驱动器、如何优化通信效率。到时候我会分享一个我亲手调试过的案例,保证干货满满。


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