3、Modbus协议详解(上):Modbus RTU/ASCII帧结构、功能码(01-06)、CRC校验计算原理与实例
各位同学,咱们今天正式进入Modbus协议的核心地带。说实话,Modbus这玩意儿在工业现场太常见了,从PLC到变频器,从温控表到智能仪表,几乎人手一份。我当年刚入行时,第一个独立调试的项目就是通过Modbus RTU读取一批老式电表的数据。那会儿不懂CRC校验,数据读出来全是乱码,折腾了两天才发现是校验没算对。嗯,今天咱们就把这些坑一个个填上。
3.1 Modbus RTU与ASCII:两种传输模式的抉择
Modbus协议支持两种传输模式:RTU和ASCII。说白了,就是数据在物理层上怎么编码的问题。
RTU模式:每个8位字节直接发送,效率高,速度快。我个人的习惯是,只要现场设备支持,优先选RTU。为什么呢?因为同样的波特率下,RTU能传输更多的数据点。
ASCII模式:每个8位字节拆成两个ASCII字符发送。比如0x4A,RTU直接发一个字节,ASCII要发'4'和'A'两个字节。效率低了一半,但好处是肉眼可读,调试方便。我记得有一次在煤矿井下调试,手头没有专业工具,就用串口助手看ASCII码,硬是把报文给解析出来了。
实际项目中,90%以上的场景都在用RTU。ASCII多见于一些老旧的仪表或者对可靠性要求极高的场合——因为ASCII模式下报文有固定的起始和结束标记,不容易误判。
3.2 RTU帧结构:一个字节都不能错
RTU的帧结构其实很简单,就四个部分:
| 字段 | 长度 | 说明 |
|---|---|---|
| 地址码 | 1字节 | 从站地址,范围1-247 |
| 功能码 | 1字节 | 告诉从站要干什么 |
| 数据区 | N字节 | 具体参数或数据 |
| CRC校验 | 2字节 | 低字节在前,高字节在后 |
这里有个关键点:帧与帧之间必须有至少3.5个字符时间的静默间隔。为什么?因为RTU没有固定的帧头帧尾,全靠这个时间间隔来区分。我曾经见过一个项目,工程师把波特率设错了,导致3.5字符时间计算有误,整个系统数据乱跳。你想想看,如果两帧数据粘在一起,从站根本不知道哪里是头哪里是尾。
避坑指南:我曾经在调试一个多从站系统时,发现某个从站偶尔会响应超时。查了半天,原来是主站在发送完请求后,没有等待3.5字符时间就切换了RS485的方向。结果从站还没收到完整帧,总线方向就变了。后来我在代码里强制加了一个延时,问题解决。
3.3 ASCII帧结构:看得见的报文
ASCII模式就友好多了。帧以冒号':'(0x3A)开头,以回车换行结束。中间的数据全部用ASCII字符表示。
举个例子,RTU模式下发送地址0x01,ASCII模式下要发送字符'0'和'1',也就是0x30和0x31。数据区后面跟一个LRC校验(纵向冗余校验),而不是CRC。
ASCII模式的好处是调试方便。你拿个串口助手,直接看字符就能读懂报文。但代价就是传输效率低,同样的数据量,ASCII需要两倍的带宽。
3.4 功能码01-06:最常用的六个指令
Modbus的功能码很多,但实际项目中80%的通信都在用01到06这六个。咱们一个一个过。
功能码01:读取线圈状态
读的是DO(数字量输出),返回的是位数据。比如你要读从站地址1的线圈0-7,请求报文是这样的:
请求:01 01 00 00 00 08 3D CC
从站地址:01
功能码:01
起始地址:00 00(从0号线圈开始)
读取数量:00 08(读8个线圈)
CRC:3D CC
响应报文:
响应:01 01 01 05 41 8C
从站地址:01
功能码:01
字节数:01(1个字节)
数据:05(二进制0000 0101,表示线圈0和2为ON)
CRC:41 8C
这里要注意,如果读取的线圈数量不是8的倍数,最后一个字节的高位补0。我刚开始做的时候,读9个线圈,返回了2个字节,结果把高位也当成有效数据了,闹了笑话。
功能码02:读取离散输入
和01码几乎一样,区别在于读的是DI(数字量输入),是只读的。报文格式完全一致,只是功能码换成02。
功能码03:读取保持寄存器
这个最常用。读的是16位的保持寄存器,比如设定值、PID参数等。请求报文:
请求:01 03 00 00 00 02 C4 0B
从站地址:01
功能码:03
起始地址:00 00
读取数量:00 02(读2个寄存器,共4字节)
CRC:C4 0B
响应报文:
响应:01 03 04 00 0A 00 14 7A 9C
从站地址:01
功能码:03
字节数:04(4个字节)
数据:00 0A(寄存器0的值=10),00 14(寄存器1的值=20)
CRC:7A 9C
个人经验:我习惯在读取多个连续寄存器时,一次读满125个(这是Modbus协议允许的最大值)。因为每次通信都有开销,一次多读点能提高效率。但要注意,有些从站实现不规范,一次读太多会死机。遇到这种情况,就分批次读,每次读10-20个。
功能码04:读取输入寄存器
和03码一样,区别在于读的是AI(模拟量输入),也是只读的。报文格式完全一样,功能码换成04。
功能码05:写单个线圈
写一个DO点。数据区固定为2字节:0xFF00表示ON,0x0000表示OFF。为什么不用0x0001表示ON?这是Modbus协议规定的,咱们照做就行。
请求:01 05 00 00 FF 00 8C 3A
从站地址:01
功能码:05
线圈地址:00 00
数据:FF 00(置ON)
CRC:8C 3A
功能码06:写单个寄存器
写一个16位的保持寄存器。这个也很常用,比如修改设定值。
请求:01 06 00 00 00 64 48 0A
从站地址:01
功能码:06
寄存器地址:00 00
数据:00 64(写入100)
CRC:48 0A
3.5 CRC校验:计算原理与手算实例
CRC校验是Modbus RTU的命根子。没有它,数据在传输过程中被干扰了都不知道。CRC-16/MODBUS的生成多项式是:
x^16 + x^15 + x^2 + 1
对应的二进制是:1100 0000 0000 0101,也就是0x8005(注意,实际计算时是0xA001,因为要反转)。
计算步骤其实不复杂:
- 预置一个16位寄存器为0xFFFF
- 取第一个字节与寄存器低8位异或
- 右移一位,高位补0
- 如果移出的最低位为1,则与0xA001异或
- 重复步骤3-4,直到处理完8位
- 取下一个字节,重复步骤2-5
- 所有字节处理完后,得到的结果就是CRC值
咱们手算一个实例。就计算地址码0x01的CRC:
初始值:0xFFFF
第一步:0xFFFF XOR 0x01 = 0xFFFE
右移1位:0x7FFF,移出位=0,不异或
右移1位:0x3FFF,移出位=1,异或0xA001 → 0x3FFF XOR 0xA001 = 0x9FFE
右移1位:0x4FFF,移出位=0,不异或
右移1位:0x27FF,移出位=1,异或0xA001 → 0x27FF XOR 0xA001 = 0x87FE
...(继续处理完8位)
最终结果:0x3DCC(低字节在前,所以报文中是CC 3D)
你看,和前面功能码01的例子里的CRC完全一致。
重要提醒:CRC计算时,数据字节的顺序是从地址码开始,到数据区结束,不包括CRC本身。另外,发送时先发低字节,再发高字节。这个顺序搞反了,从站会直接丢弃报文。我见过有人用现成的CRC计算工具,结果工具默认是大端模式,生成的CRC死活不对。
3.6 异常响应:当从站说"不"的时候
不是每次请求都能成功。如果从站发现请求有问题,它会返回异常响应。格式很简单:
从站地址 + (功能码 | 0x80) + 异常码 + CRC
比如你发了一个功能码01的请求,但从站不支持,它会返回:
01 81 01 81 90
其中81 = 01 | 0x80,01是异常码(表示功能码不支持)。
常见的异常码有:
| 异常码 | 含义 |
|---|---|
| 01 | 功能码不支持 |
| 02 | 数据地址越界 |
| 03 | 数据值非法 |
| 04 | 从站设备故障 |
我建议大家在写主站程序时,一定要处理异常响应。不能只等着超时重发,那样效率太低了。正确的做法是:收到异常码后,根据具体原因做相应处理。比如地址越界,就检查一下配置表;功能码不支持,就换个方式读。
好了,这一章的内容就到这里。Modbus RTU/ASCII的帧结构、功能码01-06、CRC校验,这些都是基本功。下一章咱们接着聊功能码07-16,以及更高级的应用技巧。记住,协议这东西,光看没用,得动手抓包、算CRC、写代码,才能真正吃透。