简介:这是一份基于STM32的智能热水器系统完整设计资源,适合嵌入式方向的学生、毕业设计者及电子竞赛参与者。方案主控采用STM32,通过PCF8591芯片加滑动变阻器模拟温度、水位检测,配合光敏电阻判断光照强度,并利用DS1302实现实时时钟,OLED屏显示各项状态;继电器与LED灯模拟水循环、太阳能加热、电辅助加热和加水等工作,完整覆盖低温防冻循环、光照不足切换电加热等典型场景。
资源包共283个文件,大小约16.94MB,以C语言源码、头文件、Keil工程文件、Proteus仿真文件为主,同时包含hex与axf固件、docx设计文档和wmv演示录屏,便于直接打开工程查看整体架构,也可配合仿真调试运行。文件目录层级清晰,适合按模块学习传感器采集、逻辑判断与执行控制的设计方法。
目前已有103人学习浏览,整体方案模块化程度高,可作为课程设计或综合实训的参考模板。
1. 先把水温忽冷忽热这件事拆成嵌入式问题
冬天洗澡最怕两件事:水温突然跳变,或者等半天热水器没反应。传统机械式热水器靠双金属片和弹簧触点做温度开关,误差能到正负五度,水压一变整段水就凉了。所谓“智能热水器”,本质上就是用一个能感知温度、能控制加热、能跟外部设备说话的控制器,替代那套笨重且迟钝的机械机构。STM32在其中的角色很明确:它不负责加热,也不负责储水,它负责把所有离散的硬件——传感器、继电器、显示屏、按键、WiFi模块——组织成一个闭环系统。
这篇内容面向两类人。第一类是正在做毕业设计或者课程项目的学生,需要一套能写进论文又能真正跑起来的架构;第二类是想把自家热水器改造成可远程控制的工程师,想知道采样、控制、通信之间怎么取舍。标题里的20250309不妨理解为项目的一个版本标识,实际开发中我习惯用日期做工程归档,方便回溯“哪一版固件配哪一版原理图”。下面这套方案,是围绕“安全、恒温、可交互”三个核心指标展开的,不含任何云平台私有协议,全部基于通用外设和公开协议实现。
2. 硬件架构选型:从传感器到加热棒之间的关键取舍
2.1 温度传感器选型:DS18B20还是NTC热敏电阻
热水器系统的第一道数据关口是温度采集。主流方案有两种:DS18B20数字温度传感器和NTC(负温度系数)热敏电阻。这两种我都实际用过,各有明显偏向。
DS18B20的优势是接口简单,单总线协议,占用一个GPIO就能完成采集,精度正负0.5摄氏度,测温范围-55到125摄氏度,对热水器场景完全够用。但它在强电环境下有隐患:单总线协议对时序要求严格,线长超过2米并且和220V加热线绑在一起走线时,容易受电磁干扰导致读数为85摄氏度(这是DS18B20复位后的默认值,也是最容易误判成“干烧”的一种假象)。
NTC方案则不一样。它本质是电阻分压,输出模拟量,需要配合ADC采样和查表换算。成本比DS18B20低大约一半,响应速度更快——这对水温控制来说是比较重要的指标,因为水的热惯性大,传感器反应慢会导致PID过冲。
我一般建议:毕业设计优先选DS18B20,轮询读取即可,代码量小,调试省时间;实际产品改选NTC,因为热水器的温度变化速率不算快,NTC的快速响应配合软件滤波后,控制效果明显更平滑。
如果选NTC,典型的分压电路是三线制接法:
// NTC分压采样引脚配置(以STM32F103C8T6为例,PA1为ADC通道1) GPIO_InitTypeDef gpio = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_ADC1_CLK_ENABLE(); gpio.Pin = GPIO_PIN_1; gpio.Mode = GPIO_MODE_ANALOG; // 模拟输入模式 gpio.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &gpio);这段配置的含义是:将PA1设为模拟输入,不启用上下拉电阻,避免额外电阻影响NTC分压精度。ADC引脚不要开下拉,否则分压比会被改变,导致换算出的温度偏高。
具体温度换算采用“查表+线性插值”的方式,因为NTC的阻值与温度呈指数关系,直接在线计算exp函数会让MCU花费大量时间在浮点运算上,定时器中断里跑实数运算还会引入采样抖动。
2.2 加热执行器:继电器与可控硅的取舍
执行器是整个系统里直接接触强电的部分,安全边界必须在这里划清楚。
方案A是机械继电器。优点是导通电阻几乎为零,不产生额外热量,成本低;缺点是机械触点寿命有限,且通断瞬间会拉弧,产生EMI干扰,可能让ADC采集出现尖刺。方案B是双向可控硅(Triac),配合MOC3021光耦做隔离驱动,支持PWM斩波调功,可以实现比通断式更细腻的加热控制。
热水器本身具备大热容量,水的温度变化是分钟级的,不需要毫秒级的功率调节。因此我倾向于:用继电器做离散通断控制,配合5到10摄氏度的滞回区间,就能把温控精度做到正负1摄氏度左右,而且比可控硅方案多一道物理断开的保护层级。
需要注意的是,无论选择哪一种,MCU的GPIO绝对不能直接驱动继电器线圈或可控硅的门极。必须经过光耦隔离。常见做法是GPIO串联一个1k电阻接到光耦输入端,光耦输出端接继电器驱动三极管或达林顿管,继电器供电用独立的12V电源,与MCU的3.3V电源在PCB走线上严格分区。
2.3 电源系统设计:为什么不能用AMS1117直接从220V取电
整个系统最容易被忽略又最致命的是电源设计。很多初学者会问:既然Control Board上的STM32是3.3V供电,能不能直接把220V整流之后接AMS1117?
答案是不能。原因有两层。第一层是安全标准:热水器属于涉水涉电设备,电源侧和用户侧之间必须满足加强绝缘或双重绝缘的要求,线性稳压器无法提供这种隔离等级。第二层是工程稳定性:加热棒启动瞬间,电网会有较大的压降和噪声,非隔离电源会把浪涌直接引到MCU复位引脚上。
规范的方案是外置一颗隔离型AC-DC电源模块,输出5V,再在板内用AMS1117线性稳压到3.3V供MCU和外设使用。
| 项目 | 隔离AC-DC模块 | 非隔离阻容降压 |
|---|---|---|
| 安全隔离 | 有(一般满足2500VAC隔离) | 无 |
| 输出纹波 | 50mV以内 | 较大,随负载变化 |
| 成本 | 中高 | 低 |
| 适用场景 | 热水器、电饭煲等人体可触及设备 | 灯具等不会触碰金属外壳的设备 |
电源设计里另一个容易踩坑的点是“复位”,也就是MCU在继电器吸合的瞬间发生复位或卡死。这不是程序逻辑错误,而是继电器的VCC引脚没有加足够大的储能电容。继电器线圈在吸合瞬间需要的电流是稳态的5到10倍,如果12V电源线较细或PCB走线过长,线圈吸合瞬间会把MCU的供电电压拉低到复位阈值以下。解决这种问题的标准做法,是在继电器电源端加一个100uF以上的电解电容就地储能,再在MCU的VDD引脚旁边放0.1uF陶瓷电容滤高频。
3. 固件分层:温度采集、滤波与PID控制代码实现
3.1 用ADC+DMA连续采样避免阻塞式读取
如果采用NTC方案,采温程序就不能用单次阻塞式ADC采集。常见的错误是这样写的:在While循环里调用HAL_ADC_Start,等待转换完成,然后读取结果。这套流程的问题在于,ADC转换期间CPU处于忙等状态,这段时间里按键扫描、显示刷新、看门狗喂狗全都被堵住了。更麻烦的是,如果在转换到一半时开启了全局中断,还会引入采样时序抖动。
正确的架构是:ADC1使用DMA循环模式,让转换结果持续不断写入缓冲区,主循环只负责读取最近的一组数据做滤波。
// ADC + DMA 循环采样配置 ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1; __ALIGN_BEGIN static uint32_t adc_buffer[8] __ALIGN_END; // DMA直接写入内存 // ADC1初始化:8次采样放在DMA缓冲区里做软件平均 void ADC_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = DISABLE; // 单通道,不扫描 hadc1.Init.ContinuousConvMode = ENABLE; // 连续转换,永远不停 hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发即可 hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; HAL_ADC_Init(&hadc1); // DMA配置:循环模式,搬完一轮自动重装 hdma_adc1.Instance = DMA1_Channel1; hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; hdma_adc1.Init.Mode = DMA_CIRCULAR; // 关键参数:循环 HAL_DMA_Init(&hdma_adc1); __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1); HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 8); } // 主循环里取8次数据的均值 uint32_t Get_Temperature_ADC(void) { uint32_t sum = 0; for(int i = 0; i < 8; i++) { sum += adc_buffer[i]; } return sum / 8; }这里有一个参数细节值得展开:DMA缓冲区长度取了8,对应8次ADC转换求平均。这个数字不是拍脑袋定的,而是基于50Hz工频干扰的周期推算出来的。我国的市电是50Hz,由加热棒等大功率器件引入的工频干扰周期是20ms,ADC从启动到完成一次转换约10到20微秒,那么8次采样可以覆盖约160微秒的时间跨度,在这个跨度内做均值,可以有效平滑掉工频干扰的局部周期性。如果你发现温度值仍跳动明显,可以把DMA缓冲区调到16或32重试,但不要无限加大——缓冲区越宽,温度响应越钝,后续PID拿到的是“陈旧”的温度。
3.2 PID控制:温度到加热之间如何做无静差调节
加热系统的控制对象是一个带很大滞后的一阶惯性环节。如果用最朴素的“低于阈值就开继电器,高于阈值就关”开关控制,结果是温度会围绕设定点呈现三角波震荡。机械热水器的双金属片开关就是这个思路,所以它的温度波动能到正负5度。
好的控制策略是位置式PID,这是嵌入式温度控制最常见的做法,公式为:
u(k) = Kp * e(k) + Ki * Σe(i) + Kd * [e(k) - e(k-1)]但直接套这个公式有一个工程陷阱:积分项不断累加误差,累积数值很大,后续一旦温度持续偏低,积分项会快速增长,导致加热器在被控温度超过设定点之后仍然长时间处于全功率状态,这个现象叫“积分饱和”。比如设置目标50度,冷水状态开始加热,误差一直很大,积分项持续累加到一个很大的值。当温度到达50度时,尽管误差已经接近零,但积分项的历史累积值还在要求输出100%的功率,于是温度继续冲到60多度才降下来。
解决积分饱和的标准做法是设置积分限幅,具体实现方式如下:
// 位置式PID + 积分限幅 typedef struct { float Kp; float Ki; float Kd; float integral; float prev_error; float out_max; // 输出上限,对应100%加热 float out_min; // 输出下限 float integral_limit; // 积分项限幅,防止饱和 } PID_TypeDef; float PID_Process(PID_TypeDef *pid, float setpoint, float measurement) { float error = setpoint - measurement; // 积分分离:误差超过阈值时不积分,避免温度快速逼近时过冲 if(error < 10.0f) { pid->integral += error; // 积分限幅 if(pid->integral > pid->integral_limit) pid->integral = pid->integral_limit; if(pid->integral < -pid->integral_limit) pid->integral = -pid->integral_limit; } float derivative = error - pid->prev_error; float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative; // 输出限幅,对应PWM占空比0-100% if(output > pid->out_max) output = pid->out_max; if(output < pid->out_min) output = pid->out_min; pid->prev_error = error; return output; }需要注意的是,PID计算结果不能直接让GPIO输出PWM占空比,因为执行器是继电器而非可控硅。把0到100的PID输出映射为继电器通断周期的占空比,通常会选10到30秒为一个控制周期。比如PID输出是60,那么在30秒的控制周期内,继电器吸合18秒,断开12秒。这样既限制了机械触点的频繁切换,又保留了连续的功率调节能力。
3.3 过温与干烧保护:看门狗之外的第二道防线
热水器最危险的故障是干烧——没水时加热棒仍通电,温度快速上升可以超过200度,外壳变形甚至引发火灾。温度PID只管恒温,不管安全。安全逻辑必须在PID之外单独形成一条路径,而且要独立于主循环的运行状态。
具体做法是这样:TIM1产生一个1kHz的中断,在中断服务函数里每隔100次(即100ms)读取一次最新温度,如果温度超过硬件阈值(比如93摄氏度),则不管当前PID输出是多少,直接拉低继电器控制引脚并置位错误标志位。这个判断必须放在中断里,因为如果放在主循环中,一旦主循环陷入某个阻塞状态——比如误用了HAL_Delay并且延时时间设置过长——保护逻辑就跟着失效了。
// TIM1中断:100ms周期安全检查,独立于主循环 void TIM1_UP_IRQHandler(void) { if(__HAL_TIM_GET_FLAG(&htim1, TIM_FLAG_UPDATE)) { static uint8_t counter = 0; counter++; if(counter >= 100) { counter = 0; uint32_t current_temp_x10 = Get_Temperature_x10(); // 过温保护:这里不用PID里的滤波值,直接读原始ADC转换结果 if(current_temp_x10 > 930) { // 93.0度 HEATER_OFF(); g_error_code |= ERROR_OVERTEMP; } } __HAL_TIM_CLEAR_FLAG(&htim1, TIM_FLAG_UPDATE); } }这里有一个容易混淆的细节:为什么不直接把PID输出限幅在90度以内就算了?因为传感器的失效模式不同——如果NTC开路或短路,ADC可能读到满量程或者零值,换算出来的温度会瞬间冲到200度以上,PID无从判断这是真实的超温还是传感器损坏。保护逻辑要做的是,在看到极端值的瞬间,宁可误判也不要漏判,直接断开继电器。
4. 人机交互与通信:按键显示之外,把系统接入网络或上位机
4.1 按键消抖与显示更新的并发设计
热水器的操作面板一般只有最基础的元素:数码管或者LCD显示当前温度,两三个按键负责调节设定温度和开关机。这部分的工程难点不在外设驱动,而在于和主循环的共享资源访问管理。
如果按键扫描使用阻塞式消抖——按住延时20ms再判断一次,那么这20ms内温度采样照常由DMA后台运行,看起来没问题,但实际上主循环里ADC取数、PID计算、继电器刷新这些操作全被堵住。所有基于While(循环)或者HAL_Delay(延时)的按键扫描代码,都在为系统引入不可预测的时序抖动。
推荐用定时器扫描加状态机消抖:
// 5ms定时器中断中扫描一次按键,状态机消抖 typedef enum { KEY_IDLE, // 空闲,等待按下 KEY_PRESSED, // 检测到按下,进入确认窗口 KEY_CONFIRMED, // 确认有效按下 KEY_RELEASED // 释放,等待下次按下 } KeyState; KeyState key_state = KEY_IDLE; uint8_t key_stable_count = 0; void KEY_Scan(void) { uint8_t level = HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin); switch(key_state) { case KEY_IDLE: if(level == GPIO_PIN_RESET) { // 按下,低有效 key_state = KEY_PRESSED; key_stable_count = 0; } break; case KEY_PRESSED: key_stable_count++; if(key_stable_count >= 4) { // 连续4次读到低电平,约20ms key_state = KEY_CONFIRMED; KEY_Event_Trigger(); // 触发一次有效按键事件 } if(level == GPIO_PIN_SET) { // 中途变为高电平,判定为抖动,复位 key_state = KEY_IDLE; } break; case KEY_CONFIRMED: if(level == GPIO_PIN_SET) { key_state = KEY_RELEASED; } break; case KEY_RELEASED: if(level == GPIO_PIN_SET) { key_state = KEY_IDLE; } break; default: break; } }这套状态机的价值在于,按键扫描不阻塞任何其他操作,消抖窗口内的采样释放了CPU。注意key_stable_count判断条件是4次连续读低,5ms乘4等于20ms,这是机械按键典型抖动窗口的保守覆盖值。如果使用的是触摸按键芯片(如TTP223),消抖会放在触摸芯片内部完成,MCU端简化到只需读取电平即可。
4.2 移植FreeModbus走RS485:把热水器接进现场总线
如果只是做一个本地独立的热水器系统,前面的内容已经够用了。但“智能”二字往往意味着必须对外通信。现在的居住场景中,嵌入式系统接入外部网络的公共方案无外乎WiFi模组(ESP8266或ESP32)或RS485总线。前者的接入方式偏向消费级应用,智能热水器的定位通常又要求与楼宇中的集中控制系统互通,所以工业现场最广泛使用的是FreeModbus协议栈,跑在RS485物理层上。
移植FreeModbus到STM32的工作量其实不大。关键在于要维护一个串口帧接收定时器(一般3.5个字符时间用于判断帧结束),Modbus协议规定RTU模式下两个帧之间的间隔必须大于等于3.5个字符时间,这是帧分割的判据。
// FreeModbus移植时需要的串口接收回调,示意 // 每次收到一个字节,重置定时器计数值 void UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART2) { // 把收到的字节交给协议栈 eMBPortSerialRxByte((uint8_t)received_byte); // 启动/重置帧间隔定时器 MB_PORT_TIMER_RESTART(); HAL_UART_Receive_IT(&huart2, &received_byte, 1); } }Modbus通信中典型的寄存器分配方案是:保持寄存器(Holding Register)存放设定温度和开/关机状态,输入寄存器(Input Register)存放当前实时温度、错误码、加热功率百分比。上位机或网关定时轮询这些地址,就能在不上门的情况下完成状态监控和参数下发。
如果你决定走WiFi路线,也可以把这个Modbus协议栈当作“内网设备侧接口”,WiFi模组通过串口和STM32通信时,数据帧格式仍然用Modbus RTU封装。这样以后从RS485升级到WiFi,MCU端固件完全不用改,只需要替换通信层硬件映射,这是目前设备侧接入常见的兼容做法。
5. 三板斧排错与验证:晶振起振、ADC漂移、继电器喊麦问题的自查顺序
系统跑不起来的时候,先不要急着改代码,按下面的顺序排查,能省出大量时间。
第一步,先把晶振问题解决掉。STM32F103的典型外部晶振是8MHz,经过PLL倍频到72MHz。如果程序烧录后完全不运行,用示波器测OSC_OUT引脚,看不到正弦波,那就要注意晶振电路的两个负载电容是否焊错。晶振的负载电容计算公式是:C = 2 × (CL - Cstray),其中CL是晶振规格书标称的负载电容,Cstray是PCB走线和引脚引入的寄生电容,一般在3到5pF。如果你用的是20pF负载电容的8MHz晶振,减去5pF寄生电容后乘以2,大约需要30pF的匹配电容,而不是随手放一对20pF。这个细节对批量生产的良率影响很大,对一次性打样来说,主要影响起振时间。
第二步,ADC读数异常散乱时,先区分问题是源于基准电压还是源于电源噪声。检查VREF+引脚是否连到了干净的3.3V。如果VREF+和MCU的数字供电直接连在一起,继电器吸合瞬间的电流变化会产生微小的压降,ADC的量化值随之跳动。将VREF+单独走线到LDO输出端的滤波电容之后,通常在示波器上能看到ADC抖动明显下降。另一种情况是K210等外部芯片与STM32共用同一个3.3V电源,彼此的数字信号噪声通过电源平面耦合进ADC通道,这时需要给ADC供电串一个磁珠,形成一个小的LC滤波岛。
第三步,听到继电器簧片快速反复吸合发出的“嗒嗒”声,通常不是硬件坏了,而是控制算法出现了极限环振荡。检查PID参数中的Kp是否过小,或者控制周期(前述10到30秒)与温度滞回区间是否匹配。判断技巧是:把输出限幅从0到100改到15到85,限制加热功率的上下界,可有效避免系统进入开-关-开-关的极限环。
验证系统可靠性,有一种更贴近真实用户的做法:模拟冷水注入。洗澡过程中会不断有冷水补充进热水器水箱,导致混合水温持续下降。你要在功能测试时做一个“扰动注入”测试——打开入水阀让冷水以1到2L/min的流量持续进入,观察PID是否能在一到两分钟内把出水口温度拉回设定值附近,并且不出现超过正负2度的突变。如果回稳时间超过3分钟,优先加大Ki;如果出现温度过冲,优先减小Kd并检查积分分离阈值是否设置过小。
还可以利用STM32的MCUViewer或者STMStudio这类工具,在运行中实时观察内部变量值,确认积分项没有触发限幅、设定温度与实际温度之间的偏差趋势是否符合控制预期。调试完成后,把PID参数以结构体形式存放在Flash的独立扇区,预留两到三组档位参数,方便后续通过Modbus寄存器或者烧录器在线切换,不需要重新编译整个工程就能完成不同工况下的适配,这一点在毕业设计答辩演示时很加分。
本文还有配套的精品资源,点击获取