传感器 · 雨滴检测 · C71
雨滴传感器 C71 STM32F103 接线与代码
雨滴传感器 C71 就是给单片机装一个「有没有水、水多不多」的检测器——水滴跨接感应板上的梳齿电极,电阻变小;AO 输出连续的湿润程度,DO 输出「过没过阈值」一个开关量。本页是 STM32F103C8T6 + C71 + 0.96 OLED 的完整实机验证记录,含接线表、完整代码和 PlatformIO 工程下载。最值得看的是「AO 和 DO 到底有什么区别」和「电位器调的不是灵敏度,是门槛」这两段。
Quick Facts
- 模块:C71,雨滴感应板 + LM393 比较器小板
- 接线:AO→PA0,DO→PA1,4 根线
- 输出:AO 模拟湿度(0~100%),DO 数字阈值
- 显示:0.96 寸 OLED,SSD1306,地址 0x3C
- 阈值:DRY 0-24 / LIGHT 25-45 / WET 46-70 / HEAVY 71-100
- 滤波:8 次平均 + EMA + 3% 状态滞回
Verified
实机验证结果
本项目实测 下面的结果全部来自我们自己的实机测试,更新于 2026-08-17。
四档湿度实测
| 测试动作 | OLED 显示 | 雨滴图标 |
|---|---|---|
| 感应板完全擦干 | LEVEL 0~24%,STATUS DRY | 云朵,无雨滴 |
| 滴少量水 | LEVEL 25~45%,STATUS LIGHT | 2 滴 |
| 水增多 | LEVEL 46~70%,STATUS WET | 3 滴 |
| 感应板基本湿透 | LEVEL 71%+,STATUS HEAVY | 4 滴 |
OLED 屏幕是一个完整的仪表盘:顶部 RAIN SENSOR 标题 + 右上角 DO 触发点(实心=触发),左侧是云朵 + 雨滴图标,右侧是大字号的 LEVEL: 72%,下方 STATUS: WET,最底一条满宽进度条。水越多,百分比越高、进度条越满、雨滴越多;擦干感应板后逐步回落。
串口输出(115200)
Rain sensor demo v1.0 starting...
OLED 0x3C: found
AO:3820 DO:1 PCT:3 DRY
AO:2890 DO:1 PCT:35 LIGHT
AO:2100 DO:0 PCT:72 WET
AO:1560 DO:0 PCT:86 HEAVY
串口每 500ms 输出一行:AO 是原始 ADC 读数(0~4095,用于校准),DO 是比较器电平(0=低/触发,1=高/未触发),PCT 是映射后的百分比,最后是状态。
实测环境
| 项目 | 本项目实际使用 |
|---|---|
| 主控 | STM32F103C8T6(BluePill) |
| 模块 | 雨滴传感器 C71(雨滴感应板 + LM393 比较器小板,4 针 VCC / GND / DO / AO) |
| 显示 | 0.96 寸 OLED,SSD1306,128×64,地址 0x3C |
| 状态灯 | BluePill 板载 LED,PC13(低电平点亮,DO 触发时亮) |
| 开发工具 | VS Code + PlatformIO |
| 框架 | Arduino / STM32duino |
| 烧录 | ST-Link |
| 串口 | 115200 |
| 依赖库 | Adafruit SSD1306、Adafruit GFX Library |
What it does
这个模块干什么
雨滴传感器 C71 就是让单片机知道「现在有没有水、水多不多」。它由两块小板组成:一块是带梳齿电极的感应板,一块是带 LM393 比较器的控制板,中间一根连接线。感应板上没水时,两排梳齿电极是断开的(电阻很大);水滴落上去,水膜把相邻梳齿「跨接」起来,等效电阻下降。电阻一变,后面的电压就跟着变。
模块有两个输出,用途完全不同:AO(模拟量)实时反映湿润程度,程序把它换算成 0~100% 的湿度百分比;DO(数字量)只回答「过没过阈值」——水多到跨过电位器设定的门槛就翻转。一个看趋势,一个看达标,两个配合才把「雨滴检测」讲完整。
三个要点
| 要点 | 意思 |
|---|---|
| 水膜跨接梳齿电极 | 不是模块「看见了水」,是水改变了导电状态:水滴把交错的梳齿连起来,电阻下降 |
| AO 看程度、DO 看达标 | AO 是连续的湿度值;DO 是比较器输出,只告诉你有没有跨过设定门槛 |
| 电位器调的是门槛 | 蓝色电位器调的不是「灵敏度放大」,是 DO 的触发门槛:调高后同样几滴雨可能就不触发 |
AO 和 DO 怎么选
| AO(模拟输出) | DO(数字输出) | |
|---|---|---|
| 输出形式 | 连续电压,随湿润程度变 | 高低电平,只二选一 |
| 能回答什么 | 现在大概多湿(0~100%) | 有没有超过门槛(是 / 否) |
| 适合场景 | 看趋势、显示百分比、做进度条 | 报警、触发、点亮 LED |
| 本项目用 | AO→PA0,算湿度百分比 | DO→PA1,判断比较器是否触发 |
对比依据为模块两类输出的定义与常规工程实践,由我们整理成表。资料参考
Wiring
接线
本项目实测 C71 模块 4 根线 + OLED 4 根线,下面接法实机跑通。
C71 雨滴模块(丝印 VCC / GND / DO / AO)
| 信号 | 模块丝印 | STM32F103C8T6 |
|---|---|---|
| 模拟输出 | AO | PA0(ADC) |
| 数字输出 | DO | PA1(内部上拉) |
| 电源 | VCC | 3.3V |
| 地 | GND | GND |
OLED
| 信号 | OLED | STM32F103C8T6 |
|---|---|---|
| 电源 | VCC / VDD | 3.3V |
| 地 | GND | GND |
| 时钟 | SCL / SCK | PB8 |
| 数据 | SDA | PB9 |
烧录与串口(不接也能跑,接上方便看数据)
| 用途 | 连接 | 说明 |
|---|---|---|
| 烧录 | ST-Link → SWD 四线(3.3V / GND / SWDIO / SWCLK) | 烧录用 |
| 串口 | PA9(TX) / PA10(RX) → CP2102 等 | 看串口数据(115200) |
1. 供电用 3.3V,不要接 5V。AO 是模拟量,直接进 STM32 的 ADC,3.3V 供电能保证 AO 输出 0~3.3V 不超量程;接 5V 有可能超 ADC 上限。
2. OLED 的 I2C 用的是重映射引脚 PB8/PB9,不是默认的 PB7/PB6。代码里必须先
Wire.setSDA(PB9)、Wire.setSCL(PB8),再 Wire.begin(),顺序反了读不到屏幕。3. 必须共地。模块、OLED、STM32 的 GND 要接在一起,否则 AO/DO 信号对不上。
| 要点 | 说明 |
|---|---|
| AO 走 ADC | PA0 是 12 位 ADC,代码 analogReadResolution(12),读数 0~4095 |
| DO 走内部上拉 | 代码用 INPUT_PULLUP 读 PA1,LM393 是开漏输出,内部上拉兜底 |
| 板载 LED | PC13 低电平点亮,DO 触发时亮,表示比较器已翻转 |
| 丝印为准 | 本块 4 针顺序按丝印 VCC / GND / DO / AO 接,不要照着照片数孔位 |
Principle
工作原理
资料参考 这一节来自公开资料和常规工程实践,不是我们实测的结论,用来帮助理解。实测结果只在上面「实机验证结果」一节。
梳齿电极:干燥时是断开的
感应板上有两组交错的梳齿电极(A 和 B),像两把梳子齿对齿插在一起,但彼此不相连。干燥时两排电极之间是空气(几乎不导电),等效电阻很大,所以读到的电压很高(接近电源)。
水膜跨接:为什么水能被检测出来
水滴落到感应板上,水(里面含少量可导电的离子)会在相邻梳齿之间形成一条导电路径,相当于在两排电极之间架了桥。水覆盖越多,桥越多、越宽,等效电阻就越小,电压随之下降。所以模块检测到的不是「水」本身,而是「电阻变了」——水改变了电极之间的导电状态。
LM393:把「过没过线」变成高低电平
LM393 是一个电压比较器:一端接感应板送来的信号电压,另一端接电位器设定的参考电压(门槛)。信号电压跨过门槛的瞬间,比较器输出翻转(DO 从高变低,或反过来),并把板载 LED 点亮。所以 DO 只告诉你「达标了没有」,不告诉你「多湿」。
| 环节 | 干燥时 | 有水时 |
|---|---|---|
| 梳齿电极之间 | 空气,不导通 | 水膜跨接,导通 |
| 等效电阻 | 大 | 小(水越多越小) |
| AO 电压 | 高(接近电源) | 低(水越多越低) |
| DO 输出 | 未触发 | 跨过门槛后触发 |
梳齿电极、水膜导电、比较器原理来自公开资料与常规工程实践,由我们重新整理表述。资料参考
电位器调的是门槛,不是「灵敏度放大」
很多人以为旋电位器是「调灵敏度」。更准确地说,它调的是 DO 的触发门槛:门槛调低,一点点水就触发;门槛调高,同样几滴雨可能就不触发。它不影响 AO 的连续读数——AO 始终如实反映湿润程度。
Program logic
程序逻辑
本项目实测 下面是对实际代码 src/main.cpp(v1.0)逻辑的说明,不展开语法,只讲「程序是怎么想的」。
整体是「AO 一路算湿度,DO 一路看触发」两条线
AO ─► 8次ADC平均 ─► EMA平滑 ─► 干湿校准 ─► 0~100% ─► 状态滞回 ─► OLED
DO ─► 5次取多数防抖 ─► LED + OLED 右上指示点
主循环 loop() 里每 20ms 采一次样、每 200ms 刷一次 OLED、每 500ms 打一行串口,三条时间线互不阻塞。
| 步骤 | 做什么 | 对应代码 |
|---|---|---|
| 1. 读 AO | 一次读 8 次 ADC 取平均,抗感应板波动 | readAdcAverage() |
| 2. 平滑 | 指数滑动平均,数值不跳 | g_smoothAdc += (raw - g_smoothAdc) * EMA_ALPHA |
| 3. 校准 | 按干/湿两点线性映射到 0~100%,方向自适应 | mapToPercent() |
| 4. 分档 | 带 3% 滞回,分 DRY/LIGHT/WET/HEAVY | classifyStateWithHysteresis() |
| 5. 读 DO | 连续 5 次取多数,避免阈值附近抖动 | readDoStable() |
| 6. 输出 | OLED 仪表盘 + 板载 LED + 串口 | drawOledUi() / LED / printSerialData() |
为什么百分比要「校准」而不是硬编码
ADC 读到的原始值(0~4095)直接显示没有意义,必须映射到「0%=干、100%=湿」。但不同模块、不同供电、不同感应板的干燥值和湿润值都不一样,所以代码把这两个点单独定义成 RAIN_DRY_VALUE 和 RAIN_WET_VALUE,实测后改这两行即可。映射公式 (raw - DRY) / (WET - DRY) × 100 是方向自适应的:无论你的模块是「越湿 AO 越小」还是「越湿 AO 越大」,OLED 上都不会出现「水越多百分比越低」。
为什么要「8 次平均 + EMA + 滞回」三层滤波
雨滴感应板本身就容易波动,如果直接读一次就显示,百分比会抖得厉害,拍视频很难看。三层各管一段:8 次平均挡掉瞬时毛刺,EMA 指数滑动平均让数值平滑过渡,3% 状态滞回防止湿度停在 45/46 边界时 DRY/LIGHT 来回跳。三者叠加,数值稳定、状态不抖。
DO 为什么要防抖
LM393 在刚好卡在电位器门槛附近时,DO 会快速 0/1 来回跳。代码连续读 5 次,至少 3 次 LOW 才算触发,避免板载 LED 跟着乱闪。
Code
完整代码
本项目实测 下面是实际烧录进板子、实机测试通过的那一份(v1.0),一字未改。环境:VS Code + PlatformIO + Arduino(STM32duino)。
关键处说明
1. 校准值单独定义。RAIN_DRY_VALUE 和 RAIN_WET_VALUE 是干燥/湿润两个 ADC 读数,实测后只改这两行。当前是占位值 3800 / 1200(越湿 AO 越小),实际以你的模块实测为准。
2. 方向自适应映射。mapToPercent() 用 (raw - DRY) / (WET - DRY),分子分母可同时为负,所以 ADC 方向反了也自动正确,OLED 永远 0%=干、100%=湿。
3. DO 极性默认「低电平=触发」。LM393 是开漏输出,多数模块有水时 DO 拉低。若你的模块实测相反,只改 readDoStable() 里的 == LOW 为 == HIGH 一处。
4. OLED 是完整仪表盘。云朵 + 雨滴图标(0/2/3/4 滴随状态变、每 0.6s 错开相位轻晃)、LEVEL 大字号百分比、STATUS 四档、满宽进度条。
5. 供电 3.3V + 12 位 ADC。PA0 用 analogReadResolution(12) 明确 12 位(0~4095),AO 直接进 ADC 不超量程。
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define FIRMWARE_VERSION "v1.0"
// =========================
// GPIO 配置
// =========================
// 雨滴检测模块(雨滴感应板 + LM393 比较器小板,4 针:VCC / GND / DO / AO)
// AO -> PA0(模拟量,ADC1_IN0,实时反映感应板湿度)
// DO -> PA1(数字量,LM393 比较器输出,超过电位器阈值时翻转)
// VCC -> 3.3V(3.3V 供电,AO 输出 0~3.3V,与 STM32 ADC 量程一致,不要接 5V)
// GND -> GND
// 板载 LED -> PC13(bluepill 板载 LED,低电平点亮;DO 触发时点亮)
constexpr uint8_t RAIN_AO_PIN = PA0;
constexpr uint8_t RAIN_DO_PIN = PA1;
constexpr uint8_t LED_PIN = PC13;
// OLED 使用 I2C1 重映射引脚:SCL=PB8 / SDA=PB9(与 01~07 系列公共标准一致)
constexpr uint8_t I2C_SDA_PIN = PB9;
constexpr uint8_t I2C_SCL_PIN = PB8;
constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr uint8_t SCREEN_WIDTH = 128;
constexpr uint8_t SCREEN_HEIGHT = 64;
constexpr int OLED_RESET = -1;
// =========================
// 校准值(实测后只改这两行)
// =========================
// 0% = 干燥,100% = 很湿。程序按这两个点做线性映射,
// 所以无论 ADC 方向是「越湿越小」还是「越湿越大」,显示都自动保持 0%=干 / 100%=湿。
// 下面两个是占位值,先让界面能跑;实测后按 README「校准」改成真实读数。
constexpr uint16_t RAIN_DRY_VALUE = 3800; // 干燥时的 ADC 读数(实测后改)
constexpr uint16_t RAIN_WET_VALUE = 1200; // 完全湿润时的 ADC 读数(实测后改)
// 状态阈值(0~100,用户实测确认后,为拍视频把 LIGHT/WET 拉宽,4 档都能停留被拍到)
// 实际切换用带滞回的 classifyStateWithHysteresis,边界上下各留 3% 防抖
constexpr uint8_t TH_DRY_MAX = 24; // 0~24 DRY
constexpr uint8_t TH_LIGHT_MAX = 45; // 25~45 LIGHT
constexpr uint8_t TH_WET_MAX = 70; // 46~70 WET
// 71~100 HEAVY
// =========================
// 采样 / 滤波 / 刷新周期
// =========================
constexpr uint32_t SAMPLE_INTERVAL_MS = 20; // 采样周期
constexpr float EMA_ALPHA = 0.08f; // 指数滑动平均系数(越小越平滑)
constexpr uint32_t OLED_REFRESH_MS = 200; // OLED 重绘周期(约 5fps)
constexpr uint32_t SERIAL_INTERVAL_MS = 500; // 串口打印周期
constexpr uint32_t DROP_ANIM_MS = 600; // 雨滴轻微动画半周期
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// 状态
enum class RainState : uint8_t { DRY, LIGHT, WET, HEAVY };
bool g_hasOled = false; // OLED 是否初始化成功
uint16_t g_rawAdc = 0; // 最近一次 ADC 读数(8 次平均后)
float g_smoothAdc = 0; // 滑动平均后的 ADC
bool g_smoothInit = false; // 是否已初始化平均
uint8_t g_percent = 0; // 0~100(0=干,100=湿)
RainState g_state = RainState::DRY;
bool g_doTriggered = false; // DO 是否触发(比较器输出低电平)
static uint32_t g_lastSampleMs = 0;
static uint32_t g_lastOledMs = 0;
static uint32_t g_lastSerialMs = 0;
// 雨滴 4 个槽位的 x 坐标 + 基准 y 坐标
static const int8_t DROP_X[4] = {21, 31, 41, 51};
static const int8_t DROP_Y = 40;
// =========================
// 读取
// =========================
// DO:LM393 开漏输出,板上一般已有上拉,这里再开内部上拉更稳。
// 默认按「低电平 = 触发(水超过电位器阈值)」写,这是最常见的情况。
// 若实测相反,把下面 == LOW 改成 == HIGH 即可(只改这一处)。
// 连续读 5 次取多数(>=3 次 LOW 才算触发),避免卡在电位器阈值附近抖动。
static bool readDoStable() {
uint8_t lowCount = 0;
for (uint8_t i = 0; i < 5; i++) {
if (digitalRead(RAIN_DO_PIN) == LOW) lowCount++;
}
return lowCount >= 3;
}
// =========================
// 百分比映射(0% = 干,100% = 湿,方向自适应)
// =========================
// pct = (raw - DRY) / (WET - DRY) * 100
// 分子分母可同时为负,所以 ADC 方向相反也自动正确。
static uint8_t mapToPercent(uint16_t raw) {
const int32_t num = (int32_t)raw - (int32_t)RAIN_DRY_VALUE;
const int32_t den = (int32_t)RAIN_WET_VALUE - (int32_t)RAIN_DRY_VALUE;
if (den == 0) return 0;
int32_t pct = (num * 100) / den;
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
return (uint8_t)pct;
}
// =========================
// 状态判断
// =========================
static RainState classifyState(uint8_t pct) {
if (pct <= TH_DRY_MAX) return RainState::DRY;
if (pct <= TH_LIGHT_MAX) return RainState::LIGHT;
if (pct <= TH_WET_MAX) return RainState::WET;
return RainState::HEAVY;
}
// 带滞回的状态判断:切换边界上下各留 HYS%,
// 避免湿度在 54/55/56 附近抖动时 DRY/LIGHT/WET 来回跳(拍视频特别明显)。
static RainState classifyStateWithHysteresis(uint8_t pct) {
constexpr uint8_t HYS = 3;
switch (g_state) {
case RainState::DRY:
if (pct > TH_DRY_MAX + HYS) return RainState::LIGHT;
break;
case RainState::LIGHT:
if (pct <= TH_DRY_MAX - HYS) return RainState::DRY;
if (pct > TH_LIGHT_MAX + HYS) return RainState::WET;
break;
case RainState::WET:
if (pct <= TH_LIGHT_MAX - HYS) return RainState::LIGHT;
if (pct > TH_WET_MAX + HYS) return RainState::HEAVY;
break;
case RainState::HEAVY:
if (pct <= TH_WET_MAX - HYS) return RainState::WET;
break;
}
return g_state;
}
static const char* getStateText() {
switch (g_state) {
case RainState::DRY: return "DRY";
case RainState::LIGHT: return "LIGHT";
case RainState::WET: return "WET";
case RainState::HEAVY: return "HEAVY";
}
return "?";
}
// 每个状态对应的雨滴个数(干燥无滴,水越多滴越多)
static uint8_t getDropCount(RainState s) {
switch (s) {
case RainState::DRY: return 0; // 干燥:无雨滴
case RainState::LIGHT: return 2; // 检测到水:1~2 滴
case RainState::WET: return 3; // 水量增加:3 滴
case RainState::HEAVY: return 4; // 水量明显:4 滴
}
return 0;
}
// =========================
// 采样(8 次平均) + 滑动平均 + 滞回
// =========================
// 一次读 8 次 ADC 取平均,再送指数滑动平均,抗感应板波动
static uint16_t readAdcAverage() {
uint32_t sum = 0;
for (uint8_t i = 0; i < 8; i++) {
sum += analogRead(RAIN_AO_PIN);
}
return (uint16_t)(sum / 8);
}
static void updateSensor() {
if (millis() - g_lastSampleMs < SAMPLE_INTERVAL_MS) return;
g_lastSampleMs = millis();
g_rawAdc = readAdcAverage();
if (!g_smoothInit) {
g_smoothAdc = (float)g_rawAdc;
g_smoothInit = true;
} else {
g_smoothAdc += ((float)g_rawAdc - g_smoothAdc) * EMA_ALPHA;
}
g_percent = mapToPercent((uint16_t)(g_smoothAdc + 0.5f));
g_state = classifyStateWithHysteresis(g_percent);
g_doTriggered = readDoStable();
digitalWrite(LED_PIN, g_doTriggered ? LOW : HIGH); // DO 触发点灯
}
// =========================
// OLED UI 绘制
// =========================
// 云朵(左侧区域)
static void drawCloud() {
display.fillCircle(22, 28, 7, WHITE);
display.fillCircle(33, 24, 9, WHITE);
display.fillCircle(44, 28, 7, WHITE);
display.fillRect(16, 27, 34, 8, WHITE);
}
// 单滴雨滴(小水滴:三角 + 圆)
static void drawDrop(int16_t x, int16_t y) {
display.fillTriangle(x - 2, y - 3, x + 2, y - 3, x, y + 2, WHITE);
display.fillCircle(x, y + 2, 2, WHITE);
}
// 底部横向进度条(满宽;百分比已在上方 LEVEL 显示,这里不再重复)
static void drawProgressBar(uint8_t pct) {
display.drawRect(0, 56, 128, 8, WHITE); // 外框
const int fill = (pct * 124) / 100; // 内部填充(左右各留 2px)
if (fill > 0) {
display.fillRect(2, 58, fill, 4, WHITE);
}
}
static void drawOledUi() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextWrap(false);
// 顶部标题
display.setTextSize(1);
display.setCursor(0, 0);
display.print(F("RAIN SENSOR"));
// 右上角 DO 触发指示点(实心 = DO 触发,空心 = 未触发)
if (g_doTriggered) display.fillCircle(120, 4, 3, WHITE);
else display.drawCircle(120, 4, 3, WHITE);
// 分隔线
display.drawFastHLine(0, 9, 128, WHITE);
// 云朵 + 雨滴(左半,雨滴错开相位轻微上下循环,避免齐刷刷一起动)
drawCloud();
const uint8_t drops = getDropCount(g_state);
for (uint8_t i = 0; i < drops; i++) {
const int offset = (int)(((millis() / DROP_ANIM_MS) + i) % 2) * 2;
drawDrop(DROP_X[i], DROP_Y + offset);
}
// LEVEL(右半,百分比右对齐到 127)
display.setTextSize(1);
display.setCursor(64, 18);
display.print(F("LEVEL:"));
display.setTextSize(2);
const int digits = (g_percent >= 100) ? 3 : (g_percent >= 10 ? 2 : 1);
display.setCursor(127 - (digits + 1) * 12, 27);
display.print(g_percent);
display.print('%');
// STATUS
display.setTextSize(1);
display.setCursor(0, 47);
display.print(F("STATUS: "));
display.print(getStateText());
// 底部进度条
drawProgressBar(g_percent);
display.display();
}
// =========================
// 串口输出
// =========================
static void printSerialData() {
Serial.printf("AO:%u DO:%d PCT:%d %s\n",
(unsigned)g_rawAdc,
g_doTriggered ? 0 : 1, // DO 原始电平:0=低(触发) 1=高
(int)g_percent,
getStateText());
}
void setup() {
Serial.begin(115200);
delay(100);
Serial.printf("Rain sensor demo %s starting...\n", FIRMWARE_VERSION);
pinMode(RAIN_DO_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
analogReadResolution(12); // 明确 12 位 ADC,读数 0~4095
// I2C1 重映射:PB9=SDA / PB8=SCL,必须先 setSDA/setSCL 再 begin()
Wire.setSDA(I2C_SDA_PIN);
Wire.setSCL(I2C_SCL_PIN);
Wire.begin();
Wire.setClock(100000); // I2C 100kHz,与 01~07 系列公共标准一致
if (display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
g_hasOled = true;
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.display();
Serial.println(F("OLED 0x3C: found"));
} else {
Serial.println(F("OLED 0x3C: NOT found (continue with serial only)"));
}
// 首帧采样(初始状态用无滞回 classifyState,之后 loop 里用滞回版)
g_rawAdc = readAdcAverage();
g_smoothAdc = (float)g_rawAdc;
g_smoothInit = true;
g_percent = mapToPercent(g_rawAdc);
g_state = classifyState(g_percent);
g_doTriggered = readDoStable();
digitalWrite(LED_PIN, g_doTriggered ? LOW : HIGH);
g_lastSampleMs = millis();
// 串口校准提示
Serial.println(F("=== 校准(先做这步,再定最终阈值)==="));
Serial.println(F("1. 感应板擦干 -> 记录串口 AO 值 = 干燥值"));
Serial.println(F("2. 感应板沾满水 -> 记录 AO 值 = 湿润值"));
Serial.println(F("3. 改 main.cpp 里 RAIN_DRY_VALUE / RAIN_WET_VALUE 后重烧"));
Serial.println(F("输出格式: AO:<原始> DO:<电平0/1> PCT:<百分比> <状态>"));
if (g_hasOled) drawOledUi();
}
void loop() {
updateSensor();
if (millis() - g_lastOledMs >= OLED_REFRESH_MS) {
g_lastOledMs = millis();
if (g_hasOled) drawOledUi();
}
if (millis() - g_lastSerialMs >= SERIAL_INTERVAL_MS) {
g_lastSerialMs = millis();
printSerialData();
}
}
依赖库:Adafruit SSD1306 和 Adafruit GFX Library,用 PlatformIO 打开工程会按 platformio.ini 自动下载,不用手动安装。
Video
视频演示
成片 下面是 204 秒完整演示动画(1920×1080 / 60fps / 带音效),一段讲透雨滴传感器 C71 的原理、AO/DO 和接入 STM32。
分段导航(10 段 · 共 204 秒)
| 时间 | 段落 | 内容 |
|---|---|---|
| 00:00 | 认识 C71 | 产品三部分:感应板 / 控制板 / 连接线 |
| 00:18 | 梳齿电极 | 电极 A/B 交错不相连,干燥状态 |
| 00:34 | 雨滴改变了什么 | 【核心】干燥→单侧→跨接→多滴,电阻高→中→低 |
| 01:04 | 为什么能检测 | 水膜 + 离子导电阻挡层 |
| 01:18 | 信号进入小板 | 信号沿连接线到 LM393 控制板 |
| 01:30 | LM393 阈值比较 | 【核心】信号 vs 阈值,越线瞬间 DO 翻转 |
| 01:54 | 电位器灵敏度 | 【核心】螺丝刀调门槛,同滴数两轮对比 |
| 02:20 | AO 和 DO 区别 | AO 连续条 + DO 方波翻转,并排总结 |
| 02:44 | 接入 STM32 | AO→PA0 / DO→PA1 / OLED PB8/PB9,真实 OLED UI |
| 03:06 | 完整流程 | 7 节点链回顾 + 产品定格 |
Download
下载
解压后用 VS Code + PlatformIO 打开这个文件夹就能 Build,一般不需要改代码,也不需要改 platformio.ini。
第一次使用
- 按上面接线表接好:模块 AO→PA0、DO→PA1、VCC→3.3V、GND→GND;OLED SDA→PB9、SCL→PB8、VCC→3.3V、GND→GND
- 下载 ZIP,解压,用 VS Code + PlatformIO 打开这个文件夹
- 等 Adafruit SSD1306 和 GFX 库自动装完,点 Build,看到 SUCCESS
- 接上 ST-Link,点 Upload,看到
** Verified OK **和[SUCCESS] - OLED 显示
RAIN SENSOR仪表盘;往感应板滴水,LEVEL 百分比上升、STATUS 依次变 DRY→LIGHT→WET→HEAVY
正常应该看到什么
| 动作 | OLED / 串口 / LED 上的表现 |
|---|---|
| 上电(感应板干燥) | OLED 显示 LEVEL 0~24%、STATUS DRY;串口 AO:高 DO:1 PCT:低 DRY;LED 灭 |
| 滴少量水 | LEVEL 25~45%、STATUS LIGHT、2 滴雨滴;串口 PCT 上升 |
| 水增多 | LEVEL 46~70%、STATUS WET、3 滴;进度条过半 |
| 基本湿透 | LEVEL 71%+、STATUS HEAVY、4 滴;DO 翻转、板载 LED 亮 |
| 擦干感应板 | 百分比逐步回落,状态逐档下降,LED 灭 |
pio device list,找 CP2102 / CP210x / CH340 对应的那个。
Troubleshooting
常见问题排查
下面每一条都是这个模块真实会遇到的。标注「实测」的是我们自己踩过的。
加水了,LEVEL 百分比不动或乱跳
按这个顺序查:
- AO 是否真接在 PA0。接错引脚或共地不良,ADC 读到的是浮空值。
- 供电是否 3.3V。接 5V 会让 AO 超出 ADC 量程,读数会顶死或异常。
- 等待几秒。程序有 8 次平均 + EMA 平滑,数值是平滑过渡的,不是瞬时跳变,这是故意的。
百分比方向反了:水越多,百分比越低
程序已经做了方向自适应,正常不会出现这种情况。映射公式 (raw - DRY) / (WET - DRY) 会自动适配「越湿越小」和「越湿越大」两种方向。
如果你确实看到反了,多半是 RAIN_DRY_VALUE 和 RAIN_WET_VALUE 两个校准值填反了。擦干感应板记一个 AO 值、沾满水记一个 AO 值,把干燥值填进 DRY、湿润值填进 WET,重新编译烧录即可。
DO 极性反了:有水时 DO:1,没水时 DO:0
不同厂家模块的比较器输出极性可能不同。程序默认按「低电平=触发」写。如果实测相反,只改 readDoStable() 里的一处:把 digitalRead(RAIN_DO_PIN) == LOW 改成 == HIGH。
状态在 DRY/LIGHT/WET 之间来回跳
如果只是偶尔在边界附近闪一下,先看是不是正好卡在阈值边界。程序已加 3% 状态滞回,正常不会来回跳。
如果仍跳得厉害,一是确认 AO 接得牢、供电稳;二是可以在 main.cpp 顶部按你的实测重新调 TH_DRY_MAX / TH_LIGHT_MAX / TH_WET_MAX 三个阈值。
OLED 不亮 / 串口打印 OLED 0x3C: NOT found
十有八九是 I2C 引脚或地址。按这个顺序查:
- 确认 SDA 接 PB9、SCL 接 PB8,不是默认的 PB7/PB6。
- 确认代码里
setSDA/setSCL在Wire.begin()之前,顺序反了配置不生效。 - 确认 OLED 地址。常见 0.96 寸 SSD1306 大多是
0x3C,少数是0x3D。 - 确认共地。OLED 的 GND 必须和 STM32 接在一起。
串口看不到数据
先查当前串口:
pio device list
找到 CP2102 / CP210x / CH340 对应的那个 COM,然后在自己电脑的 platformio.ini 里加:
monitor_port = COMx
monitor_speed = 115200
COMx 要换成你自己电脑实际识别到的端口号,不要直接抄。只有需要看串口时才加这两行,正常用 ST-Link 烧录不需要。本模块也可以不看串口——OLED 上直接看 LEVEL 百分比和 STATUS 就行。
长期用感应板发绿、腐蚀、不灵敏了
这是铜箔 + 通电 + 水的电化学腐蚀,是这类感应板的固有特性,不是坏了。短时间演示、拍视频完全没问题。长期室外雨水检测建议改成「上电 → 稳定 → 采样 → 断电」的间歇供电,或换耐腐蚀的检测方案。
References
资料来源
本页信息来源声明
| 内容 | 来源 |
|---|---|
| 接线、代码、OLED UI、四档阈值、滤波参数、串口输出格式 | 本项目实测 2026-08-17 实机测试 |
| 梳齿电极、水膜导电、LM393 比较器、电位器门槛 | 资料参考 公开资料与常规工程实践 |
| AO / DO 两类输出的对比 | 资料参考 模块输出定义,由我们整理成表 |
| RAIN_DRY_VALUE / RAIN_WET_VALUE 具体读数 | 待校准 当前为占位值 3800/1200,以实测 AO 为准 |
| DO 触发电平极性(低=触发) | 待确认 按最常见情况假设,实测若相反改一处代码 |
本页所有「资料参考」内容均由我们重新整理表述,未直接复制原文。不同厂家 / 批次模块可能存在引脚顺序或检测逻辑差异,参数以你手上模块的实际丝印和卖家说明为准。