2. 环控数据采集网关的Modbus RTU轮询配置与校验

从站地址与寄存器映射的确定

本案例的采集对象是同一根 RS-485 总线上的两类从站:温湿度变送器与氨气(NH₃)传感器。Modbus RTU 是主从问答式协议,网关作为主站,必须为每个从站分配唯一地址,并明确每个物理量落在哪个寄存器、用什么功能码读、如何换算成工程量。配置前先建立一张“地址—寄存器—换算”对照表,后续轮询参数都围绕它展开。

假设现场设备手册给出的信息如下(示例参数,实际以设备铭牌与手册为准):温湿度变送器从站地址 1,温度值在保持寄存器 0x0000,湿度值在 0x0001,均为无符号 16 位整数,缩放系数 0.1,即 253 表示 25.3 ℃;氨气传感器从站地址 2,NH₃ 浓度在输入寄存器 0x0000,缩放系数 1,单位 ppm。注意两类设备寄存器区不同,读取功能码也不同:保持寄存器用 0x03,输入寄存器用 0x04。

从站设备功能码寄存器地址物理量数据类型换算
1温湿度变送器0x030x0000温度uint16×0.1 ℃
1温湿度变送器0x030x0001湿度uint16×0.1 %RH
2氨气传感器0x040x0000NH₃ 浓度uint16×1 ppm

轮询周期、超时与重试的取值逻辑

轮询周期要同时满足两个约束:一是不能超过环控决策对数据新鲜度的要求,二是不能把总线占满。以 9600 bit/s、8N1 为例,每帧 1 位起始 + 8 位数据 + 1 位停止,共 10 位/字节。读 2 个寄存器的请求帧约 8 字节、响应帧约 9 字节,单次问答约 17 字节 ≈ 170 位,加上帧间静默 3.5 字符时间,单次事务约 20 ms 量级。三个采集事务合计不到 100 ms,因此把轮询周期设为 2 s 完全可行,且远低于总线容量上限。

超时时间应大于“最慢从站响应时间 + 传输时间”。工程上常取 300~500 ms;本例取 500 ms。重试次数取 2 次,即单次事务最多尝试 3 回。若仍失败,则将该从站标记为通信故障,但不阻塞其他从站的轮询——这是网关配置中最容易被忽略的一点:一个从站掉线不应拖垮整条采集链。

轮询顺序建议把响应慢或易受干扰的从站(如氨气传感器)放在前面,避免其超时重试占用时间后挤压后续从站的时间片;同时为每个从站设置独立的超时计时,而不是共用一个全局超时。

轮询时序与故障隔离结构

下图给出网关主站在一个轮询周期内对两个从站的问答时序,以及超时重试与故障标记的分支关系。可以看到,从站 1 的两次读取被合并为一次连续寄存器读取(0x0000 起读 2 个),从站 2 单独一次读取;任一事务超时后进入重试,重试耗尽则打故障标记并继续下一从站。

t = 0 周期 2 s 网关主站 (轮询调度) 从站 1 温湿度变送器 · 0x03 从站 2 氨气传感器 · 0x04 故障处理 重试 / 标记 请求 01 03 0000 0002 响应 温度/湿度 各2字节 请求 02 04 0000 0001 响应 NH₃ 2字节 请求(无响应) 超时 500 ms 重试 1/2 仍失败 → 标记故障 继续下一从站,不阻塞 下一轮询周期开始(2 s 后)

配置实现:轮询表与读取校验

把上述对照表落成网关的轮询配置。下面是一份可直接对照设备手册填写的轮询表结构(以 JSON 表示,字段含义见注释),以及一段用于验证读取值合理性的校验逻辑。校验逻辑不依赖具体网关品牌,只依赖“原始值 → 工程量 → 范围判断”这条链。

{
  "serial": { "baud": 9600, "dataBits": 8, "parity": "none", "stopBits": 1 },
  "timeoutMs": 500,
  "retries": 2,
  "pollIntervalMs": 2000,
  "slaves": [
    {
      "addr": 1,
      "name": "温湿度变送器",
      "tasks": [
        { "fc": 3, "start": 0, "count": 2,
          "fields": [
            { "name": "temp", "index": 0, "scale": 0.1, "unit": "℃", "min": -10, "max": 45 },
            { "name": "rh",   "index": 1, "scale": 0.1, "unit": "%RH", "min": 0, "max": 100 }
          ] }
      ]
    },
    {
      "addr": 2,
      "name": "氨气传感器",
      "tasks": [
        { "fc": 4, "start": 0, "count": 1,
          "fields": [
            { "name": "nh3", "index": 0, "scale": 1, "unit": "ppm", "min": 0, "max": 100 }
          ] }
      ]
    }
  ]
}

读取值合理性检查分三层:通信层看是否收到合法响应(功能码回显、字节数匹配、CRC 正确);量程层看换算后的工程量是否落在传感器量程内;物理层看变化率是否异常(例如温度 2 s 内跳变超过 2 ℃ 通常意味着解析错位或寄存器映射错误)。下面给出校验函数的核心实现。

def decode_and_check(task, raw_words, prev=None):
    """raw_words: 从响应帧解析出的 16 位寄存器值列表"""
    out = {}
    for f in task["fields"]:
        raw = raw_words[f["index"]]
        val = raw * f["scale"]
        # 量程层
        if not (f["min"] <= val <= f["max"]):
            return None, f"{f['name']}={val}{f['unit']} 超出量程"
        # 物理层:变化率检查(仅对已有上一周期值的量)
        if prev and f["name"] in prev:
            rate = abs(val - prev[f["name"]])
            limit = 2.0 if f["name"] == "temp" else 5.0
            if rate > limit:
                return None, f"{f['name']} 变化率 {rate:.1f} 异常"
        out[f["name"]] = val
    return out, "ok"

# 示例:从站1响应数据区为 [253, 612] → 25.3 ℃, 61.2 %RH
print(decode_and_check(
    {"fields": [
        {"name": "temp", "index": 0, "scale": 0.1, "unit": "℃", "min": -10, "max": 45},
        {"name": "rh",   "index": 1, "scale": 0.1, "unit": "%RH", "min": 0, "max": 100}
    ]},
    [253, 612]
))  # 预期输出: ({'temp': 25.3, 'rh': 61.2}, 'ok')
常见配置错误:把温湿度变送器的寄存器地址写成 1 起始(即 0x0001 读温度),会导致温度读到湿度值、湿度读到下一寄存器;把氨气传感器的功能码误用 0x03 读取输入寄存器区,会收到异常码 0x02(非法数据地址)。校验时若发现“温度恒定等于湿度值”或“持续返回异常码”,应优先回查寄存器映射与功能码。

验证配置是否正确的判据

  • 通信正常:每个从站在一个周期内均返回合法响应,CRC 校验通过,无异常码。
  • 映射正确:用手持温湿度计或标准气体对传感器施加已知激励,网关读数与参考值偏差在传感器标称精度内(如温度 ±0.5 ℃、湿度 ±3 %RH)。
  • 周期稳定:连续观察 10 个周期,各从站采集时间戳间隔稳定在 2 s ± 100 ms,无因重试导致的周期漂移。
  • 故障隔离有效:拔掉从站 2 的通信线,从站 1 的数据仍按周期更新,从站 2 在 3 次尝试后被标记故障,恢复接线后自动重新入轮询。

满足以上四条,即可认为从站地址、寄存器映射、轮询周期、超时重试四项配置均正确,采集数据可进入后续的异常识别与清洗环节。