ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32雨滴传感器实战:ADC采样、OLED显示、蜂鸣器报警与串口调试

STM32雨滴传感器实战:ADC采样、OLED显示、蜂鸣器报警与串口调试 简介这是一份基于STM32单片机设计的雨滴检测与报警系统完整工程代码适合嵌入式初学者及单片机课程设计、电子竞赛等场景学习使用。项目以STM32为主控结合雨滴传感器、OLED显示屏、蜂鸣器与串口通信实现了雨滴信息实时采集、可视化显示、阈值报警及数据发送到串口调试助手等功能覆盖从硬件连接到软件编写的完整开发流程。资源包共232个文件压缩包大小6.48MB主要包含C语言源码、H头文件、工程配置文件及编译生成文件其中.c与.h为系统核心控制代码可直接在Keil等IDE中打开调试便于对照学习外设驱动、中断处理和通信协议。目前已有682人浏览学习适合需要参考完整项目结构、快速理解STM32外设调用及传感器处理逻辑的开发者。通过该资源可掌握GPIO、I2C/SPI驱动OLED、ADC采集雨滴信号、UART串口发送数据以及蜂鸣器联动报警等关键技术为后续独立开发嵌入式系统积累实战经验。1. 雨滴传感器 OLED 蜂鸣器 串口调试助手从“会亮”到“能跑”的 STM32 雨情站这个标题看起来是四块外设的叠加真正决定成败的不是哪个外设能点亮而是四路数据流如何在主循环里共存雨滴传感器的 AO 信号进 ADCOLED 靠 I2C 刷屏无源蜂鸣器要 PWM 才能发出不刺耳的报警音串口还要以固定节奏把雨量数据推到上位机的串口调试助手。踩过坑的人都知道直接在 while(1) 里一次把四件事做完OLED 刷新会慢半拍蜂鸣器报警时 ADC 数据还会周期性跳变。这套方案适合正在做 STM32 课设、毕业设计或者想搭一套低成本雨情监测原型的人搭的顺序建议是先读准传感器原始值再做显示、报警、上报最后用 OLED 上的原始 AD 值完成阈值标定。2. 雨滴传感器的 AO 采样与 ADC 读取先把原始值读准2.1 DO 与 AO 的差别以及为什么要走模拟量雨滴传感器模块常见是叉指式探头加一块 LM393 比较器小板给两个输出DO 是经比较器后的数字量只能感知“有没有到达阈值”阈值要手动拧电位器AO 是探头分压后的原始模拟电压直接进 STM32 的 ADC能分辨小雨、中雨、大雨还能通过软件调整等级阈值比反复拧电位器省事。我的惯用接线是放弃 DO只把 AO 接 STM32 的 ADC 输入引脚。以 STM32F103C8T6 为例模块引脚接 STM32 引脚说明VCC3.3V模块比较器供电GNDGND必须与 STM32 共地AOPA0ADC1_IN0 模拟输入DO不接阈值比较输出本项目不用这里有一个方向问题AO 输出是“电压越高表示水膜越薄”还是“电压越高表示水膜越厚”不同模块的探头布局可能相反。选线时先记下干态读数再滴水看方向之后在代码里做一次反向映射即可不必改动硬件。注意供电电压模块用 5V 供电时AO 最高能拉到 5V而 STM32 的 ADC 参考是 3.3V直接进 PA0 有烧引脚风险。用 3.3V 供电最省事或者中间加电阻分压。2.2 STM32 ADC 初始化分辨率、采样时间、滤波雨滴信号本身是缓慢变化的物理量采样率要求极低几十毫秒读一次就够。真正的麻烦是探头上的水雾会带来抖动所以 ADC 要开多次采样取平均。以下是一个 HAL 库写法重点参数写在注释里。// rain_adc.c - STM32F103 雨滴传感器 AO 采样 #include rain_adc.h ADC_HandleTypeDef hadc1; void RainADC_Init(void) { __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_0; // PA0 gpio.Mode GPIO_MODE_ANALOG; // ADC 输入必须配成模拟模式 HAL_GPIO_Init(GPIOA, gpio); hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV6; hadc1.Init.Resolution ADC_RESOLUTION_12B; // 0~4095 hadc1.Init.ScanConvMode DISABLE; hadc1.Init.ContinuousConvMode ENABLE; hadc1.Init.NbrOfConversion 1; HAL_ADC_Init(hadc1); ADC_ChannelConfTypeDef ch {0}; ch.Channel ADC_CHANNEL_0; ch.SamplingTime ADC_SAMPLETIME_55CYCLES_5; // 采样时间长抗毛刺 HAL_ADC_ConfigChannel(hadc1, ch); HAL_ADCEx_Calibration_Start(hadc1); // 旧版 HAL 库没有就删掉这行 } uint16_t RainADC_ReadFiltered(void) { uint32_t sum 0; for (int i 0; i 8; i) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); sum HAL_ADC_GetValue(hadc1); } return (uint16_t)(sum / 8); }参数说明分辨率为 12 位读值 0~4095 对应 0V~3.3VADC_SAMPLETIME_55CYCLES_5让采样时间偏长雨滴电压不是高频信号长采样能减少前级干扰影响。读 8 次取平均是最简单的滑窗滤波代价是一次转换变慢但雨滴变化是秒级的这个速度完全够用。如果嫌阻塞式读取占 CPU可以改成 ADC DMA 循环采样给一个uint16_t dma_buf[16]启动 DMA 后硬件自动填缓冲主循环直接取 16 个值的均值。这样 ADC 采样过程完全不占用指令周期。2.3 从原始值映射到雨量等级阈值不要抄要实测拿到干净的原始值后下一步是分级。不要在代码里写死所谓“标准阈值”先把探头放在干燥桌面上读一次再滴一滴水读一次记录数据后填表。3.3V 供电下的参考范围大致是这样等级ADC 范围参考现象无雨2900探头干燥读数偏高小雨2000~2900少量水珠局部搭接中雨1200~2000水膜连续覆盖大雨1200水膜完全导通读数接近低值映射函数用区间判断即可typedef enum {RAIN_NONE, RAIN_LIGHT, RAIN_MID, RAIN_HEAVY} RainLevel; RainLevel RainADC_ToLevel(uint16_t raw) { if (raw 2900) return RAIN_NONE; if (raw 2000) return RAIN_LIGHT; if (raw 1200) return RAIN_MID; return RAIN_HEAVY; }如果读数是“干时低、湿时高”和上表方向相反不要急着改硬件。在RainADC_ToLevel里先做一次反向映射例如raw 4095 - raw;再进入分级就能适配两种探头布局。这个反向映射的问题用 OLED 显示原始值时一眼就能看出来。3. OLED 显示雨量数据SSD1306 的 I2C 驱动与刷新节奏3.1 选 I2C 版本的理由两线、地址、上拉电阻0.96 寸 OLED 常见接口有 I2C 和 SPI 两种。这个项目的显示内容只有两三行数字和文字数据量不大I2C 版本只需 SDA、SCL 两根数据线能省下几个 GPIO所以我的选择是 I2C SSD1306 控制器。引脚连接SCL 接 PB6I2C1_SCLSDA 接 PB7I2C1_SDA模块 VCC 接 3.3V。I2C 总线需要上拉电阻多数 OLED 模块板上已经自带如果用的是裸屏SDA 和 SCL 各接一个 4.7kΩ 电阻到 3.3V。关于地址有一个高频问题模块背面写着 0x78代码里却填 0x3C。两者不矛盾0x78 是 7 位地址左移一位后再加读写位的写法0x3C 才是 7 位地址本身。HAL 库的HAL_I2C_Master_Transmit内部会把传入地址再左移一位所以传 0x3C。部分模块的地址选择电阻会把地址改成 0x3D初始化前先用 I2C 扫描程序确认一遍能省掉半小时黑屏排查。3.2 I2C 控制字节与 SSD1306 初始化命令和数据不能走同一个通道SSD1306 的 I2C 通信和普通 EEPROM 不同每个数据包在从机地址之后要先发一个控制字节0x00 表示后续是命令0x40 表示后续是显存数据。写函数如下// ssd1306.c - 0.96 寸 OLED I2C 写命令/写数据 #include ssd1306.h extern I2C_HandleTypeDef hi2c1; static void OLED_WriteCmd(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; // 0x00 控制字节命令 HAL_I2C_Master_Transmit(hi2c1, 0x3C, buf, 2, 50); } static void OLED_WriteData(uint8_t dat) { uint8_t buf[2] {0x40, dat}; // 0x40 控制字节数据 HAL_I2C_Master_Transmit(hi2c1, 0x3C, buf, 2, 50); } void OLED_Init(void) { // 精简初始化序列完整序列还含对比度、时钟分频等参数 static const uint8_t cmds[] { 0xAE, // 关闭显示 0x20, 0x00, // 水平寻址模式 0xB0, // 页地址从 PAGE0 开始 0xC8, // 从上到下扫描 0x8D, 0x14, // 开启内部电荷泵 0xAF // 开启显示 }; for (int i 0; i sizeof(cmds); i) { OLED_WriteCmd(cmds[i]); } OLED_Clear(); }初始化顺序有讲究先关显示再配寻址模式和扫描方向最后开电荷泵、开显示。顺序反了或电荷泵没开屏幕会白屏但不一定完全不亮表现为“有影子但很淡”。如果你手里是 1.3 寸 SH1106 屏SSD1306 的命令序列整体兼容驱动代码可以直接迁移。显示内容建议走显存缓冲分配一个uint8_t oled_buf[1024]画点、画字符串都往缓冲里写画完一次性把整帧推到 SSD1306 的 GDDRAM。这样能避免一边刷显存一边被 I2C 中断打断造成的闪烁代价是占 1KB RAM对 STM32F103C8T6 的 20KB RAM 来说完全可以接受。3.3 刷新节奏全屏刷一次约 30ms别在主循环里死刷一个 128x64 单色屏显存是 1024 字节。I2C 速率 400kHz 时传 1024 字节加控制字节大约 20ms 以上再加上 SSD1306 内部的页面地址开销单帧全量刷新在 30ms 这个量级。如果主循环每 10ms 刷一次I2C 总线几乎被占满ADC 采样、串口发送都会被拖慢。这个项目里固定 200ms 的 OLED 刷新周期由系统时基计数触发。显示内容每 200ms 更新一次刷新函数只改变化区域而不是每次都全屏推一遍。常用做法是让显示函数接收 raw 和 level 两个参数内部拼好字符串写缓冲void OLED_ShowRainInfo(uint16_t raw, RainLevel level) { char line[16]; OLED_ClearBuffer(); // 只清缓冲不清屏幕 OLED_ShowString(0, 0, Rain Sensor, 12); sprintf(line, RAW:%4d, raw); OLED_ShowString(0, 2, line, 12); sprintf(line, LV:%d, (int)level); OLED_ShowString(0, 4, line, 12); OLED_Refresh(); // 一次性推送整帧 }注意line[16]的空间要足够容纳RAW:4095这类最大输出sprintf后的长度不要超过缓冲避免越界覆盖相邻变量。OLED 显示里的 y 坐标按页算一页 8 像素常见的 8x16 点阵字符正好占两页所以代码里用 0、2、4 这样间隔的页码。4. 蜂鸣器报警与主循环调度无源蜂鸣器 PWM 发声不让延时卡死系统4.1 有源 vs 无源报警音差异和驱动电路蜂鸣器分两类有源蜂鸣器内部带振荡电路给高电平就响不需要驱动频率但音色单调音量没法分档无源蜂鸣器需要外部提供方波才发声频率由你决定用 PWM 控制时可以做出急促、长鸣、旋律等不同报警模式。这个项目的报警语义跟着雨量等级走小雨短促响两下中雨连续急促响大雨长鸣。有源蜂鸣器做不到这种区分所以我用无源蜂鸣器加 TIM3 PWM。驱动电路不能把蜂鸣器直接怼在 GPIO 上STM32 GPIO 直接驱动蜂鸣器不但声音小还可能触发引脚过流。常见接法是 NPN 三极管S8050 即可做开关GPIO 经 1kΩ 电阻接基极蜂鸣器正极接 3.3V 或 5V负极接集电极发射极接地并在蜂鸣器两端反向并联一个续流二极管防止关断瞬间的反电动势干扰电源。4.2 TIM3 PWM 初始化频率、占空比、通道选择假设用 TIM3_CH3 对应的 PB0 输出 PWM// buzzer.c - 无源蜂鸣器 PWM 初始化输出 1kHz 方波 TIM_HandleTypeDef htim3; void Buzzer_Init(void) { __HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_0; // PB0 TIM3_CH3 gpio.Mode GPIO_MODE_AF_PP; // 复用推挽 gpio.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, gpio); htim3.Instance TIM3; htim3.Init.Prescaler 72 - 1; // 72MHz/72 1MHz htim3.Init.Period 1000 - 1; // 1MHz/1000 1kHz htim3.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(htim3); TIM_OC_InitTypeDef oc {0}; oc.OCMode TIM_OCMODE_PWM1; oc.Pulse 500; // 50% 占空比 oc.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim3, oc, TIM_CHANNEL_3); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_3); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_3, 0); // 默认静音 }频率公式是72MHz / (Prescaler1) / (Period1)上例得到 1kHz。占空比由Pulse / Period决定Pulse 为 500 时正好 50%。占空比取 50% 的原因是无源蜂鸣器靠电压变化的边沿振动发声50% 方波交流分量最大声音响占空比调到 10% 声音会发闷。需要改变报警音调时不要动占空比改Period并重新配置比较值。4.3 用 10ms 状态机做非阻塞报警蜂鸣器不再拖死 ADC 和串口新手最常见的问题是报警时写HAL_Delay(500)响 500ms 停 500ms。这个写法在纯蜂鸣器示例里没问题放到本项目里OLED 刷新、ADC 采样在等待期间全部被冻住。正确做法是把报警节奏拆成时间状态机在系统滴答10ms里扫一遍响多久、停多久由计数值决定// 报警级别由雨量等级映射0 静音 / 1 短促 / 2 急促 / 3 长鸣 volatile uint8_t buzzer_level 0; void Buzzer_Task(void) { static uint8_t phase 0; // 0响 1停 static uint16_t cnt 0; if (buzzer_level 0) { __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_3, 0); // 静音 phase 0; cnt 0; return; } switch (phase) { case 0: __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_3, 500); // 响 if (cnt 20) { cnt 0; phase 1; } // 200ms break; case 1: __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_3, 0); // 停 if (cnt 30) { cnt 0; phase 0; } // 300ms break; } }Buzzer_Task()由定时器中断或 SysTick 回调每 10ms 调一次。蜂鸣器报警变成了异步任务主循环的空闲时间全部留给 ADC 和 OLED。不同的报警级别不要靠改延时实现而是改状态机的两组计数值急促报警可以响 50ms 停 50ms长鸣可以只响不停。Proteus 仿真里遇到蜂鸣器没声音往往也是因为仿真模型只接受方波驱动直流电平不会让模型发声先确认 PWM 有没有真正开启。5. 串口把雨量送到调试助手数据帧格式、整机排错与阈值标定5.1 USART1 数据帧一个 sprintf 解决但发送节奏要控调试阶段用串口把 raw 和 level 打到上位机注意两点数据帧格式要让串口调试助手按行分开发送节奏不能占满主循环。帧格式用纯文本行形如RAIN:2980 LV:0以\r\n收尾。上位机零配置就能看也能直接复制进 Excel 做统计。USART1 初始化 PB6/PB7 之外的 PA9TX、PA10RX波特率 1152008 位数据、无校验、1 位停止位。发送代码// uart_debug.c #include stdio.h #include string.h void Debug_SendRain(uint16_t raw, RainLevel level) { char buf[32]; int len sprintf(buf, RAIN:%4d LV:%d\r\n, raw, level); HAL_UART_Transmit(huart1, (uint8_t*)buf, len, 100); }sprintf的返回值直接作为发送长度比再调一次strlen干净。HAL_UART_Transmit是阻塞发送115200 波特率下 32 字节耗时不过 3ms用一个 500ms 的软件定时任务触发不会卡主循环。不要在 ADC 中断或定时器中断里调用这个函数阻塞式发送会给中断响应引入不确定延迟。5.2 整机排错先分链路再调参数现象检查点常见修复串口乱码晶振频率、HSE_VALUE、APB2 分频8MHz 晶振按 25MHz 配置波特率必然不准OLED 白屏或黑影I2C 地址、上拉电阻、电荷泵先跑 I2C 扫描确认地址是 0x3C 还是 0x3D蜂鸣器不响TIM 时钟、复用功能、占空比用逻辑分析仪看 PB0 是否有方波ADC 读数方向反了模块 AO 与参考电压方向在分级前做4095 - raw反向映射OLED 刷新卡顿全屏刷新周期太短、I2C 速率太低刷新周期放到 200msI2C 配 400kHz这些现象大多不是“坏了”而是链路某一环配置与实物不一致。逐个模块单独验证后再合并比整体下断点调试更快。5.3 用 OLED 显示 ADC 原始值做阈值标定最后的落地技巧是把 OLED 临时当作“AD 值显示器”让阈值标定变成可见的过程。在OLED_ShowRainInfo里把 raw 四位数显示出来后分别记录干态、滴水、覆盖水膜时的读值再把这三个读数填回RainADC_ToLevel。我的做法是先把阈值写成分段数组标定过程中只改数组不改业务逻辑const uint16_t rain_threshold[3] {2900, 2000, 1200}; // 下标 0无雨下限1小雨下限2中雨下限以后换一批传感器复制一段串口日志里的 raw 值按同样顺序更新数组即可。这个流程不需要示波器也不需要改接线比对着数据手册猜阈值快得多。调试完成后可以把显示函数里刷 new 的等级字段保留为工作模式OLED 第一行显示 RAW 原始值第二行显示等级整个雨情站的运行状态一眼就能确认。本文还有配套的精品资源点击获取
返回列表