第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。

小提示: 很多新入行的朋友会纠结「RTU 和 TCP 哪个好?」其实没有好坏,只有合不合适。我见过一个项目,明明设备就在隔壁,非要走 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℃。这个「缩放因子」每个厂家可能不同,一定要看数据手册。

注意: 我曾经在一个项目里,没仔细看手册,直接把收到的原始值当成真实温度传给了上位机。结果现场显示「300℃」,差点把运维师傅吓出心脏病……从那以后,我拿到任何新传感器,第一件事就是看数据手册里的寄存器映射表。

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 库实现更复杂的读写操作。到时候我会分享一个我当年做过的「远程抄表」项目,那才叫一个刺激。

记住:协议是死的,人是活的。多动手,多踩坑,自然就熟了。