4. Modbus RTU通信协议:物理层与数据帧结构、功能码详解、CRC校验计算
各位同学,今天我们来聊聊Modbus RTU。说实话,这个协议在工业现场太常见了。我做了十几年自动化,几乎每个项目都离不开它。你想想看,从PLC到变频器,从温控表到智能仪表,Modbus RTU就像工业界的“普通话”。
嗯,咱们先搞清楚一件事:Modbus RTU不是最先进的协议,但它绝对是最实用的。为什么?因为它简单、可靠、成本低。我见过太多复杂的协议最后搞出一堆问题,反而是Modbus RTU稳如老狗。
4.1 物理层:RS-485与RS-232
Modbus RTU的物理层,说白了就是决定信号怎么在线上跑。最常见的是RS-485,也有用RS-232的,但现场我几乎只用RS-485。
核心区别:
- RS-485:差分信号传输,抗干扰能力强,最远1200米,支持多站(最多32个节点)
- RS-232:单端信号传输,抗干扰弱,最远15米,只能点对点
我个人习惯,只要距离超过10米,或者现场有变频器、电机这些干扰源,一律上RS-485。记得有一次在钢厂做项目,现场电磁环境恶劣得不行,用RS-232根本没法通信,换成RS-485加双绞屏蔽线,问题立马解决。
接线要点:
- RS-485用A、B两根线(也叫D+、D-),千万别接反
- 屏蔽层单端接地,别两端都接,容易形成地环路
- 终端电阻:长距离(>300米)或高速(>9600bps)时,在总线两端各加一个120Ω电阻
注意:我曾经遇到过一个问题,现场通信时好时坏,查了半天发现是终端电阻没加。加了之后,波形干净多了,再也没有丢包过。
4.2 数据帧结构:从字节到报文
Modbus RTU的数据帧,结构非常清晰。每个报文由四部分组成:
| 字段 | 长度 | 说明 |
|---|---|---|
| 地址码 | 1字节 | 从站地址,范围1-247,0为广播地址 |
| 功能码 | 1字节 | 指示操作类型(读/写线圈、寄存器等) |
| 数据区 | N字节 | 具体的数据内容,长度可变 |
| CRC校验 | 2字节 | 循环冗余校验,低字节在前 |
举个例子,读取从站地址为1的设备的保持寄存器,起始地址0,读取2个寄存器:
请求报文:01 03 00 00 00 02 C4 0B
- 01:从站地址
- 03:功能码(读保持寄存器)
- 00 00:起始地址(高字节在前)
- 00 02:寄存器数量
- C4 0B:CRC校验
响应报文:
响应报文:01 03 04 00 0A 00 14 3A 0F
- 01:从站地址
- 03:功能码
- 04:数据字节数(2个寄存器×2字节)
- 00 0A:第一个寄存器值(10)
- 00 14:第二个寄存器值(20)
- 3A 0F:CRC校验
这里有个细节:Modbus RTU是大端模式,也就是高字节在前。很多新手在这里栽跟头,我刚开始也搞错过。你想想看,如果大小端搞反了,读出来的数据完全不对。
4.3 功能码详解:常用的几个
Modbus RTU的功能码很多,但实际项目中常用的就那么几个。我给大家列个表:
| 功能码 | 名称 | 操作对象 | 说明 |
|---|---|---|---|
| 01 | 读线圈 | DO(数字量输出) | 读取开关量输出状态 |
| 02 | 读离散输入 | DI(数字量输入) | 读取开关量输入状态 |
| 03 | 读保持寄存器 | AO(模拟量输出) | 读取模拟量输出值或参数 |
| 04 | 读输入寄存器 | AI(模拟量输入) | 读取模拟量输入值 |
| 05 | 写单个线圈 | DO | 控制单个开关量输出 |
| 06 | 写单个寄存器 | AO | 设置单个模拟量输出值 |
| 15 | 写多个线圈 | DO | 批量控制开关量输出 |
| 16 | 写多个寄存器 | AO | 批量设置模拟量输出值 |
我个人最常用的是03和06,一个读一个写,搞定大部分场景。功能码01和05用在数字量模块上,比如控制继电器、读取按钮状态。
小技巧:如果你不确定设备支持哪些功能码,可以发一个03功能码试试。大部分设备都支持读保持寄存器。如果返回异常码01(非法功能码),那就换个功能码试试。
4.4 CRC校验计算:手算与代码实现
CRC校验是Modbus RTU的“安全锁”。没有它,数据在传输过程中被干扰了都不知道。我曾经遇到过现场通信偶尔出错,就是因为CRC校验没做好,设备收到了错误的数据还在傻傻地执行。
CRC-16/MODBUS算法特点:
- 多项式:0x8005(实际计算用0xA001,因为反转了)
- 初始值:0xFFFF
- 结果异或:0x0000
- 低字节在前,高字节在后
手算太麻烦,咱们直接上代码。这是我在项目中反复使用的Python实现:
def crc16_modbus(data: bytes) -> int:
"""
计算Modbus RTU CRC-16校验
:param data: 待校验的字节数据
:return: 16位CRC值
"""
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 0x0001:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
return crc
# 示例:计算请求报文的CRC
data = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x02])
crc_value = crc16_modbus(data)
print(f"CRC值: 0x{crc_value:04X}")
# 输出:CRC值: 0xC40B
# 注意:发送时低字节在前,即0x0B 0xC4
代码很简单,但有几个坑要注意:
- CRC计算的是地址码+功能码+数据区,不包括CRC本身
- 发送时先发低字节,再发高字节。比如CRC=0xC40B,先发0x0B,再发0xC4
- 接收时也要验证CRC,如果不对,直接丢弃报文
避坑指南:我曾经在项目中遇到一个设备,它返回的CRC总是和我算的不一样。查了半天,发现那个设备用的是CRC-16/IBM,不是Modbus的CRC-16/MODBUS。虽然多项式一样,但初始值和结果异或不同。所以,一定要确认设备手册里写的是“Modbus RTU CRC-16”。
4.5 实战经验总结
说了这么多,最后给大家几点实战建议:
- 波特率统一:主站和从站的波特率、数据位、停止位、校验位必须完全一致。我一般用9600、8、N、1,兼容性最好。
- 地址不能重复:总线上每个从站地址必须唯一。我曾经犯过这个错,两个设备都设成地址1,结果通信乱套了。
- 超时处理:主站发送请求后,要设置超时时间。一般100ms-500ms,根据现场情况调整。如果超时了,重试3次,还不行就报错。
- 帧间隔:Modbus RTU要求帧与帧之间至少有3.5个字符时间的间隔。这个时间很短,但很重要。如果间隔太短,从站可能把两帧当成一帧处理。
嗯,今天就讲到这里。Modbus RTU看起来简单,但真正用好它,还是需要一些实战经验的。下一章我们讲Modbus TCP,那个和RTU有些不同,但核心思想是一样的。
记住一句话:协议是死的,人是活的。理解原理,灵活运用,才是王道。