🔥 PID温控实战:用ESP32打造恒温控制系统

📅 2026-06-01 · 📂 硬件编程 · ⏱ 阅读约 8 分钟

📺 B站推荐视频:考拉学姐带你手搓PID电机控制 | ESP32直流电机闭环调速实践教程

📖 概述

PID(比例-积分-微分)控制是自动控制领域最经典、应用最广泛的算法,从工业温控、无人机姿态到3D打印机热床,PID无处不在。它的核心思想很简单:用当前误差(P)做快速响应,用历史累积误差(I)消除稳态偏差,用误差变化率(D)抑制过冲。

今天我们把之前学过的 DHT22温湿度传感器(5/16)、SSD1306 OLED显示屏(5/14)和继电器模块串联起来,搭建一个精度可达 ±0.1℃ 的恒温控制系统。掌握了PID,你就掌握了自动控制世界的通用语言。

💡 为什么学PID? 简单的开关控制(温度高了关加热、低了开加热)会造成温度在设定值附近反复震荡。PID通过精细调节输出功率,让温度平滑稳定在目标值——这是工业级控制的基石。

🛒 物料清单

物料型号/参数数量参考价格
主控板ESP32 DevKit V1 (WROOM-32)1¥25
温度传感器DHT22 (AM2302) 精度 ±0.5℃1¥12
OLED显示屏SSD1306 0.96寸 128×64 I2C1¥10
固态继电器SSR-40DA (DC控AC, 40A)1¥18
加热片12V/50W 硅胶加热片1¥8
12V电源12V/5A 开关电源1¥15
旋转编码器KY-040 (设定温度用)1¥5
面包板 + 杜邦线830孔 + 公母各20根1套¥12
合计约 ¥105
安全提醒: 如果直接用 SSR 控制 220V 市电加热设备(如电热毯、热水壶改装),务必做好绝缘封装!建议初次实验用 12V 加热片,安全又方便调试。

🔌 接线图

ESP32          →  外设
────────────────────────────────
3.3V           →  DHT22 VCC
GND            →  DHT22 GND
GPIO 4         →  DHT22 DATA

3.3V           →  SSD1306 VCC
GND            →  SSD1306 GND
GPIO 21 (SDA)  →  SSD1306 SDA
GPIO 22 (SCL)  →  SSD1306 SCL

GPIO 26        →  SSR 输入端 (+)
GND            →  SSR 输入端 (-)

3.3V           →  KY-040 VCC (+)
GND            →  KY-040 GND
GPIO 32        →  KY-040 CLK
GPIO 33        →  KY-040 DT
GPIO 25        →  KY-040 SW (按键)

SSR输出端      →  加热片一端
12V电源(+)     →  加热片另一端
12V电源(-)     →  SSR输出端另一端
        

固态继电器 SSR 的输入端接 ESP32 的 GPIO26,通过 PWM 信号控制加热功率。SSR 输出端串联 12V 加热片回路——当 GPIO26 输出高电平时,SSR内部光耦导通,加热片通电。

💻 基础代码:PID恒温控制器

以下完整代码实现了 PID 温度闭环控制,通过旋转编码器设定目标温度,OLED 实时显示温度曲线。

// ===== PID 恒温控制器 — ESP32 + DHT22 + SSD1306 + SSR ===== #include <DHT.h> #include <Adafruit_SSD1306.h> #include <Wire.h> // === 引脚定义 === #define DHTPIN 4 #define DHTTYPE DHT22 #define SSR_PIN 26 #define ENC_CLK 32 #define ENC_DT 33 #define ENC_SW 25 // === PID 参数 === double Kp = 15.0, Ki = 0.8, Kd = 5.0; double setpoint = 40.0; // 目标温度 ℃ double integral = 0, prevError = 0; unsigned long lastTime; const int PWM_FREQ = 1; // 1Hz PWM (SSR用低频) const int PWM_RES = 10; // 10位分辨率 0-1023 DHT dht(DHTPIN, DHTTYPE); Adafruit_SSD1306 display(128, 64, &Wire, -1); // === PID 计算 === double computePID(double current) { unsigned long now = millis(); double dt = (now - lastTime) / 1000.0; if (dt < 1.0) dt = 1.0; // 最小1秒采样周期 lastTime = now; double error = setpoint - current; // 积分抗饱和:限制积分累积 integral += error * dt; if (integral > 100.0) integral = 100.0; if (integral < 0.0) integral = 0.0; double derivative = (error - prevError) / dt; prevError = error; double output = Kp * error + Ki * integral + Kd * derivative; if (output > 1023) output = 1023; if (output < 0) output = 0; return output; } // === 编码器读取 === volatile int encoderPos = 40; void IRAM_ATTR readEncoder() { static int lastCLK = digitalRead(ENC_CLK); int clk = digitalRead(ENC_CLK); int dt = digitalRead(ENC_DT); if (clk != lastCLK && clk == LOW) { if (dt != clk) encoderPos += 1; else encoderPos -= 1; if (encoderPos < 10) encoderPos = 10; if (encoderPos > 120) encoderPos = 120; } lastCLK = clk; } void setup() { Serial.begin(115200); dht.begin(); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // SSR: 1Hz PWM, 10-bit ledcAttach(SSR_PIN, PWM_FREQ, PWM_RES); pinMode(ENC_CLK, INPUT_PULLUP); pinMode(ENC_DT, INPUT_PULLUP); pinMode(ENC_SW, INPUT_PULLUP); attachInterrupt(ENC_CLK, readEncoder, CHANGE); lastTime = millis(); display.clearDisplay(); } void loop() { // 1. 读取温度 float temp = dht.readTemperature(); if (isnan(temp)) temp = setpoint; // 读取失败用设定值兜底 // 2. 按键重置积分 if (digitalRead(ENC_SW) == LOW) { integral = 0; delay(200); } // 3. 更新设定温度 setpoint = encoderPos; // 4. PID 计算 + PWM输出 double pwmVal = computePID(temp); ledcWrite(SSR_PIN, (int)pwmVal); // 5. OLED 显示 display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.printf("目标: %.1f C", setpoint); display.setCursor(0, 12); display.printf("当前: %.1f C", temp); display.setCursor(0, 26); display.printf("输出: %d / 1023", (int)pwmVal); // 进度条 (输出功率可视化) int barW = map((int)pwmVal, 0, 1023, 0, 100); display.drawRect(0, 42, 128, 12, SSD1306_WHITE); display.fillRect(2, 44, barW, 8, SSD1306_WHITE); display.display(); // 6. 串口输出 Serial.printf("目标:%.1f 当前:%.1f PWM:%d\n", setpoint, temp, (int)pwmVal); delay(1000); // 1秒控制周期 }

