简介基于STM32的蓝牙PID水温控制程序是一套面向嵌入式初学者与电子爱好者的完整工程源码适合学习温度闭环控制与PID算法应用。项目以STM32F103C8T6为核心通过DS18B20采集水温使用PWM信号控制继电器以调节500W热得快的加热功率实现温差大时大功率输出、温差小时精细通断的智能控温策略并支持OLED实时显示与按键调整目标值。资源包共84个文件以36个C源文件和39个头文件为主体涵盖DS18B20驱动、PID控制、按键、OLED、蓝牙等模块代码同时包含hex固件可直接烧录、uvprojx工程文件便于Keil编译整体大小仅314KB结构清晰。目前已有1283人学习下载适合课程设计或DIY恒温装置时参考。借助这套程序读者可掌握PWM占空比调节加热功率、继电器通断控制及PID参数整定方法并能利用手机蓝牙模块便捷修改目标水温是一份高性价比的嵌入式实践资料。1. 基于STM32的蓝牙PID水温控制程序在解决什么问题一个500W的“热得快”直接插220V水里通断之间水温会像荡秋千一样反复超调温度过冲轻松超过10℃到不了设定值还浪费时间。把STM32、温度传感器、继电器和蓝牙模块组合成闭环用PID算法去控制加热丝的导通比例水温才能平稳逼近并稳定在目标温度。这个场景对应的就是“基于STM32的蓝牙PID水温控制程序”它本质上是一个典型的小型温度控制对象常见于智能鱼缸、水浴锅、洗脚盆DIY和实验室恒温装置。适合打算用单片机做温控但又不想只写开关式控制的人也适合毕设或竞赛项目需要展示闭环控制、无线交互和工程调参能力的人。做这类程序的核心不是把PID公式背下来而是要理解“500W热得快 水”这个被控对象的大滞后、大惯性特征并在此基础上选择输出执行方式、采样周期和PID形式。下面这套方案以STM32F103C8T6为控制核心使用DS18B20测温、光耦驱动继电器控制220V加热棒、通过HC-05蓝牙模块与手机或上位机通讯按照工程上最常见的方式组织代码和参数可以直接在自己的开发板上复现。2. 系统拆解从500W热得快到STM32的PID温控回路2.1 温控对象的特性500W热得快为什么不能直接通断500W在220V下的稳态电流约2.3A发热丝热容量小但水体热容量大热传导路径很长。用“达到设定温度就断开、低过设定温度就吸合”的开关控制水温会出现明显的周期性波动。这是因为水加热时热量从加热丝表面向周围水体扩散需要时间温度传感器测到的温度滞后于加热丝表面温度等到传感器读数到达设定值水体里已经积累了过多热量切断加热后余热还会让温度继续上升几度。温度越高、水量越少这种滞后越明显。工程上解决滞后有两种思路一个是引入PID的微分项做提前判断另一个是采用PWM方式按比例输出功率而不是0/1满功率切换。常见的做法是两者结合即把控制周期固定成比如2秒或5秒在这个周期内按PID计算结果控制继电器的导通时间百分比。例如设定PID输出为60%则每个控制周期内继电器导通60%时间、断开40%时间。这样加热功率连续可调水温变化更平滑。需要考虑的是继电器机械寿命和通断噪声。控制频率越低温度波动越大频率越高继电器触点磨损越严重。实际中控制周期设在210秒是折中方案也有人用固态继电器提高通断频率。无论哪种采样周期都应当让PID看到足够多的温度变化量避免在温度噪声上反复输出。2.2 控制回路选型位置式PID与增量式PID在加热场景怎么选PID标准公式中位置式输出直接对应执行机构的绝对位置。对于加热棒位置式PID输出的绝对值就是加热功率百分比理解直观代码也简单。但位置式算法存在两个问题积分项会累计历史误差系统长时间运行后可能累计出很大的积分值切换设定值或手动干预后输出容易猛跳一下对继电器和供电线路冲击明显。增量式PID输出的是本次控制量相对上一次的变化量最终执行器还需要把增量累加回当前输出。增量式算法天然带有记忆功能即使积分项没有单独计算系统也能通过累积输出量体现出积分效果。此外增量式PID自动剔除了过去所有误差的累加和误动作影响小手动/自动切换时冲击也小。在STM32这类资源充足但实时性要求高的场景我一般推荐增量式因为它的输出结果在数值上更平稳适合配合PWM周期输出。增量式PID的计算公式可以表达成以下形式delta_u Kp * (error - error_prev) Ki * error Kd * (error - 2 * error_prev error_prev2)其中delta_u是本次输出增量error是当前温度偏差设定值减测量值error_prev是上一次偏差error_prev2是上上次偏差。Kp、Ki、Kd分别是比例、积分、微分系数。最终输出u delta_u;然后对u做限幅比如限制在0到100之间防止积分饱和导致输出长时间卡在最大值。这个限幅动作在加热控制中比PID公式本身更值得重视因为水温对象的滞后大如果不限制累积量启动阶段加热棒会一直满功率工作水温超过设定值很多才停下来。2.3 硬件清单与引脚规划STM32、DS18B20、继电器/固态继电器、HC-05蓝牙模块搭建这套系统用到的常见物料清单如下表。以STM32F103C8T6最小系统板为例ADC、定时器、USART等外设均够用不需要外扩芯片。器件选型建议说明主控MCUSTM32F103C8T6便宜易买Keil MDK或STM32CubeIDE均可开发温度传感器DS18B20防水探头单总线协议测温范围-55~125℃精度0.5℃继电器模块5V低电平触发继电器带光耦隔离MCU与220V强电避免地线干扰加热执行500W热得快功率固定无法直接调功只能靠通断占空比控制蓝牙模块HC-05经典蓝牙2.0串口透传手机配对后即可收发数据电源12V/1A适配器 AMS1117-3.3给STM32、继电器和蓝牙模块供电上位机手机蓝牙串口助手或PC虚拟串口用于下发设定值、实时显示温度和PID参数引脚规划方面DS18B20的数据线接到PB1并外接4.7kΩ上拉电阻到3.3V继电器控制引脚用PB0配置为推挽输出低电平触发HC-05的TXD接STM32的PA10USART1_RXRXD接PA9USART1_TX波特率一般设置为9600或38400如果想在调试时同时查看串口日志可以再分配一个USART2给调试口。PB0输出驱动继电器时STM32和继电器模块之间虽然模块自带了光耦仍建议把继电器供电和MCU供电的GND在一点连接避免220V侧干扰串入温度测量。3. 用STM32把PID温控程序跑起来最小代码与配置3.1 温度采样DS18B20的读取与滤波DS18B20使用单总线协议一根数据线既能送指令又能读结果。初始化时主机先拉低总线480μs以上然后释放并检测从机的存在脉冲。随后发送ROM命令跳过地址检测再发功能命令启动温度转换。转换时间由精度决定12位转换约750ms因此采样周期至少要大于800ms。代码实现时可以用定时器触发每1秒读取一次温度这样还能规避带有版权的CRC校验库问题自己实现单总线时序。float DS18B20_ReadTemperature(void) { uint8_t tempL, tempH; int16_t raw; DS18B20_Reset(); DS18B20_WriteByte(0xCC); // 跳过ROM匹配 DS18B20_WriteByte(0x44); // 启动温度转换 // 等待转换完成这里用延时函数阻塞等待 HAL_Delay(750); DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读取暂存器 tempL DS18B20_ReadByte(); tempH DS18B20_ReadByte(); raw (tempH 8) | tempL; return raw * 0.0625f; }这段代码先把温度转换为12位原始值再乘以0.0625得到摄氏度。读取到的温度还需要做中值滤波或滑动平均因为水体在加热过程中存在微循环传感器表面可能短暂出现波动。常见做法是连续取5次样本排序后取中位数再对这个中位数做一阶惯性滤波滤波系数取0.2左右既保留温度变化趋势又不会让PID看到一个剧烈跳变的测量值。滤波参数不要取太大否则会让滞后更严重PID的微分项也容易失效。3.2 PID计算增量式PID的C语言实现以下是一个适合STM32的增量式PID结构体和计算函数。typedef struct { float target; // 目标温度 float actual; // 当前温度 float kp; // 比例系数 float ki; // 积分系数 float kd; // 微分系数 float integral; // 累积输出注意限幅 float error; // 当前误差 float error_prev; // 上一次误差 float error_prev2; // 上上次误差 float output; // PID输出值 0~100 float output_limit; } PID_Typedef; void PID_Increment_Calc(PID_Typedef *pid) { float delta_u; pid-error pid-target - pid-actual; delta_u pid-kp * (pid-error - pid-error_prev) pid-ki * pid-error pid-kd * (pid-error - 2.0f * pid-error_prev pid-error_prev2); // 累积输出并限幅防止积分饱和 pid-integral delta_u; if (pid-integral pid-output_limit) pid-integral pid-output_limit; else if (pid-integral 0.0f) pid-integral 0.0f; pid-output pid-integral; // 保存历史误差 pid-error_prev2 pid-error_prev; pid-error_prev pid-error; }这段代码中delta_u是本次增量积分项采用误差值直接叠加等效于标准的增量式PID中的Ki*e(k)。限幅值output_limit通常设置为100代表加热功率百分比的上限。注意当目标温度接近当前温度时delta_u会逐渐变小如果此时积分项已经偏大输出会在一段时间内继续加热这就是必须限幅的原因。若液体的热容量大且散热大输出上限可以放宽到100若热得快功率相对水量过大建议把上限限制在6080靠牺牲部分启动速度换取更小的超调。3.2.1 参数与抗饱和增量式PID在加热控制中的积分饱和现象比想象中更常见。如果设定温度为50℃实际温度从20℃开始误差大PID输出很快顶到100并持续累积。水温到达50℃时积分项可能已远远超过100即使此时delta_u变负也要好几拍才能把输出拉下来结果就是极大的超调。缓解手段有三种输出限幅、积分分离、遇限削弱。积分分离的做法是当误差绝对值超过阈值如2℃时Ki直接置0只用PD控制快速靠近设定值误差进入阈值内后再启用Ki消除稳态误差。遇限削弱则是在输出达到上限时只累加负向delta_u正向增量不再累计。把这两种方式加到PID_Increment_Calc函数中代码量不大但能在温度控制场景显著改善超调。3.3 输出执行过零检测与PWM控制的加热功率调节增量式PID计算出的output是0~100之间的浮点数如何应用到500W热得快上决定了实际控温效果。常见的执行方案有两种定时器产生PWM波控制继电器或者在每个控制周期内用output换算成导通时间。采用PWM方案时把PWM周期设成2秒是比较保险的。比如output60则在一个2秒周期内前1.2秒继电器吸合加热后0.8秒断开。STM32定时器的比较输出寄存器可以很容易产生这个波形但要注意定时器中断里不要直接做DS18B20读取因为单总线时序对延时精度要求高会被中断打断。工程中更稳妥的方式是主循环按时间片调度每1秒触发温度采集每2秒进行一次PID计算并更新输出。输出更新时用TIM2产生1Hz或0.5Hz的PWM通过修改比较寄存器设置占空比。代码大致如下uint32_t duty (uint32_t)(pid.output * TIM2_Period / 100.0f); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, duty);若使用继电器而非固态继电器PWM频率不宜超过0.5Hz否则继电器触点会频繁通断导致发热熔焊。频率太低又会造成水温小幅波动需要在实验时折中。由于热得快是纯阻性负载过零检测电路虽然能减少通断瞬间的电流冲击但增加电路复杂度。在继电器方案中可以不用真正的过零检测而是把导通时间段固定为交流电的自然周波整倍数比如每次控制周期导通时间用0.5秒的整数倍切分减少随机断相带来的积不对称发热。3.4 通过蓝牙下发设定温度与读取实时曲线3.4.1 HC-05蓝牙模块的连接与AT指令配置HC-05蓝牙模块默认作为从机上电后指示灯慢闪可以用手机搜索配对配对密码一般是1234。若需要改配对密码或波特率需要在未连接状态下按住模块上的按键上电进入AT模式指示灯慢闪变为每2秒闪一次再通过USB转TTL连接模块的串口。AT指令测试最常见的问题是“HC05蓝牙模块连接不上”原因多为电平不匹配或波特率错误。HC-05的TXD和RXD都是3.3V电平接STM32的3.3V USART引脚刚刚好但如果用5V单片机的串口或USB转TTL模块则需要确认电平转换。AT模式下默认波特率38400偶校验与正常透传模式下的9600不同很多人在这一步卡住。进入AT模式后发送指令序列AT ATNAMESTM32-Heater ATROLE0 ATUART9600,0,0 ATRESET每条指令返回OK后进入下一条。设置波特率为9600、无校验、1个停止位后模块会记住参数重新上电后以从机模式用9600波特率透传。STM32侧的USART1初始化也使用9600波特率8N1这样手机APP发送“SET:60.5”这样的指令STM32就能在串口中断中收到并解析出来。3.4.2 串口通信协议设计设定值、反馈值、PID参数调温和观察运行状态需要一个轻量级协议二进制或文本均可。文本协议更直观便于用串口调试助手验证。常见设计如下SET:60.0 # 设置目标温度 GET # 查询当前温度 PID:Kp,Ki,Kd # 动态调整PID参数STM32在串口接收中断中按行缓冲遇到回车换行后解析。解析时注意接收缓冲区和解析缓冲区共享的问题简单做法是把解析逻辑放在主循环串口中断只负责数据收集void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { rx_buf[rx_len] rx_byte; if (rx_byte \n) { frame_ready 1; } HAL_UART_Receive_IT(huart, rx_byte, 1); } }主循环中判断frame_ready后用sscanf提取数值。解析完成后自动回复数据帧TEMP:59.8,TARGET:60.0,OUT:45,PID:120,3,50这个格式包含当前温度、目标温度、输出百分比和当前PID参数手机端的蓝牙串口助手可以直接显示。如果自行开发APP也建议保持同样的文本分帧方式方便后期在串口助手下调试。把这种无线反馈通道和“stm32串口调试pid”结合起来就能在不接下载器的情况下调整整定参数大大缩短调参迭代时间。4. 参数怎么调PID整定与水温控制的常见坑4.1 手动整定的步骤临界比例度法与试凑法水温控制对象没有统一固定的Kp、Ki、Kd经验值因为水量的多少、初始温度、散热条件、热得快功率都直接影响被控对象特性。一个可行的手动整定流程如下先设置积分和微分系数为0只保留比例Kp。把目标温度设定为高于当前水温10℃左右。观察温度曲线的振荡情况。如果温度到达后衰减缓慢且振荡次数多增大Kp如果上升太慢且稳态误差大也增大Kp如果温度曲线剧烈震荡则减小Kp。找到临界比例增益记录此时温度振荡周期或约等于控制周期的数值。根据临界比例度法经验公式估算初始PID参数。在此基础上做积分和微分微调直到超调量小于10%稳态误差小于±0.5℃。这种方法是把被控对象近似为线性系统。实际水温对象在温度升高后对外散热加快系统的静态增益会变化所以整定结果只能在某个目标温度附近有效。如果目标温度从40℃改到80℃建议重新检查参数。4.2 水温控制特有的滞后与超调问题水温控制最大的问题是纯滞后。热量从加热丝传到DS18B20探头需要时间水越深、流动性越差滞后时间越长。滞后小时内PID看到的是过去状态下的温度反馈信息滞后任何控制器都会因此产生超调。常见的补偿思路包括传感器位置尽量靠近出水口或水流通道而不是贴在加热丝边上。用循环水泵增加水流扰动让水温和传感器温度更接近。如果应用允许加一个小水泵可以让控制效果提升一个档次。增大采样周期的同时适当减小微分系数。微分项对滞后系统很敏感滞后大时微分作用会被测量噪声放大导致控制阀振荡反而加剧温度波动。如果想通过程序补偿滞后可以尝试Smith预估器。那需要建立水体的近似一阶惯性加纯滞后模型。对STM32来说实现并不复杂但工程上少有人用因为模型参数也随水量变化。更实用的方式是采用“级联PID控制”思路内环控制加热丝附近温度外环控制水体温度。内环反应快外环修正目标值两个PID级联后对滞后的适应能力更强。使用500W热得快时内环温冲过大级联PID的调参难度比单回路高不建议初级玩家一上来就采用。4.3 常见异常排查温度跳变、蓝牙掉线、继电器粘连温度跳变是最常被问到的问题。DS18B20通过长导线连接时如果导线靠近继电器模块和220V电源线测温结果会周期性出现85℃或-127℃这样的尖峰。原因是强电侧通断产生的电磁干扰耦合到单总线数据线上MCU读取到错误的暂存器内容。排查时把传感器线缩短并与电源线分开布线或者改用屏蔽线将屏蔽层单端接地。程序层面也可以连续读3次温度舍弃与前后值差异超过5℃的数据。HC-05掉线的案例也不少。蓝牙模块和STM32共用一个3.3V电源时继电器吸合瞬间电流波动可能让蓝牙射频前端复位。继电器模块的电源单独从5V取电蓝牙模块的电源引脚用10μF0.1μF电容滤波。如果掉线频繁可以在蓝牙模块VCC和GND之间并联一个ESD保护二极管但核心还是电源去耦。继电器粘连则是机械继电器的通病。500W热得快虽在继电器触点规格内但继电器吸合和断开时电弧仍会缓慢磨损触点。如果发现温度一路超过设定值无法下降大概率是触点熔焊。程序上可以做“输出0时禁止加热引脚持续为高”的自检逻辑更保险的做法是用检测电路量测加热丝两端的电压但这个会增加复杂度。工程上更推荐的还是把继电器换成固态继电器输入侧用光耦隔离输出侧只承受阻性负载寿命高得多。5. 进阶把PID参数自整定和故障保护加进去5.1 简单的自整定方案继电器通断激励与极限环法手动整定只能在工作温度范围内取得一个可接受的参数组合环境变化后需要重新调。可以让程序自己完成一次简单的整定过程进入自整定模式后程序控制继电器以50%占空比交替通断观察温度响应曲线。这种方法在工控领域称为极限环法本质上是让系统在继电特性下产生等幅振荡再从振荡周期和幅值估算临界增益和临界周期。实现时用两个状态变量记录最近一次升温段和降温段的峰谷温度值当检测到相邻两个波峰的幅值差异小于0.3℃时认为振荡稳定。然后按以下规则计算初始PID参数Ku 4 * d / (π * a) Tu 周期时间 Kp 0.6 * Ku Ki 1.2 * Ku / Tu Kd 0.075 * Ku * Tu其中d为继电输出幅值的一半a为温度振荡幅度的一半。这个算法在STM32上实现只需一个状态机和若干全局变量但要注意振荡周期可能长达数十秒或几分钟程序不能陷入轮询等待应继续通过实时时钟调度完成巡检。自整定得到的参数是带有人工规则的经验值实际运行时可能还需要手动微调。如果你的PLC或控温仪表支持自整定原理与之类似可以参考它的整定结果来验证自己的算法是否正确。5.2 干烧保护、掉线保护和看门狗逻辑水温控制程序只写好PID是远远不够的工程上更重视保护逻辑。热得快是直接接触水的电热器干烧会让加热丝烧断甚至引发危险因此程序必须采集温度变化速率和设定值这两个保护条件。干烧检测的核心逻辑是如果持续加热超过一定时间且温度上升速率超过阈值比如2秒内升高超过5℃立即切断继电器并置故障标志。另一种更简单的判定是温度低于20℃且连续加热5分钟后温升不足1℃认为传感器不在水里或水量过少。这两种逻辑要同时具备防止传感器脱落导致误判。蓝牙掉线不能影响加热控制的闭环。如果手机发来设定值后长时间不再连接程序不应停止加热而应当保持最近一次有效设定值但把输出上限降低到50%。同时启用独立看门狗主循环每个周期翻转一次喂狗引脚防止程序跑飞导致继电器常闭。由于STM32的IWDG一旦开启无法在运行时关闭需要在初始化时设置合适的超时时间比如2秒主循环执行周期本身不超过100ms这样即使出现异常也能在几个周期内复位复位相关外设。5.3 验证方法用蓝牙串口记录数据并绘制温度曲线调完参数后如何确认PID效果用眼睛观察数码管的数字抖动不够准确建议把蓝牙发送的实时数据帧记录到手机或PC端再离线绘制温度曲线。轻量级的做法是使用串口助手自带的日志保存功能在电脑上接收STM32发来的TEMP数据帧保存成CSV文件再用Excel或Python画出曲线。import csv import matplotlib.pyplot as plt times [] temps [] targets [] with open(heater_log.csv, r) as f: reader csv.reader(f) for row in reader: times.append(float(row[0])) temps.append(float(row[1])) targets.append(float(row[2])) plt.plot(times, temps, labeltemperature) plt.plot(times, targets, labeltarget) plt.xlabel(time (s)) plt.ylabel(temperature (℃)) plt.legend() plt.show()这个脚本把蓝牙串口保存的数据文件绘制成曲线通过曲线可以直观看到超调量、稳态误差和振荡周期。实际验证时先记录一段从启动到稳定的全过程再把数据导入脚本。如果发现超调超过2℃优先减小Kp及增加积分分离阈值如果稳态误差明显则小幅增加Ki如果温度曲线在设定值附近高频抖动则减小Kd或检查温度滤波是否过于激进。这种“蓝牙数据回传 Python绘图”的验证链路可以快速发现手动调参时不易察觉的周期性波动。本文还有配套的精品资源点击获取