第二讲:Modbus实战——地址映射、功能码详解与CRC校验

各位工程师朋友,欢迎来到第二讲。

上一讲我们聊了Modbus的协议栈结构,算是打了个底。今天咱们直接上手,把袖子撸起来干点实在的。我个人觉得,Modbus协议里最核心、最容易出问题的地方,就是地址映射和功能码的理解。说白了,你搞懂了这两样,Modbus通信就算入门了。

2.1 地址映射:别把“地址”和“数据”搞混了

先问大家一个问题:Modbus协议里,地址是从0开始还是从1开始?

嗯,这个问题我当年刚入行时也纠结过。答案是:协议层地址从0开始,但用户层地址从1开始

什么意思呢?举个例子:

  • 你写程序时,要读取“保持寄存器地址40001”,这是用户看到的地址。
  • 但在Modbus报文里,实际发送的地址是0x0000(对应40001)。

为什么会这样?因为Modbus协议设计时,把地址空间分成了几类:

地址范围(用户视角) 协议地址(报文) 数据类型 功能码
00001 - 09999 0x0000 - 0x270F 线圈(DO) 01, 05, 15
10001 - 19999 0x0000 - 0x270F 离散输入(DI) 02
30001 - 39999 0x0000 - 0x270F 输入寄存器(AI) 04
40001 - 49999 0x0000 - 0x270F 保持寄存器(AO) 03, 06, 16

关键点:用户地址40001,在报文里填的是0x0000。用户地址40002,报文里填0x0001。以此类推。

我见过不少新手,直接把40001填进报文,结果设备死活没反应。嗯,这里要注意:报文地址 = 用户地址 - 地址偏移量。保持寄存器的偏移量是40001。

2.2 功能码详解:01-06,一个都不能少

Modbus功能码很多,但实战中最常用的就是01到06。咱们一个一个过。

功能码01:读取线圈状态

这个最简单。你发一个请求,设备返回一堆0和1,代表线圈的通断。

请求报文格式:

从站地址 + 0x01 + 起始地址(2字节) + 线圈数量(2字节) + CRC(2字节)

响应报文格式:

从站地址 + 0x01 + 字节数 + 数据(N字节) + CRC(2字节)

举个例子:读取从站1,从线圈地址0x0000开始,读10个线圈。

请求:01 01 00 00 00 0A BC 0D
响应:01 01 02 CD 0B B2 0F

这里返回了2个字节的数据:0xCD(二进制11001101)和0x0B(二进制00001011)。注意,线圈0对应最低位,线圈7对应最高位。所以线圈0-7的状态是:1,0,1,1,0,0,1,1。

我的经验:读线圈时,数量最好凑成8的倍数,这样数据处理起来方便。我曾经在一个项目里读7个线圈,结果返回1个字节,高位还得自己屏蔽,麻烦得很。

功能码02:读取离散输入

和01几乎一样,只是读取的是输入状态(只读)。

请求:01 02 00 00 00 0A 79 C9
响应:01 02 02 CD 0B B2 0F

报文结构完全一致,就是功能码从01变成了02。

功能码03:读取保持寄存器

这个最常用。保持寄存器是16位的,可以存整数、浮点数、甚至字符串。

请求报文:

从站地址 + 0x03 + 起始地址(2字节) + 寄存器数量(2字节) + CRC(2字节)

响应报文:

从站地址 + 0x03 + 字节数 + 数据(N字节) + CRC(2字节)

注意:寄存器数量是“个数”,不是字节数。每个寄存器占2字节,所以数据字节数 = 寄存器数量 × 2。

举个例子:读取从站1,从地址0x0000开始,读3个寄存器。

请求:01 03 00 00 00 03 05 CB
响应:01 03 06 00 01 00 02 00 03 4A 3B

返回了6个字节:00 01、00 02、00 03。对应三个寄存器的值分别是1、2、3。

注意:Modbus默认是大端模式(Big-Endian)。高字节在前,低字节在后。比如0x1234,发送顺序是0x12 0x34。有些设备支持小端模式,但那是非标用法,我建议你尽量用标准大端,否则兼容性问题会让你头疼。

功能码04:读取输入寄存器

和03一样,只是读取的是模拟量输入(只读)。报文结构完全一致,功能码换成04。

功能码05:写单个线圈

这个简单,就是控制一个线圈的通断。

请求:从站地址 + 0x05 + 线圈地址(2字节) + 数据(2字节) + CRC(2字节)

数据字段:0xFF00表示“通”,0x0000表示“断”。其他值无效。

举个例子:让从站1的线圈地址0x0000导通。

请求:01 05 00 00 FF 00 8C 3A
响应:01 05 00 00 FF 00 8C 3A(回显)

注意,响应报文和请求报文一模一样,这叫“回显确认”。

功能码06:写单个保持寄存器

写一个16位的寄存器值。

请求:从站地址 + 0x06 + 寄存器地址(2字节) + 数据(2字节) + CRC(2字节)

举个例子:向从站1的寄存器地址0x0000写入0x1234。

请求:01 06 00 00 12 34 08 0B
响应:01 06 00 00 12 34 08 0B(回显)

避坑指南:我曾经在一个项目里,用06功能码写寄存器,结果设备没反应。查了半天,发现是地址搞错了。用户手册上写的是“寄存器40001”,我直接填了40001到报文里,结果地址变成了0x9C41。嗯,记住:报文地址 = 用户地址 - 40001。

2.3 CRC校验计算:别怕,有套路

Modbus RTU用的是CRC-16(Modbus多项式)。多项式是0x8005,初始值是0xFFFF。

计算步骤其实不复杂:

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

这里给个C语言示例,我平时项目里直接复制粘贴用的:

unsigned short crc16_modbus(unsigned char *data, unsigned int len)
{
    unsigned short crc = 0xFFFF;
    unsigned int i, j;
    
    for (i = 0; i < len; i++)
    {
        crc ^= data[i];
        for (j = 0; j < 8; j++)
        {
            if (crc & 0x0001)
                crc = (crc >> 1) ^ 0xA001;
            else
                crc >>= 1;
        }
    }
    return crc;
}

注意:CRC在报文里是低字节在前,高字节在后。比如CRC结果是0x1234,发送顺序是0x34 0x12。

我的习惯:调试时先用Modbus调试工具(比如ModScan)发一帧正确的报文,然后自己算一遍CRC,对比一下。这样能快速验证你的CRC算法对不对。我当年第一次写CRC代码时,就是靠这个办法找到bug的。

2.4 实战:读写保持寄存器实例

咱们来个完整的例子。假设有一个温控器,从站地址是0x01,保持寄存器地址40001存当前温度(单位0.1℃),40002存设定温度。

场景1:读取当前温度

用户地址40001,报文地址0x0000。读1个寄存器。

请求:01 03 00 00 00 01 84 0A
响应:01 03 02 01 2C B8 23

返回数据0x012C,十进制是300。温度 = 300 × 0.1 = 30.0℃。

场景2:修改设定温度到35.0℃

35.0℃对应数值350,十六进制0x015E。用户地址40002,报文地址0x0001。

请求:01 06 00 01 01 5E 98 3B
响应:01 06 00 01 01 5E 98 3B(回显)

搞定。

注意:写寄存器后,一定要读回来确认。我遇到过设备写成功了但没生效的情况,原因是设备内部有写保护。读回来对比一下,能避免很多坑。

好了,这一讲的内容就到这儿。地址映射、功能码、CRC校验,这三样是Modbus实战的基石。下一讲我们聊聊多寄存器读写和异常处理,到时候会更有意思。