第二讲: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。
计算步骤其实不复杂:
- 初始化CRC寄存器为0xFFFF。
- 对每个字节,与CRC寄存器的低8位异或。
- 右移1位,如果移出的位是1,则与多项式0xA001异或。
- 重复8次(一个字节处理完)。
- 处理完所有字节后,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实战的基石。下一讲我们聊聊多寄存器读写和异常处理,到时候会更有意思。