简介:这是一份基于STM32单片机的雨滴监测系统完整源码包,适合嵌入式初学者及课程设计开发者参考。系统整合雨滴传感器采集、OLED显示、蜂鸣器报警和串口发送到调试助手等功能,覆盖典型外设控制流程。压缩包共280个文件,体积8.92MB,主要包含C源码与头文件、Keil工程文件、编译产物(axf、o)及备份中间文件,工程结构完整,可用Keil直接打开编译。已有70人学习下载。通过该源码可学习STM32标准外设库初始化、ADC读取雨滴传感器、OLED驱动显示、GPIO控制蜂鸣器以及串口打印数据等关键代码,同时配套的说明文件也有助于快速理解项目布局,适合入门实践与改造扩展。注意资源来自网络分享,仅供学习交流使用,勿作商业用途。
1. 雨滴监测不是把传感器焊上就能用
很多第一次做雨滴监测的开发者,以为把雨滴传感器的 DO 脚接到 STM32 GPIO 上,读到低电平就报警,工作就算完成了。实际上 DO 只是模块内部和电位器比较后的数字结果,永远无法区分毛毛雨和水膜、无法告诉你应该在哪个雨量点触发蜂鸣器。这套源码的价值在于把 AO 模拟量同时接进 OLED 显示、串口上报和蜂鸣器联动,构成一个可标定的闭环。它适合做智能窗、户外机柜防雨、农业大棚这类需要把“是否下雨”升级成“雨量等级”的项目,也适合刚接触 STM32 ADC、I2C、串口外设配合的开发者拿来当作学习模板。
2. 从雨滴传感器到 STM32 ADC:信号链路、分压与量程换算
雨滴传感器表面是裸露的梳状铜箔,水溅上去后两条电极之间的等效电阻下降。常规模块内部会用 LM393 比较器把变化转成数字量 DO,同时把传感器原始电压从比较器前端引出一路模拟量 AO。源码里出现的stm32f10x_adc.c,就是让 STM32 通过 ADC 读取 AO 电压,从而把“有没有水”扩展成“水量有多少”。
2.1 雨滴传感器模块的信号类型与接口定义
这类模块通常有四根引脚,接线方式有固定套路:
| 模块引脚 | 信号方向 | STM32 端接法 | 作用 |
|---|---|---|---|
| AO | 模拟电压输出 | PA0(ADC1_IN0) | 雨量越大,电压越低 |
| DO | 数字量输出 | PB1 或任意输入引脚 | 低于阈值输出低电平,仅供简单判断 |
| VCC | 供电 | 3.3V 或 5V | 供电选择直接决定 AO 是否超量程 |
| GND | 电源地 | GND | 共地 |
大多数雨滴模块标注电压范围 3.3~5V。如果拿 5V 供电,AO 在干燥时可能输出接近 5V,直接进 STM32 的 ADC 脚会超 3.3V 量程,严重时会损坏引脚。我一般习惯直接给模块供 3.3V,让 AO 最高点落在 3.0~3.3V 范围内,省掉一颗分压电阻。若工程里已经接了 5V,则必须在 AO 与 PA0 之间加两个电阻分压,把最大值压到 3.3V 以下。
DO 脚在源码里其实不是必须的。ADC 读到的数据已经能反映雨量,DO 只是提前帮你判断了一次。真正做雨量分级时,用 AO 更合理:把 DO 留给后期做“传感器断线检测”或“极端暴雨保护”反而更有价值。
2.2 STM32F10x ADC 初始化与连续采样代码
源码基于 STM32F10x 标准外设库,ADC 部分的核心动作是:开时钟、配置引脚为模拟输入、配置 ADC1 独立模式、开启连续转换。下面这段是可在 Keil 里直接编译的模板,以 AO 接 PA0 为例:
void Rain_ADC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); } uint16_t Rain_ADC_Read(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); return ADC_GetConversionValue(ADC1); }这段代码里有两个参数最值得注意。第一个是ADC_SampleTime_239Cycles5,雨滴传感器的输出阻抗偏高,如果采样时间太短,采样电容充不满,数值会跳得很厉害,所以直接用 ADC 最多的 239.5 个周期。第二个是ADC_ExternalTrigConv_None,表示不接定时器触发,直接在循环里软件启动转换。源码里的stm32f10x_tim.c如果被用到,通常是给蜂鸣器做节拍延时,不是用来触发 ADC。
2.3 采样值与雨量强度的换算
ADC 是 12 位的,参考电压 3.3V 时,读到的数字和电压的关系是电压 = adc / 4095 * 3.3。干燥状态电阻非常大,AO 接近 VCC,ADC 值通常高;有水后电阻下降,AO 被拉低,ADC 值变小。以下是一份不需要改电位器时的经验参照表:
| 雨滴状态 | AO 电压参考范围 | ADC 参考范围 |
|---|---|---|
| 完全干燥 | 2.8V 以上 | 3500~4095 |
| 零星水珠 | 1.5V~2.8V | 1800~3500 |
| 连续水膜 | 0.5V~1.5V | 600~1800 |
| 大面积积水 | 0.5V 以下 | 小于 600 |
注意,这个范围不是绝对的。同一块模块,滴矿泉水、滴自来水、滴盐水,ADC 数值差异很大;涂了疏水涂层的传感器响应又会慢一拍。所以源码里阈值不建议写死在注释里,应该先跑一遍串口看真实值,再往#define里填。
3. 串口上报与数据帧设计:从原始值到可解析的雨滴状态
雨滴监测系统如果只在本地 OLED 显示,调参时会非常痛苦:你既看不到传感器波形,也无法确认蜂鸣器是在哪一档被触发。把雨滴数据通过串口发到调试助手,才能把“当时发生了什么”完整还原出来。源码虽然没把 USART 单独立成文件,但串口初始化在标准外设库里非常简单,自己补一个函数即可。
3.1 USART1 初始化与 printf 重定向
以 USART1 为例,PA9 做 TX、PA10 做 RX,波特率 115200,8 数据位、1 停止位、无校验。这样配置最通用,也方便直接用串口模块接调试助手:
void USART1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, &USART_InitStructure); USART_Cmd(USART1, ENABLE); } // Keil 环境下重定向 fputc,让 printf 直接走串口 int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }代码里的USART_Mode_RX | USART_Mode_TX同时开了收发,虽然本系统只发不收,但留着接收方向不会影响功能,也方便日后把串口指令“CAL:1200”写成校准命令。重定向fputc后,printf的输出会一条条从 TX 引脚发出去,在源码的主循环里,只需要把上文 ADC 读取函数的结果打印出来即可。
3.2 雨滴数据帧格式设计
调试助手直接看裸数字很难判断“这个值是哪一档”,所以上传的数据最好既有原始 ADC、又有换算后的等级。推荐使用换行分隔的键值对文本格式,常见做法如下:
RAIN:2048,LEVEL:2 RAIN:0512,LEVEL:3 RAIN:3902,LEVEL:0主循环里只需要这样发送:
printf("RAIN:%04d,LEVEL:%d\r\n", rain_value, rain_level);每帧字段的含义固定:RAIN是传感器直接读到的 ADC 原始值,LEVEL是雨量等级。等级不建议直接由传感器电压算线性比例,最好用分段映射,因为雨滴传感器本身不是线性器件。字段前补%04d是为了让数字在调试助手里的宽度一致,不会因为位数不同导致对齐乱跳。
| 字段 | 类型 | 范围 | 含义 |
|---|---|---|---|
| RAIN | 无符号整数 | 0~4095 | 雨滴传感器 ADC 原始值 |
| LEVEL | 无符号整数 | 0~3 | 雨量等级,0 干燥、3 大雨 |
| 帧结束 | 回车换行 | \r\n | 方便调试助手按行解析 |
3.3 串口乱码与丢数据排查
串口出现乱码时不要先怀疑传感器,先检查两处。第一是芯片实际工作频率和SystemCoreClock是否一致:如果外置 8M 晶振没起振,STM32 内部 HSI 会兜底,但波特率就会被算错,实际速率不是 115200。第二是串口上位机的“发送新行”设置,本系统只上传不下发,上位机会不会自动发送无关字节并不重要,但如果你准备做双向指令,那串口调试助手也要同样配置成 115200、8N1。
丢数据通常不是串口本身的问题,而是主循环里OLED_Clear()太频繁。OLED 的 I2C 刷新一次可能占用几十毫秒,在这段时间里如果串口中断没开,printf会一直等TXE标志,数据帧排队后表现为“一串一串地到”。解决方法是把 ADC 采样、串口发送和 OLED 刷新放到同一个 100ms 周期节拍里,而不是在while(1)里裸奔。
4. OLED 显示与蜂鸣器报警:I2C 外设与阈值去抖
雨滴数据除了串口上报,还要在本地 OLED 显示。源码里stm32f10x_i2c.c和OLED.c提供了这套显示链路:STM32 通过 I2C 总线把雨滴状态、ADC 原始值写到屏幕。蜂鸣器报警则是另一个独立逻辑,但要注意,直接拿单阈值做判据会让蜂鸣器在水滴边缘反复跳动。
4.1 OLED 模块的 I2C 接线与地址确认
市面上多数小尺寸单色 OLED 使用 SSD1306 控制器,I2C 接口只用四根线:VCC、GND、SCL、SDA。在 STM32F10x 上,硬件 I2C1 的 SC L 和 SDA 默认复用为 PB6、PB7,接线时不要把 OLED 的 SCL 接到 SDA 上,这种低级错误会导致 OLED 白屏时怎么初始化都没反应。
| I2C 引脚 | STM32F103 引脚 | 说明 |
|---|---|---|
| SCL | PB6 | I2C1 时钟,必须配置为 AF_OD |
| SDA | PB7 | I2C1 数据,必须配置为 AF_OD |
| 模块地址 | 0x78 或 0x7A | 7 位地址 0x3C 左移一位 |
OLED.c 里通常已经写好了OLED_Init(),它会做 GPIO 与 I2C 时延初始化。如果源码里的 OLED.c 不是用硬件 I2C,而是模拟时序,那stm32f10x_i2c.c可能只被用来驱动其它外设。判断方法是看OLED_Init()里是否调用了I2C_Cmd()或I2C_GenerateSTART(),如果没有,说明是 IO 模拟,此时 PB6/PB7 要配置成普通推挽输出。
4.2 OLED 刷新雨滴状态的核心调用
常见的 OLED.c 接口屏蔽了底层 SSD1306 命令,主函数只要负责拼内容。下面是基于这类接口的刷新代码:
extern void OLED_Init(void); extern void OLED_Clear(void); extern void OLED_ShowString(uint8_t x, uint8_t y, char *str); extern void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len); while (1) { uint16_t adc = Rain_ADC_Read(); int level = Rain_GetLevel(adc); OLED_Clear(); OLED_ShowString(0, 0, "Rain ADC:"); OLED_ShowNum(56, 0, adc, 4); OLED_ShowString(0, 2, "Level:"); OLED_ShowNum(56, 2, level, 1); OLED_ShowString(0, 4, adc < 1500 ? "WARN !!!" : "SAFE"); OLED_Refresh(); }这里的关键是刷新频率。OLED 不必每次都清屏,连续清屏会产生明显的闪烁,而OLED_ShowString内部往往自带取模延时,大约每字符 1~2ms,清一次全屏可能要 30ms。对雨滴监测这种低频应用,100ms 刷一次已经足够。如果发现 OLED 上数字重影,一般是OLED_Clear()后没有延时再写,或者OLED_ShowNum的长度参数比实际数字位数少,导致旧字符残留在右侧。
4.3 蜂鸣器报警与阈值迟滞
蜂鸣器接在普通 GPIO 上,有源蜂鸣器直接给电平就能响,无源蜂鸣器则需要 PWM 驱动。源码里的stm32f10x_tim.c更有可能服务于无源蜂鸣器的 PWM 输出,或者用于驱动系统节拍。无论哪种,报警逻辑都不应该只用一个阈值比较,否则传感器表面水珠半干不干时,ADC 值会在阈值附近来回穿越,蜂鸣器就会连续“叭、叭、叭”。
正确做法是迟滞比较:开启报警用低阈值,关闭报警用高阈值,中间留一个缓冲区。以下代码假设有源蜂鸣器高电平触发:
#define RAIN_ON_THRESHOLD 1200 // ADC 低于该值触发报警 #define RAIN_OFF_THRESHOLD 1600 // ADC 高于该值解除报警 void Beep_Update(uint16_t adc, uint8_t *buzzer_state) { if (*buzzer_state == 0 && adc < RAIN_ON_THRESHOLD) { *buzzer_state = 1; GPIO_SetBits(GPIOB, GPIO_Pin_0); printf("ALARM:ON\r\n"); } else if (*buzzer_state == 1 && adc > RAIN_OFF_THRESHOLD) { *buzzer_state = 0; GPIO_ResetBits(GPIOB, GPIO_Pin_0); printf("ALARM:OFF\r\n"); } }两个阈值之间那 400 个数是迟滞区。当 ADC 从 3900 开始下降时,要跌到 1200 以下才响;从积水状态恢复时,要升回 1600 以上才停。这样即使传感器表面有一层不均匀的水膜,报警状态也能保持稳定。报警触发后再叠一个 500ms 延时翻转的节奏,比如响 200ms、停 300ms,会比一直长鸣更易察觉。
5. 源码工程文件导读与 Keil 环境重建
下载包里有不少看起来陌生的文件,比如project.uvguix.86187、project.axf、project_sct.Bak。它们不是源码,而是 Keil 在编译过程中产生的用户界面记录和链接中间文件。拿到压缩包后,先分清哪些该留、哪些该删,能省下不少排查时间。
5.1 你下载的包里各文件是干什么的
| 文件名 | 类型 | 作用 |
|---|---|---|
| project.uvprojx / project.uvoptx | Keil 工程文件 | 真正的工程入口,双击.uvprojx打开项目 |
| project.uvguix.86187 | 界面布局文件 | 记录窗口位置,可删除,不影响编译 |
| project.axf | 链接输出镜像 | 编译后生成的调试文件,可删除 |
| project_sct.Bak | 分散加载文件备份 | Keil 自动生成的配置备份,可删除 |
| keilkill.bat | Windows 批处理 | 一键清理编译中间文件 |
| stm32f10x_adc.c | STM32 标准外设库 | ADC 外设驱动 |
| OLED.c | 用户驱动 | OLED 显示初始化与刷屏函数 |
| stm32f10x_i2c.c | STM32 标准外设库 | I2C 外设驱动 |
| stm32f10x_tim.c | STM32 标准外设库 | 定时器外设驱动 |
| stm32f10x_flash.c | STM32 标准外设库 | Flash 驱动,通常不是雨滴逻辑主文件 |
| stm32f10x_rcc.c | STM32 标准外设库 | 时钟配置的关键依赖 |
看到stm32f10x_flash.c不要紧张,它通常是标准外设库工程创建时自动加入的 Flash 驱动文件,雨滴监测主逻辑用不到 Flash 写入,只保留文件但不调用 API 即可。
5.2 keilkill.bat 到底删了什么
keilkill.bat是一个本地清理脚本,作用是把编译产生的*.o、*.axf、*.crf、*.uvguix.*、*.bak这些文件删除,让工程回到“刚刚下载”的原始状态。如果直接分享工程,先跑一遍这个脚本能大幅缩减压缩包体积。类似脚本的常见写法如下:
@echo off del /s /q *.uvguix.* 2>nul del /s /q *.bak 2>nul del /s /q *.axf 2>nul del /s /q *.o *.__i *.crf *.d 2>nul del /s /q BuildingLog.htm 2>nul echo clean done pause脚本里的2>nul是让删除动作不因文件不存在而中断打印错误。执行后如果 Keil 还开着工程,系统会提示某些文件被占用,所以务必先关掉 Keil 再双击。这个脚本不会删除.uvprojx和源代码.c,所以也经常被放在 git 仓库里作为clean.bat使用。
5.3 从标准外设库重建 Keil 工程的步骤
如果打开工程后报Cannot open source file stm32f10x.h,说明编译器找不到标准外设库头文件路径,这时不需要下载新工程,重建一次更稳。常见重建步骤如下:
- 在 Keil MDK 里新建一个空工程,芯片型号选择 STM32F103 系列对应容量,例如 STM32F103C8 或 RCT6。
- 在当前工程目录下新建
User和Library两个目录,把源码里的OLED.c放到User. - 将标准外设库的
src目录下的stm32f10x_adc.c、stm32f10x_i2c.c、stm32f10x_tim.c、stm32f10x_rcc.c、stm32f10x_flash.c添加到工程。 - 添加启动文件
startup_stm32f10x_hd.s。 - 点击魔术棒,在 C/C++ 选项卡增加宏定义
USE_STDPERIPH_DEVICE, STM32F10X_HD。 - 在 Include Paths 中分别添加标准外设库的
inc目录、源码头文件所在目录。 - 编译时如果报
SystemInit未定义,再向工程添加标准外设库里的system_stm32f10x.c。
重建之后,再去对照 2.2 节和 3.1 节的代码,把 ADC、USART 初始化补进main,整个系统就会重新跑起来。Keil 打开工程时报错missing device时,再去确认芯片包安装版本,这通常和源码本身无关。
6. 调试技巧:用串口波形校准两个关键阈值
雨滴传感器能读回数字,但“多大给蜂鸣器报警”必须靠实测校准,不能照抄网上的常量。校准的核心思想是:先把 ADC 原始值完整打印出来,再分步做滴水测试,最后根据测试数据反向填阈值。
6.1 校准前的串口准备
先用第 3 节代码把printf("RAIN:%04d\r\n", adc)跑起来,只发原始值,不要带任何业务逻辑。把串口调试助手打开,选用带 ASCII 曲线绘制功能的工具,或者用串口日志把数据存下来。这时不需要 OLED 和蜂鸣器,因为它们会干扰你判断传感器本来的表现。
校准步骤建议分成五档:
| 测试档位 | 操作 | 观察目标 |
|---|---|---|
| 干燥基线 | 传感器裸露在空气中 | 记录稳定 ADC 最大值 |
| 单滴接触 | 滴 1 滴清水到中间 | 记录瞬时最低 ADC |
| 分散水珠 | 在 4 个不同位置各滴 1 滴 | 记录 ADC 回落和恢复趋势 |
| 连续水膜 | 用喷壶均匀喷湿表面 | 记录稳定 ADC 值 |
| 擦干恢复 | 纸巾吸干表面 | 记录恢复时间和最终值 |
每次测试之间至少间隔 5 秒,因为水会在梳状电极间缓慢扩散,ADC 会持续下降多拍。等到测试数据稳定后,开报警阈值取“干燥基线和单滴接触”的中间偏低值,关报警阈值取“连续水膜”和“擦干后”的中间偏高值。
6.2 把采样频率放到 50ms 以内
校准过程最容易踩的坑是 ADC 采样太慢,导致单滴水珠还没被采到就蒸发了。单滴水滴从接触电极到铺展开通常只有几秒钟,如果主循环里 OLED 清屏、串口等多任务占用了 200ms,采到的只是“水已经干了之后”的值。可以把 ADC 读取放到定时器中断里,也可以把主循环简化为只采样、只发送。常见做法是先用一个 10ms 延时的GetADCValue刷串口,确认曲线连续后再把 OLED 加回去。
一个经验曲线是:干燥值 3800,单滴瞬间跌到 900,稳定在 2200;喷湿后稳定在 300;擦干后 10 秒回到 3000 以上。这种情况下,开报警阈值可以设置在 1200,关报警阈值设置在 2600。开阈值必须低于“单滴稳定值”,否则偶尔溅上水滴就会触发;关阈值必须高于“恢复后的残余水膜值”,否则擦干后蜂鸣器还会持续响。
校准完成后把数值写回第 4 节的宏里,然后做一次不带人的自动测试:用滴管在传感器上方 5cm 处连续滴三滴水,蜂鸣器应该在第二滴落下后的 1 秒内稳定触发,OLED 和串口同一帧里都能看到LEVEL从 0 跳到 2,而不是先跳 1 又跳回 0。
本文还有配套的精品资源,点击获取