简介:这是电子设计大赛参赛作品,基于STM32微控制器实现水温PID闭环控制,适合嵌入式初学者、竞赛选手及自动控制爱好者,也可作为智能恒温设备开发的参考原型。压缩包共254个文件,约4.19MB,主要由C源码、H头文件与编译生成的O/CRF中间文件构成,同时包含HEX/MAP烧录调试文件、UV2工程及初始化配置文件,目录结构清晰,可完整还原开发环境。已有2449人学习,参考价值经过验证。工程实现ADC采集温度传感器信号、PID算法计算输出、控制加热元件通断等核心流程,并涵盖定时器、中断、GPIO等STM32外设配置。仔细研读可掌握PID参数调节方法,理解比例、积分、微分对水温响应速度与稳态精度的影响,也能为其他温度控制系统提供可直接移植的代码框架。 做水温控制这个题目,是我当年参加电子设计大赛印象最深的一段经历。题目本身不复杂:给定目标水温,让系统自动加热并稳定住。但真上手才发现,温度读准只是第一步,加热器怎么驱动、PID参数怎么定、为什么到了设定值还会冲过头,这些才是真正卡人的地方。这篇就把这个基于STM32的水温控制PID源程序从头拆一遍,包括方案选型、硬件电路、核心代码,以及我调参踩过的一堆坑。不管你是准备电赛、做课程设计,还是第一次接触PID闭环控制,都可以把这个项目当作一个能直接落地的参考。
这里要提前说清楚一个认知:水温控制是典型的大惯性、大滞后系统,和电机调速那种快速响应完全不同。控制周期、参数初值、执行器策略都有讲究。网上很多源码能下载,但换到你的板子上就失控,多半不是代码问题,而是没理解这套系统和PID的配合关系。下面我从一个参赛者的视角,把整件事讲透。
1. 题目拆解与整体方案选型
1.1 电赛水温控制,到底在考什么
电赛控制类题目一般不会直接说“你给我做个PID”,而是描述成:容器里有水,加热装置工作,要求把水温快速加热到设定温度并稳下来,允许误差可能是±1℃甚至±0.5℃,有的题还加风冷,要求能主动降温。
如果你只看表面指标,很容易一上来就写个“低于设定就开,高于设定就关”的开关逻辑。跑起来就会发现水温在目标值附近反复震荡,而且因为加热体有余热,冲过设定值后还会继续往上飘,误差远超要求。这就是闭环控制的经典问题:反馈量变化惯性大,开关控制永远在过度纠偏。核心矛盾是加热不能瞬间停止,温度读数也代表的是过去一段时间的状态,所以需要PID这种能结合当前差值、历史累计、变化趋势一起做决策的控制器。
1.2 主控、传感器和执行器怎么选
主控我选的是STM32F103C8T6。电赛里这颗芯片出现频率极高,72MHz主频,ADC、定时器PWM、串口、I2C全都够用,资料铺天盖地。源程序用的标准外设库,其实用HAL也一样,核心算法跟库本身没关系。
传感器对比过几个方案:
| 方案 | 精度 | 响应速度 | 成本 | 适合场景 |
|---|---|---|---|---|
| DS18B20 | ±0.5℃ | 慢,转换约750ms | 低 | 课程设计、简易控温 |
| NTC 10K B3950 + ADC | 标定后可达±0.2℃ | 快 | 极低 | 电赛、需要实时控制 |
| PT100 + 运放 | 很高 | 中 | 高 | 工业级高精度平台 |
我用了NTC方案。原因很简单:水温控制需要快速感知变化趋势,NTC响应比DS18B20快很多,ADC采样平滑后稳定性也够用。代价是需要自己做阻值转温度公式,这个不复杂,后面代码里直接给。
执行器选了固态继电器SSR,PID输出PWM占空比控制SSR通断。普通电磁继电器撑不住高频动作,噪音和寿命都是问题。SSR内部是可控硅,无触点,可以把通断周期做到1秒级别,对控制算法很友好。
2. PID算法:为什么是它,怎么落地
2.1 从洗澡调水温理解PID
理解PID最直观的方式就是洗澡。觉得水凉了往热水方向拧,太烫了赶紧调回来,而且会根据温差决定拧多少,看到快烫了提前减速。PID本质就是把这种经验数字化:P看现在差多少,I看以前累计差多少,D预测下一步会怎么变。
水温加热系统跟洗澡很像:加热器功率大、热惯性大、温度反馈滞后。如果没有D的预判,到目标才停,余热会继续推高温度。这正是PID适合这个题目的原因——它不要求精确数学模型,只要参数合理,就能用简单公式控制住复杂物理过程。
2.2 位置式、增量式,以及代码实现
PID有两个常用形态。位置式输出的是控制量绝对值,直接就是“应该给多少功率”:
out = Kp * err + Ki * err_sum + Kd * (err - err_last);增量式输出的是本次相对上次的变化量,一般用于电机调速这类执行器需要平滑变化的场景。水温控制我让PID直接输出0~100的占空比,更适合位置式。位置式的坑是积分项不断累积,如果加热过程中误差一直为正,积分项会冲到限幅,温度到设定后输出迟迟降不下来。所以代码里必须做抗积分饱和和输出限幅,这比公式本身更重要。
typedef struct { float Kp; float Ki; float Kd; float target; float err; float err_last; float err_sum; float out; float out_max; } PID_t; float PID_Calc(PID_t *pid, float actual) { pid->err = pid->target - actual; pid->out = pid->Kp * pid->err + pid->Ki * pid->err_sum + pid->Kd * (pid->err - pid->err_last); if (pid->out > pid->out_max) pid->out = pid->out_max; else if (pid->out < 0) pid->out = 0; pid->err_sum += pid->err; if (pid->err_sum > pid->out_max) pid->err_sum = pid->out_max; else if (pid->err_sum < -pid->out_max) pid->err_sum = -pid->out_max; pid->err_last = pid->err; return pid->out; }这个函数有两个细节:积分项只在输出限幅内累积,防止积分饱和;输出下限设为0,因为加热系统不能负功率。想快速降温需要额外加风冷,那就变成双通道结构,这里不展开。
2.3 控制周期和参数初值
水温系统惯性大,控制周期选1秒比较合理。太短会把采样噪声算进去,尤其D项容易让输出发抖;太长又跟不上温度变化,震荡周期变大。
调参顺序和教材一致:先P后I再D。初值怎么给?先做开环测试,固定PWM输出50%,记录水温从室温升到目标附近的时间。假设满功率升温速率约每分钟5℃,那误差20℃时输出100%,Kp取5左右起步;然后逐步增加,观察曲线。千万不要一上来就乱试一组参数。
3. 硬件电路与STM32资源分配
3.1 引脚规划参考
我的作品里引脚分配是这样的:
| 功能 | 引脚 | 说明 |
|---|---|---|
| NTC温度采集 | PA1 | ADC1_CH1 |
| 加热PWM输出 | PA0 | TIM2_CH1 |
| OLED显示 | PB6/PB7 | I2C1 |
| 设定温度按键 | PB0~PB2 | 加减、切换 |
| 蜂鸣器报警 | PB12 | 超温、按键反馈 |
PA0和PA1相邻,方便布线。注意TIM2_CH1的复用功能,GPIO要配置成AF_PP,部分板子可能需要重映射,先查原理图再写代码。
3.2 NTC采集电路与温度换算
NTC测温原理就是分压。用10K电阻和10K@25℃的NTC串联,接到3.3V,中间抽头进ADC。温度升高NTC阻值下降或上升取决于接法,公式并不复杂。
float NTC_GetTemp(uint16_t adc_val) { float Rntc = 10.0f * (4096.0f / adc_val - 1.0f); float tempK = 1.0f / (1.0f / 298.15f + logf(Rntc / 10.0f) / 3950.0f); return tempK - 273.15f; }这里用B值公式,ADC是12位,满量程4096。采样时间尽量配置长一点,硬件上加一个10K+1uF的RC低通滤波,软件上每轮控制用20次采样取平均,能明显减小温度跳变。
3.3 加热驱动与安全隔离
PWM信号从PA0出来后不能直接驱动SSR。我加了一级光耦隔离和限流电阻,再接到SSR控制端。好处是单片机地和强电地彻底分开,避免地环路干扰,保护调试器不被高压击穿。强电部分必须提醒:调试时先确认弱电和强电隔离可靠再上电,加热部分加独立保险丝,这是保命的事。
SSR用1秒PWM周期驱动。过零型SSR必须在交流过零点附近动作,1Hz的PWM对它完全没有压力,表现为“1秒内导通时间占多少百分比”。如果你用随机导通型SSR,可以适当提高频率,但水温系统响应不过来,没必要。
4. 工程搭建与源程序结构
4.1 Keil工程和固件库的坑
这套源程序用的是STM32F1标准外设库。在Keil MDK新建工程最容易踩三个坑:一是忘了把stm32f10x_it.c、system_stm32f10x.c加入Source Group,编译报一堆未定义;二是头文件路径没加全,出现“cannot open source file stm32f10x.h”;三是宏定义漏了STM32F10X_MD,导致器件类型不对,外设注册表错乱。
如果你用VSCode或CLion打开工程报“找不到.h”,先检查c_cpp_properties.json里有没有把固件库include路径全部列全,宏定义要写STM32F10X_MD和USE_STDPERIPH_DRIVER。这个原理和Keil魔棒设置一样。
4.2 定时器PWM和ADC初始化
PWM用TIM2_CH1,输出周期设为1秒。72MHz主频下,PSC=7199,ARR=9999,计数频率10kHz,溢出周期约1秒。比较值CCR从0到9999对应占空比0~100%。
ADC用轮询方式,选ADC1通道1,采样时间55.5 cycles,每轮读20次取平均。没有用DMA,因为水温控制对实时性要求不高,主循环里完成完全够。如果后面要处理复杂显示和菜单,再上DMA也不迟。
4.3 主循环逻辑与串口调试
主循环代码很简单:
while (1) { if (flag_1s) { flag_1s = 0; temp = NTC_GetTemp(ADC_GetAverage(20)); out = PID_Calc(&pid, temp); TIM_SetCompare1(TIM2, (uint16_t)(out * 99.9f)); printf("temp:%.2f,set:%.2f,out:%.1f\r\n", temp, pid.target, out); OLED_ShowTemp(temp, pid.target, out); } Key_Scan(); }1秒定时用Systick中断置位flag,主循环处理按键和显示。这套框架很容易扩展,想改10ms控制周期,把flag改为对应时间即可。
5. 从调参到排障:实测经验
5.1 先开环标定,别急着上PID
我见过太多人拿到源码改一下参数就烧录,结果温度要么不动要么乱震荡。正确做法是先做开环标定:把PID暂时去掉,固定PWM占空比50%,加热几分钟,记录温度曲线。这个曲线能告诉你系统的时间常数和滞后量。
比如实测室温25℃,PWM=50%,水温4分钟升到45℃。这意味着满功率升温速率约每分钟5℃。那误差5℃时输出100%不算过分,误差1℃时输出20%可能已经能维持温度。Kp初值可以通过“满量程输出/预期最大误差”估算,然后逐步往下试。这个动作不花太多时间,但对后面调参帮助极大。
5.2 三步调参法
第一步只留P,Ki和Kd先设0。Kp从小到大,比如10、20、40、80,观察温度曲线。太小升温慢、稳态误差大;太大开始震荡。找到临界值,比如Kp=55时温度开始等幅震荡,取它的一半约27作为工作Kp。
第二步加I。Ki从0.02开始,每次调整观察稳态误差。太小,温度离设定值永远差一点;太大,系统低频震荡。我最终Ki在0.1左右,误差能进±0.3℃。
第三步加D。D对水温系统是双刃剑,确实能抑制超调,但温度采样有噪声,D放大了噪声会让PWM输出跳变。我最后Kd只给了1.0,效果足够。
5.3 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 温度显示乱跳 | NTC走线太长、电源噪声 | 加RC滤波、多采样取平均 |
| 到设定值还在冲 | 加热棒余热大 | 加大Kd、限制接近目标时的输出 |
| 稳态误差消不掉 | Ki太小、积分限幅太小 | 增大Ki、放宽积分限幅 |
| 输出狂跳、PWM闪 | D过大、采样噪声 | 降低Kd、增加滤波 |
| 升温速度不够 | Kp太小、输出限幅太低 | 增大Kp、检查输出上限 |
| 程序上电卡死 | HSE起振失败、延时阻塞 | 查晶振、查初始化顺序 |
| 编译找不到stm32f10x.h | include路径没配置 | 添加固件库头文件目录 |
5.4 余热问题的实战处理
水温控制最讨厌的就是余热。加热器表面温度很高,即使PID输出已经降到0,热量还会继续传进水里。我后来加了两个处理:一是接近设定值时限制最大占空比,比如误差小于2℃时输出上限强制降为40%,让系统提前减速;二是加死区,误差在±0.3℃之内不改变PWM输出,避免输出频繁变化造成温度锯齿状波动。
调水温PID那几天,我最深的体会是:代码不难,难的是愿意静下心去观察系统的性子。每套加热器都有自己的滞后和余热,换一个水杯、换一个加热功率,参数都要重来。所以我后来把调试工具链搭完整,串口曲线、开环标定、参数表格都顺手做了。这个项目之后,再写电机调速、智能车里的PID,核心思路完全通用,只是执行器和响应速度变了。如果你准备做类似的比赛题,建议先把硬件摸透,再开始改参数,你会发现一切顺很多。
本文还有配套的精品资源,点击获取