STM32 PID温控实战:自整定+PWM输出完整方案

📅 发布时间:2026/9/9 0:48:18
STM32 PID温控实战:自整定+PWM输出完整方案 简介由STM32标准库实现的PID自整定、PID控温与PWM输出综合示例包面向嵌入式开发者与温度控制应用场景可解决PID参数手动反复整定、控温波动大、PWM输出配置复杂等实际问题适合作为学习闭环控制与定时器应用的入门工程。压缩包内共89个文件其中包含37个头文件与35个C源文件覆盖驱动与业务代码另有8个汇编启动文件、3个hex固件以及Keil工程配置、批处理脚本等辅助内容整体大小约364KB目录划分清晰便于阅读与移植。工程已集成DS18B20温度采集、SysTick延时、高级与通用定时器PWM输出、PID参数自整定算法可烧录至开发板直接观察控温效果也可作为智能恒温器或工业温控模块的参考母版。目前已有2358人学习下载适合需要快速掌握STM32标准库下PID控温与PWM输出实现方法的初中级开发者。 做过嵌入式温控项目的朋友应该都有体会PID本身不复杂但真正把温度控稳、控准比想象中要难不少。最近整理了一份基于 STM32 标准库的 PID 控温工程包含 PID 自整定、PWM 输出完整代码正好有朋友在问干脆把整个项目的设计思路和实操过程写成一篇总结。这篇文章适合正在做温控类项目的开发者——比如热水器、恒温箱、3D 打印机热床、激光器 TEC 控温——也适合那些想搞懂 PID 自整定到底怎么落地到单片机上的同学。这个项目核心就三件事用 STM32 标准库输出 PWM 控制加热器用增量式 PID 闭环控温再用临界比例度法实现 PID 参数自整定。整套代码在 STM32F103 上跑通硬件上只需要一个 NTC 或者 DS18B20 做温度采样加上一个 MOS 管或固态继电器驱动加热负载。1. 项目到底在做什么一个温控系统的全貌拆解1.1 需求解析从控温到自整定的真正含义先把这个项目的需求拆开看。标题里写了三个关键词PID 自整定、PID 控温、PWM 输出。三个词对应三件事——控温是最终目标PWM 是执行手段自整定则是为了解决PID 参数怎么定这个实际问题。控温系统的闭环逻辑是这样的温度传感器采集当前温度和设定目标温度对比得到误差PID 算法根据误差计算输出量这个输出量通过 PWM 占空比控制加热器的平均功率。整个循环以固定周期运行典型周期是 100ms 到 1s 之间取决于系统的热惯性。热惯性越大控制周期可以越长。这里有个容易忽略的点PWM 控温的本质是时间比例控制加热器在 PWM 周期内开的时间比例决定了输入热量。比如 PWM 频率 1Hz占空比 50%就是每 1 秒内加热 0.5 秒。市电环境下通常用低频率 PWM——有些项目用 1Hz 甚至 0.5Hz目的是配合固态继电器的过零触发减少浪涌电流和 EMI。直流加热场景可以用更高频率比如 1kHz 到 10kHzPWM 频率越高温度波动越小但对驱动电路要求也更高。1.2 为什么选 STM32 标准库而不是 HAL 库这个工程我用的标准库也就是 StdPeriph_Lib V3.5。现在新项目很多人直接用 HAL 库加 CubeMX 生成但标准库在温控这类项目里依然有不可替代的优势代码体积小、执行效率高、寄存器操作透明。标准库本质上是寄存器的轻量封装你清楚每条语句最终在操作哪个寄存器对 PWM 周期、占空比、定时器中断这些时序敏感的逻辑掌控力更强。另外很多老工程师手里的存量工程就是标准库写的温控项目经常需要快速移植标准库代码改起来比 HAL 库利索得多。不过要提一句标准库官方已经停止维护新入行的朋友如果是从零开始学建议 HAL 库和标准库都了解一下能看懂标准库对你理解 STM32 底层非常有帮助。1.3 系统整体架构和硬件选型整套系统的工作流程可以概括为温度采集 → PID 运算 → PWM 输出 → 加热器 → 温度变化 → 再采集。核心外设只需要三个ADC或单总线接口采集温度、定时器输出 PWM、定时器中断做控制周期。我实际验证用的硬件清单如下主控STM32F103C8T6Blue Pill 开发板就能跑温度传感器NTC 热敏电阻 10k 分压电阻用 ADC 采样或者 DS18B20单总线数字温度传感器加热负载12V 50W 加热电阻MOS 管驱动IRLZ44N低电平导通电源12V 电源给加热电阻5V/3.3V 给控制板注意共地选择 NTC 还是 DS18B20 有个权衡NTC 响应速度快但需要查表或者用 Steinhart-Hart 公式换算温度且 ADC 采样有噪声DS18B20 直接输出数字温度精度够用但单总线时序要求严格且采样速度慢12 位分辨率下每次转换约 750ms。我的工程里两个都做了适配后面会具体讲。2. PID 算法的工程落地从公式到结构体2.1 位置式 PID 和增量式 PID 怎么选PID 公式大家都很熟悉u(k) Kp * e(k) Ki * Σe(i) Kd * (e(k) - e(k-1))。这是位置式 PID输出的是绝对控制量。增量式 PID 输出的是控制量的增量Δu(k) Kp * (e(k) - e(k-1)) Ki * e(k) Kd * (e(k) - 2e(k-1) e(k-2))。温控系统里我推荐增量式原因是温控执行器——尤其是通过 PWM 控制的加热器——天然适合叠加增量。每次 PID 计算出一个增量 Δu加到当前占空比上即使 PID 参数出了问题输出也是在原占空比附近小幅震荡不会一下子冲到 100%安全性好。另外增量式不需要累加积分项天然避免了积分饱和导致的大幅超调。实际的输出映射逻辑是当前 PWM 占空比 Δu然后做限幅0% 到 100%。这里限幅非常重要必须做否则占空比可能算成负数或者超过 100%PWM 输出就乱了。2.2 STM32 标准库下的 PID 结构体设计标准库工程里我定义了一个 PID 结构体把所有状态量封装起来typedef struct { float Kp; /* 比例系数 */ float Ki; /* 积分系数 */ float Kd; /* 微分系数 */ float target; /* 目标温度 */ float current; /* 当前温度 */ float error; /* 当前误差 */ float last_error; /* 上一次误差 */ float prev_error; /* 上上次误差 */ float output; /* PID输出即占空比增量对应的累计值 */ float out_min; /* 输出下限 */ float out_max; /* 输出上限 */ } PID_TypeDef;很多人写 PID 结构体只放 Kp、Ki、Kd 和误差忽略了输出限幅参数。我调试温控的经验是输出上下限必须进结构体因为在自整定过程中PID 参数会变化输出限幅也需要配合调整。把限幅和参数放一起封装性更好后续加死区、加前馈也方便。2.3 增量式 PID 的核心代码核心计算函数如下float PID_IncrementalCalc(PID_TypeDef *pid, float target, float current) { float delta_out 0.0f; pid-target target; pid-current current; pid-error target - current; /* 增量式PID核心Δu Kp*(e[k]-e[k-1]) Ki*e[k] Kd*(e[k]-2e[k-1]e[k-2]) */ delta_out pid-Kp * (pid-error - pid-last_error) pid-Ki * pid-error pid-Kd * (pid-error - 2.0f * pid-last_error pid-prev_error); /* 更新历史误差 */ pid-prev_error pid-last_error; pid-last_error pid-error; /* 输出累加并做限幅 */ pid-output delta_out; if (pid-output pid-out_max) pid-output pid-out_max; if (pid-output pid-out_min) pid-output pid-out_min; return pid-output; }注意有个细节增量式 PID 的输出是累计值所以实际上你的 PWM 占空比就是这个 output。这样设计的好处是当你切换 PID 参数比如自整定结束后换上新的 Kp、Ki、Kd输出量是平滑过渡的不会产生跳变。2.4 控温周期怎么确定PID 计算不能每个 PWM 周期都执行一次也不该在 while(1) 里死循环跑必须有固定的控制周期。温控系统的控制周期主要看系统的时间常数。纯水加热器的热时间常数大可能 30 秒以上控制周期 1 秒完全够小体积铝块加热块热惯性小控制周期可以设 200ms如果是 TEC 制冷片响应快控制周期可以短到 50ms。我用定时器中断产生控制周期代码框架是void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); temp_now Read_Temperature(); /* 采集温度 */ duty PID_IncrementalCalc(pid, set_temp, temp_now); /* PID计算 */ TIM_SetCompare1(TIM2, (uint16_t)(duty / 100.0f * arr_value)); /* 更新占空比 */ } }控制周期和 PWM 频率是两个独立的定时器。我用 TIM2 做 PWM 输出TIM3 做控制周期。很多新手会犯一个错控制周期和 PWM 周期混用一个定时器这样当你要调控制周期的时候 PWM 频率也跟着变系统行为完全乱掉。务必分离。3. PID 自整定让参数自己算出来3.1 为什么手动调参不靠谱手动调 PID 参数对于温控系统来说是个折磨。Kp 太小升温慢Kp 太大温度震荡Ki 加多了超调巨大Kd 加多了噪声敏感。最要命的是不同系统的热惯性差很多——一个大水箱和一个小铝块参数完全是两个量级。手动试凑往往调了一下午换一个环境温度又开始震荡。所以工程上成熟的温控方案都会做自整定。自整定原理并不神秘系统先以固定功率加热观察温度曲线上振荡的周期和幅值根据控制理论算出 PID 参数。最经典的方法是 Ziegler-Nichols 临界比例度法先把 I 和 D 关掉只留 P逐步增大 Kp 直到系统产生等幅振荡记录临界增益 Ku 和振荡周期 Tu然后按经验公式算出 Kp、Ki、Kd。3.2 Ziegler-Nichols 临界比例度法实操流程整个自整定流程是这样走的将目标温度设置为略微高于环境温度通常高于环境 20%~30%或者直接设置用户期望的目标温度强制输出一个固定 PWM 占空比比如 50%让系统开始升温当温度接近目标温度时切换为纯 P 控制Kp 从较小值开始每个控制周期检查温度响应如果温度出现等幅振荡记录此时的 Kp 作为临界增益 Ku同时记录振荡周期 Tu按 Ziegler-Nichols 公式计算 PID 参数对于增量式 PID比例度需要用输出量纲换算标准整定公式是这样的控制方式KpKiKdP0.5*Ku--PI0.45*Ku0.54*Ku/Tu-PID0.6*Ku1.2*Ku/Tu0.075KuTu这里需要强调换算问题。临界比例度法本来是针对连续系统的在离散单片机实现里输出是占空比百分比误差是温度所以 Ku 这一步实际上是占空比百分比 / 温度误差量纲要理清楚。我在工程里最终算出来的 Kp、Ki、Kd 是直接给增量式 PID 用的。如果按连续系统公式算完发现系统不对优先排查量纲换算。3.3 自整定在裸机上的状态机实现自整定不能阻塞主循环我用一个简单状态机来管理typedef enum { AUTO_TUNE_START 0, AUTO_TUNE_HEATING, AUTO_TUNE_FINDING_KU, AUTO_TUNE_DONE } AutoTuneState; void AutoTune_Task(void) { switch (tune_state) { case AUTO_TUNE_START: /* 设置输出为固定50%开始升温 */ duty 50.0f; tune_state AUTO_TUNE_HEATING; break; case AUTO_TUNE_HEATING: /* 温度超过设定阈值切换纯P控制Kp从小开始递增 */ if (temperature tune_start_temp 5.0f) { pid.Kp 1.0f; pid.Ki 0.0f; pid.Kd 0.0f; tune_state AUTO_TUNE_FINDING_KU; } break; case AUTO_TUNE_FINDING_KU: /* 检测温度震荡每次过峰/过谷按步进增加Kp直到等幅震荡 */ if (Detect_Oscillation()) { if (ts_amplitude tune_target_amplitude) { tune_ku pid.Kp; tune_tu ts_period; /* 带入Ziegler-Nichols公式 */ pid.Kp 0.6f * tune_ku; pid.Ki 1.2f * tune_ku / tune_tu; pid.Kd 0.075f * tune_ku * tune_tu; tune_state AUTO_TUNE_DONE; } else { pid.Kp 0.5f; } } break; case AUTO_TUNE_DONE: /* 整定完成切换正常PID控温 */ break; } }状态机核心逻辑就是逐步加大 P、找等幅振荡临界点。实际操作中振荡周期 Tu 的测量是关键——我用一个简单的峰谷检测器记录相邻两次温度峰值的时间间隔取平均效果不错。噪声较大时温度曲线会有假峰需要在检测器里加一个小的滞回窗口比如温度变化超过 0.3 度才认为是一个峰值。3.4 自整定过程中的保护机制自整定有个工程安全隐患找临界增益的过程中是有意让系统振荡如果保护没做好可能把加热设备搞坏。我在代码里加了几层保护第一目标温度不能超过安全阈值比如水箱控温最多整定到 80 度第二自整定超时保护比如 30 分钟内没整定完成就退出恢复为手动参数第三系统只需要加热不需要制冷场景下振荡过程中温度过高直接停掉输出。这里插一句恒温场景和制冷场景的 PID 逻辑差异很大。纯加热系统只有一个方向能控制功率降温只能靠自然散热所以 PID 输出的下限不能是负占空比而是 0。如果你的硬件是 TEC 半导体制冷片需要 H 桥驱动那么 PID 输出才可以是负值表示反向制冷。标题里的工程是纯加热控温限幅就设 0~100 占空比。4. PWM 输出与硬件驱动的关键细节4.1 标准库下 PWM 初始化配置PWM 输出用定时器的比较输出模式代码片段如下void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 999; /* 自动重装载值 */ TIM_TimeBaseStructure.TIM_Prescaler 71; /* 预分频 72MHz/(711)1MHz */ TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_Cmd(TIM2, ENABLE); }这套配置下PWM 频率 72MHz / 1000 / 72 1kHz。1kHz 适合直流加热负载加热电阻 MOS 管如果换成固态继电器控制交流加热器记得把 PWM 频率降低到 1Hz 左右也就是把定时器周期改大。频率选择在温控系统里有个权衡PWM 频率越高加热器电流纹波越小但 MOS 管开关损耗越大且对 PID 控制周期有影响频率太低温度曲线锯齿状明显。直流加热 1kHz 是我常用值实测纹波很小MOS 管发热在可接受范围。4.2 100% 占空比异常的坑网上经常有人遇到STM32 定时器输出 PWM 时 100% 占空比异常的问题。这个坑我也踩过。现象是设置比较值等于自动重装载值时输出应该一直是高电平但示波器上看到并不是干净的直流高电平偶尔有毛刺。根因在于 PWM 极性和比较值的关系。当 TIM_Pulse 设置成 TIM_Period 时理论上输出全程为高。但如果计数器的计数模式是中心对齐模式Center-aligned或者在 PWM 模式下比较值设置超过自动重装载值行为就不可预测。另外 STM32 的 TIM 在比较值等于重装值时如果是 PWM 模式 2输出电平逻辑会反转。解决方案有两个一是占空比 100% 时直接把 GPIO 拉高不走 PWM 输出二是占空比映射时做个上限钳位比如最大输出到 99.5%避免达到 100% 的临界值。我在温控工程里用的是第二种方案——占空比上限设成 99.5%因为加热器实际不需要 100% 占空比也能控温而且避免了临界值带来的不确定性。当然更好的做法是用一个 GPIO 来做强制加热开关当 PID 输出满量程且持续一段时间后直接拉高开关管。4.3 驱动电路和硬件保护PWM 输出的是 3.3V 逻辑电平不能直接驱动大功率加热器。我的电路用 N-MOS 管源极接地栅极通过 100Ω 电阻接 STM32 引脚漏极接加热器负极加热器正极接 12V。注意 MOS 管栅极需要 10kΩ 下拉电阻防止单片机复位期间引脚悬空导致 MOS 误导通。这是血泪教训单片机复位或者烧录的时候 IO 是高阻态没有下拉电阻加热器就可能自行启动非常危险。还有续流二极管的问题。如果驱动的是继电器线圈或者感性负载必须并在负载两端并联一个续流二极管如 1N4007否则关断瞬间的感性反电动势会击穿 MOS 管。如果是阻性发热丝比如纯电阻加热器不存在感性反电动势但为了保险我还是加了 TVS 管。5. 代码工程结构与实操调参实录5.1 标准库工程目录规划工程结构上我拆分了几个模块方便日后移植。目录如下App/主逻辑层包括main.c、control_task.c控制任务、auto_tune.c自整定状态机BSP/板级驱动层包括bsp_pwm.c、bsp_adc.c、bsp_uart.c、bsp_ds18b20.cPID/算法层pid.c放增量式 PID、auto_tune.c放自整定SYSTEM/标准库的启动文件、系统时钟配置模块划分的原则是算法层不依赖硬件PID 模块只做数学运算不管温度怎么来、PWM 怎么输出。这样换平台或者换传感器只需要改 BSP 层算法层完全不动。凡是做嵌入式时间长了的人都知道模块解耦是后期维护成本的胜负手。5.2 温度采集的实现要点ADC 采样 NTC 有两个细节要注意一是 ADC 采样值要滤波滑动平均滤波或者一阶低通滤波都可以一阶低通滤波实现简单、响应快#define FILTER_ALPHA 0.2f float g_filtered_adc 0.0f; float Filter_AddSample(float adc_val) { g_filtered_adc g_filtered_adc * (1.0f - FILTER_ALPHA) adc_val * FILTER_ALPHA; return g_filtered_adc; }滤波系数 FILTER_ALPHA 越大滤波响应越快但噪声越大0.1~0.3 是折中区间。注意滤波之后温度采样会有滞后这个滞后本质上是给控制系统增加了一个惯性环节对 PID 整定有影响——这也是为什么自整定出来的参数比理论计算值会偏保守。二是温度换算NTC 热敏电阻用 B 值公式R R25 * exp(B * (1/T - 1/T25))其中 T 是开尔文温度R25 是 25 度时的阻值B 是热敏指数。实测下来B 值公式在窄温区0~100℃精度足够比查表法省 flash而且代码干净。5.3 串口调试PID 调参的可视化手段调 PID 最痛苦的是看不到内部状态。我用串口把关键变量打出来用一个 PC 端的小工具做波形显示。数据格式很简单printf(T%.2f Set%.2f Duty%.2f\r\n, g_temp, g_set_temp, g_duty);输出格式是纯文本逗号分隔用匿名四轴的上位机或者 QPlot 之类的工具可以可视化温度曲线。如果没有波形工具用串口终端直接看数据也能判断基本趋势升温响应快慢、超调量、振荡频率。我强烈建议大家在做 PID 控温项目时先把串口调试打通再调 PID因为所有 PID 整定方法都需要观察温度响应曲线。没有数据反馈就像闭着眼开车只能靠猜。5.4 一次真实的整定过程记录我在 50W 加热电阻 铝块加热器的系统上跑了一次完整自整定。初始温度 27 度目标温度 60 度自整定流程固定 50% 占空比加热温度从 27 度爬升大约 90 秒后到达 45 度此时切入纯 P 控制初始 Kp1温度到达 60 度附近开始振荡但振幅只有 1 度说明 Kp 还不够大继续每次加 0.5Kp 加到 8 时温度在 55~65 度之间持续等幅振荡振幅约 5 度记录 Ku8振荡周期 Tu 约 25 秒按 Ziegler-Nichols 公式计算Kp4.8Ki0.384Kd15换上参数后实测系统约 120 秒达到稳定超调量约 2 度稳态精度 ±0.2 度效果符合预期后端整定过程 3~4 分钟就能完成。对于热惯性大的系统时间会再拉长一些这个时间成本是可以接受的。如果你的系统每次开机工况一样自整定一次后把参数存到 Flash下次直接加载即可不必每次都重新整定。6. 常见问题与排查技巧实录6.1 温度过冲大怎么处理这是温控系统最常见的现象温度一路冲过目标值然后慢慢回落。过冲本质上是因为系统热惯性大加热器停止加热后温度还会持续上升这一段。处理方案优先级从高到低先检查控制周期是否过长控制周期超过系统纯滞后时间的一半就可能出问题然后检查微分项是否被滤波滞后抵消必要时微分改为测量值微分而非误差微分避免设定值变化时微分项跳变最后考虑加入 Bang-Bang 预判逻辑距离目标温度 2 度以内时提前降低输出。6.2 系统自激振荡温度反复在目标附近摆动如果整定完成后系统仍然持续振荡大概率是 Ku 判断不准。临界增益测量偏大的话算出的 Kp 偏大系统必然振荡。我的经验是实际使用中把 Ziegler-Nichols 算出来的 Kp 再乘 0.8牺牲一点响应速度换取稳定性特别适合热惯性大的系统。如果振荡周期很短比如 2~3 秒那可能是 ADC 采样噪声被微分项放大需要在温度采集处加强滤波并且给微分项加死区或低通滤波。6.3 PWM 输出有异常波形如果 PWM 输出波形抖动先检查 DMA 和中断有没有和 PWM 更新冲突。标准库工程里如果用同一个定时器的更新事件既做控制周期中断又更新 PWM 比较值会有竞争风险。另外观察 PWM 输出脚有没有被复用GPIO 配置是否正确。一个容易忽略的点PWM 引脚的 GPIO 时钟和定时器时钟的 RCC 使能顺序不能反先 GPIO 后 TIM顺序反了会偶发初始化失败。6.4error: no stm32 target found这类调试器连接问题调试 PID 项目时如果突然连不上调试器优先检查 BOOT0 引脚电平、SWDIO/SWCLK 线序还有目标板电源是否稳定。很多情况是因为给加热器供电的大电流拉低了电源电压导致调试器目标电压检测失败。这属于硬件层面干扰排查时先把加热器断电看看能不能连上——连得上就说明是电源干扰问题给控制板单独供电就能解决。写在最后再分享一个实际心得PID 控温项目能不能做好七分在硬件设计三分在算法调参。电源要稳定传感器要尽量贴近被测对象加热器功率要合理——如果加热功率过大PID 输出稍微动一点温度就大起大落什么算法都白搭。这个工程我后续计划扩展的方向是加一点自适应功能根据当前温度和目标温度的差值自动切换控制参数让升温和恒温两个阶段都能有更好的性能。如果大家对这个扩展方向有兴趣或者遇到温控项目里的其他坑欢迎一起交流。本文还有配套的精品资源点击获取