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,因为要反转)。

计算步骤其实不复杂:

  1. 预置一个16位寄存器为0xFFFF
  2. 取第一个字节与寄存器低8位异或
  3. 右移一位,高位补0
  4. 如果移出的最低位为1,则与0xA001异或
  5. 重复步骤3-4,直到处理完8位
  6. 取下一个字节,重复步骤2-5
  7. 所有字节处理完后,得到的结果就是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、写代码,才能真正吃透。