简介:围绕PID算法在温室环境调控中的应用,这份技术文献以单篇PDF形式呈现,面向自动化、智慧农业与嵌入式控制方向的学习者及研究人员,回应传统温湿度控制系统精度偏低、稳定性不足的问题。包内仅1个PDF文件,约1.44MB,为完整论文版式,依次展开摘要、硬件设计、软件设计、实验对比与参考文献等模块。硬件部分给出SLZ2016-558微处理器、TL-W温度传感器、TL-N湿度传感器及电源电路的选型与性能指标;软件部分说明如何以温湿度为目标函数,通过参数编码、解码与误差调节完成PID算法落地;实验环节以葡萄种植温室8小时采集数据,对比传统系统,验证控制精度可达80%以上、最优约90%。目前已有331人学习。读者可据此借鉴硬件架构、算法实现步骤与实验验证思路,适合毕业设计、课程论文及智慧农业控制系统方案参考,兼具专业指导与参考文献价值。
1. 温室温湿度控制为什么要用 PID 算法做闭环
凌晨三点,棚内温度从 18℃ 掉到 13.5℃,湿度因为叶面蒸腾爬到 93%,值班的人只能看一眼手机再手动开加热和风机。用开关量做温湿度控制,加热器要么全开要么全关,温度在设定值上下 2~3℃ 来回振荡,湿度更糟,除湿一停就反弹,这就是典型的继电特性震荡。问题不在执行器,在控制律。PID 算法做的事情很朴素:把当前温湿度偏离目标多少、偏离持续了多久、偏离得有多快这三件事加权求和,换算成一个连续输出量,送给加热片、加湿器、风机和湿帘。一套能长期无人值守跑起来的温湿度智能控制系统,核心就是这条闭环。下面针对的读者是已经能读传感器、能驱动继电器,但回路一上电就震荡、或者半天拉不回设定值的嵌入式与物联网工程师,STMicroelectronics 与 Espressif 两条常见硬件路线的参数和代码都会给到。
2. 温湿度被控对象建模与 PID 算法选型
2.1 温室温湿度对象的动态特性近似
调参之前先搞清楚被控对象长什么样。温室温度回路的阶跃响应可以近似成一阶惯性加纯滞后:
G(s) = K / (T·s + 1) · e^(-τ·s)K 是静态增益,单位是「℃/输出百分比」,实测方法是让加热输出从 30% 阶跃到 70%,等温度稳定后看温升除以输出变化量。T 是时间常数,取温度走完阶跃总变化量 63% 所需的时间。τ 是纯滞后,从输出变化的时刻算起,到温度传感器第一次出现可辨识变化为止,温室里通常在 60~300 秒量级,取决于传感器位置和空气循环。
湿度回路更麻烦。相对湿度不是独立变量,它随温度反向变化:温度每升高 1℃,在含湿量不变的前提下相对湿度大约下降 4%~6%。这意味着加热回路一动,湿度回路立刻被扰动,两个回路天然耦合。空气循环差的小棚里,这个耦合强度足以让两个独立 PID 互相打架——加热一开,湿度跌到下限,加湿一开,温度又掉下来。
我一般的处理是:先按两个独立回路整定,把加热/加湿的触发在时间上错开至少一个采样周期,观察是否出现交替震荡;如果出现,再引入耦合补偿项,把温度偏差的符号和幅值折算成一个湿度设定值修正量。多数中小型温室做到这一步就够用了,不必上完整的解耦控制。
2.2 位置式 PID 与增量式 PID 算法在嵌入式上的取舍
位置式公式:
u(k) = Kp·e(k) + Ki·Σe(j)·dt + Kd·(e(k) - e(k-1))/dt增量式公式:
Δu = Kp·(e(k) - e(k-1)) + Ki·e(k)·dt + Kd·(e(k) - 2e(k-1) + e(k-2))/dt u(k) = u(k-1) + Δu两者数学上等价,工程上差别很大。位置式每一步都要对偏差做累加,累加项一旦因为输出限幅而饱和,退饱和时会带出一大段超调;增量式只累加输出量本身,限幅时把输出回写即可,抗饱和天然容易。另外增量式在手动转自动时,只要保持 u(k-1) 连续,就不会有阀门或继电器被「跳一下」的问题,这一点在温室这种需要人工干预的场景里很关键。
我通常的做法是:温度回路用增量式,湿度回路也用增量式,只有在需要直接观察绝对输出量(比如调试阶段记录占空比曲线)时才临时切到位置式。
/* 增量式 PID:输出为累积占空比,范围 0 ~ 1000 */ typedef struct { float Kp, Ki, Kd; float e1, e2; /* 上两次偏差 */ float out; /* 累积输出,必须长期保存 */ float out_min, out_max; /* 输出限幅,对应执行器有效区间 */ } PID_Inc; float pid_inc_calc(PID_Inc *p, float set, float fb, float dt) { float e = set - fb; /* 增量式三项:比例看待偏差变化,积分看当前偏差,微分看偏差加速度 */ float du = p->Kp * (e - p->e1) + p->Ki * e * dt + p->Kd * (e - 2.0f * p->e1 + p->e2) / dt; p->e2 = p->e1; p->e1 = e; p->out += du; /* 限幅时把累积量回写,否则积分项会继续累积造成退饱和滞后 */ if (p->out > p->out_max) p->out = p->out_max; if (p->out < p->out_min) p->out = p->out_min; return p->out; }逻辑上,du 是这一拍输出的增量,p->out 是历史累积值,因此任何一次限幅都必须写回结构体,否则下一次计算会拿着一个「虚高」的基准值继续累加。参数上,Kp 决定响应速度,Ki 消除稳态误差,Kd 抑制超调;dt 是采样周期,单位秒,必须和定时器实际节拍一致,写成常数最容易埋雷——一旦后续把节拍从 1 秒改成 2 秒,Ki 的实际作用会翻倍。
2.3 采样周期、死区与积分限幅的工程取值
采样周期不是越快越好。温室热惯性大,温度采样做到 1 秒已经足够,更小的周期只会让微分项被传感器噪声放大。湿度响应稍快,1~2 秒合适。下面是常用来做起步的取值:
| 参数 | 温度回路 | 湿度回路 | 说明 |
|---|---|---|---|
| 采样周期 dt | 1 s | 2 s | 与定时器中断节拍严格一致 |
| 控制死区 | ±0.3 ℃ | ±2 %RH | 死区内保持上一输出,抑制执行器频繁动作 |
| 输出范围 | 0~1000 | 0~1000 | 映射到 PWM 或最小开/关时间 |
| 积分限幅 | 输出的 ±40% | 输出的 ±40% | 防止长时间偏差导致积分项撑满 |
| 微分先行 | 开启 | 开启 | 设定值阶跃时不产生微分冲击 |
死区的处理方式要写进代码,不能在比较器里随手一个 if 了事,因为死区边界附近的抖动会让继电器一分钟吸合十几次,机械寿命和温控品质一起完蛋。
/* 死区处理:偏差落在死区内时冻结积分增量,只保留比例与微分 */ float e = set - fb; if (fabsf(e) < DEADBAND) { e = 0.0f; /* 比例项归零,输出保持不变 */ p->Ki = 0.0f; /* 临时屏蔽积分,避免死区内缓慢爬升 */ } else { p->Ki = p->Ki_nominal; /* 出死区立刻恢复 */ }这段代码的关键点在于「冻结」而不是「清零输出」。如果死区内直接把输出置零,加热器会周期性停摆,温度反而在设定值下方形成一个低频波动,比死区本身的代价更大。
3. 基于 STM32 与 ESP32 的温湿度终端硬件与驱动
3.1 传感器与执行器器件选型对比
传感器选型直接决定控制回路的上限。DHT22 便宜、单总线、免外围,但响应时间约 2 秒、长期漂移大,放在湿度长期 90% 以上的棚里,半年后读数能偏 5 %RH。SHT30/SHT31 是 I2C 数字输出,出厂校准,湿度精度 ±2 %RH,价格高一些但重复性好,适合做闭环反馈。DS18B20 只测温,防水探头版本适合埋在基质或水肥管路里做第二路温度参考。
| 器件 | 接口 | 温度精度 | 湿度精度 | 响应时间 | 适用位置 |
|---|---|---|---|---|---|
| DHT22 | 单总线 | ±0.5 ℃ | ±2 %RH | 约 2 s | 成本敏感的展示型终端 |
| SHT30 | I2C | ±0.3 ℃ | ±2 %RH | 约 8 s | 闭环反馈主传感器 |
| SHT31 | I2C | ±0.2 ℃ | ±2 %RH | 约 8 s | 对精度要求更高的场景 |
| DS18B20 | 单总线 | ±0.5 ℃ | 无 | 约 0.75 s | 基质、水温等辅助测点 |
MCU 侧,STM32 系列外设规整、定时器资源多,适合同时跑多路 PWM 和 ADC;ESP32 自带 Wi-Fi,适合需要把温湿度数据推到上位机或手机端的场景,代价是 Wi-Fi 射频工作瞬间的电流尖峰可能干扰模拟采样,布局时要把传感器走线和天线拉开。全球行业数据也提示,带联网能力的温湿度监测终端在设施农业里的部署比例逐年上升,但联网只是链路,控制品质仍取决于回路本身。
3.2 用 CubeMX 配置 I2C 与定时器采样节拍
在 STM32CubeMX 里要配的主要是四件事:I2C1 用于 SHT30,TIM6 做 1 秒基准节拍,TIM1 的 PWM 通道驱动加热与加湿,USART 用于调试输出。
| 配置项 | 取值 | 理由 |
|---|---|---|
| I2C1 速率 | 100 kHz | SHT30 标准模式,长排线时更抗干扰 |
| I2C1 上拉 | 4.7 kΩ 外置 | 内部上拉偏弱,长线通信易失败 |
| TIM6 预分频 | 7199 | 72 MHz 主频下得到 10 kHz 计数 |
| TIM6 周期 | 9999 | 合计 1 Hz 中断 |
| TIM1 PWM 频率 | 1 kHz | 高于加热丝热惯性,低于继电器可听噪声 |
| 时钟源 | 外部晶振 | 内部 RC 温漂会影响采样节拍 |
采样和控制建议不在中断里做,中断只置标志位,浮点运算放在主循环,避免中断执行时间过长影响其他外设。
/* TIM6 中断只置标志,控制运算留在主循环 */ volatile uint8_t g_ctrl_tick = 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM6) { g_ctrl_tick = 1; } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); MX_TIM1_Init(); MX_TIM6_Init(); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); /* 加热 */ HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2); /* 加湿 */ HAL_TIM_Base_Start_IT(&tim6); /* 1 Hz 节拍 */ PID_Inc temp_loop = { .Kp = 40.0f, .Ki = 0.6f, .Kd = 0.0f, .out_min = 0.0f, .out_max = 1000.0f }; PID_Inc humi_loop = { .Kp = 25.0f, .Ki = 0.4f, .Kd = 0.0f, .out_min = 0.0f, .out_max = 1000.0f }; while (1) { if (g_ctrl_tick) { g_ctrl_tick = 0; float t, h; if (sht30_read(&t, &h) == 0) { actuator_apply(pid_inc_calc(&temp_loop, 26.0f, t, 1.0f), pid_inc_calc(&humi_loop, 65.0f, h, 1.0f)); } else { /* 读取失败按最保守策略处理:全部执行器关闭并报警 */ actuator_safe_off(); } } } }逻辑上,TIM6 每满 1 秒产生一次中断,主循环检测到标志后执行一次「采样—计算—输出」。参数说明:pid_inc_calc 的第四个参数 dt 必须写成 1.0f,与 TIM6 的 1 Hz 严格对应;两个回路的设定值分别是 26.0℃ 和 65 %RH,实际部署时从 Flash 读取,便于现场修改而不用重新烧录。
SHT30 的读取用单次测量模式最省事:
#define SHT30_ADDR (0x44 << 1) /* 7 位地址左移一位,HAL 要求 */ int sht30_read(float *temp, float *humi) { uint8_t cmd[2] = {0x2C, 0x06}; /* 单次测量,高重复性,禁用时钟拉伸 */ uint8_t rx[6]; if (HAL_I2C_Master_Transmit(&hi2c1, SHT30_ADDR, cmd, 2, 100) != HAL_OK) return -1; HAL_Delay(20); /* 等待转换完成,高重复性约 15 ms */ if (HAL_I2C_Master_Receive(&hi2c1, SHT30_ADDR, rx, 6, 100) != HAL_OK) return -1; uint16_t rawT = ((uint16_t)rx[0] << 8) | rx[1]; uint16_t rawH = ((uint16_t)rx[3] << 8) | rx[4]; *temp = -45.0f + 175.0f * rawT / 65535.0f; *humi = 100.0f * rawH / 65535.0f; return 0; }两个字节高位在前,后一个字节是 CRC8 校验,产品代码里必须校验,工棚电磁环境差,I2C 偶发位翻转会导致湿度读数直接跳到 100 %RH,进而把加湿器打开,后果比读不到数据严重得多。
3.3 执行机构驱动与安全互锁
温室里同时存在加热和制冷(湿帘、风机)、加湿和除湿,物理上互斥。互锁必须在软件和硬件两层做:软件层保证同一时刻只有一组输出非零,硬件层用继电器的常闭触点串联另一个回路的供电。
安全策略表:
| 异常条件 | 检测方式 | 动作 |
|---|---|---|
| 温度超过上限 5℃ | 软件比较 + 独立温控开关 | 切断加热,全开风机 |
| 传感器读数越界 | 取值范围校验 | 关闭全部执行器,上报故障 |
| 通信超时 | I2C 超时返回 | 保持最后输出不超过 60 秒,之后关断 |
| 加湿与除湿同时请求 | 逻辑互斥 | 按优先级只执行加湿,延迟除湿 |
| 断电恢复 | 上电初始化 | 输出从 0 起步,禁止直接跳到历史值 |
最后一条容易被忽略。增量式 PID 的累积输出如果存在 Flash 里,上电直接恢复上一次的占空比,一旦上次是因为故障关机,重启后就会立刻重复同样的错误动作。稳妥做法是上电后强制从 0 开始,用 2~3 个采样周期缓慢爬升到计算值。
4. 控制回路落地:从双回路到整定与抗饱和
4.1 温湿度双通道独立回路与耦合补偿
先按两个独立回路跑,可以解决大部分场景。为了削弱耦合,可以在输出侧做互斥约束:当温度回路的输出大于 200(占空比 20%)时,湿度回路的加湿输出按系数衰减;反之当加湿输出较大时,温度回路的加热输出按系数衰减。这个系数不建议超过 0.3,否则等于人为放慢了主回路。
另一种做法是切到串级结构:外环是湿度,输出作为内环温度的设定值修正。这在食用菌菇房这种湿度优先级高于温度的场所更合适,实现上只需把外环输出限幅到 ±2℃ 之后叠加到温度设定值上。
4.2 PID 参数整定:临界比例度法与工程试凑
临界比例度法的步骤是:先把 Ki 和 Kd 置零,逐步增大 Kp 直到系统出现等幅振荡,记录此时的 Kp 为临界增益 Ku,振荡周期为 Tu,然后按经验关系取值。温室回路滞后大,这套方法实操中振荡容易持续很久,我更常用的是「先比例、后积分、最后微分」的试凑法,每一步观察 5~10 分钟。
| 整定阶段 | 温度回路 | 湿度回路 | 观察指标 |
|---|---|---|---|
| 只用 Kp | 从 20 逐步加到 60 | 从 15 逐步加到 40 | 升到设定值的时间、有无持续振荡 |
| 加入 Ki | 0.2 起,每次加 0.2 | 0.15 起,每次加 0.15 | 稳态误差是否收敛,有无低频摆动 |
| 加入 Kd | 5~15,仅在升降温快时加 | 一般不使用 | 超调量是否下降,输出是否抖动 |
| 冻结死区 | ±0.3℃ | ±2 %RH | 执行器动作频率是否降到可接受 |
一个常见的误用是把 Kp 调得过大来「加快响应」。温度回路的输出一旦长期顶在 100%,加热器的余热会让温度冲过设定值好几度,接着冷却过程又要等很久,最终形成 10 分钟量级的极限环,比不控还差。判断方法很直接:看输出曲线的形状,如果长期贴着上限走,就是 Kp 或前馈不够,而不是 Kp 需要再大。
4.3 输出限幅、抗积分饱和与手动自动无扰切换
执行器有物理下限。加湿器开度低于 10% 时基本喷不出雾,继电器型加热在 5% 占空比下会频繁吸合。所以输出映射要做成「死区 + 线性段」:
/* 把 0~1000 的 PID 输出映射到执行器可用区间 */ uint16_t map_output(float u, uint16_t min_on, uint16_t max_on) { if (u <= 0.0f) return 0; /* 完全关闭 */ if (u < 50.0f) return 0; /* 低区死区,避免无效动作 */ /* 50~1000 线性映射到 min_on~max_on */ float ratio = (u - 50.0f) / 950.0f; return (uint16_t)(min_on + ratio * (max_on - min_on)); } /* 手动/自动无扰切换:切换瞬间把 PID 累积输出同步为手动值 */ void pid_bumpless(PID_Inc *p, float manual_out) { p->out = manual_out; /* 累积量对齐,下一拍不会跳变 */ p->e1 = 0.0f; /* 清历史偏差,避免微分冲击 */ p->e2 = 0.0f; }逻辑说明:map_output 把连续输出转成执行器真正能用的区间,50 这个阈值是按加湿器最小雾化量实测定的,每个项目要重新标。pid_bumpless 解决的是从手动切回自动那一瞬间的输出跳变,不这样做的话,PID 会拿着旧的累积值加上一个新算的增量,输出可能直接从 30% 跳到 80%,湿帘阀门会发出明显的撞击声。
5. 阶跃响应验证与长期运行排错技巧
5.1 用阶跃响应曲线量化超调与稳态误差
调完之后一定要做一次完整验证:让系统从室温稳定运行,然后手动把设定值从 24℃ 阶跃到 26℃,用串口按秒打印设定值、实测值、PID 输出三列,画成曲线看四个指标——上升时间(到达设定值 90% 的耗时)、超调量(峰值超出设定值的百分比)、调节时间(进入 ±0.5℃ 且不再出的时间)、稳态误差。温室温度回路,超调控制在 1℃ 以内、调节时间 15~25 分钟属于正常;如果超调超过 3℃,先查微分项和执行器余热,再考虑降 Kp。
# 采集串口数据并落盘,1 秒一行,便于离线画图 python -c " import serial, csv, time s = serial.Serial('COM3', 115200, timeout=1) with open('step_response.csv', 'w', newline='') as f: w = csv.writer(f) w.writerow(['t', 'set', 'temp', 'humi', 'out']) t0 = time.time() while time.time() - t0 < 1800: # 采 30 分钟 line = s.readline().decode().strip() if line: w.writerow([round(time.time() - t0, 1)] + line.split(',')) "参数说明:波特率 115200 要和固件一致,采集 1800 秒覆盖了完整阶跃过程,行首加时间戳是为了让不同步的采样点还能对齐。
5.2 温室长期运行的高频故障排查清单
| 现象 | 首要怀疑 | 验证方法 | 处理 |
|---|---|---|---|
| 湿度读数长期偏高 | 传感器结露 | 与手持表对比 | 加装透气防尘罩,定期烘干校准 |
| 温度控制周期性振荡 | Kp 过大或死区过小 | 看输出是否顶限幅 | 降 Kp,放大死区到 ±0.5℃ |
| 加湿器频繁启停 | 死区边界抖动 | 统计 10 分钟动作次数 | 增加最小开启时间 30 秒 |
| 白天正常夜间失控 | 夜间通风差、耦合增强 | 分时段记录数据 | 夜间启用耦合补偿系数 |
| I2C 偶发读取失败 | 排线过长、上拉不足 | 示波器看波形边沿 | 改 4.7 kΩ 上拉,缩短走线 |
| 上电后输出直接拉满 | 断电恢复未清零 | 查初始化代码 | 强制从 0 起步缓慢爬升 |
最后补一个实战细节:把 PID 参数、死区、设定值都放到 Flash 或 NVS 里,现场调参时通过串口指令改写,不要每次改参数都重新编译烧录。同时给参数加一个 CRC 和默认值回退,一旦存储区校验失败就加载一组保守参数,宁可控得慢一点,也不要让温室在深夜跑在一组被写坏的数字上。
本文还有配套的精品资源,点击获取