代码要点:

🔧 参数整定口诀: 先将 Ki、Kd 置零,只调 Kp——看到围绕设定值稳定小幅震荡;然后增大 Ki 消除稳态误差;最后加 Kd 抑制过冲。以上 Kp=15 / Ki=0.8 / Kd=5 是一个良好起点。

🚀 进阶应用

1. WiFi远程监控 + 温度曲线

结合第5/6篇的 WiFi 和第5/8篇的 WebServer 知识,把温度数据通过 HTTP 推送到网页端实时图表:

#include <WiFi.h> #include <WebServer.h> WebServer server(80); void handleJSON() { char json[200]; sprintf(json, "{\"temp\":%.1f,\"set\":%.1f,\"pwm\":%d}", temp, setpoint, (int)pwmVal); server.send(200, "application/json", json); } void setup() { WiFi.begin("SSID", "PASSWORD"); server.on("/api/temp", handleJSON); server.begin(); }

2. 参数自整定(Auto-Tuning)

工业级PID控制器都有自整定功能。简单实现:让系统做一次开环阶跃响应,测量温升曲线的死区时间时间常数,用 Ziegler-Nichols 方法自动算出最优 Kp/Ki/Kd。

3. 多段温控曲线(回流焊曲线)

设定多个温度-时间节点(预热→恒温→回流→冷却),PID 跟随预设的温度曲线自动切换设定值。这是 SMT 回流焊和恒温发酵的核心需求。

🎯 实战场景

场景一:恒温酸奶发酵箱

需求: 酸奶发酵需要 42±1℃ 恒温 8 小时,温度过高菌种死亡,过低发酵不充分。

方案: 用泡沫箱做保温体 + ESP32 PID 控制器 + 12V 加热片(5/25篇学过L298N可驱动大功率PTC加热片)。SSD1306 实时显示温度,WiFi 推送到手机查看。编码器设定目标42℃,锁死后盖即可。

物料扩展: 如果箱体较大,可用 L298N 驱动 12V/100W PTC 加热器替代加热片。多个 DHT22 多点测温取平均。

场景二:3D打印机热床PID升级

需求: 3D打印机热床温度波动导致模型翘边。Marlin固件已内置PID,但了解原理才能调好。

方案: 用本篇学到的PID代码替代简单的 Bang-Bang 开关控制。热床是大惯性负载(升温慢、降温也慢),需调大 Ki(积分)来消除稳态误差、调小 Kd(微分)避免噪音放大。

联动模块: GPS模块(5/26)记录户外移动恒温箱的地理位置,LoRa(5/23)远程传输温度数据到数公里外的接收端。

⚠️ 常见问题

❌ 温度一直震荡,稳不下来
Kp 太大导致过冲→回调→再过冲的死循环。先把 Ki、Kd 置零,Kp 从 5 开始逐步增加,直到温度在设定值附近以 ±2℃ 小幅波动(不衰减也不发散),这个 Kp 就是临界增益。然后引入 Ki=0.5~2.0 消除稳态偏差。
❌ DHT22 读数偶尔跳变(NAN)
DHT22 的 OneWire 协议对时序敏感,ESP32 的 FreeRTOS 任务切换可能打断读数。代码中用 isnan() 做了兜底处理,但更好的做法是连续读3次取中位数。工业场景建议升级为 DS18B20 数字温度传感器,抗干扰更强。
❌ SSR 继电器发烫严重
SSR 导通时有 ~1.2V 压降,电流越大发热越厉害。40A SSR 带 5A 负载时功耗约 6W——必须加散热片!如果只是驱动 12V/50W 加热片(约4A),一个小型铝散热片即可。长时间满功率运行建议选大一档的 SSR。
❌ 加热片温度超过设定值很多才降下来
这是大惯性系统的典型「热惯性」问题。加热片断电后,其自身余热还会继续传导给传感器,导致超调。解决方案:①加大 Kd(微分项提前刹车);②传感器尽量贴近加热片安装,减少传导延迟;③在设定值前 2-3℃ 开始降功率(前馈控制)。