

近期在调试一套工业环境监控系统时,遇到了关于Modbus连接保持的问题。现场部署了一批网口温湿度变送器,PoE取电、Modbus TCP上云,同时利旧接入存量RS485探头,并开启了SNMP和UDP Trap服务。档案馆恒温恒湿系统由冷水阀、加热段、加湿器、除湿转轮、风机变频器等多台设备组成,组态平台需要实时读取设备运行状态并下发控制指令,形成温湿度的闭环调节。这篇从Modbus寄存器规划、组态平台通信配置、PID闭环控制逻辑到防抖动与安全联锁,完整记录档案馆恒温恒湿设备的Modbus对接与自动控制实现。
┌─────────────────────────────────────────────────────────────────┐
│ 组态平台(SCADA) │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ 画面组态 │ │ 实时数据库 │ │ 控制策略引擎 │ │
│ │ (HMI) │ │ (Modbus映射) │ │ (PID/逻辑联锁) │ │
│ └──────┬──────┘ └──────┬───────┘ └───────────┬───────────┘ │
│ │ Modbus TCP │ │ │
└─────────┼─────────────────┼──────────────────────┼──────────────┘
│ │ │
══════════╪═════════════════╪══════════════════════╪══════════════
│ │ │
┌─────────▼─────────────────▼──────────────────────▼──────────────┐
│ 现场控制层(PLC/控制器) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ 冷水阀执行器 │ │ 加热段SSR │ │ 加湿器(电极式) │ │
│ │ Modbus RTU │ │ Modbus RTU │ │ Modbus RTU │ │
│ │ 寄存器:42001 │ │ 寄存器:42002 │ │ 寄存器:42003 │ │
│ └──────────────┘ └──────────────┘ └───────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ 除湿转轮 │ │ 风机变频器 │ │ 温湿度传感器 │ │
│ │ Modbus RTU │ │ Modbus RTU │ │ Modbus TCP │ │
│ │ 寄存器:42004 │ │ 寄存器:42005 │ │ 寄存器:40001~40004 │ │
│ └──────────────┘ └──────────────┘ └───────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘设备 | 功能 | 寄存器类型 | 地址范围 | 读写 | 数据格式 |
|---|---|---|---|---|---|
温湿度传感器 | 温度值 | 保持寄存器 | 40001 | 只读 | int16 (×0.01) |
温湿度传感器 | 湿度值 | 保持寄存器 | 40002 | 只读 | uint16 (×0.1) |
冷水阀执行器 | 阀门开度 | 保持寄存器 | 42001 | 读写 | uint16 (0~1000) |
加热段SSR | 加热功率 | 保持寄存器 | 42002 | 读写 | uint16 (0~1000) |
加湿器 | 加湿量 | 保持寄存器 | 42003 | 读写 | uint16 (0~1000) |
除湿转轮 | 转速 | 保持寄存器 | 42004 | 读写 | uint16 (0~3000) |
风机变频器 | 风机频率 | 保持寄存器 | 42005 | 读写 | uint16 (0~500) |
风机变频器 | 运行状态 | 线圈 | 00001 | 读写 | bool |
系统状态 | 运行模式 | 线圈 | 00002 | 读写 | bool (0=手动,1=自动) |
系统状态 | 急停状态 | 输入线圈 | 10001 | 只读 | bool |
系统状态 | 故障汇总 | 输入线圈 | 10002 | 只读 | bool |
参数 | 值 |
|---|---|
协议 | Modbus TCP |
PLC/控制器IP | 10.10.90.50 |
端口 | 502 |
从站地址 | 1 |
扫描周期 | 1000ms(快速变量)/ 5000ms(慢速变量) |
超时时间 | 3000ms |
重试次数 | 3 |
字节序 | 大端(Big Endian) |
寄存器排列 | CDAB(浮点数跨寄存器时) |
┌─────────────────────────────────────────────────────────────────┐
│ 组态平台变量表(部分) │
├──────────────┬──────────┬────────┬──────┬──────────────────────┤
│ 变量名 │ 地址 │ 数据类型│ 读写 │ 描述 │
├──────────────┼──────────┼────────┼──────┼──────────────────────┤
│ AI_Temp │ 40001 │ INT16 │ RO │ 当前温度(×0.01) │
│ AI_Hum │ 40002 │ UINT16 │ RO │ 当前湿度(×0.1) │
│ AO_CoolValve│ 42001 │ UINT16 │ RW │ 冷水阀开度(0~1000) │
│ AO_HeatPower│ 42002 │ UINT16 │ RW │ 加热功率(0~1000) │
│ AO_HumPower │ 42003 │ UINT16 │ RW │ 加湿量(0~1000) │
│ AO_DehumRPM │ 42004 │ UINT16 │ RW │ 除湿转轮转速 │
│ AO_FanFreq │ 42005 │ UINT16 │ RW │ 风机频率(0~500) │
│ DO_FanRun │ 00001 │ BOOL │ RW │ 风机启停 │
│ DO_AutoMode │ 00002 │ BOOL │ RW │ 自动模式 │
│ DI_Emergency│ 10001 │ BOOL │ RO │ 急停按钮 │
│ DI_FaultSum │ 10002 │ BOOL │ RO │ 故障汇总 │
└──────────────┴──────────┴────────┴──────┴──────────────────────┘┌─────────────────────────────────────────────────────────────────┐
│ 档案馆特藏库 - 恒温恒湿控制系统 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 温湿度状态: │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 温度: 19.2 ℃ │ │ 湿度: 51.5 %RH │ │
│ │ ████████████░░░ │ │ ██████████░░░░░░ │ │
│ │ 设定: 18.0 ℃ │ │ 设定: 50.0 %RH │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ 设备控制: │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐│
│ │ 冷水阀: 35% │ │ 加热: 0% │ │ 风机: 45Hz (运行) ││
│ │ ██████░░░░░░ │ │ ░░░░░░░░░░░░ │ │ █████████░░░░░░░░░░░░░░ ││
│ └──────────────┘ └──────────────┘ └──────────────────────────┘│
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 加湿: 12% │ │ 除湿: 800rpm│ │
│ │ ███░░░░░░░░░ │ │ ████░░░░░░░░░░ │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ 运行模式: [自动] [手动] 系统状态: 正常运行 │
│ │
│ 趋势曲线: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 温度/湿度 24小时趋势 │ │
│ │ ┌─────────────────────────────────────────────────────────┐│ │
│ │ │ 25 ──┐ ┌──││ │
│ │ │ │ ╱╲ ╱╲ │ ││ │
│ │ │ 20 │ ╱ ╲ ╱ ╲ ╱╲ ╱╲ ╱│ ││ │
│ │ │ │╱ ╲╱ ╲╱ ╲╱ ╲ ╱ │ ││ │
│ │ │ 15 ──┴────────────────────╲────────────────────╱──┴──││ │
│ │ │ 0 6 12 18 24 (小时) ││ │
│ └──┴─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘┌──────────────────────┐
│ 组态平台PID引擎 │
│ │
┌──────────┐ │ ┌───┐ ┌───┐ │ ┌──────────────┐
│ 温湿度 │ │ │PID│ │PID│ │ │ 执行设备 │
│ 传感器 │─────►│ │温度│ │湿度│───┐─┼─────►│ 冷水阀/加热 │
│ Modbus │ │ └───┘ └───┘ │ │ │ 加湿/除湿 │
│ TCP │ │ │ │ │ Modbus RTU │
└──────────┘ │ 防抖动/死区 │ │ └──────────────┘
│ 输出限幅 │ │
│ 安全联锁 │ │
└────────────────────┘ │
▲ │
│ │
└────────────┘
反馈回路档案馆恒温恒湿系统属于大惯性、大滞后系统,PID参数需要保守整定:
回路 | Kp | Ki | Kd | 采样周期 | 备注 |
|---|---|---|---|---|---|
温度控制 | 2.0 | 0.05 | 5.0 | 30s | 大滞后,Kd适当加大 |
湿度控制 | 1.5 | 0.1 | 2.0 | 30s | 湿度惯性更大 |
冷水阀 | 1.0 | 0.02 | 1.0 | 10s | 阀门响应快 |
风机频率 | 0.5 | 0.01 | 0.5 | 60s | 风机惯性大 |
#!/usr/bin/env python3
"""
组态平台PID控制引擎
"""
import time
from dataclasses import dataclass
from typing import Callable
@dataclass
class PIDParams:
Kp: float
Ki: float
Kd: float
output_min: float
output_max: float
deadband: float = 0.0
sample_time: float = 1.0
class PIDController:
"""PID控制器"""
def __init__(self, params: PIDParams, output_cb: Callable[[float], None]):
self.params = params
self.output_cb = output_cb
self.integral = 0.0
self.prev_error = 0.0
self.last_time = time.time()
self.last_output = 0.0
self.enabled = False
def set_enabled(self, enabled: bool):
if enabled and not self.enabled:
# 重新使能时重置积分
self.integral = 0.0
self.prev_error = 0.0
self.enabled = enabled
def update(self, setpoint: float, process_value: float) -> float:
"""PID计算"""
if not self.enabled:
return self.last_output
now = time.time()
dt = now - self.last_time
if dt < self.params.sample_time:
return self.last_output
error = setpoint - process_value
# 死区判断
if abs(error) < self.params.deadband:
return self.last_output
# 比例项
proportional = self.params.Kp * error
# 积分项(抗积分饱和)
self.integral += error * dt
# 积分限幅
integral_max = self.params.output_max / max(self.params.Ki, 0.001)
self.integral = max(-integral_max, min(integral_max, self.integral))
integral = self.params.Ki * self.integral
# 微分项(对测量值微分,避免设定值突变引起冲击)
derivative = -self.params.Kd * (process_value - self.prev_process_value) / dt if hasattr(self, 'prev_process_value') else 0.0
# 计算输出
output = proportional + integral + derivative
output = max(self.params.output_min, min(self.params.output_max, output))
# 防抖动:输出变化率限制
max_change = (self.params.output_max - self.params.output_min) * 0.1 # 每次最大变化10%
if abs(output - self.last_output) > max_change:
output = self.last_output + max_change if output > self.last_output else self.last_output - max_change
self.last_output = output
self.prev_error = error
self.prev_process_value = process_value
self.last_time = now
# 调用输出回调
self.output_cb(output)
return output
# 温度控制回路
temp_pid = PIDController(
params=PIDParams(Kp=2.0, Ki=0.05, Kd=5.0, output_min=0.0, output_max=1000.0, deadband=0.2, sample_time=30.0),
output_cb=lambda v: write_modbus_register(42001, int(v)) # 冷水阀
)
# 湿度控制回路
hum_pid = PIDController(
params=PIDParams(Kp=1.5, Ki=0.1, Kd=2.0, output_min=0.0, output_max=1000.0, deadband=1.0, sample_time=30.0),
output_cb=lambda v: write_modbus_register(42003, int(v)) # 加湿器
)
# 控制循环
def control_loop():
while True:
# 读取温湿度
temp = read_modbus_register(40001) * 0.01
hum = read_modbus_register(40002) * 0.1
# 获取设定值
temp_sp = get_setpoint("temperature") # 18.0
hum_sp = get_setpoint("humidity") # 50.0
# PID计算
temp_pid.update(temp_sp, temp)
hum_pid.update(hum_sp, hum)
time.sleep(1.0)冷启动保护流程:
系统上电 → 风机先启动(最低频率30Hz)
→ 延时5分钟(让空气流通)
→ 逐步开启冷水阀/加热(每30秒增加5%)
→ 达到设定值附近后切入PID自动调节┌─────────────────────────────────────────────────────────────────┐
│ 安全联锁矩阵 │
├─────────────────────────────────────────────────────────────────┤
│ 条件 │ 动作 │
├────────────────────────────────┼────────────────────────────────┤
│ 急停按钮按下 │ 所有设备停机 │
│ 风机故障 │ 关闭冷水阀、加热、加湿 │
│ 温度传感器故障 │ 切换到备用传感器,报警 │
│ 湿度传感器故障 │ 保持当前输出,报警 │
│ 冷水阀开度>80%但温度仍>设定值 │ 启动除湿转轮,报警 │
│ 湿度>65%RH │ 强制启动除湿,关闭加湿 │
│ 温度>28℃ │ 强制最大冷水阀,报警 │
│ 通信中断>3次 │ 保持最后输出,报警 │
└─────────────────────────────────────────────────────────────────┘def safety_interlock():
"""安全联锁检查"""
# 读取状态
emergency = read_modbus_coil(10001)
fault_sum = read_modbus_coil(10002)
fan_fault = read_modbus_coil(10003)
temp_sensor_fault = read_modbus_input_register(30001)
hum_sensor_fault = read_modbus_input_register(30002)
if emergency or fault_sum:
# 紧急停机
write_modbus_coil(00001, False) # 风机停机
write_modbus_register(42001, 0) # 冷水阀关闭
write_modbus_register(42002, 0) # 加热关闭
write_modbus_register(42003, 0) # 加湿关闭
write_modbus_register(42004, 0) # 除湿关闭
temp_pid.set_enabled(False)
hum_pid.set_enabled(False)
trigger_alarm("EMERGENCY_STOP")
return
if fan_fault:
# 风机故障,关闭所有执行器
write_modbus_register(42001, 0)
write_modbus_register(42002, 0)
write_modbus_register(42003, 0)
temp_pid.set_enabled(False)
hum_pid.set_enabled(False)
trigger_alarm("FAN_FAULT")
return
if temp_sensor_fault:
# 温度传感器故障,切换到备用
switch_to_backup_sensor("temperature")
trigger_alarm("TEMP_SENSOR_FAULT")
if hum_sensor_fault:
# 湿度传感器故障,保持当前输出
hum_pid.set_enabled(False)
trigger_alarm("HUM_SENSOR_FAULT")def read_modbus_register(address: int) -> int:
"""读取单个保持寄存器"""
# Modbus TCP ADU
transaction_id = get_next_transaction_id()
protocol_id = 0x0000
length = 0x0006
unit_id = 0x01
function_code = 0x03
start_addr = address - 40001
reg_count = 1
adu = struct.pack(">HHHBBHH",
transaction_id, protocol_id, length,
unit_id, function_code, start_addr, reg_count)
response = send_modbus_request(adu)
if response and len(response) >= 11:
return struct.unpack(">H", response[9:11])[0]
return 0def write_modbus_register(address: int, value: int):
"""写入单个保持寄存器"""
transaction_id = get_next_transaction_id()
protocol_id = 0x0000
length = 0x0006
unit_id = 0x01
function_code = 0x06
reg_addr = address - 40001
adu = struct.pack(">HHHBBHH",
transaction_id, protocol_id, length,
unit_id, function_code, reg_addr, value)
response = send_modbus_request(adu)
if not response:
logger.error(f"写入寄存器{address}失败")
return False
return Truedef read_multiple_registers(start_address: int, count: int) -> list:
"""批量读取寄存器,减少通信次数"""
transaction_id = get_next_transaction_id()
protocol_id = 0x0000
length = 0x0006
unit_id = 0x01
function_code = 0x03
start_addr = start_address - 40001
adu = struct.pack(">HHHBBHH",
transaction_id, protocol_id, length,
unit_id, function_code, start_addr, count)
response = send_modbus_request(adu)
if response and len(response) >= 9 + count * 2:
byte_count = response[8]
data = response[9:9+byte_count]
return [struct.unpack(">H", data[i:i+2])[0] for i in range(0, len(data), 2)]
return []组态平台同时作为BACnet设备,向BA系统提供数据:
组态平台BACnet对象映射:
Analog Input 1: 当前温度
Analog Input 2: 当前湿度
Analog Output 1: 冷水阀开度
Analog Output 2: 加热功率
Binary Output 1: 风机启停
Binary Output 2: 自动模式# 数据转发到InfluxDB
from influxdb_client import InfluxDBClient, Point
client = InfluxDBClient(url="http://10.10.90.100:8086", token="xxx", org="archive")
write_api = client.write_api()
def forward_to_influxdb(timestamp, temperature, humidity, valve, heating, humidifier, fan_freq):
point = Point("hvac_control") \
.tag("room", "special_collection") \
.field("temperature", temperature) \
.field("humidity", humidity) \
.field("cool_valve", valve) \
.field("heating", heating) \
.field("humidifier", humidifier) \
.field("fan_freq", fan_freq) \
.time(timestamp)
write_api.write(bucket="archive_env", record=point)指标 | 标准 | 测试方法 |
|---|---|---|
温度控制精度 | ±0.5℃ | 稳定运行24小时,记录波动范围 |
湿度控制精度 | ±3%RH | 同上 |
响应时间 | <30分钟(从扰动到恢复) | 人为改变设定值 |
过冲量 | <1℃ / <5%RH | 冷启动测试 |
稳态误差 | <0.2℃ / <1%RH | 长期运行统计 |
项目 | 标准 | 方法 |
|---|---|---|
急停响应 | <1秒 | 按下急停按钮,观察设备停机 |
通信中断保护 | 保持最后输出 | 断开通信线,观察执行器状态 |
传感器故障切换 | <5秒 | 拔掉传感器,观察备用切换 |
联锁逻辑 | 全部正确 | 逐一模拟故障条件 |
组态平台、档案馆、恒温恒湿、Modbus协议、闭环控制、PID、冷水阀、加热段、加湿器、除湿转轮、风机变频器、安全联锁、防抖动、BACnet/IP、InfluxDB、SCADA
组态平台对接档案馆恒温恒湿设备的核心在于"Modbus协议读写+PID闭环控制+安全联锁保护"三位一体——通过合理的寄存器规划实现设备状态与控制的双向通信,PID参数保守整定适应大惯性系统,防冷启动过冲与输出限幅避免设备冲击,安全联锁矩阵覆盖急停、故障、传感器失效等异常场景,最终形成一套稳定可靠的恒温恒湿自动控制系统。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。