
简介基于STM32的蓝牙PID水温控制程序面向电子爱好者、嵌入式初学者及STM32项目开发者解决500W热得快加热过程中水温恒定控制与远程调参问题。设计采用7805线性电源为单片机和继电器模块供电STM32输出1Hz可调PWM方波结合PID算法计算占空比温差大时大功率加热温差小时继电器频繁通断实现精细控温OLED屏实时显示参数与水温四按键本地调参并接入蓝牙模块支持手机端修改目标温度。压缩包共84个文件以C语言源码和头文件为主含36个c、39个h并附带hex固件、启动文件及Keil工程文件约314KB便于直接编译烧录和二次开发。资源已吸引1283人学习适合想掌握PID控制、PWM调功、蓝牙串口通信及DS18B20测温综合应用的开发者参考。1. 为什么 500W 热得快控温用 1Hz 低频开关而不是高频 PWM先亮结论这套基于 STM32 的蓝牙 PID 水温控制程序核心并不在 PID 算法本身而在于它用 1Hz 频率的继电器通断来实现功率调节。500W 热得快是纯阻性负载热惯性比半导体加热片大得多如果按常规 PWM 思路用几 kHz 频率去切继电器触点电弧会在几分钟内把继电器烧死。低频时间比例控制Time Proportional Control才是这类大功率加热负载的可行解。程序把 PID 计算出的 0~100% 功率映射到一秒周期内的导通时间温差大时输出接近 100%继电器近乎常通快速升温温差减小时占空比下降继电器以秒级节奏通断把水温稳定在设定值附近。这套逻辑对想用继电器方案做热水器、鱼缸恒温、小型反应釜温控的嵌入式开发者很有参考价值手机蓝牙调参也免去了反复拔线改参数的麻烦。2. 硬件链路DS18B20、7805、继电器模块的供电与接线坑2.1 7805 线性电源的压差与功耗原方案用 220V 转 12V 变压器加 7805 稳压到 5V。这个选择的优点是电路简单、纹波小但代价是线性稳压的发热。12V 输入减去 5V 输出压差 7V整机电流按单片机约 30mA、继电器模块约 70mA、蓝牙模块峰值 40mA、OLED 约 15mA 估算总电流约 150mA 上下7805 上要消耗约 1W 功率必须加散热片否则热保护会让系统间歇性复位。输入侧建议 470uF 电解电容配 104 陶瓷电容输出侧也并 100uFDS18B20 的读数会稳很多。我遇到过温度值随机跳 0.5℃ 的情况最后定位是 12V 整流滤波后纹波过大7805 输出的 5V 上叠加了毛刺影响 ADC 和单总线时序加大电容后恢复正常。注意继电器模块的 VCC 也是接这路 5V。STM32 的 GPIO 是 3.3V 电平5V 继电器模块输入端大多通过光耦或三极管驱动3.3V 高电平能可靠触发。但模块分高电平触发和低电平触发两种接线前先看板子上的丝印低电平触发模块的输入脚默认要接高用 GPIO 拉低才吸合逻辑写反会导致上电瞬间继电器乱跳。2.2 DS18B20 单总线时序与上拉电阻DS18B20 的 DQ 引脚必须接 4.7k 上拉电阻到 VCCSTM32 侧 GPIO 配成开漏输出。如果线缆超过 30cm上拉电阻可以换成 2.2k否则长线分布电容会让时序沿变缓读回来的数据在 85℃ 和实际值之间反复横跳。温度读取建议固定 12 位分辨率转换时间最长 750ms配合程序的 1s 采样周期刚好。读取时序顺序是复位脉冲、存在脉冲、写 0xCC 跳过 ROM、写 0x44 启动转换、等待、复位、写 0xCC、写 0xBE 读暂存器。每次读回 9 个字节第三第四字节是温度值的整数和小数部分第五字节起是告警阈值和 CRC。CRC 校验不能省单总线受继电器通断时的电磁干扰影响偶发一位翻转很常见不校验的话 PID 会把毛刺当成真实温度导致输出突变。2.3 继电器触点寿命与热得快负载500W 热得快在 220V 下的工作电流约 2.3A普通 5V 继电器触点额定 10A 能扛住但要考虑电弧烧蚀。电磁继电器没有过零检测开断瞬间的电流冲击会缩短触点寿命。程序里 1Hz 的通断频率相当于每小时 3600 次动作长期运行对继电器要求不低。如果是我做量产方案会用 MOC3063 过零触发双向可控硅替换继电器或者选用固态继电器 SSR。程序接口完全不用改GPIO 输出的 1Hz PWM 直接接可控硅触发电路即可。临时实验用电磁继电器也能跑但建议在热得快两端并联一个压敏电阻把开断瞬间的浪涌电压钳住。3. 软件架构PWM 定时、温度采样、OLED 显示与按键状态机3.1 用定时器中断生成 1Hz PWM别用 delay这套系统的时基是 10ms 定时器中断TIM2 每 10ms 进一次中断累计 100 次即为 1 秒周期。占空比通过比较当前计数值和导通计数值实现/* 10ms 定时器中断周期 100 tick 1s */ volatile uint16_t pwm_tick 0; volatile uint16_t pwm_period 100; /* 1Hz */ volatile uint16_t pwm_on 50; /* 占空比 50% */ void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); if (pwm_tick pwm_period) { pwm_tick 0; } /* 前 pwm_on 个 tick 吸合继电器 */ GPIO_WriteBit(RELAY_PORT, RELAY_PIN, (pwm_tick pwm_on) ? Bit_SET : Bit_RESET); } }pwm_on 的范围是 0~100由 PID 输出直接赋值。温度采样、按键扫描、OLED 刷新都放在主循环不要在任何中断里做温度转换等待否则 750ms 的阻塞时间会直接打乱 PWM 节奏。TIM2 的中断优先级设高于串口中断保证蓝牙调参时 PWM 输出不被影响。3.2 DS18B20 读取流程与 1s 采样节奏主循环里维护一个 1 秒状态机第 N 秒启动温度转换第 N1 秒读取上一步的结果并启动下一次转换。这样每个采样周期都拿到一个真实测量值同时避开转换等待阻塞。/* 交替执行启动转换和读取结果 */ void temp_task(void) { static uint8_t convert_pending 0; if (convert_pending) { last_temp read_temp_result(); /* 上次启动的转换已就绪 */ convert_pending 0; } else { ds18b20_start_convert(); /* 发出 0x44启动转换 */ convert_pending 1; } }read_temp_result 里做 CRC8 校验校验失败时保留上一次有效值。宁可让温度显示滞后一秒也不能把错数据喂给 PID。单总线的毛刺多数出现在继电器吸合瞬间这个简单策略能挡住绝大部分干扰。3.3 OLED 局部刷新与按键去抖IIC 四针 OLEDSSD1306的 7 位设备地址通常是 0x3C翻页刷新会全屏闪烁画面里有明显雪花。实际使用时只刷新变化区域温度值变化超过 0.1℃ 才更新对应坐标的字符PID 输出和设定值同理。刷新间隔放进 100ms 的节拍里不要在定时器中断中直接操作 I2CI2C 通信会被高优先级中断反复抢占导致 OLED 响应迟缓。四个独立按键承担菜单与调参功能SET 切换菜单页UP/DOWN 调整数值START 启动停止加热。按键扫描采用状态机加时间戳uint8_t btn_scan(GPIO_TypeDef *port, uint16_t pin) { static uint32_t last_debounce 0; static uint8_t stable_level 1; static uint8_t press_handled 0; uint8_t level GPIO_ReadInputDataBit(port, pin); uint32_t now get_tick_ms(); if (level ! stable_level) { last_debounce now; stable_level level; press_handled 0; } if (level 0 !press_handled (now - last_debounce 20)) { press_handled 1; return 1; /* 一次有效短按 */ } return 0; }按下和释放都经过 20ms 消抖窗口松手后才会触发下一次按键避免长按连发。菜单状态用一个 uint8_t 变量切换每个状态对应一个 OLED 绘制函数比在中断里处理按键干净得多。4. PID 控制与蓝牙调参积分限幅、串口协议与 AT 指令4.1 位置式 PID 与积分分离采样周期和 PWM 周期都固定为 1 秒温度对象是大惯性纯滞后系统所以程序直接用位置式 PID输出映射到 0~100% 占空比float kp 25.0f, ki 0.8f, kd 0.0f; float integral 0.0f; float e_prev 0.0f; float pid_calc(float cur, float set) { float e set - cur; /* 积分分离温差超过 10℃冻结积分防止严重超调 */ if (fabsf(e) 10.0f) { integral 0.0f; } else { integral e; } /* 积分限幅 */ if (integral 300.0f) integral 300.0f; if (integral -300.0f) integral -300.0f; float out kp * e ki * integral kd * (e - e_prev); e_prev e; /* 输出限幅继电器只有加热能力不能制冷 */ if (out 100.0f) out 100.0f; if (out 0.0f) out 0.0f; return out; }积分限幅是关键。没有限幅时从冷水升温到设定值的过程中积分项会不断累积等到温度接近设定值时积分项已经把输出推到饱和实际温度会大幅冲过设定值然后经历漫长的回落。上面的代码把积分输出限在 300 以内意思是积分项最多贡献 ki×300240 的占空比防止它喧宾夺主。积分分离的阈值 10℃ 根据水体热惯性调整热得快功率大、水量少时阈值应收小到 5℃否则刚进入积分区还是会冲过头。Kp 的值反映温差对功率的放大程度。温差 1℃、Kp25 时P 项贡献 25% 占空比这个比例对 500W 加热、5L 水量是合理的起始值。Kd 在原设计中默认不开因为 DS18B20 的温度量化噪声在 0.1℃ 级别温度读数的微分会被放大成剧烈的功率抖动只有在温度曲线出现明显周期性波动时才需要试探性加一点 D。4.2 蓝牙串口调参协议中断收帧主循环解析蓝牙模块接 USART1手机端任意串口调试助手发送以回车换行结尾的字符串命令。协议设计成能直接手打的文本格式不需要专用 App/* 串口中断收帧 */ #define CMD_BUF_SIZE 64 static char cmd_buf[CMD_BUF_SIZE]; static uint8_t cmd_idx 0; volatile uint8_t cmd_ready 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { char ch USART_ReceiveData(USART1); if (ch \n || ch \r) { if (cmd_idx 0) { cmd_buf[cmd_idx] \0; cmd_ready 1; cmd_idx 0; } } else if (cmd_idx CMD_BUF_SIZE - 1) { cmd_buf[cmd_idx] ch; } } }主循环探测 cmd_ready 标志解析命令并执行void parse_cmd(char *s) { float v; if (strncmp(s, SV:, 3) 0) { v atof(s 3); if (v 20.0f v 99.0f) { target_temp v; uart_send_str(ACK SV\r\n); } } else if (strncmp(s, KP:, 3) 0) { v atof(s 3); if (v 0.0f v 200.0f) kp v; } else if (strncmp(s, KI:, 3) 0) { v atof(s 3); if (v 0.0f v 50.0f) ki v; } else if (strncmp(s, KD:, 3) 0) { v atof(s 3); if (v 0.0f v 50.0f) kd v; } else if (strncmp(s, SAVE, 4) 0) { save_params_flash(); uart_send_str(ACK SAVE\r\n); } }每帧命令后面都带范围校验越界值直接丢弃。设定温度限到 99℃ 是给热得快留安全余量如果想测沸点需要程序里另加高温保护逻辑后面会专门讲。命令示例SV:45.5 设置目标温度 45.5℃ KP:20.0 设置比例系数 KI:0.8 设置积分系数 KD:0.5 设置微分系数 SAVE 参数保存到内部 Flash手机端用蓝牙 SPP 类的调试工具就能连。蓝牙模块上电后默认是 AT 模式还是透传模式取决于模块固件。用 USB-TTL 转接板通过串口给蓝牙模块发指令ATUART9600,0,0 设置波特率 9600停止位 1无校验 ATROLE0 设为从模式 ATNAMEWaterPID 设置广播名波特率选 9600 而不是 115200是为了长距离蓝牙串口传输时的抗干扰能力。串口初始化时对应设置 9600 8N1中断接收使能。如果蓝牙模块的 TX 是 3.3V 电平和 STM32 的 RX 直连没问题但 STM32 的 TX 是 3.3V接到 5V 供电蓝牙模块的 RX 前要确认模块 RX 是否兼容 3.3V不兼容就加一级二极管电平转换。4.3 参数持久化到单片机内部 FlashC8T6 没有独立 EEPROM但内部 Flash 有 128KB最后一页 2KB 可以用来存参数。每次改完参数执行 SAVE 命令时先擦除整页再写入#define PARAM_ADDR 0x0801F800 /* 最后一页起始地址 */ void save_params_flash(void) { FLASH_Unlock(); FLASH_ErasePage(PARAM_ADDR); float params[4]; params[0] target_temp; params[1] kp; params[2] ki; params[3] kd; uint32_t *p (uint32_t *)params; for (int i 0; i 4; i) { FLASH_ProgramWord(PARAM_ADDR i * 4, p[i]); } FLASH_Lock(); }上电启动时读这 16 字节前 4 字节判断是否全为 0xFF是则视为首次上电加载默认参数否则直接使用 Flash 里的值。注意 Flash 擦写寿命约 1 万次不要每次蓝牙改数都自动保存用显式 SAVE 命令更稳妥。5. 临界比例度法整定与排错温度曲线和继电器抖动怎么办5.1 用临界比例度法做第一轮粗调先把 Ki、Kd 设 0Kp 从 5 开始每 5 分钟记录一次温度曲线。逐渐增大 Kp直到温度在设定值附近出现等幅振荡记下此时的 Kp 为临界增益 Kcr振荡周期为 Tcr。然后用经验公式计算参数参数经验公式Kp0.45 × KcrKiKp / (0.83 × Tcr)KdKp × 0.125 × Tcr加热水这类一阶惯性加纯滞后对象临界振荡周期通常在 60 到 180 秒之间。整定过程中观察温度曲线的形状比看数字更直观可以把串口输出的时间、温度、占空比喂给匿名上位机用波形窗口实时看响应。5.2 常见现象与对应调整现象原因调整方向温度超调大过冲后缓慢回落积分过强或 Kp 偏大减小 Kp 或 Ki检查积分限幅是否饱和设定值附近持续振荡周期稳定Kp 偏大临界振荡按公式减小 Kp必要时加一点 Kd温度长期低于设定值 1~2℃ 不消除积分太弱增大 Ki或检查积分限幅是否夹得太小继电器频繁通断肉眼可见抖动输出在阈值附近小幅波动对输出做 2% 滞回或降低 Kd 和采样灵敏度大温差下不加热或全功率不受控程序保护逻辑介入或积分分离误判查温差保护阈值确认目标温度设置合法5.3 安全保护与最终验证程序里保留两类保护。一是输出保护目标温度设置在 20~99℃ 之间超过 100℃ 直接清空输出占空比实测水温超 105℃ 时无论 PID 输出多少都强制断开继电器。二是干烧提醒热得快必须浸没在水中才能加热如果做实际烧水实验建议在加热管附近加一个干簧管液位开关干烧时硬件断开继电器这是电磁继电器方案下最容易忽视的安全问题。一个实用的最终验证方法设定目标温度 60℃记录从冷水开始到第一次达到设定值的时间同时记录超调量和稳定时间。把 PID 参数换成临界比例度法算出的值再跑一次两次曲线对比通常能把超调从 8℃ 压到 2℃ 以内稳定时间控制在 10 分钟量级。调到这个程度这套 1Hz 继电器 PID 温控就可以放心用于鱼缸、热水壶或者小型恒温水浴场景了。本文还有配套的精品资源点击获取