4、Modbus协议详解:RTU与TCP模式、寄存器地址映射、数据帧结构
各位同学,今天我们来啃一块硬骨头——Modbus协议。说实话,这玩意儿在工业现场的地位,就跟普通话在中国的重要性一样。你只要跟PLC、变频器、传感器打交道,就绕不开它。
我最早接触Modbus是在一个水处理项目上。当时要读取30多台仪表的温度数据,厂家说支持Modbus RTU,我心想这还不简单?结果一上手就发现,寄存器地址映射表看得我头皮发麻。嗯,今天我就把当年踩过的坑,一个一个给你们讲清楚。
核心要点:Modbus本质上是一个主从通信协议。一个主站(通常是PLC或上位机)发起请求,多个从站(传感器、执行器)响应。主站问,从站答,就这么简单。
4.1 RTU模式 vs TCP模式:到底选哪个?
很多新手会纠结这个问题。我直接说结论:物理层不同,应用层几乎一样。
| 对比项 | Modbus RTU | Modbus TCP |
|---|---|---|
| 物理层 | RS-232 / RS-485 | 以太网(RJ45) |
| 传输距离 | RS-485可达1200米 | 100米(交换机可扩展) |
| 节点数量 | 最多247个 | 理论上无限制 |
| 数据帧结构 | 带CRC校验 | 带MBAP报文头 |
| 典型场景 | 工厂车间、户外设备 | 上位机、SCADA系统 |
我个人习惯:距离远、环境干扰大,用RTU;距离近、需要高速通信,用TCP。有一次我在一个钢铁厂调试,现场电机变频器离控制室有800米,用RS-485走RTU模式,稳得很。要是用TCP,光拉网线就得花掉预算的一半。
避坑指南:我曾经遇到过一个案例,客户坚持用TCP模式连接分布在三个车间的设备。结果因为交换机级联太多,延迟飙到了200ms,电机报警根本来不及响应。后来全部改成RTU+中继器,问题解决。记住:TCP不是万能的。
4.2 寄存器地址映射:别被文档骗了
这是最容易出问题的地方。你想想看,一个变频器可能有几百个参数,每个参数对应一个寄存器地址。但不同厂家的地址映射规则,有时候真能把你绕晕。
Modbus定义了四种数据对象:
- 线圈(Coil):位操作,可读可写。地址范围00001-09999
- 离散输入(Discrete Input):位操作,只读。地址范围10001-19999
- 输入寄存器(Input Register):16位,只读。地址范围30001-39999
- 保持寄存器(Holding Register):16位,可读可写。地址范围40001-49999
说白了,我们最常用的是保持寄存器。电机的电流、电压、转速,一般都映射到这里。
但有个坑:地址偏移。比如文档说“电机电流在地址40001”,但实际发送报文时,地址字段填的是0x0000还是0x0001?
我告诉你标准做法:协议层地址从0开始。40001对应协议地址0x0000,40002对应0x0001,以此类推。有些厂家文档直接写“地址1”,你得自己换算。
注意:我曾经被一个国产温控器坑过。文档写“温度寄存器地址40003”,我发0x0002死活读不到数据。折腾了两小时,最后发现它家地址从1开始计数,得发0x0003才行。所以拿到新设备,先读一个已知值验证地址映射,这是铁律。
4.3 数据帧结构:拆开看看里面是什么
理解了地址映射,我们来看看报文到底长什么样。我画了一张图,帮你把结构理清楚。
看到没?RTU的帧结构就这么简单。主站发请求,从站回响应。功能码0x03是读保持寄存器,0x06是写单个寄存器,0x10是写多个寄存器。常用的就这几个。
TCP模式呢?其实就是把RTU的CRC校验去掉,换成MBAP报文头。MBAP头包含事务标识符、协议标识符、长度等字段。说白了,TCP模式依赖以太网自身的校验机制,不需要CRC了。
实战经验:我建议你在调试时,先用串口助手抓一下报文。看看主站发了什么,从站回了什么。有一次我发现从站一直不回数据,抓包一看,原来是CRC算错了。很多现成的CRC计算库可以直接用,别自己手算,容易出错。
4.4 功能码速查表
这里我整理了一份常用功能码,建议你收藏。我在现场调试时,经常要翻这个表。
| 功能码 | 名称 | 操作对象 | 说明 |
|---|---|---|---|
| 0x01 | 读线圈 | 线圈 | 读取DO状态 |
| 0x02 | 读离散输入 | 离散输入 | 读取DI状态 |
| 0x03 | 读保持寄存器 | 保持寄存器 | 最常用,读模拟量 |
| 0x04 | 读输入寄存器 | 输入寄存器 | 只读模拟量 |
| 0x05 | 写单个线圈 | 线圈 | 控制DO |
| 0x06 | 写单个寄存器 | 保持寄存器 | 设置参数 |
| 0x0F | 写多个线圈 | 线圈 | 批量控制 |
| 0x10 | 写多个寄存器 | 保持寄存器 | 批量设置 |
小技巧:如果你要读取电机电流,用功能码0x03。如果要启动电机,用功能码0x05写线圈。记住:读用0x03/0x04,写用0x06/0x10,这八个字够你应付90%的场景。
4.5 实战:用Python读取电机电流
光说不练假把式。我写一段Python代码,用modbus_tk库读取电机电流。这个库我用了好几年,稳定可靠。
import modbus_tk.modbus_rtu as modbus_rtu
import modbus_tk.defines as cst
import serial
# 配置串口
ser = serial.Serial(
port='COM3', # 根据实际情况修改
baudrate=9600,
bytesize=8,
parity='N',
stopbits=1,
timeout=1
)
# 创建Modbus RTU主站
master = modbus_rtu.RtuMaster(ser)
master.set_timeout(1.0)
try:
# 读取从站1,保持寄存器起始地址0,读取1个寄存器
# 对应文档中的40001地址
data = master.execute(
slave=1,
function_code=cst.READ_HOLDING_REGISTERS,
starting_address=0,
quantity_of_x=1
)
# data返回的是元组,例如(300,)
current_value = data[0]
print(f"电机电流值: {current_value} A")
except Exception as e:
print(f"读取失败: {e}")
finally:
ser.close()
这段代码的核心就一行:master.execute()。你只需要告诉它从站地址、功能码、起始地址、读取数量,剩下的库帮你搞定。
注意:有些电机的电流值是浮点数,但Modbus寄存器只能传整数。这时候需要做数据缩放。比如实际电流300.5A,寄存器存的是3005,你得自己除以10。这个缩放比例,设备文档里一定会写,千万别漏看。
好了,Modbus协议的核心内容就这些。RTU和TCP怎么选、地址映射怎么避坑、报文结构怎么拆解,我都用自己的经验给你讲了一遍。下一节我们开始实战,用Python写一个完整的电机监测程序。
总结一句话:Modbus不难,难的是细心。地址别搞错,CRC别算错,波特率别配错。做到这三点,你就能在工业现场横着走。