第2章:工业通信协议(上):Modbus RTU/TCP 协议原理、数据帧结构、寄存器映射、实战:使用Python的 minimalmodbus 库读取温湿度传感器
大家好,欢迎来到第二章。
上一章我们聊了工业物联网的整体架构,从传感器到云平台,链路很长。但真正让设备「开口说话」的,是通信协议。今天咱们就啃下工业通信里最经典、也最绕不开的一个——Modbus。
说实话,Modbus 这玩意儿年纪比我还大(1979年诞生),但直到今天,你去任何一个工厂,十台设备里至少有七八台支持它。为什么?因为它简单、稳定、够用。我当年刚入行时,第一个项目就是拿 Modbus RTU 去读一个老掉牙的温控器,折腾了一整天,最后发现是波特率设错了……嗯,这些坑咱们今天一并填上。
2.1 Modbus 是什么?为什么它还没被淘汰?
Modbus 是一种主从(Master/Slave)通信协议。说白了,就是一台主机(通常是 PLC、工控机或你的电脑)轮询地问从机(传感器、执行器、变频器):「嘿,你温度多少?」从机回答:「26.5℃。」
它有两种最常见的物理层实现:
- Modbus RTU:走串口(RS-232/RS-485),数据是二进制编码,效率高。适合短距离(几百米内)、低速(常见9600、19200 bps)场景。
- Modbus TCP:走以太网,把 Modbus 帧封装在 TCP/IP 包里。适合长距离、高速、与上位机或云平台对接。
我个人习惯是:现场设备多、距离远、环境嘈杂,优先选 RS-485 + Modbus RTU;如果设备就在机房里,或者需要和 MES/ERP 系统对接,直接上 Modbus TCP。
2.2 数据帧结构:读懂设备在说什么
Modbus 的数据帧结构非常规整。咱们先看 RTU 的帧格式:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 地址码 | 1 | 从机地址(1-247),0为广播 |
| 功能码 | 1 | 操作类型,如03读保持寄存器 |
| 数据区 | N | 起始地址、寄存器数量、数据值等 |
| CRC校验 | 2 | 循环冗余校验,低字节在前 |
举个例子,你要读取地址为01的从机,从寄存器地址0x0000开始,读2个寄存器(温湿度各占一个)。发送的请求帧就是:
01 03 00 00 00 02 C4 0B
│ │ │ │ │ └─ CRC校验
│ │ │ │ └─ 寄存器数量(2个)
│ │ │ └─ 起始地址高字节
│ │ └─ 起始地址低字节
│ └─ 功能码(03:读保持寄存器)
└─ 从机地址
从机正常回复:
01 03 04 01 2C 00 7B 7A 5B
│ │ │ │ │ └─ CRC
│ │ │ │ └─ 湿度值(0x007B = 123,即12.3%RH)
│ │ │ └─ 温度值(0x012C = 300,即30.0℃)
│ │ └─ 数据字节数(4字节)
│ └─ 功能码
└─ 从机地址
为什么会这样?因为很多温湿度传感器会把实际值放大10倍或100倍来传输,避免小数。所以收到300,你要知道其实是30.0℃。这个「缩放因子」每个厂家可能不同,一定要看数据手册。
2.3 寄存器映射:传感器的「内存地图」
Modbus 定义了四种数据对象,但咱们最常用的是保持寄存器(Holding Register),功能码03(读)和06(写单个)。
寄存器映射表,说白了就是一张表格,告诉你哪个地址存了什么数据。比如一个典型的温湿度传感器:
| 寄存器地址 | 数据类型 | 说明 | 缩放因子 |
|---|---|---|---|
| 0x0000 | 16位无符号整数 | 温度值 | 除以10得℃ |
| 0x0001 | 16位无符号整数 | 湿度值 | 除以10得%RH |
| 0x0002 | 16位无符号整数 | 设备状态 | 0正常,1故障 |
你想想看,如果没有这张表,你拿到一串二进制数据根本不知道哪个字节是温度、哪个是湿度。所以,拿到设备的第一件事,就是找寄存器映射表。我习惯把它打印出来贴在工位上,调试时一眼就能看到。
2.4 实战:用 Python 读取温湿度传感器
理论说完了,咱们上手。这里我用 minimalmodbus 库,它封装了 Modbus RTU 的底层细节,几行代码就能搞定。
先安装:
pip install minimalmodbus
然后写代码。假设传感器接在 COM3 口,波特率9600,从机地址1:
import minimalmodbus
import time
# 初始化串口
instrument = minimalmodbus.Instrument('COM3', 1) # 端口,从机地址
instrument.serial.baudrate = 9600
instrument.serial.bytesize = 8
instrument.serial.parity = 'N' # 无校验
instrument.serial.stopbits = 1
instrument.serial.timeout = 0.5 # 超时0.5秒
def read_temperature():
"""读取温度,寄存器地址0,返回摄氏度"""
raw = instrument.read_register(0, 0) # 参数:寄存器地址,小数位数
# 注意:read_register的第二个参数是小数位数,如果传感器返回300表示30.0,这里传1即可
# 但为了演示手动缩放,我们传0,然后自己处理
temperature = raw / 10.0
return temperature
def read_humidity():
"""读取湿度,寄存器地址1,返回百分比"""
raw = instrument.read_register(1, 0)
humidity = raw / 10.0
return humidity
# 主循环
try:
while True:
temp = read_temperature()
hum = read_humidity()
print(f"温度: {temp:.1f}℃, 湿度: {hum:.1f}%RH")
time.sleep(2)
except KeyboardInterrupt:
print("停止读取")
except minimalmodbus.NoResponseError:
print("没收到回复,检查接线和地址")
except minimalmodbus.InvalidResponseError:
print("数据校验失败,可能干扰严重")
read_register(地址, 小数位数)是核心函数。如果传感器已经帮你处理了缩放(比如传300,你设小数位数=1,它自动返回30.0),那就直接用。否则自己手动除。- 超时时间别设太长,0.5秒足够。我见过有人设5秒,程序卡死半天才发现。
- 异常处理一定要做。现场环境复杂,线松了、干扰大了,随时可能没回复。
2.5 避坑指南:我踩过的那些坑
最后,分享几个实战中容易翻车的地方:
- 地址偏移问题: 有些设备手册写的是「寄存器编号」,比如40001,对应地址是0x0000。千万别直接拿40001去读,要减去40001再转成十六进制。我刚开始就犯过这个错,读了一整天全是0。
- 字节序(Endianness): Modbus 默认是大端(Big-Endian),但有些国产传感器会用小端。如果你读到的数据明显不对(比如温度显示-2000℃),试试交换高低字节。
- RS-485 的 A/B 线: 接反了不会烧设备,但通信会失败。我习惯用万用表量一下:A线对地电压通常比B线高0.2V左右。如果没万用表,就交换一下试试。
- 终端电阻: 长距离(>100米)或高速(>19200 bps)时,记得在总线两端并联120Ω电阻。否则信号反射会导致数据错乱。
好了,这一章的内容就到这儿。Modbus 看起来简单,但真正用好需要不少实战经验。下一章咱们继续聊 Modbus TCP,以及如何用 Python 的 pymodbus 库实现更复杂的读写操作。到时候我会分享一个我当年做过的「远程抄表」项目,那才叫一个刺激。
记住:协议是死的,人是活的。多动手,多踩坑,自然就熟了。