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,低字节在前)。

网关(主站) 构建请求帧 串口发送 write + flush 从站设备 解析并响应 响应帧 地址+功能+数据+CRC 接收与校验 CRC + 地址匹配 解析寄存器值 大端拼接

完整代码

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()

运行与验证

  1. 确认从站地址、波特率、校验位与代码中配置一致,接好A/B线(RS-485)或TX/RX(RS-232)。
  2. 执行 python modbus_rtu.py。
  3. 通过条件:程序打印出 寄存器 0 = ...、寄存器 1 = ...,且数值与从站实际寄存器一致;若CRC错误、地址不匹配或超时,会抛出对应异常,说明接线或参数需排查。

常见问题

  • 一直超时:检查A/B线是否接反、波特率/校验位是否一致、从站地址是否正确。
  • CRC失败:多为波特率或校验位不匹配导致采样错位,或线路干扰。
  • 异常码0x02:请求的寄存器地址或数量超出从站支持范围。