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 | 温湿度变送器 | 0x03 | 0x0000 | 温度 | uint16 | ×0.1 ℃ |
| 1 | 温湿度变送器 | 0x03 | 0x0001 | 湿度 | uint16 | ×0.1 %RH |
| 2 | 氨气传感器 | 0x04 | 0x0000 | NH₃ 浓度 | 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 单独一次读取;任一事务超时后进入重试,重试耗尽则打故障标记并继续下一从站。
配置实现:轮询表与读取校验
把上述对照表落成网关的轮询配置。下面是一份可直接对照设备手册填写的轮询表结构(以 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')
验证配置是否正确的判据
- 通信正常:每个从站在一个周期内均返回合法响应,CRC 校验通过,无异常码。
- 映射正确:用手持温湿度计或标准气体对传感器施加已知激励,网关读数与参考值偏差在传感器标称精度内(如温度 ±0.5 ℃、湿度 ±3 %RH)。
- 周期稳定:连续观察 10 个周期,各从站采集时间戳间隔稳定在 2 s ± 100 ms,无因重试导致的周期漂移。
- 故障隔离有效:拔掉从站 2 的通信线,从站 1 的数据仍按周期更新,从站 2 在 3 次尝试后被标记故障,恢复接线后自动重新入轮询。
满足以上四条,即可认为从站地址、寄存器映射、轮询周期、超时重试四项配置均正确,采集数据可进入后续的异常识别与清洗环节。