
恒湿机
关键词:档案库房、净化恒湿一体机、闭环控制、PID整定、稳定性测试、温湿度稳态、加湿/除湿切换、过滤器压差、回风取样、扰动注入、边缘DDC、Modbus TCP 标签:#物联网 #Modbus #TCP/IP #POE供电 #InfluxDB #以太网温湿度传感器 #网口温湿度变送器 #机房监控 #边缘计算 #洁净度控制 #PID控制
前一篇讲了净化恒湿一体机的选型与部署。设备就位、风管接好、通信通了,不等于系统能用。档案库房的特殊性在于——环境波动的代价不是停机,是文献不可逆损伤。一次过调导致相对湿度从 50% 跌到 35%,纸质档案失水脆化;一次振荡让温湿度在设定值上下反复横跳,霉菌就在这种"呼吸式"波动中滋生。
调试的核心目标只有两个:闭环跟得上、稳态稳得住。
本文按工程交付的真实顺序展开:从单机点动→开环验证→闭环整定→扰动测试→长期稳定性验证,每一步给可执行的方法、判定标准和避坑点。
设备上电前,先做四件事:
检查项 | 方法 | 合格标准 |
|---|---|---|
供电相序/电压 | 万用表测量,三相设备查相序 | 电压±10%,相序正确 |
接地连续性 | 接地电阻测试仪 | ≤4Ω |
控制板自检 | 上电观察指示灯/面板 | 无故障码,通信灯闪烁 |
风机转向 | 点动启动,观察叶轮方向 | 与标识一致,无异响 |
然后逐项点动,确认每个执行机构都能被控制:
风机启动 → 确认转向、电流、振动、噪声
电加热/再热投入 → 确认逐级投入、电流平稳
压缩机启动 → 确认吸排气压力、运行电流、无液击异响
加湿器投入 → 确认补水阀开、水位上升、加湿量输出
冷凝水提升泵 → 手动注水测试启停和排水
UV/杀菌 → 确认灯管点亮(可见光/臭氧检测)关键:点动时记录每个执行器的启动电流和稳定运行电流,后续闭环调试中用于判断负载状态。
"""
point_check.py - 通信点表逐项核对脚本
上电后逐点读写,确认地址、数据类型、缩放因子正确
"""
import asyncio
from pymodbus.client import AsyncModbusTcpClient
POINTS = {
# 只读点:回风温湿度、运行状态、故障字、过滤器压差
"ret_temp": {"type": "read", "fc": 3, "addr": 0, "scale": 0.1, "unit": "℃"},
"ret_hum": {"type": "read", "fc": 3, "addr": 1, "scale": 0.1, "unit": "%RH"},
"supply_temp": {"type": "read", "fc": 3, "addr": 2, "scale": 0.1, "unit": "℃"},
"supply_hum": {"type": "read", "fc": 3, "addr": 3, "scale": 0.1, "unit": "%RH"},
"fan_status": {"type": "read", "fc": 1, "addr": 0, "scale": 1, "unit": "bool"},
"comp_status": {"type": "read", "fc": 1, "addr": 1, "scale": 1, "unit": "bool"},
"heat_status": {"type": "read", "fc": 1, "addr": 2, "scale": 1, "unit": "bool"},
"hum_status": {"type": "read", "fc": 1, "addr": 3, "scale": 1, "unit": "bool"},
"fault_word": {"type": "read", "fc": 3, "addr": 10, "scale": 1, "unit": "hex"},
"filter_dp": {"type": "read", "fc": 3, "addr": 20, "scale": 1.0, "unit": "Pa"},
# 可写点:设定值、启停、模式
"sp_temp": {"type": "write", "fc": 6, "addr": 100, "scale": 0.1, "unit": "℃"},
"sp_hum": {"type": "write", "fc": 6, "addr": 101, "scale": 0.1, "unit": "%RH"},
"run_cmd": {"type": "write", "fc": 5, "addr": 0, "scale": 1, "unit": "bool"},
"mode_cmd": {"type": "write", "fc": 6, "addr": 1, "scale": 1, "unit": "enum"},
}
async def check_read(c, point, cfg):
"""读单个点并验证合理性"""
if cfg["fc"] == 3: # 保持寄存器
rr = await c.read_holding_registers(cfg["addr"], 1, slave=1)
else: # 线圈
rr = await c.read_coils(cfg["addr"], 1, slave=1)
if rr.isError():
return False, "error"
raw = rr.registers[0] if hasattr(rr, 'registers') else rr.bits[0]
val = raw * cfg["scale"] if isinstance(raw, (int, float)) else raw
return True, val
async def check_write_readback(c, point, cfg, test_val):
"""写设定值并回读验证"""
scaled = int(test_val / cfg["scale"])
await c.write_registers(cfg["addr"], [scaled], slave=1)
await asyncio.sleep(1)
ok, val = await check_read(c, point, cfg)
return ok and abs(val - test_val) < 0.2
async def main():
c = AsyncModbusTcpClient("10.20.1.50", port=502)
await c.connect()
print("=== 只读点核对 ===")
for point, cfg in POINTS.items():
if cfg["type"] == "read":
ok, val = await check_read(c, point, cfg)
status = "✓" if ok else "✗"
print(f" {status} {point}: {val} {cfg['unit']}")
print("\n=== 可写点回读验证 ===")
for point, cfg in POINTS.items():
if cfg["type"] == "write":
if point.startswith("sp_"):
test_val = 22.0 if "temp" in point else 50.0
ok = await check_write_readback(c, point, cfg, test_val)
status = "✓" if ok else "✗"
print(f" {status} {point}: write {test_val} → readback OK={ok}")
asyncio.run(main())逐点核对后,把确认过的点表固化到配置库,标注版本号。后续所有调试都基于这张表。
闭环整定前,先做开环——手动给定输出,观察设备响应,建立对系统动态特性的直觉。
步骤:
1. 机组手动模式,风机运行
2. 加湿器从 0% → 50% 输出,记录回风湿度上升曲线
3. 加湿器从 50% → 0%,记录湿度回落曲线
4. 电加热从 0% → 30%,记录回风温度上升曲线
5. 压缩机启动,记录温湿度联合变化记录关键参数:
参数 | 含义 | 典型值参考 |
|---|---|---|
纯滞后 τ_d | 输出变化到回风检测到的延迟 | 30~120s(取决于风管长度) |
时间常数 τ | 达到 63% 终值的时间 | 5~20min(库容大则长) |
增益 K | 输出 100% 对应的稳态变化量 | 湿度 +15~25%RH / 温度 +3~6℃ |
超调量 | 首次越过目标的最大偏差 | <10% 为优,<20% 可接受 |
"""
step_response.py - 阶跃响应数据采集
手动操作设备后,采集并标注时间,用于后续 PID 参数估算
"""
import time
from datetime import datetime
import csv
class StepResponseRecorder:
def __init__(self, device_host, csv_path):
self.device_host = device_host
self.csv_path = csv_path
self.data = []
def record(self, sp_hum_pct, actual_hum, fan_rpm=None):
"""每次调用记录一行"""
self.data.append({
"ts": datetime.now().isoformat(),
"elapsed_s": time.time() - self.t_start,
"output_pct": sp_hum_pct,
"ret_hum": actual_hum,
"fan_rpm": fan_rpm or 0
})
def start(self):
self.t_start = time.time()
def save(self):
with open(self.csv_path, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=self.data[0].keys())
writer.writeheader()
writer.writerows(self.data)
print(f"阶跃响应数据已保存: {self.csv_path}")调试经验:
档案库房温湿度强耦合——降温必然除湿,升温可能加湿(取决于再热策略)。开环阶段要摸清耦合方向:
操作 | 温度影响 | 湿度影响 | 耦合强度 |
|---|---|---|---|
压缩机启动 | ↓ | ↓↓ | 强 |
电加热投入 | ↑ | ↑(轻微) | 弱 |
加湿器投入 | ↓(轻微) | ↑↑ | 中 |
风机变速 | 均匀化 | 均匀化 | 间接 |
耦合方向决定了后续 PID 是独立回路还是解耦控制。中小型项目用独立 PID + 优先级仲裁就够了,不必上解耦矩阵。
湿度是档案保护的核心参数,先调湿度回路。
整定步骤(Ziegler-Nichols 改进法):
1. 置 Ki=0, Kd=0,Kp 从 0.5 开始逐步增大
2. 人为注入扰动(开门 30s),观察系统响应
3. 找到临界增益 Ku(出现持续等幅振荡时的 Kp)
4. 记录临界周期 Tu(振荡周期)
5. 按以下参数设定:
Kp = 0.45 × Ku
Ki = 0.54 × Ku / Tu
Kd = 0.075 × Ku × Tu档案场景经验参数范围(供参考,需按实际微调):
库容 | Kp | Ki (min⁻¹) | Kd (min) | 采样周期 |
|---|---|---|---|---|
<50㎡ | 1.5~3.0 | 0.3~0.8 | 0.5~2.0 | 30s |
50~150㎡ | 2.0~5.0 | 0.2~0.5 | 1.0~3.0 | 60s |
>150㎡ | 3.0~8.0 | 0.1~0.3 | 2.0~5.0 | 60~120s |
"""
humidity_pid.py - 湿度 PID 回路参考实现
运行在边缘 DDC/网关上,与一体机通信
"""
import time
from dataclasses import dataclass
@dataclass
class PIDParams:
Kp: float = 3.0
Ki: float = 0.4
Kd: float = 1.5
Ts: float = 60.0 # 采样周期(s)
out_min: float = 0.0 # 输出下限(%)
out_max: float = 100.0 # 输出上限(%)
anti_windup: bool = True
class HumidityPID:
def __init__(self, params: PIDParams):
self.p = params
self.integral = 0.0
self.prev_error = 0.0
self.last_output = 0.0
self.last_time = time.time()
def compute(self, setpoint: float, process_var: float) -> float:
now = time.time()
dt = now - self.last_time
if dt < self.p.Ts * 0.5: # 防抖:距上次计算不足半个周期则跳过
return self.last_output
error = setpoint - process_var
# P
P_term = self.p.Kp * error
# I(带抗积分饱和)
self.integral += error * dt / 60.0 # 积分项单位:%·min
if self.p.anti_windup:
# 回算抗饱和
I_term = self.p.Ki * self.integral
output_unsaturated = P_term + I_term + self.p.Kd * (error - self.prev_error) / (dt / 60.0)
if output_unsaturated > self.p.out_max:
self.integral = (self.p.out_max - P_term) / self.p.Ki if self.p.Ki != 0 else 0
elif output_unsaturated < self.p.out_min:
self.integral = (self.p.out_min - P_term) / self.p.Ki if self.p.Ki != 0 else 0
I_term = self.p.Ki * self.integral
# D(对 PV 微分,避免设定值阶跃时微分冲击)
D_term = self.p.Kd * (-(process_var - (process_var - self.prev_error))) / (dt / 60.0)
# 等价于 -Kd * d(PV)/dt
derivative = -(process_var - (process_var - (error - self.prev_error))) / (dt / 60.0)
D_term = self.p.Kd * derivative
output = P_term + I_term + D_term
output = max(self.p.out_min, min(self.p.out_max, output))
self.prev_error = error
self.last_output = output
self.last_time = now
return output
def reset(self):
"""模式切换/故障恢复时重置"""
self.integral = 0.0
self.prev_error = 0.0
self.last_output = 0.0档案库房温度控制通常比湿度宽松(±2℃可接受),有两种策略:
推荐:中小型项目用独立 PID + 优先级仲裁——
优先级:
1. 湿度越限 → 湿度回路全权控制(压缩机/加湿器)
2. 湿度在带内 → 温度回路微调(电加热/风机转速)
3. 两者冲突时,湿度优先(档案保护核心)模式 | 触发条件 | 控制策略 |
|---|---|---|
正常模式 | 温湿度均在设定带内 | PID 闭环,维持稳态 |
除湿优先 | RH > 设定值+2% | 压缩机持续运行,电加热按需再热防过冷 |
加湿优先 | RH < 设定值-2% | 加湿器运行,压缩机停 |
快速降温 | T > 设定值+3℃ | 压缩机+风机高速,湿度可能暂时波动 |
维护模式 | 闭馆时段 | 净化+杀菌全开,温湿度放宽 |
安全模式 | 故障/通信丢失 | 本地 DDC 按最后有效参数运行,平台只读 |
"""
mode_manager.py - 运行模式管理
"""
class ModeManager:
def __init__(self):
self.current_mode = "normal"
self.mode_hold_time = {} # 模式保持计时
def evaluate(self, ret_t, ret_h, sp_t, sp_h, fault_active, schedule):
"""根据当前状态评估应进入的模式"""
# 故障优先
if fault_active:
return "safe"
# 时间调度
if schedule.get("is_off_hours"):
return "maintenance"
# 湿度偏差判定
hum_dev = ret_h - sp_h
temp_dev = ret_t - sp_t
if hum_dev > 2.0:
return "dehumidify_priority"
elif hum_dev < -2.0:
return "humidify_priority"
elif temp_dev > 3.0:
return "cool_fast"
else:
return "normal"
def get_pid_params(self, mode):
"""不同模式下使用不同的 PID 参数"""
params_map = {
"normal": {"hum": PIDParams(Kp=3.0, Ki=0.4, Kd=1.5)},
"dehumidify_priority":{"hum": PIDParams(Kp=4.0, Ki=0.3, Kd=2.0)}, # 更激进
"humidify_priority": {"hum": PIDParams(Kp=2.5, Ki=0.5, Kd=1.0)}, # 稍温和
"cool_fast": {"hum": PIDParams(Kp=2.0, Ki=0.2, Kd=0.5)}, # 弱湿度控制
"maintenance": {"hum": PIDParams(Kp=2.0, Ki=0.3, Kd=1.0)},
"safe": {"hum": PIDParams(Kp=1.0, Ki=0.1, Kd=0.5)}, # 保守
}
return params_map.get(mode, params_map["normal"])参考前几篇建立的测试框架,针对一体机做四级测试:
条件:无人为扰动,门窗关闭,无人进出
持续:24h
判定:
- 温度波动 ≤ ±1.0℃(设定值 20℃)
- 湿度波动 ≤ ±3.0%RH(设定值 50%RH)
- 数据完整率 ≥ 99.5%扰动 | 注入方式 | 恢复目标 | 最大超调 | 恢复时间 |
|---|---|---|---|---|
开门扰动 | 库门全开 60s | RH 回 50±3% | <5%RH | <15min |
人员扰动 | 2人库内走动 5min | T 回 20±1℃ | <0.5℃ | <10min |
加湿阶跃 | 手动+10%设定 | 稳态新值 | <3%RH | <20min |
断电恢复 | 切断一体机电源 5min | 自启后回稳 | 允许短暂越限 | <30min |
场景:开门(湿度冲击)+ 同时注入加湿阶跃(控制对抗)
目的:验证 PID 在对抗性输入下的稳定性,不发散、不持续振荡
判定:30min 内回到设定带,无持续振荡(振幅递减)持续:7 天连续运行
条件:模拟日常使用(每天定时开门 2 次、人员进出)
监测:
- 稳态精度趋势(逐日对比)
- 执行器动作频次(压缩机启停次数/天)
- 过滤器压差趋势(是否异常上升)
- 能耗(可选)
判定:7 天内稳态指标不劣化,执行器无异常频繁动作"""
stability_test.py - 稳定性测试数据记录与分析
"""
import statistics
from collections import defaultdict
class StabilityAnalyzer:
def __init__(self, setpoint_t=20.0, setpoint_h=50.0):
self.sp_t = setpoint_t
self.sp_h = setpoint_h
self.samples = []
def add_sample(self, ts, temp, hum, mode, actuator_state):
self.samples.append({
"ts": ts, "temp": temp, "hum": hum,
"mode": mode, "actuator": actuator_state
})
def analyze(self):
if len(self.samples) < 10:
return {"error": "样本不足"}
temps = [s["temp"] for s in self.samples]
hums = [s["hum"] for s in self.samples]
results = {
"temp": {
"mean": statistics.mean(temps),
"stdev": statistics.stdev(temps) if len(temps)>1 else 0,
"min": min(temps), "max": max(temps),
"range": max(temps) - min(temps),
"within_band": sum(1 for t in temps if abs(t-self.sp_t)<=1.0) / len(temps)
},
"hum": {
"mean": statistics.mean(hums),
"stdev": statistics.stdev(hums) if len(hums)>1 else 0,
"min": min(hums), "max": max(hums),
"range": max(hums) - min(hums),
"within_band": sum(1 for h in hums if abs(h-self.sp_h)<=3.0) / len(hums)
},
"actuator_stats": self._actuator_stats()
}
return results
def _actuator_stats(self):
"""统计执行器动作频次"""
stats = defaultdict(lambda: {"on_count": 0, "total_time": 0, "last_state": None, "last_ts": None})
for s in self.samples:
for act, state in s["actuator"].items():
if state and not stats[act]["last_state"]:
stats[act]["on_count"] += 1
if state:
stats[act]["total_time"] += 1 # 采样间隔的倍数
stats[act]["last_state"] = state
return dict(stats)现象:湿度在设定值上下反复波动,振幅不衰减,周期 20~40min。
原因排查:
现象:长期运行后湿度稳定在 47%,设定值 50%,差 3%。
原因排查:
现象:正常模式↔除湿优先反复切换,执行器频繁动作。
解决:
现象:设备刚启动,库温从 25℃ 降到 20℃,湿度先飙升到 65% 再慢慢回落。
原因:降温过程中相对湿度随温度下降而升高(绝对含湿量不变时,RH ∝ 1/T)。
对策:
测试项 | 标准 | 测试方法 |
|---|---|---|
稳态精度(24h) | T ±1℃,RH ±3% | 连续记录分析 |
开门恢复 | <15min 回到设定带 | 人为开门 60s |
断电自启 | 5min 内恢复运行 | 模拟断电 |
通信中断 | 本地 DDC 维持运行 | 断开网线 |
过滤器报警 | 压差超限触发告警 | 遮挡过滤器 |
联动验证 | 平台下发设定值,设备跟随 | 手动改设定 |
数据完整率 | ≥99.5%(7天) | 统计缺失点 |
净化恒湿一体机的调试,核心在于先摸清设备动态特性,再整定控制参数,最后用系统化测试验证稳定性。工程上容易犯的错是跳过开环直接上 PID,结果参数调不出来还以为是设备不行。按本文的四步走——点动核对→开环阶跃→闭环整定→分级测试——每一步都有数据支撑,调试过程可复现、可回溯。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。