传感器 · 雨滴检测 · 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。

0~100%AO 换算成湿度百分比
4 档DRY/LIGHT/WET/HEAVY
8 次ADC 平均抗波动
200 msOLED 刷新周期

四档湿度实测

测试动作OLED 显示雨滴图标
感应板完全擦干LEVEL 0~24%,STATUS DRY云朵,无雨滴
滴少量水LEVEL 25~45%,STATUS LIGHT2 滴
水增多LEVEL 46~70%,STATUS WET3 滴
感应板基本湿透LEVEL 71%+,STATUS HEAVY4 滴

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
未验证的环境不当成支持。本项目没有在 Arduino IDE、Keil、STM32CubeIDE 下编译或验证过,所以不提供这些环境的支持说明。

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
模拟输出AOPA0(ADC)
数字输出DOPA1(内部上拉)
电源VCC3.3V
地GNDGND

OLED

信号OLEDSTM32F103C8T6
电源VCC / VDD3.3V
地GNDGND
时钟SCL / SCKPB8
数据SDAPB9

烧录与串口(不接也能跑,接上方便看数据)

用途连接说明
烧录ST-Link → SWD 四线(3.3V / GND / SWDIO / SWCLK)烧录用
串口PA9(TX) / PA10(RX) → CP2102 等看串口数据(115200)
C71 雨滴模块:感应板 + LM393 控制板 + 连接线
C71 雨滴模块由三部分组成:雨滴感应板、LM393 比较器控制板、连接线
最容易接错的三处,接错了都不会报错,就是表现不对。
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 走 ADCPA0 是 12 位 ADC,代码 analogReadResolution(12),读数 0~4095
DO 走内部上拉代码用 INPUT_PULLUP 读 PA1,LM393 是开漏输出,内部上拉兜底
板载 LEDPC13 低电平点亮,DO 触发时亮,表示比较器已翻转
丝印为准本块 4 针顺序按丝印 VCC / GND / DO / AO 接,不要照着照片数孔位

Principle

工作原理

资料参考 这一节来自公开资料和常规工程实践,不是我们实测的结论,用来帮助理解。实测结果只在上面「实机验证结果」一节。

梳齿电极:干燥时是断开的

感应板上有两组交错的梳齿电极(A 和 B),像两把梳子齿对齿插在一起,但彼此不相连。干燥时两排电极之间是空气(几乎不导电),等效电阻很大,所以读到的电压很高(接近电源)。

水膜跨接:为什么水能被检测出来

水滴落到感应板上,水(里面含少量可导电的离子)会在相邻梳齿之间形成一条导电路径,相当于在两排电极之间架了桥。水覆盖越多,桥越多、越宽,等效电阻就越小,电压随之下降。所以模块检测到的不是「水」本身,而是「电阻变了」——水改变了电极之间的导电状态。

LM393:把「过没过线」变成高低电平

LM393 是一个电压比较器:一端接感应板送来的信号电压,另一端接电位器设定的参考电压(门槛)。信号电压跨过门槛的瞬间,比较器输出翻转(DO 从高变低,或反过来),并把板载 LED 点亮。所以 DO 只告诉你「达标了没有」,不告诉你「多湿」。

环节干燥时有水时
梳齿电极之间空气,不导通水膜跨接,导通
等效电阻大小(水越多越小)
AO 电压高(接近电源)低(水越多越低)
DO 输出未触发跨过门槛后触发

梳齿电极、水膜导电、比较器原理来自公开资料与常规工程实践,由我们重新整理表述。资料参考

电位器调的是门槛,不是「灵敏度放大」

很多人以为旋电位器是「调灵敏度」。更准确地说,它调的是 DO 的触发门槛:门槛调低,一点点水就触发;门槛调高,同样几滴雨可能就不触发。它不影响 AO 的连续读数——AO 始终如实反映湿润程度。

