STM32蓝牙PID水温控制:1Hz低频继电器实现大功率恒温
2026/9/16 16:11:16 网站建设 项目流程

简介:基于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 陶瓷电容,输出侧也并 100uF,DS18B20 的读数会稳很多。我遇到过温度值随机跳 0.5℃ 的情况,最后定位是 12V 整流滤波后纹波过大,7805 输出的 5V 上叠加了毛刺,影响 ADC 和单总线时序,加大电容后恢复正常。

注意继电器模块的 VCC 也是接这路 5V。STM32 的 GPIO 是 3.3V 电平,5V 继电器模块输入端大多通过光耦或三极管驱动,3.3V 高电平能可靠触发。但模块分高电平触发和低电平触发两种,接线前先看板子上的丝印,低电平触发模块的输入脚默认要接高,用 GPIO 拉低才吸合,逻辑写反会导致上电瞬间继电器乱跳。

2.2 DS18B20 单总线时序与上拉电阻

DS18B20 的 DQ 引脚必须接 4.7k 上拉电阻到 VCC,STM32 侧 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 秒启动温度转换,第 N+1 秒读取上一步的结果并启动下一次转换。这样每个采样周期都拿到一个真实测量值,同时避开转换等待阻塞。

/* 交替执行:启动转换和读取结果 */ 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 四针 OLED(SSD1306)的 7 位设备地址通常是 0x3C,翻页刷新会全屏闪烁,画面里有明显雪花。实际使用时只刷新变化区域:温度值变化超过 0.1℃ 才更新对应坐标的字符,PID 输出和设定值同理。刷新间隔放进 100ms 的节拍里,不要在定时器中断中直接操作 I2C,I2C 通信会被高优先级中断反复抢占,导致 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×300=240 的占空比,防止它喧宾夺主。

积分分离的阈值 10℃ 根据水体热惯性调整:热得快功率大、水量少时阈值应收小到 5℃,否则刚进入积分区还是会冲过头。

Kp 的值反映温差对功率的放大程度。温差 1℃、Kp=25 时,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 转接板通过串口给蓝牙模块发指令:

AT+UART=9600,0,0 设置波特率 9600,停止位 1,无校验 AT+ROLE=0 设为从模式 AT+NAME=WaterPID 设置广播名

波特率选 9600 而不是 115200,是为了长距离蓝牙串口传输时的抗干扰能力。串口初始化时对应设置 9600 8N1,中断接收使能。如果蓝牙模块的 TX 是 3.3V 电平,和 STM32 的 RX 直连没问题;但 STM32 的 TX 是 3.3V,接到 5V 供电蓝牙模块的 RX 前,要确认模块 RX 是否兼容 3.3V,不兼容就加一级二极管电平转换。

4.3 参数持久化到单片机内部 Flash

C8T6 没有独立 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 设 0,Kp 从 5 开始,每 5 分钟记录一次温度曲线。逐渐增大 Kp,直到温度在设定值附近出现等幅振荡,记下此时的 Kp 为临界增益 Kcr,振荡周期为 Tcr。然后用经验公式计算参数:

参数经验公式
Kp0.45 × Kcr
KiKp / (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 温控就可以放心用于鱼缸、热水壶或者小型恒温水浴场景了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询