简介:面向STM32初学者的雨滴监测实战源码,围绕雨水检测与报警防护场景,覆盖传感器采集、显示反馈与串口通信等关键环节,适用于智能家居、户外设备监测、嵌入式课程设计与毕业设计参考。代码基于STM32F10x系列,包含雨滴传感器状态读取的底层驱动逻辑、基于I2C控制OLED屏幕实时刷新、触雨时蜂鸣器报警,以及将雨滴数据发送至串口调试助手的完整处理流程。资源包共280个文件,以C源程序、头文件、Keil工程配置为主,另有编译生成的中间文件和调试信息文件,压缩包整体约8.92MB,目前已有70人学习下载。压缩包提供可直接打开的Keil工程结构,源码模块划分较清晰,便于二次移植与功能扩展,适合作为学习外设驱动和状态机设计的参考。资料为网络分享,仅供学习交流,请勿用于商业用途。
1. 把“STM32单片机雨滴监测系统源代码”当成搜索词时,你要找的其实是稳定的状态判断
把“STM32单片机雨滴监测系统源代码”当成一个搜索词来理解,大多数人要的不是能把每一滴水都数出来的计数器,而是一套从采集到执行的闭环:F103 在跑、传感器在读数、继电器在按预期动作、串口在上报结果。雨滴监测和温湿度监测在工程上完全不同:水珠砸在 PCB 交叉走线上的瞬间电阻变化迅速,但场景决策是慢的,你不需要每毫秒都看一眼传感器,真正要回答的是“现在有没有雨、雨大致有多强、要不要关窗/排水/报警”。代码结构因此应该围绕 ADC 采裸值、滑动滤波、阈值判断三段展开,而不是在网上找一份语焉不详的库然后反复猜接线。做“基于stm32的毕业设计”“stm32鱼缸”这类项目,最容易卡住人的不是硬件,而是采样节奏怎么定、阈值怎么不抖。本文把这套判断逻辑和对应源码拆开讲,读完你可以直接整理出一个能丢进 Keil 编译的最小工程。
2. 雨滴模块的 AO/DO 引脚与 STM32 外设选型:读模拟量还是读电平状态
2.1 常见雨滴模块内部电路和引脚定义
市面上的雨滴传感器模块(常见丝印对应 FC-37、FR04 等)基本是同一种结构:一块梳齿状的 PCB 做水敏感面,板上带一片 LM393 比较器,外加一个电位器用来整定阈值。模块引出的四根线一般固定为 VCC、GND、DO(数字输出)、AO(模拟输出)。你拿到模块的第一件事不是写代码,而是先测一下上电后 DO 和 AO 的空载电压,因为不同批次模块在干态下的输出电压能差出三四百毫伏,直接决定阈值要不要按你的板子单独校准。
| 引脚 | 电平性质 | 干燥环境 | 有水膜/雨滴 | 推荐接 STM32 |
|---|---|---|---|---|
| VCC | 电源 | 3.3V 或 5V | 3.3V 或 5V | 接 3.3V,与 MCU 同源 |
| GND | 地 | 0V | 0V | GND |
| DO | 数字 | 高电平 | 低电平(多数模块) | 任意 GPIO 输入,可接 EXTI |
| AO | 模拟 | 接近 VCC | 随水膜增厚而下降 | 任意 ADC 通道 |
把 VCC 接成 3.3V 是很多人会忽略的一个细节。雨滴传感器在 5V 供电下 AO 输出范围更大,但要记得 STM32 的 ADC 参考电压就是 3.3V,5V 供电会导致引脚电压超过 VDDA,轻则读数满量程,重则损伤 ADC。我一般直接把模块 VCC 接到开发板的 3V3,MCU 和传感器共地,出来的 AO 值就在 0~3.3V 之间,进 ADC 也不需要分压。
DO 的电平极性最好在写入代码前确认一遍。多数模块的默认状态是上拉高电平,水膜足够厚时传感器面到地电阻变小,比较器翻转输出低电平。这个极性对程序逻辑本身没有影响,你只要在初始化时记录一次“干态电平”作为参考。但如果你把 DO 接到 EXTI 外部中断上,建议用下降沿而不是上升沿触发,因为从“干到湿”的变化才是系统需要立刻响应的事件。
2.2 为什么雨滴监测优先读 AO,而不是只依赖 DO 数字量
DO 是电位器分压和传感器动态电阻相互作用的结果,你拧电位器让阈值变敏感,同时也会丢失对降雨强度的感知。雨滴监测里最常见的误用是只接一根 DO 线就把系统当成完成了,结果一团薄雾压过阈值,系统在晴天误关了一次窗户。读 AO 的真正好处是拿到一条连续的判断带,而不是一个孤立的 0/1。
按 F103 的 12 位 ADC 来划分,一套常见参考区间是:
- ADC 值 3800 以上:完全干燥;
- 2600~3800:空气潮湿或轻微水雾;
- 1800~2600:小雨,传感器面可见离散水珠;
- 1800 以下:水膜连续,中到大雨程度。
这套分级在代码里实现就是多阈值区间,写法和单阈值差不多,但后续做雨量趋势、联动策略都会方便很多。比如电动雨棚的逻辑可以设计成“低于 1800 才关窗”,而鱼缸补水的防溢逻辑则可以用“低于 2600 就停止补水”,都是同一份原始数据的不同切片。
普通 STC 单片机或 51 单片机也能做这个采集,但 F103 的优势在于 12 位 ADC、多通道以及 DMA 配合,你可以让同一个 ADC 顺带读光敏电阻或土壤湿度,51 常规型号在这些流程上要写大量软件模拟。毕业设计或竞赛选型时,蓝桥杯这类平台上 F103 也是出现率最高的芯片,生态资料比 STC 完整,换芯片的成本反而更低。
2.3 外设分配和 CubeMX 初始化思路
一套最小可用的雨滴监测系统,四组外设就够了:一路 ADC 读 AO,一路 GPIO 读 DO 做二次确认,一路 GPIO 控制 LED 或继电器,一路串口做调试输出。推荐引脚分配如下:
| STM32 引脚 | 功能 | 说明 |
|---|---|---|
| PA1(ADC1_IN1) | 雨滴模块 AO | 模拟输入,不加上下拉 |
| PC14 | 雨滴模块 DO | 数字输入,内部上拉 |
| PC13 | 板载 LED / 继电器 | 输出指示或开关执行 |
| PA9/PA10(USART1) | 上位机或 ESP8266 | TTL 电平,注意共地 |
PA1 和 PC14 分别属于 GPIOA 和 GPIOC,初始化时分开配置并不麻烦。CubeMX 里 ADC1 选择通道 1,采样时间选最长一档(239.5 周期)即可明显降低高阻抗输出带来的读数抖动;PC13 在大多数 F103 最小系统板上默认接了 LED,可以先拿它当执行机构,避免一开始就去外接继电器。这个阶段不要上 DMA,雨滴系统的采样频率很低,每 100ms 手动触发一次 ADC 足够,代码直白,也方便后续加滤波逻辑。
3. STM32 雨滴监测源代码的核心三段式:初始化、滤波、迟滞判断
3.1 初始化代码:GPIO 和 ADC1 的最小工程
下面这段代码基于 STM32CubeMX 生成工程改造而来,省略了系统时钟和 SystemInit 部分,保留外设初始化的核心。编译环境是 Keil5,芯片选 STM32F103C8T6,标准 HAL 库。
#include "main.h" ADC_HandleTypeDef hadc1; UART_HandleTypeDef huart1; static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); /* PC13 作为执行输出,初始为高电平,表示“无雨” */ HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); GPIO_InitStruct.Pin = GPIO_PIN_13; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); /* PC14 读 DO 信号,内部上拉 */ GPIO_InitStruct.Pin = GPIO_PIN_14; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); /* PA1 作为模拟输入,引脚不进数字输入缓冲 */ GPIO_InitStruct.Pin = GPIO_PIN_1; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); } static void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; HAL_ADC_Init(&hadc1); sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); }这段初始化逻辑分三层理解:先使能 GPIO 时钟,再配置数字输出和模拟输入,最后配置 ADC1 的通道和采样时间。PA1 设为GPIO_MODE_ANALOG之后,数字输入缓冲被关闭,ADC 直接采样引脚电压;ScanConvMode关闭是因为只转换一个通道,ContinuousConvMode关闭代表每次转换由软件触发,正适合低速轮询。
NbrOfConversion设置为 1,意味着每次注入或规则转换只扫一组通道。SamplingTime选239CYCLES_5,在 F103 上这是可选的最长采样时间,对雨滴模块这种内阻偏大的源效果最稳。如果去掉这一长采样时间,你经常能看到同一个传感器面在读数上跳 200 个 LSB,那不是 MCU 坏了,是采样电容还没充到引脚实际电压。
读取函数单独封装一层,后面接滤波或者换传感器都不需要反复动初始化代码:
uint16_t read_rain_adc(void) { HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); uint16_t val = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); return val; }HAL_ADC_PollForConversion的第二个参数是等待超时单位毫秒,F103 单次 ADC 转换只要十几微秒,基本不会走到超时分支。返回的val是 12 位右对齐,范围 0~4095。
3.2 滑动滤波:为什么雨滴数据用窗口均值而不是中值
雨滴模块在干燥状态下读数通常在 3800 附近稳定,但一旦表面有半干水珠,读数会在几百个 LSB 的范围内跳动。原因是水珠在梳齿间滚动,接触电阻本身就一直在变,这个阶段做单点判断就是在找罪受。
常见做法是维护一个长度为 16 的循环队列,每次采样进去一个数、出来一个数,只做一次加法和一次减法:
#define FILTER_LEN 16 static uint16_t filter_buf[FILTER_LEN]; static uint8_t filter_idx; uint16_t rain_filtered(uint16_t raw) { static uint32_t sum = 0; sum -= filter_buf[filter_idx]; /* 去掉最旧的一个采样 */ sum += raw; filter_buf[filter_idx] = raw; /* 存入当前采样 */ filter_idx = (filter_idx + 1) % FILTER_LEN; return (uint16_t)(sum / FILTER_LEN); }这个实现叫滑动平均,它的特点是永远保留最近 16 次采样的平均值,一个异常尖峰最多贡献 1/16 的权重,不会让系统产生一次“假下雨”的跳变。为什么不用中值滤波?中值滤波对脉冲噪声的抑制更强,但它每收到一个新点都要做排序,16 个点的插入排序放在 Cortex-M3 上不划算;而且雨滴信号里的“连续偏低”才是雨膜存在,均值更贴合这个物理过程。
窗口长度和采样周期需要一起调。以 100ms 采样一次、窗口 16 来算,等效时间常数是 1.6 秒,这个值能滤掉单滴水珠的抖动,也不会把阵雨的开头抹没了。想更灵敏就改FILTER_LEN为 8,想更平滑就改 32,不需要动其他代码。但窗口改到 32 之后,从第一滴雨到状态翻转的响应会明显变慢,雨稍小时可能刚翻到“有雨”,雨已经停了,用于关窗可以,用于报警会偏迟钝。
前文提过 F103 也可以配合 DMA 和定时器做自动连续采样,工业上那种需要精确到毫秒的雨强模拟才会用。这里的低速系统完全没必要,手动HAL_ADC_Start加HAL_Delay的方式可读性更好。
3.3 迟滞判断:防止干湿临界点的状态反复
滤波后的值如果只给一个固定阈值,比如“小于 2500 就算下雨”,逻辑看起来没问题,现场却会出状况:傍晚湿度上升时读数可能在 2490 和 2510 之间来回摆动,每次越过 2500 就触发一次继电器动作,蜂鸣器复读机一样响。
迟滞(hysteresis)是这类系统必须的结构,不是优化项。它的核心是设置两个阈值:进入有雨状态的阈值,和退出有雨状态的阈值。两段之间留一个滞回带,状态一旦进入潮湿侧,就要越过更干的一个阈值才会回来。
#define RAIN_DRY_TH 2800 /* 从“有雨”回到“无雨”的读数上限 */ #define RAIN_WET_TH 2200 /* 从“无雨”进入“有雨”的读数下限 */ static uint8_t rain_state; uint8_t rain_judge(uint16_t adc_val) { if (rain_state == 0 && adc_val < RAIN_WET_TH) { rain_state = 1; /* 无雨 -> 有雨 */ } else if (rain_state == 1 && adc_val > RAIN_DRY_TH) { rain_state = 0; /* 有雨 -> 无雨 */ } return rain_state; }两个阈值之间的 2200~2800 区间就是滞回带。状态转移表如下:
| 当前状态 | 本次滤波值 | 下一状态 |
|---|---|---|
| 无雨 | 大于 2800 | 无雨 |
| 无雨 | 2200~2800 | 保持无雨 |
| 无雨 | 小于 2200 | 有雨 |
| 有雨 | 小于 2200 | 保持有雨 |
| 有雨 | 2200~2800 | 保持有雨 |
| 有雨 | 大于 2800 | 无雨 |
这段代码的关键在于“只有一边越过阈值时状态才变化”,和普通的比较器不同。滞回带宽的设置直接决定系统灵敏度,带宽太窄,比如只留 100 个 LSB,仍然有概率在雷阵雨和阴天过渡时抖一次;带宽太宽,雨停十分钟系统还认为在下雨。我的经验是把宽度控制在满量程的 10%~15%,即两个阈值的差在 400~600 LSB 之间比较合适。
4. 从雨滴状态到闭环执行:继电器、串口输出和与 ESP8266 的对接
4.1 根据 rain_state 驱动继电器和蜂鸣器
状态机输出的是一个干净的数字量 0/1,执行层就变得很直接。F103 的 GPIO 驱动能力有限,不能直接接继电器线圈,需要经过三极管或光耦驱动;先把 PC13 上默认的板载 LED 作为执行机构调通逻辑,再接真实继电器更稳妥。
void rain_actuate(uint8_t state) { /* 有雨时 PC13 输出低电平,LED 点亮 */ HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, state ? GPIO_PIN_RESET : GPIO_PIN_SET); }这段代码把“判断”和“执行”分开,以后再换成 MOS 管驱动的雨棚电机,只改这个函数内部的引脚号即可。大多数继电器模块是低电平触发,和这里GPIO_PIN_RESET的写法正好对应;高电平触发的模块要把逻辑反过来,别直接照抄。
主循环里串起来的完整流程:
while (1) { uint16_t raw = read_rain_adc(); /* 裸采样 */ uint16_t avg = rain_filtered(raw); /* 滑动平均 */ uint8_t st = rain_judge(avg); /* 迟滞判断 */ rain_actuate(st); /* 执行动作 */ HAL_Delay(100); /* 100ms 采样节奏 */ }HAL_Delay(100)定了整个系统的采样节奏。做“stm32鱼缸”这种需要长期运行的场景,可以把延时放宽到 500ms,再配合 MCU 进入低功耗模式,平均电流能明显下降。判断放在主循环里是教科书式的写法,适合入门;真正小体积应用会把读取和判断挪进定时器中断,避免掉进延时阻塞 UART 的坑,但工程结构是同一套。
注意:PC13 在多数 F103 最小系统板上直接连着 LED,主循环工作正常时它每秒闪 10 次其实看不出问题,只有逻辑反转时才会暴露。接入真实继电器之前,先用万用表量一次继电器输入端的电平变化,确认无误再上强电。
4.2 用 printf 重定向串口调试输出
只看一个 LED 永远不够,你还要知道滤波前后的数据长什么样。给 Keil 工程的fputc做重定向是最省事的串口观察方案,方法是在任意 C 文件里重写标准库的字符输出函数:
int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 100); return ch; }注意 Keil 工程需要勾选 MicroLIB,否则printf打出来的是半主机模式的输出,串口上什么都看不到。函数签名里FILE *f在这个场景下不会被用到,保留即可。主循环里加一行,整条调试链就通了:
printf("RAW=%u AVG=%u STATE=%u\r\n", raw, avg, st);串口助手按 115200/8N1 打开,能看到类似RAW=3820 AVG=3812 STATE=0的输出。看这三个值就是一套判断系统是否正常的方法:RAW 跳动大说明传感器面有动态变化;AVG 平滑说明滤波窗口生效;STATE 稳定说明迟滞带在干活。如果 RAW 稳定但 AVG 一直偏高,多半是传感器表面已经干了或者模块供电掉了,这类问题不用示波器就能定位。
4.3 与 ESP8266、k210 或 Modbus 设备通信的帧设计
雨滴监测系统往往要把状态交出去,最常见的接收方是 ESP8266 做 WiFi 网关。ESP8266 AT 固件模式下,STM32 直接通过 UART 发送数据即可,发送内容用一段 JSON 最省事:
char buf[64]; snprintf(buf, sizeof(buf), "{\"rain\":%u,\"level\":%u}\r\n", st, avg); HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 200);如果 ESP8266 已经工作在透传模式,这段 JSON 可以直达服务器或上位机。要注意 STM32 和 ESP8266 之间必须共地,否则 UART 会在湿度高的环境里出现乱码,雨滴监测放在户外尤其容易踩这个。
如果对接的是 k210 这类 AI 协处理器,或者走 Modbus RTU 与变频器通信,JSON 这种变长格式在裸机接收端解析成本偏高。我一般固定成“帧头 + 长度 + 数据 + 校验”的二进制结构,比如0xAA 0x55 len data checksum。接收方拿到一个字节就进中断缓存,判一帧结束则用“空闲间隔”方法:开一个定时器,在接收中断里清零,超过 3.5 个字符时间没有新字节就认为一帧结束。这个规则也是“modbus单片机帧接收数据程序”这类实践中反复被问到的核心点——帧结束判定不是靠回车换行,而是靠超时窗口。
5. 雨滴监测系统的校准、调试和移植关键点位
5.1 Keil5 调试雨滴监测工程时“当前不会命中断点”的常见原因
带 ST-Link 烧录成功,却在 Keil5 下断点时提示“当前不会命中断点”,优先看三处设置。第一是优化等级,Options → C/C++ → Optimization 若被改成 -O1 以上,局部变量和判断语句会被合并或删除,断点自然失效,改回 -O0 重新编译。第二是调试器配置,Debug 里选择 ST-Link Debugger,勾选 Flash Download 中的 “Reset and Run”,重新上电再进入调试,断点一般能命中。第三是驱动,使用 USB-TTL 走系统 BootLoader 烧录的场合,先确认 USB 识别正常,异常时换数据线再换驱动,别反复去怀疑程序。工具链上,装上 KEIL 后再用 Pack Installer 安装 STM32F1 系列 Device Pack,和 C51 的 Pack 共存于同一工程环境,互不覆盖。
5.2 干湿两点校准:把 ADC 读数映射成百分比
固定阈值不是银弹,换一批传感器板,基线差异可能有三四百 LSB。常见做法是上电后进入 5 秒校准流程:先擦干传感器面采样记作dry_val,再滴一点水采样记作wet_val,两个变量存入全局:
uint8_t rain_percent(uint16_t avg) { if (avg >= dry_val) return 0; if (avg <= wet_val) return 100; return (uint8_t)((dry_val - avg) * 100 / (dry_val - wet_val)); }上位机只显示 0~100 的百分比,不需要理解 ADC 原始值。校准结果通过串口打印出来,如果发现dry_val与wet_val相差不到 200 个 LSB,主要怀疑传感器表面老化或污损,不是代码问题。
5.3 从 F103 换型时 CubeMX 配置和晶振匹配
许多项目做汇编转产时,直接替代料如 APM32F103 与 STM32F103 管脚兼容,程序大体可以平移,但外设库要用厂商各自的版本。真正该注意的是在“stm32 cube 程序更改单片机型号”时重新选择器件型号并生成代码,随后确认 HAL 库版本差异。换开发板时还要核对外部晶振负载电容,匹配经验公式是:
CL ≈ (C1 × C2) / (C1 + C2) + Cstray8MHz 晶振通常配 12pF~22pF 的 C1/C2,计算出的 CL 要贴近晶振手册标注的负载电容,偏差过大会导致起振慢甚至失败。雨滴监测系统对时钟精度要求不高,但 HSE 起振失败会让程序卡死在时钟初始化循环里,此时串口一片静默,最容易被人误判成逻辑或接线问题。
本文还有配套的精品资源,点击获取