🔥 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 I2C | 1 | ¥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秒控制周期
}
代码要点:
- 积分抗饱和(Anti-Windup) — 当输出已满(0或1023)时,不再累积积分,避免「积分饱和」导致超调后长时间回不来
- 1Hz PWM — SSR固态继电器开关有响应延迟,不宜用高频PWM(高频闪烁也没用);1秒周期,占空比从0到100%平滑调节加热功率
- 中断读取编码器 — 用硬件中断 +
IRAM_ATTR 确保旋转编码器不被主循环阻塞
🔧 参数整定口诀: 先将 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℃ 开始降功率(前馈控制)。