基于STM32的智能抽水装置设计:从传感器融合到电机驱动的完整实现
1. 项目概述与核心思路
最近在折腾一个挺有意思的小玩意儿——一个能自己“看”水位、“看”人、然后自动抽水的智能小装置。起因很简单,家里养鱼,出差几天总担心自动喂食器旁边的水蒸发太快,或者给阳台的自动滴灌系统补水不够及时。市面上成品要么功能单一,要么价格不菲,索性自己动手,用最经典的STM32F103(也就是大家常说的“蓝桥杯”神器)为核心,攒一个功能全面、稳定可靠的自动抽水机。
这个装置的核心目标就三个:省心、智能、皮实。省心在于它得能自己判断什么时候该工作,什么时候该休息;智能体现在它不止是机械地抽水,还要结合环境(比如有没有人需要用水)和水源状态(储水瓶还有没有水)来做决策;皮实则是硬件设计上的要求,供电要稳,保护要全,别用两天就烧了电机或者单片机。整个设计围绕STM32F103V8T6这颗MCU展开,通过ADC采集电池电压和水位,通过GPIO和外部中断响应雷达与红外传感器,再用一个三极管去控制水泵电机,逻辑清晰,成本可控,非常适合电子爱好者复现。
2. 硬件系统设计与核心器件选型
硬件是整个系统的骨架,选型合理与否直接决定了项目的成败和后期调试的难度。我的设计思路是“核心够用,外设可靠,电源独立”。
2.1 微控制器:STM32F103V8T6为何是首选
选择STM32F103V8T6几乎是嵌入式入门项目的“标准答案”,但这里有其必然性。首先,它基于ARM Cortex-M3内核,72MHz的主频对于处理多路传感器数据、进行简单的阈值判断和逻辑控制绰绰有余,不会出现性能瓶颈。其次,它拥有足够多的外设资源:多路12位ADC(用于采集电压和水位模拟信号)、多个定时器(可用于PWM控制电机转速,虽然本项目用开关控制,但为扩展留有余地)、以及丰富的GPIO和外部中断引脚,完美匹配雷达、红外传感器的接入需求。最后,其庞大的社区生态和丰富的资料(库函数、例程)能极大降低开发门槛,遇到问题很容易找到解决方案。
注意:STM32F103系列有多个型号,尾缀“V8T6”中的“8”代表64KB Flash,“T6”是LQFP48封装。对于本项目,64KB Flash完全足够,48引脚封装也提供了充足的IO。如果为了降低成本或板子尺寸,也可以考虑STM32F103C8T6(俗称“最小系统板”核心型号),但引脚会少一些,布局时需要更精细的规划。
2.2 传感器模块:感知世界的“眼睛”和“耳朵”
传感器是系统智能化的基础,选型需要平衡精度、成本、接口复杂度和功耗。
1. 水位检测:从简单到复杂的方案权衡最初考虑过成本最低的浮球开关(机械式),它只有“有水”和“无水”两种状态,无法进行水位分级预警。为了实现“水位低预警”和“水满停止”的精细控制,我选择了超声波测距模块(如HC-SR04)。它的原理是发射超声波并接收回波,通过时间差计算距离。将其安装在储水瓶顶部,向下测量水面距离,就能换算出实时水位。其输出是数字脉冲信号,STM32通过定时器捕获即可,无需ADC,精度在厘米级,完全满足要求。另一种方案是使用压力式液位传感器,它输出模拟电压信号,需要ADC采集,精度更高但成本也高,且需要做好防水密封。
2. 人体接近感知:雷达与红外的分工协作这里采用了双传感器融合的策略,目的是兼顾探测距离、响应速度和降低误触发。
- 雷达传感器(如HLK-LD2410):负责中远距离(2-5米可调)的运动和静止人体感知。它通过发射微波并分析多普勒频移来检测,穿透力强,不受光线、温度影响。其输出通常是串口(UART)或GPIO高低电平。我选择GPIO输出模式,将其检测到人的信号线连接到STM32的外部中断引脚,实现即时响应。雷达的作用是“唤醒”系统,当检测到有人进入监测范围,才启动下一级检测,以降低整体功耗。
- 红外传感器(如热释电红外PIR传感器):负责近距离(约0-30厘米)的精确触发。当雷达“唤醒”系统后,STM32会打开PIR传感器的电源。PIR传感器检测人体发出的特定波长红外线变化,当手伸到出水口时,它会产生一个跳变脉冲。这个信号同样接入STM32的外部中断或普通GPIO进行轮询。PIR传感器成本低,但对静止目标不敏感,正好与雷达互补。
3. 电池电压监测:系统的“健康管家”直接用STM32的ADC采集锂电池电压是不可行的,因为锂电池电压(最高4.2V)可能超过ADC的参考电压(通常3.3V)。因此,必须使用电阻分压电路。我选用两个精度为1%的金属膜电阻,比如一个100kΩ和一个33kΩ串联。当电池电压为4.2V时,中间点的电压 = 4.2V * (33k / (100k+33k)) ≈ 1.04V,安全地落在ADC量程内。通过测量这个分压点的电压,再反推回电池总电压,即可实现电量监控。
2.3 执行机构与驱动:控制水流的“手”
水泵的选择:根据抽水高度(扬程)和流量需求,我选择了12V供电的直流隔膜水泵。它噪音小、功耗相对较低、干转不易损坏。关键参数是扬程和流量,需要根据你的储水瓶高度和期望的上水速度来选择。
电机的驱动:STM32的GPIO引脚驱动能力(通常仅20mA左右)远不足以直接驱动12V水泵(工作电流可能达数百mA)。因此必须使用“小电流控制大电流”的开关电路。最经典、最经济的方案是使用NPN三极管(如S8050)作为低压侧开关。具体连接为:STM32的GPIO通过一个1kΩ的限流电阻连接到三极管的基极(B);三极管的发射极(E)接地;集电极(C)接水泵的负极;水泵的正极直接接电池正极。当GPIO输出高电平(3.3V)时,三极管饱和导通,相当于水泵负极接地,形成回路,水泵工作。当GPIO输出低电平时,三极管截止,回路断开,水泵停止。
重要心得:务必在水泵两端反向并联一个续流二极管(如1N4007),阴极接电源正,阳极接水泵负(即三极管集电极)。因为水泵是感性负载,断电瞬间会产生很高的反向电动势,这个二极管为其提供泄放回路,保护三极管不被击穿。这是很多新手容易忽略,导致三极管“莫名其妙”烧毁的关键点。
2.4 电源管理系统:稳定运行的“心脏”
电源设计是硬件稳定的基石,特别是当系统中同时存在数字电路(MCU、传感器)和感性负载(电机)时。
1. 电压转换与分配:系统采用单节锂电池(3.7V标称,4.2V满电)供电。它需要产生两个电压轨:
- 3.3V轨:为STM32、雷达传感器(部分型号)、超声波模块等供电。使用低压差线性稳压器(LDO),如AMS1117-3.3。LDO噪声小,电路简单。
- 5V轨:为一些需要5V供电的传感器(如部分PIR模块)、状态指示灯供电。可以从锂电池先通过一个升压模块(如MT3608)升压至5V,再供给设备;或者如果系统中有其他5V来源(如USB),也可直接使用。注意,水泵电机不经过这些稳压器,直接由电池驱动,以避免大电流烧毁稳压芯片。
2. 去耦与滤波:在每一片芯片的电源引脚附近(越近越好),都必须放置一个0.1uF的陶瓷电容到地,用于滤除高频噪声。在电源输入处,可以并联一个10uF~100uF的电解电容,用于缓冲低频波动和电机启动时的瞬间大电流。
3. 保护电路:
- 电源反接保护:在电池输入正极串联一个肖特基二极管(如SS34),防止电源接反烧毁电路。虽然会有约0.3V的压降,但对于锂电池系统通常可以接受。
- 电机过流保护:在水泵的电源路径中串联一个自恢复保险丝(PPTC)。当电机堵转导致电流过大时,保险丝电阻急剧增大,限制电流,故障排除后又能自动恢复。这是比一次性保险丝更便捷的选择。
3. 软件逻辑与关键代码实现
硬件搭好了,接下来就是赋予它灵魂的软件。程序整体采用前后台(超级循环)加中断的架构,确保实时性要求高的信号(如雷达触发、红外触发)能被立即响应。
3.1 系统初始化与主循环框架
系统上电后,首先进行一系列初始化操作,然后进入主循环,持续进行水位和电池电压的监测。
// 伪代码,示意主要流程 int main(void) { // 1. 硬件初始化 System_Init(); // 系统时钟、延时函数初始化 LED_GPIO_Init(); // 状态指示灯GPIO初始化 ADC_Init(); // ADC初始化,用于电池电压和水位采集 UART_Init(); // 串口初始化,用于调试信息打印(可选) Motor_GPIO_Init(); // 电机控制GPIO初始化 Sensor_GPIO_Init(); // 雷达、红外传感器GPIO初始化 EXTI_Init(); // 外部中断初始化,用于连接雷达/红外信号 // 2. 变量初始化 uint16_t battery_adc = 0; uint16_t water_level_adc = 0; float battery_voltage = 0.0; float water_level_cm = 0.0; uint8_t system_state = STATE_NORMAL; // 系统状态机 // 3. 主循环 while(1) { // 3.1 监测电池电压(每秒一次即可) battery_adc = Get_ADC_Value(BATTERY_ADC_CHANNEL); battery_voltage = (battery_adc / 4095.0) * 3.3 * (133.0/33.0); // 计算实际电压,133k和33k为分压电阻 Update_Battery_Indicator(battery_voltage); // 根据电压更新LED状态 // 3.2 监测水位(频率可更高,如500ms一次) water_level_adc = Get_ADC_Value(WATER_ADC_CHANNEL); water_level_cm = Convert_ADC_to_Distance(water_level_adc); // 根据传感器特性转换 Update_WaterLevel_Indicator(water_level_cm); // 更新水位LED,并判断是否水满 // 3.3 系统状态机处理 switch(system_state) { case STATE_NORMAL: // 等待雷达中断触发 break; case STATE_PERSON_DETECTED: // 雷达已触发,开启红外传感器电源,等待红外触发 Enable_PIR_Sensor_Power(); // 可以加一个超时,比如10秒后无人操作就返回NORMAL状态 break; case STATE_PUMPING: // 正在抽水,此时可以持续监测水位,达到上限则停止 if(water_level_cm >= FULL_LEVEL_THRESHOLD) { Stop_Pump(); system_state = STATE_NORMAL; Disable_PIR_Sensor_Power(); } break; } Delay_ms(500); // 主循环延时,降低CPU占用率 } }3.2 中断服务程序:响应即时事件
雷达和红外传感器的信号响应要求实时性高,适合用外部中断。
// 雷达传感器中断服务程序(假设雷达输出高电平表示检测到人) void EXTI0_IRQHandler(void) { // 假设雷达接在EXTI0上 if(EXTI_GetITStatus(EXTI_Line0) != RESET) { // 清除中断标志 EXTI_ClearITPendingBit(EXTI_Line0); // 只有在正常状态且水位不是过低时才响应 if((system_state == STATE_NORMAL) && (water_level_cm > LOW_LEVEL_THRESHOLD)) { system_state = STATE_PERSON_DETECTED; // 可以点亮一个蓝色LED提示“已就绪” } } } // 红外传感器中断服务程序(假设PIR输出上升沿触发) void EXTI1_IRQHandler(void) { // 假设PIR接在EXTI1上 if(EXTI_GetITStatus(EXTI_Line1) != RESET) { EXTI_ClearITPendingBit(EXTI_Line1); // 只有在“人员已检测”状态下才启动水泵 if(system_state == STATE_PERSON_DETECTED) { Start_Pump(); system_state = STATE_PUMPING; } } }3.3 核心功能函数详解
1. 电池电量指示函数
void Update_Battery_Indicator(float voltage) { if(voltage >= 3.7) { // 电量充足 GREEN_LED_ON(); RED_LED_OFF(); } else if(voltage >= 3.4) { // 电量中等,可以闪烁绿灯 static uint32_t blink_tick = 0; if(HAL_GetTick() - blink_tick > 500) { GREEN_LED_TOGGLE(); blink_tick = HAL_GetTick(); } RED_LED_OFF(); } else { // 电量过低 GREEN_LED_OFF(); RED_LED_ON(); // 常亮或快闪报警 } }2. 水位监测与水泵控制函数
void Update_WaterLevel_Indicator(float level_cm) { if(level_cm <= LOW_LEVEL_THRESHOLD) { // 水位过低,比如小于5cm // 红灯快闪,强烈警告 // 同时,无论何种状态,强制停止水泵并禁止启动,防止干烧 Stop_Pump(); system_state = STATE_NORMAL; Disable_PIR_Sensor_Power(); // ... 红灯闪烁代码 } else if(level_cm >= FULL_LEVEL_THRESHOLD) { // 水满,比如小于1cm(距离传感器近) // 绿灯常亮,表示水满 // 如果正在抽水,则停止 if(system_state == STATE_PUMPING) { Stop_Pump(); system_state = STATE_NORMAL; Disable_PIR_Sensor_Power(); } // ... 绿灯常亮代码 } else { // 水位正常区间 // 可以熄灭水位指示灯,或慢闪绿灯 } } void Start_Pump(void) { MOTOR_CTRL_GPIO_PIN = 1; // 输出高电平,使三极管导通 // 记录水泵启动时间,用于后续超时保护 pump_start_tick = HAL_GetTick(); } void Stop_Pump(void) { MOTOR_CTRL_GPIO_PIN = 0; // 输出低电平,关闭三极管 }4. PCB设计、组装与调试实录
原理图设计完成后,画PCB是连接虚拟与实物的关键一步。对于这种包含模拟信号(ADC采集)和数字开关信号(电机控制)的混合电路,布局布线尤为重要。
4.1 PCB布局布线核心要点
分区布局:将PCB板大致划分为电源区、数字区、模拟区、功率区。
- 电源区:放置LDO、输入输出滤波电容、电源接口。尽量靠近板边入口。
- 数字区:放置STM32、晶振、数字传感器接口(雷达的UART部分)。此区域噪声容限较高。
- 模拟区:放置ADC分压电阻、超声波传感器接口、去耦电容。务必远离数字区和功率区。
- 功率区:放置电机驱动三极管、续流二极管、自恢复保险丝。此区域电流大,走线要宽。
地平面与电源走线:
- 地(GND):尽可能使用完整的铺铜作为地平面,为所有信号提供低阻抗的回流路径。模拟地和数字地可以在一点通过磁珠或0欧电阻连接,本项目如果复杂度不高,单点连接即可。
- 电源走线:根据电流大小决定线宽。单片机、传感器等小电流走线可用10-20mil;电机供电走线必须加粗,建议不小于50mil,或者开窗上锡增加载流能力。
关键信号线处理:
- ADC采样线:从分压电阻中间点连接到STM32 ADC引脚的走线要尽量短,周围用地线包围进行屏蔽,避免靠近高频数字信号线(如时钟线)。
- 电机控制线:虽然GPIO输出是数字信号,但驱动三极管基极的走线也应避免与模拟信号线长距离平行,防止开关噪声耦合。
过孔与丝印:为每个电源网络(3.3V, 5V, BAT+)在芯片电源引脚附近放置足够的过孔连接到电源平面。丝印清晰标注关键测试点(如BAT+、3.3V、ADC采样点)、接口方向(USB, 电机接口)。
4.2 焊接与组装注意事项
- 焊接顺序:建议先焊接高度最低的器件,如电阻、电容、二极管,再焊接芯片座、排母,最后焊接接线端子等较高的器件。使用热风枪焊接LQFP封装的STM32时,温度不要过高(建议350°C左右),风速适中,预热要充分,避免芯片受热不均导致虚焊或损坏。
- 电源检查:焊接完成后,切勿直接上电。先用万用表二极管档或电阻档,测量电源(BAT+)与地(GND)之间的电阻,确认没有短路(电阻不应接近0欧)。然后,可以先不插MCU,只上电测量3.3V和5V输出是否正常、稳定。
- 分模块调试:
- 核心板:插入STM32最小系统板(如果自制则焊接MCU),连接ST-Link,测试能否正常烧录程序、运行最简单的LED闪烁例程。
- 电源与ADC:编写程序读取电池电压ADC值,通过串口打印出来,与万用表实际测量值对比,校准分压计算公式。
- 传感器:单独测试雷达和PIR模块,确认其输出信号是否符合预期(用逻辑分析仪或另一个单片机GPIO读取)。
- 电机驱动:先用一个LED代替水泵,测试三极管开关电路是否正常。确认无误后,再接上水泵,并务必在水泵出口接一段水管放入水中进行测试,严禁水泵空转。
4.3 系统联调与功能验证
所有模块单独测试通过后,进行系统集成调试。
- 编写测试固件:将各个功能函数(电池检测、水位读取、雷达中断、红外中断、电机控制)整合到主程序中,先使用简单的“打印日志”方式,在串口助手上观察每个状态的转换是否正常。
- 状态机验证:模拟各种场景。
- 场景一:正常水位,电池有电。用手在雷达前晃动,观察蓝色“就绪”灯是否亮起,再将手放到出水口,观察水泵是否启动,离开后是否停止。
- 场景二:向储水瓶加水,观察水位上升到满阈值时,水泵是否会自动停止(如果正在抽水)。
- 场景三:将储水瓶水放至低水位以下,观察红色报警灯是否亮起,并尝试触发雷达和红外,水泵应被禁止启动。
- 场景四:使用可调电源模拟电池电压下降,观察电量指示LED的变化是否符合设定。
- 压力与稳定性测试:让系统连续运行24小时以上,模拟频繁的抽水动作,观察是否有死机、内存泄漏(如果用了动态内存)、电机发热是否严重、电源电压是否波动过大等情况。
5. 常见问题排查与进阶优化
在实际制作过程中,你几乎一定会遇到下面这些问题。这里我把踩过的坑和解决方案整理出来,希望能帮你节省大量时间。
5.1 硬件类问题
问题1:水泵不转或转动无力。
- 排查步骤:
- 测量电压:用万用表直接测量水泵两个引脚间的电压,在启动命令发出时是否达到电池电压(如12V)?如果远低于电池电压,可能是导线电阻太大或接口接触不良。
- 测量控制信号:测量STM32控制引脚(连接三极管基极电阻的那一端)在启动时的电压,是否为高电平(约3V)?如果不是,检查程序GPIO配置是否正确(应设置为推挽输出)。
- 检查三极管:测量三极管CE极间电压。启动时,CE压降应很小(饱和压降约0.2V)。如果CE压降很大,接近电源电压,说明三极管未导通。检查基极电阻是否过大(导致基极电流Ib太小),或三极管型号是否正确(NPN),或三极管已损坏。
- 检查续流二极管:确认二极管方向是否正确?反接会导致电源短路。二极管是否损坏(开路)?损坏后关断的尖峰电压可能已击穿三极管。
问题2:ADC采集的电池电压或水位值跳动剧烈、不准。
- 排查步骤:
- 硬件滤波:在ADC输入引脚到地之间,并联一个0.1uF的陶瓷电容,可以滤除大部分高频噪声。如果信号本身变化慢(如水位),可以再并联一个10uF的电解电容,形成RC滤波。
- 软件滤波:采用中位值平均滤波法。连续采样N次(如10次),去掉最大最小值,再求剩下数据的平均值。STM32的HAL库ADC读取函数本身可能就有波动,软件滤波是必须的。
- 参考电压:确保STM32的ADC参考电压稳定。如果使用VDDA(通常与VDD相连)作为参考,要确保供电电源干净。可以在VDDA和VSSA引脚附近放置高质量的10uF和0.1uF去耦电容。
- 接地问题:模拟部分(分压电阻、传感器信号地)必须采用“星型接地”或单点连接到主地,避免数字地噪声串入。
问题3:雷达传感器误触发,没人也乱亮灯。
- 排查步骤:
- 调整灵敏度与延时:很多雷达模块(如LD2410)可以通过串口或按键配置探测距离、灵敏度和存在判断延时。适当调低灵敏度,增加存在判断时间(例如,要求信号持续1秒才判定为有人),可以滤除宠物、窗帘晃动等干扰。
- 检查安装环境:避免将雷达正对风扇、空调出风口、窗户等有规律运动物体的方向。雷达对金属物体反射敏感,也要避开金属框架。
- 供电噪声:为雷达模块单独增加一个LC滤波电路(如一个10uH电感串联,再加一个10uF电容到地),提供干净的电源。
5.2 软件与逻辑类问题
问题4:系统运行一段时间后死机或不响应。
- 排查思路:
- 看门狗:启用STM32的独立看门狗(IWDG)。在主循环中定期“喂狗”。如果程序跑飞或陷入死循环,看门狗超时会导致系统复位,这是一种有效的恢复手段。
- 中断冲突:检查中断服务函数中是否进行了耗时过长的操作(如打印大量数据)。中断服务应快进快出,复杂的处理可以置位标志位,在主循环中处理。
- 堆栈溢出:如果程序中使用了较大的局部数组或递归调用,可能导致堆栈溢出。可以在启动文件里适当增大堆栈大小。
- 电源跌落:电机启动瞬间电流很大,可能导致整个系统电压瞬间跌落,造成MCU复位。检查电源路径的导线是否足够粗,电池容量是否充足,并在MCU的电源入口处加大缓冲电容(如220uF电解电容)。
问题5:红外检测不灵敏或距离太近。
- 解决方案:
- 调整透镜:PIR传感器上的菲涅尔透镜决定了探测范围和角度。尝试更换不同焦距(如短焦、广角)的透镜。
- 调整电位器:模块上通常有灵敏度(SEN)和延时时间(TIME)调节电位器。顺时针调高灵敏度。
- 优化安装:确保透镜清洁,安装位置避免正对热源(如阳光直射、暖气片),且探测范围内没有障碍物。
5.3 功能进阶与优化建议
当基础功能稳定后,可以考虑以下优化,让装置更“聪明”:
- 增加无线通信(如ESP-01S WiFi模块):通过UART连接ESP8266,接入家庭局域网。可以开发一个简单的网页或使用HomeAssistant,远程查看水位、电池电量,甚至手动控制抽水、接收低水位报警推送。
- 水泵软启动与PWM调速:目前是直接开关控制,启动瞬间冲击电流大。可以改用MOS管驱动,并使用STM32的PWM功能,实现缓慢启动(软启动),减少对电源的冲击,同时PWM调速可以控制抽水流量。
- 数据记录与学习:利用STM32的内部Flash或外接一个小容量SPI Flash,记录每天的抽水次数、时长、水位变化。分析这些数据,可以学习用水习惯,甚至预测加水时间。
- 低功耗优化:如果使用电池供电且希望续航更久,可以大幅优化功耗。主要策略:让STM32在大部分时间进入停止(Stop)模式,仅由雷达传感器的检测信号(可配置为中断唤醒模式)来唤醒MCU。唤醒后,快速完成检测和抽水任务,再次进入休眠。同时,断开所有不必要的外设(如调试串口、状态LED)的电源。
