拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32雨滴监测系统设计与实现:从传感器选型到状态机

简介这是一套基于STM32单片机与雨滴传感器的嵌入式监测项目源码适合电子工程、自动化及物联网方向的初学者和开发者参考可快速理解传感数据采集、OLED显示驱动与报警机制。系统实现雨滴实时采集、OLED显示、蜂鸣器报警及串口数据回传覆盖传感器驱动、外设控制、中断处理等常见开发环节适用于智能家居、农业大棚、户外设备防雨提醒等场景。压缩包共280个文件约8.92MB包含Keil工程文件、C/H源码、编译生成文件与说明文档目录结构清晰便于对照学习目前已吸引70人学习下载。通过阅读代码可掌握STM32的GPIO、ADC、I2COLED及串口通信配置方法并了解雨滴数据的采集判定与报警联动逻辑同时工程内附带清理脚本与备份文件适合用于项目复盘、课程设计或功能二次开发资源来自网络分享仅供学习交流。1. 雨滴监测为什么需要一个独立的STM32系统屋顶光伏板、大棚卷帘机、stm32鱼缸换水控制器这类靠近户外的项目对“是否下雨”的判断不能只靠一个电平。雨滴传感器买回来是一块栅格板加一个LM393比较器模块直接接单片机IO的话雨滴落板瞬间信号会在0和1之间来回抖轻则误触继电器重则逻辑错乱。真正需要的是一个能在毫秒级完成采样、滤波、分级的独立处理单元这个标题里的源码要解决的是抖动抑制、雨量分级和联动执行三件事。适合正在做stm32项目、要上报环境量以及拿单片机做毕业设计或产品原型的人继续往下读。2. 雨滴传感器选型与STM32硬件接线模拟量与数字量的取舍2.1 LM393模块到底输出什么信号市面上常见的雨滴模块结构一致一片梳状栅格板负责把“水量”转换成电阻变化后级用LM393比较器把结果整理成两路输出。干燥时栅格间阻抗极高分压点电压稳定有水滴落在栅格上两路电极之间形成导电通路分压点电压明显变化。模块上的蓝色电位器调整的是比较器的翻转点DO输出是数字量只告诉你“低于或高于某个设定值”AO输出则是分压点原始模拟值通常为03.3V或者05V。多数人第一反应是接DO因为代码短。但真正做产品我一般只用AO。DO的翻转点依赖电位器同一批模块电位器精度差两台的阈值可能差30%以上而AO进单片机ADC以后干湿的区别可以从原始数值上量化整机的每个阈值都能在固件里统一校准。另一个原因是DO存在严重的临界抖动雨滴恰好落在翻转点附近时输出是一串几十毫秒宽度的毛刺软件去抖做得再好也会拉长响应时间。从数据层面看AO方案得到的是一连串ADC采样值后续不管做滑动滤波、状态机还是上报给上位机数据形式上都统一。如果系统里已经有modbus单片机帧接收数据程序这类处理流程模拟量直接进数据区比开关量多一个维度给后面做雨量累计统计留了余地。2.2 芯片选型低功耗和采样精度哪个更优先STM32型号很多做环境监测不一定非用F103。按现场供电条件常见选型如下表。型号架构关键点适合场景STM32F103C8T6Cortex-M312位ADCStop模式电流偏大资料多供电充足的家用改造、鱼缸控制器、毕业设计STM32G0B1RECortex-M012位ADC低功耗模式丰富多个唤醒源电池供电的长期雨量监测STM32L071Cortex-M0超低功耗约1uA量级片内Flash带ECC户外光伏板、野外气象站如果手上只有F103的核心板也能直接跑这套代码差别主要在低功耗环节。在stm32开发环境里新建工程后用Cube把芯片型号从F103改到G0系列外设初始化几乎可以原样迁移主循环和状态机代码不用动。选型时另一个容易忽略的是ADC参考电压。绝大多数最小系统板把VREF直接连到VDDA3.3VAO量程就是03.3V对应的ADC原始值为04095。假如传感器模块给AO供电用的是5V直接去接3.3V的ADC引脚轻则读数饱和重则超过绝对最大额定值损伤引脚。2.3 引脚分配与电平匹配的硬件细节以一块F103C8T6核心板为例推荐的引脚分配是PA0接AO作为ADC1_IN0采样输入PA1接DO作为低功耗唤醒的备份输入PB12控制继电器PB13驱动蜂鸣器。雨滴板VCC接3.3V而非5VGND与主板共地AO经一个0.1uF电容到地再接PA0DO直接进PA1并在单片机内部配置为上拉输入。不要直接拿5V电源给雨滴板再期望ADC算得准。传感器板几乎是纯电阻分压供电电压越高AO输出越接近电源轨超过单片机量程的部分会被直接截断。若现场必须用5V给栅格板常见做法是在AO与PA0之间串一个10k电阻再并一个20k电阻到地做成2:1分压。这个折算关系放到后面的阈值标定里处理比硬件上反复改电阻更直观。ADC通道的初始化代码很短采样时间参数值得单独说明。// main.c 中配置ADC1通道0PA0为模拟输入 ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel ADC_CHANNEL_0; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig);SamplingTime选 55.5 周期对雨滴传感器这种中等阻抗源已经足够。如果AGC电路或线缆特别长可以改成 239.5 周期提升采样稳定性转换速度对雨滴场景没有压力慢一点反而能少受高频干扰。把接线做完先用万用表量一下AO在干燥和滴水两种情况下的电压记录一到两组实际数据。接下来进入软件部分采样率不用高200Hz足够核心在于滤波和状态机。3. 从源代码落地采样、去抖与雨量状态机3.1 为什么直接读引脚不是工程方案直接读引脚是51单片机点亮一个LED那种思路但对雨滴不适用。雨滴落下、板面水珠滚动、风引起的电极间阻抗变化都会让AO在几十毫秒内连续波动。如果主循环里用delay加读引脚程序会被卡死其他任务全部停摆。所以我把ADC采样独立成模块每200毫秒采一轮在应用层维护独立的状态机。代码结构按模块拆开雨滴相关的文件单独放stm32_raindrop/ ├── Core/ │ ├── Inc/raindrop.h │ └── Src/raindrop.c ├── Drivers/ └── application/ └── rain_event.craindrop.c只负责采样、滤波和状态判定rain_event.c负责继电器、蜂鸣器和对外上报。这样换板子时只需要改前者联动逻辑完全不用动。3.2 采样与滤波中位值平均去毛刺雨滴信号的特点是个别采样点会出现非常大的跳变不是均匀噪声。滑动平均对这种毛刺不敏感去掉极值再求平均是更合适的思路。// raindrop.c -- 雨滴ADC采样与滤波 #include raindrop.h #include adc.h #define RAIN_SAMPLE_NUM 16 // 每轮采样次数 static uint16_t raw_buf[RAIN_SAMPLE_NUM]; uint16_t rain_read_adc(void) { uint16_t adc_value 0; HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { adc_value (uint16_t)HAL_ADC_GetValue(hadc1); } HAL_ADC_Stop(hadc1); return adc_value; } uint16_t rain_filter(void) { uint32_t sum 0; uint16_t min_v 4096, max_v 0; for (uint8_t i 0; i RAIN_SAMPLE_NUM; i) { uint16_t v rain_read_adc(); raw_buf[i] v; sum v; if (v min_v) min_v v; if (v max_v) max_v v; } // 去掉一个最大值和一个最小值再求平均 return (uint16_t)((sum - min_v - max_v) / (RAIN_SAMPLE_NUM - 2)); }代码逻辑循环16次启动ADC转换PollForConversion的10ms超时保证不阻塞。去掉一个最大值和一个最小值后求平均能滤掉水滴飞溅瞬间产生的尖峰。这里没用滑动平均原因是滑动平均对阶跃变化的响应滞后较大雨滴从干到湿是明显的阶跃过程去极值平均可以更早反映趋势变化。3.3 四级雨量状态机与不对称去抖雨量等级划分成干燥、小雨、中雨、大雨四档就够了再多对继电器和蜂鸣器没有实际意义。阈值定义放在结构体里便于批量修改。// raindrop.h typedef enum { RAIN_DRY 0, RAIN_DRIZZLE, RAIN_MODERATE, RAIN_HEAVY } rain_level_t; typedef struct { uint16_t dry; // ADC原始值高于该值判定为干燥 uint16_t drizzle; // 水膜轻微覆盖 uint16_t moderate; // 连续水珠 uint16_t heavy; // 板面大部分被水覆盖 } rain_thr_t;状态机切换要防止阈值边界上的抖动。雨下大时响应可以快一点雨停时要等更久才回退这个不对称去抖非常实用。static uint8_t level_stable_cnt 0; static rain_level_t cur_level RAIN_DRY; rain_level_t rain_level_update(uint16_t filtered_adc) { rain_level_t target RAIN_DRY; if (filtered_adc rain_thr.dry) target RAIN_DRY; else if (filtered_adc rain_thr.drizzle) target RAIN_DRIZZLE; else if (filtered_adc rain_thr.moderate)target RAIN_MODERATE; else target RAIN_HEAVY; if (target cur_level) { if (level_stable_cnt 4) { // 升级快连续4次同方向 level_stable_cnt 0; cur_level target; } } else if (target cur_level) { if (level_stable_cnt 8) { // 回退慢连续8次同方向 level_stable_cnt 0; cur_level target; } } else { level_stable_cnt 0; } return cur_level; }每次主循环调用一次rain_level_update传入滤波后的ADC值。target是当前采样值对应的理论等级与实际等级一致时清零计数。4次和8次的差别对应的是约0.8秒和1.6秒的确认时间这种不对称设计让系统在雨停间隙不会误报干燥。3.4 为什么要用HAL库而不是标准外设库这段代码基于HAL库原因不是标准库不能写而是雨滴监测系统通常还需要配UART、定时器和低功耗管理HAL在这几块的代码一致性更好。比如前面用的HAL_ADC_PollForConversion和后面的低功耗唤醒、定时器中断函数风格统一上手排查问题时心智负担小。标准库在ADC和EXTI上确实更简洁但一旦涉及G0/L0系列标准库的适配成本会明显变高。4. 联动执行与阈值标定把“感到雨滴”变成“执行动作”4.1 继电器与蜂鸣器的联动输出状态机稳定后应用层只需要读等级结果不需要关心ADC原始值。继电器和蜂鸣器的动作按等级拆分干燥时全部复位小雨时只通知不上动作中雨开始拉起继电器大雨时启动完整联动。// rain_event.c -- 雨滴联动执行 void rain_task(void) { uint16_t adc rain_filter(); rain_level_t level rain_level_update(adc); switch (level) { case RAIN_DRY: HAL_GPIO_WritePin(RELAY_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(BUZZER_PIN, GPIO_PIN_RESET); break; case RAIN_DRIZZLE: // 小雨只做状态记录不动作 HAL_GPIO_WritePin(RELAY_PIN, GPIO_PIN_RESET); break; case RAIN_MODERATE: HAL_GPIO_WritePin(RELAY_PIN, GPIO_PIN_SET); buzzer_set_pattern(BUZZER_SLOW); break; case RAIN_HEAVY: HAL_GPIO_WritePin(RELAY_PIN, GPIO_PIN_SET); buzzer_set_pattern(BUZZER_FAST); break; } }buzzer_set_pattern内部通过定时器中断翻转PB13慢速模式每500毫秒翻转一次快速模式每100毫秒翻转一次。这里不直接用HAL_GPIO_TogglePin是因为蜂鸣器是电感负载长时间高频翻转会发热分模式驱动可以让声音变得有辨识度也能降低继电器动作频率。联动逻辑还要加一个手动优先开关现场调试时断开自动模式避免系统反复拉合继电器。4.2 三分法标定雨量阈值阈值不靠模块上的电位器靠实际标定数据。准备工作是把雨滴板平放避免水滴聚集在低洼处然后用万用表或串口助手持续观察ADC值。我常用的标定顺序如下干燥静置10分钟每5秒读一次ADC记录平均值为DRY_AVG用滴管在栅格板两侧各滴1滴水等待2分钟读数稳定记录DRIZZLE_AVG用喷壶连续喷水3秒让板面形成连续水膜记录MOD_AVG用水杯缓慢倾倒少量水让板面2/3被水覆盖记录HEAVY_AVG标定结果可以填进下表作为固件阈值设置的依据。标定步骤操作记录变量ADC实测参考1干燥静置10分钟DRY_AVG3700~40002两侧各滴1滴水DRIZZLE_AVG2800~33003喷雾成连续水膜MOD_AVG1400~19004浸没板面大半HEAVY_AVG300~800如果实测数据不是这个趋势比如干燥时电压反而低说明AO分压方向和预期相反只需要把状态机里的比较符号翻转不需要改硬件。阈值标定后还要加一个回差。雨停时水膜慢慢变干ADC值缓慢上升如果阈值是严格等于状态会在边界处反复弹跳。实现方式是在判定逻辑里加一个偏移量进入某个等级时按原始阈值判断退出时按阈值加减一个固定偏移。偏移量取满量程的1%比较合适也就是F103的12位ADC下取40左右太小没有作用太大会让回退变得迟钝。4.3 阈值掉电保存与看门狗兜底标定好的阈值不能每次开机重新标要存进内部Flash。F103一般在最后一页留一小块区域G0系列可以直接用Flash模拟EEPROM。下面这行代码把干燥阈值写入指定地址顺序是先解锁再写写完后立即读回校验。uint32_t addr 0x0800F800; // 最后一个扇区前64字节避开程序区 HAL_FLASH_Unlock(); HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr, (uint16_t)dry_thr); HAL_FLASH_Lock(); uint16_t check *(volatile uint16_t *)addr; if (check ! dry_thr) { // 写入校验失败回退到默认阈值 load_default_threshold(); }注意Flash写入前必须整页擦除如果程序区也在这个扇区擦除会把代码一起抹掉。F103的页大小是1KB把存储区放在最后一个1KB页并单独给它分配链接地址是最稳妥的做法。看门狗方面IWDG设置4秒溢出主循环定期调用HAL_IWDG_Refresh(hiwdg)。即使状态机在采样循环里出现异常阻塞看门狗也能把系统拉回来这在户外无人值守环境是必备项。5. 调试技巧雨滴采样波形观察、低功耗唤醒与上报帧设计5.1 用串口打印原始采样值调阈值的前提是能看到当前ADC值。用UART1重定向printf串口助手以200毫秒间隔刷新一边滴水一边看数值变化。// 重定向printf到UART1 int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; }主循环里打印一行printf(ADC%d Lv%d\r\n, adc, level);。注意不要在定时器中断里调用这个函数HAL_UART_Transmit是阻塞式发送中断里调用会把整个中断优先级体系拖垮。如果调试器下断点后发现程序停在不该停的位置先检查SWDIO引脚是否和雨滴板的电源走线靠得太近模拟信号耦合会导致调试接口复位。5.2 停止模式下让雨滴自己唤醒系统户外场景最耗电的不是传感器本身而是ADC持续采样和主循环空跑。低功耗的常见做法是让系统进入停止模式把传感器供电也断掉只靠DO输出触发EXTI唤醒。F103停止模式下只能靠EXTI唤醒G0/L0系列额外支持RTC唤醒用RTC每3秒唤醒一次采样比长时间开着ADC省一个数量级。void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin RAIN_WAKE_PIN) { rain_sensor_power_on(); // GPIO控制MOS管给传感器上电 __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); // 清除唤醒标志 } }传感器供电用一颗MOS管控制唤醒前供电关闭唤醒命令发出后再打开。需要注意AO在停止模式下无法被ADC采样所以这条EXTI只能接DO输出如果模块的DO比较器阈值漂移严重可以在主板上加一颗独立比较器把AO分压后的信号整形后接入EXTI。5.3 上报帧格式与交付前测试清单数据上报统一用一行CSV格式固定为$REQ,LEVEL,RAW_ADC,THR,STATUS\r\n。上位机按逗号拆分就能解析如果后面要接K210这类边缘端做视觉联动按帧头过滤即可不需要额外约定复杂的协议。交付前建议按这几项过一遍滴水观察状态是否逐级上升满水状态静置10分钟确认不会偶尔回到干燥在干燥板上快速挥手模拟风吹确认状态不翻转断电重启后确认阈值参数能从Flash正常恢复。产线首次上电做一次自动标定把干板和浸没湿两种状态的ADC均值直接写入Flash再跟着整机功能测试这套雨滴监测系统才算真正能交付。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门