2. 网关串口设备接入与Modbus RTU采集
任务目标
在IIoT网关上实现一个最小可用的Modbus RTU主站:打开串口、按RTU帧格式发送“读保持寄存器”请求、接收并校验响应,最终得到从站寄存器值。串口参数与从站地址、寄存器地址、数量由配置决定。
环境与依赖
- Python 3.8+,依赖
pyserial:pip install pyserial - 一台Modbus RTU从站(如温湿度变送器、PLC),通过RS-485/RS-232接入网关串口
- 串口参数需与从站一致:波特率、数据位、校验、停止位、从站地址
实现思路
Modbus RTU帧结构为:从站地址(1) + 功能码(1) + 数据(N) + CRC16(2)。读保持寄存器功能码为 0x03,请求数据为“起始地址(2) + 寄存器数量(2)”,响应数据为“字节数(1) + 寄存器值(2×N)”。CRC采用Modbus标准CRC16(多项式0xA001,初值0xFFFF,低字节在前)。
完整代码
import serial
import struct
import time
# ---- 串口与从站配置(按实际设备修改)----
PORT = "COM3" # Linux 下如 "/dev/ttyUSB0"
BAUDRATE = 9600
BYTESIZE = 8
PARITY = serial.PARITY_NONE
STOPBITS = 1
SLAVE_ID = 1
START_ADDR = 0 # 起始保持寄存器地址
REG_COUNT = 2 # 读取寄存器数量
TIMEOUT = 1.0 # 读超时(秒)
def crc16(data: bytes) -> bytes:
"""Modbus CRC16,返回低字节在前。"""
crc = 0xFFFF
for b in data:
crc ^= b
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
return struct.pack("<H", crc)
def build_request(slave, addr, count):
pdu = struct.pack(">BHH", 0x03, addr, count)
frame = struct.pack(">B", slave) + pdu
return frame + crc16(frame)
def read_holding_registers(ser, slave, addr, count):
ser.reset_input_buffer()
ser.write(build_request(slave, addr, count))
ser.flush()
# 期望响应长度: 地址1 + 功能1 + 字节数1 + 数据2N + CRC2
expect_len = 5 + 2 * count
resp = ser.read(expect_len)
if len(resp) != expect_len:
raise TimeoutError(f"响应长度不足: 收到 {len(resp)} 字节, 期望 {expect_len}")
if resp[0] != slave:
raise ValueError(f"从站地址不匹配: {resp[0]}")
if resp[1] & 0x80:
raise IOError(f"从站异常响应, 异常码={resp[2]}")
if resp[1] != 0x03:
raise ValueError(f"功能码不匹配: {resp[1]}")
body, recv_crc = resp[:-2], resp[-2:]
if crc16(body) != recv_crc:
raise ValueError("CRC 校验失败")
byte_count = resp[2]
if byte_count != 2 * count:
raise ValueError(f"字节数异常: {byte_count}")
values = struct.unpack(">" + "H" * count, resp[3:3 + byte_count])
return list(values)
def main():
ser = serial.Serial(
port=PORT, baudrate=BAUDRATE, bytesize=BYTESIZE,
parity=PARITY, stopbits=STOPBITS, timeout=TIMEOUT,
)
try:
time.sleep(0.1) # 等待串口稳定
values = read_holding_registers(ser, SLAVE_ID, START_ADDR, REG_COUNT)
for i, v in enumerate(values):
print(f"寄存器 {START_ADDR + i} = {v}")
finally:
ser.close()
if __name__ == "__main__":
main()
运行与验证
- 确认从站地址、波特率、校验位与代码中配置一致,接好A/B线(RS-485)或TX/RX(RS-232)。
- 执行
python modbus_rtu.py。 - 通过条件:程序打印出
寄存器 0 = ...、寄存器 1 = ...,且数值与从站实际寄存器一致;若CRC错误、地址不匹配或超时,会抛出对应异常,说明接线或参数需排查。
常见问题
- 一直超时:检查A/B线是否接反、波特率/校验位是否一致、从站地址是否正确。
- CRC失败:多为波特率或校验位不匹配导致采样错位,或线路干扰。
- 异常码0x02:请求的寄存器地址或数量超出从站支持范围。