长期通电 + 有水会腐蚀感应板。感应板是裸露铜箔,长期通电又沾水会产生电化学腐蚀。B 站演示、短时间测试直接采样没问题;如果做长期室外雨水检测,建议改成「上电 → 稳定 → 采样 → 断电」的间歇供电,或换更耐腐蚀的检测方案。

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/HEAVYclassifyStateWithHysteresis()
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。

网页版视频待压缩上传。完整动画约 33 MB,网页嵌入前需压成 720p 小体积版本再放到站点资源目录。当前先在下方给出分段导航,完整视频见下载包或视频源文件。

分段导航(10 段 · 共 204 秒)

时间段落内容
00:00认识 C71产品三部分:感应板 / 控制板 / 连接线
00:18梳齿电极电极 A/B 交错不相连,干燥状态
00:34雨滴改变了什么【核心】干燥→单侧→跨接→多滴,电阻高→中→低
01:04为什么能检测水膜 + 离子导电阻挡层
01:18信号进入小板信号沿连接线到 LM393 控制板
01:30LM393 阈值比较【核心】信号 vs 阈值,越线瞬间 DO 翻转
01:54电位器灵敏度【核心】螺丝刀调门槛,同滴数两轮对比
02:20AO 和 DO 区别AO 连续条 + DO 方波翻转,并排总结
02:44接入 STM32AO→PA0 / DO→PA1 / OLED PB8/PB9,真实 OLED UI
03:06完整流程7 节点链回顾 + 产品定格

Download

下载

解压后用 VS Code + PlatformIO 打开这个文件夹就能 Build,一般不需要改代码,也不需要改 platformio.ini。

PlatformIO 工程 ZIP 含 platformio.ini、src/main.cpp、README.md。已去掉本机构建路径,解压即可用。
下载 ZIP

第一次使用

  1. 按上面接线表接好:模块 AO→PA0、DO→PA1、VCC→3.3V、GND→GND;OLED SDA→PB9、SCL→PB8、VCC→3.3V、GND→GND
  2. 下载 ZIP,解压,用 VS Code + PlatformIO 打开这个文件夹
  3. 等 Adafruit SSD1306 和 GFX 库自动装完,点 Build,看到 SUCCESS
  4. 接上 ST-Link,点 Upload,看到 ** Verified OK ** 和 [SUCCESS]
  5. 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 灭
不要照抄 COM 号。截图里出现的串口号是我们这台电脑的,你的电脑多半不一样。查当前串口:pio device list,找 CP2102 / CP210x / CH340 对应的那个。
这块板子不能用网页烧录器。网页烧录(WebSerial)主要面向 ESP32 / ESP8266 这类自带 USB 串口下载的芯片。STM32F103C8T6 本项目用的是 ST-Link + SWD,浏览器接触不到 SWD 接口。

Troubleshooting

常见问题排查

下面每一条都是这个模块真实会遇到的。标注「实测」的是我们自己踩过的。

加水了,LEVEL 百分比不动或乱跳

按这个顺序查:

  1. AO 是否真接在 PA0。接错引脚或共地不良,ADC 读到的是浮空值。
  2. 供电是否 3.3V。接 5V 会让 AO 超出 ADC 量程,读数会顶死或异常。
  3. 等待几秒。程序有 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 引脚或地址。按这个顺序查:

  1. 确认 SDA 接 PB9、SCL 接 PB8,不是默认的 PB7/PB6。
  2. 确认代码里 setSDA/setSCL 在 Wire.begin() 之前,顺序反了配置不生效。
  3. 确认 OLED 地址。常见 0.96 寸 SSD1306 大多是 0x3C,少数是 0x3D。
  4. 确认共地。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 触发电平极性(低=触发)待确认 按最常见情况假设,实测若相反改一处代码

本页所有「资料参考」内容均由我们重新整理表述,未直接复制原文。不同厂家 / 批次模块可能存在引脚顺序或检测逻辑差异,参数以你手上模块的实际丝印和卖家说明为准。