电阻式液位检测从原理到实战

电阻式液位检测:用几根不锈钢探针实现可靠的分档液位测量

一位 40 年电子工程师的实战笔记 —— 最简单、最可靠、最便宜的多点液位检测方案,没有之一。


目录


一、老工程师的开场白

在嵌入式这行摸爬滚打了 40 年,我做过的液位检测方案少说也有二三十种。电容式的、超声波式的、压力式的、磁浮子的、光电式的……每一种我都亲手做过量产产品。

但要说最简单、最可靠、最便宜的方案,我还得把票投给今天要聊的这个——电阻式分档液位探针

它的原理简单到一句话就能说完:水能导电,空气不能。伸几根长短不同的不锈钢探针到容器里,哪根碰到水哪根就和公共电极导通,读一下 GPIO 就知道水位到哪了。

是的,就这么简单。

但"简单"不等于"粗糙"。这篇文章我会把探针的选材、密封结构、电路设计、交流激励抗腐蚀、故障自检、量产工艺……从头到尾给你讲透。读完你会发现,原来一个被很多人瞧不上的"土办法",做精了一样能用在工业级产品上。


二、基本原理:水就是一根导线

2.1 一图胜千言

      ┌─────────────────────────┐
      │      液体容器            │
      │                         │
  ~~ ├── 液面 1 (最高) ~~~~┤
      │         ● 探针 4 (最长)  │
  ~~ ├── 液面 2       ~~~~┤
      │         ● 探针 3        │
  ~~ ├── 液面 3       ~~~~┤
      │         ● 探针 2        │
  ~~ ├── 液面 4 (最低)~~~~┤
      │         ● 探针 1 (最短)  │
      │         ▌               │
      │         ▌ 公共电极 COM   │
      │         ▌ (最长,始终浸没) │
      │         ▌               │
      └─────────┴───────────────┘
           ↑
      安装法兰(密封)
           │
      接线端子
           │
        MCU 检测

工作逻辑

  • 公共电极 COM 始终浸没在液体中(最长的一根)
  • 探针 1~4 长度依次递增,分别对应 25%、50%、75%、100% 液位
  • 当液位淹过某根探针时,探针与 COM 之间通过液体导通
  • MCU 检测每根探针与 COM 之间的通断状态
  • 由此判断液位达到了哪一档

2.2 等效电路

    COM ────┐
            │
            ├──── 探针 1 ←──────── 液位 ≥ 25% 时导通
            │        │
            │       R_water_1
            │        │
            ├──── 探针 2 ←──────── 液位 ≥ 50% 时导通
            │        │
            │       R_water_2
            │        │
            ├──── 探针 3 ←──────── 液位 ≥ 75% 时导通
            │        │
            │       R_water_3
            │        │
            ├──── 探针 4 ←──────── 液位 ≥ 100% 时导通
            │        │
            │       R_water_4
            │        │
            └────────┘
            等效液体电阻 (几 kΩ ~ 几百 kΩ)

每根探针和 COM 之间可以看作一个液体电阻串在回路中:

        VCC
         │
        R_pullup (10kΩ ~ 100kΩ)
         │
         ├──────→ MCU GPIO (ADC 或数字输入)
         │
       探针
         │
    ─────┴───── 液面
         │
    ~~~~~ 液体 电阻 R_water ~~~~~
         │
       COM (GND)

2.3 液体电阻到底有多大?

这是最关键的工程参数,决定了上拉电阻怎么选、检测阈值怎么设。

液体类型 电阻率 (Ω·cm) 两探针间距 10mm 时的电阻
纯净水(去离子) ~ 18 MΩ·cm ~ 18 MΩ(几乎不导电!)
蒸馏水 50k ~ 500k 50k ~ 500k
自来水(典型) 1k ~ 5k 1k ~ 5k
矿泉水 500 ~ 2k 500 ~ 2k
河水/井水 200 ~ 2k 200 ~ 2k
含盐废水 10 ~ 100 10 ~ 100
酸碱溶液 < 10 < 10
咖啡/茶/饮料 500 ~ 2k 500 ~ 2k

⚠️ 老工程师提醒:这个表是判断"这个方案能不能用"的第一道门槛。

  • 自来水、饮用水、河水、污水、饮料→ ✅ 完全没问题
  • 去离子水、超纯水→ ❌ 电阻太大,测不出来,换电容方案
  • 油类、有机溶剂→ ❌ 不导电,此方案不适用,换电容或超声波方案

三、为什么这个方案值得认真对待?

3.1 一句话总结优势

BOM 成本几块钱,结构件就是几根不锈钢棒,电路就是几个电阻加 GPIO——简单到出不了大问题。

3.2 详细对比

对比维度 电阻探针 电容外贴 超声波 浮球/磁簧
BOM 成本 ¥2~5 ¥8~15 ¥30~60 ¥5~20
结构复杂度 探针 + 密封 贴片(需平整面) 安装支架 浮球导轨
精度 分档(N 档) 连续(模拟量) 连续 连续
可靠性 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐
受挂料影响 ❌ 不受影响 ✅ 严重 ❌ 不受影响 ❌ 不受影响
受泡沫影响 ❌ 不受影响 ❌ 不受影响 ✅ 严重 ❌ 不受影响
受电导率变化影响 ⚠️ 需要阈值自适应 ❌ 不受影响 ❌ 不受影响 ❌ 不受影响
安装维护 需开孔密封 外贴,无需开孔 顶部安装 内部安装
卫生等级 ⚠️ 探针在液体中 ✅ 非接触 ✅ 非接触 ❌ 浮球在水中
寿命 10 年+ 10 年+ 5~8 年 2~5 年(机械磨损)

3.3 电导率变化的应对

有人担心:自来水的电导率随水源、季节变化,会不会导致误判?

答案是:用 ADC 读电压而不是用 GPIO 读高低电平。

                            GPIO 读法 (粗糙):
                            电压 > 2.0V → 1 (不通)
                            电压 < 0.8V → 0 (导通)
                            问题是阈值固定,电导率变化时可能误判

                            ADC 读法 (可靠):
                            干态: ADC ≈ 4095 (全高,VCC)
                            湿态 (自来水): ADC ≈ 500~2000
                            湿态 (纯水): ADC ≈ 3500~4000
                            通过百分比变化来判断,而不是绝对阈值

用 ADC 读取每个探针的电压,通过相对变化率而不是绝对阈值来判断通断:

/**
 * 自适应阈值判断
 * 思路:比较当前读数与"干态基准值"的变化幅度
 */
bool isProbeWet(int probeIndex, uint16_t currentADC) {
    // dryBaseline[probeIndex] 在上电初始化时采样
    // 或定期在确定空液时更新
    float dropRatio = 1.0f - (float)currentADC / dryBaseline[probeIndex];

    // 电压下降超过 30% 即判定为导通
    // 这个阈值对大多数液体都适用
    return dropRatio > 0.30f;
}

四、探针设计与结构工艺

这是最容易被人忽视、却最影响产品品质的环节。电路画对了、代码写好了,探针密封不行,三个月就漏水——电路板烧了算轻的,水漏到带电部分,那就是安全事故。

4.1 探针材料

材料 食品级 耐腐蚀 成本 推荐场景
304 不锈钢 ⭐⭐⭐ 饮用水、家用
316L 不锈钢 ⭐⭐⭐⭐⭐ 食品/医疗/海水
镀镍铜棒 ⭐⭐ 极低 工业废水
钛合金 ⭐⭐⭐⭐⭐ 强酸碱环境
石墨棒 ⭐⭐⭐⭐ 强酸碱、特殊场合

📌 常规产品首选 304 不锈钢。直径 2~4mm,长度根据液位档位定制。

4.2 探针直径选择

直径 (mm) 刚性 适用长度 成本参考
1.5 较软,易弯 ≤ 80mm ¥0.05/cm
2.0 适中 ≤ 150mm ¥0.08/cm
3.0 ≤ 300mm ¥0.12/cm
4.0 非常好 ≤ 500mm ¥0.18/cm
5.0+ 极好 > 500mm ¥0.25/cm

家用/小容器用 2mm 就行。工业大容器用 3~4mm

4.3 密封结构设计

这是整个方案中唯一有技术含量的部分。

        ┌────────────────────────┐
        │      接线端子台         │
    ────┼── 螺丝压接探针上端 ────┼─── 引出到 PCB
        │       (露出 10mm)      │
        │                        │
    ────┼────────────────────────┼─── 密封件
        │    ┌──────────────┐    │
        │    │ 硅胶密封圈 ×2 │    │  ← O 型圈 × 2(双道密封)
        │    │ (食品级)      │    │
        │    └──────────────┘    │
    ────┼────────────────────────┼─── 法兰盖(POM / PP / 尼龙)
        │                        │
        │      ~~~~~~      │  ← 液面以上
 ═══════╪════════════════════════╪═══ 容器壁
        │      ~~~~~~      │  ← 液面以下
        │                        │
        │   ▌ 探针(不锈钢棒)    │
        │   ▌                    │
        │   ▌                    │
        └───┴────────────────────┘

密封要点

要素 推荐方案 说明
密封件 双 O 型圈(食品级硅胶) 两道密封,第一道失效第二道还能顶
O 型圈沟槽 按 O 型圈规格加工,压缩率 15~20% 太紧难装配,太松密封差
法兰材质 POM(聚甲醛)、PP(聚丙烯) 食品级、耐温好、机械强度够
探针表面 与密封圈接触段镜面抛光 粗糙表面会磨坏密封圈
锁紧方式 螺母压紧或卡簧固定 确保探针不会轴向窜动
防水等级 目标 IP67 以上 密封件处涂抹食品级硅脂辅助

4.4 探针长度计算

"""
探针长度计算示例
容器高度 200mm,4 档检测 + 公共电极
"""

# 容器参数
tank_height = 200.0   # 容器总高 (mm)
probe_count = 4       # 检测档位数量
min_immersion = 10.0  # 最小浸没深度 (mm),保证可靠导通

# 档位液位目标(容器底部 = 0)
target_levels = [50, 100, 150, 200]  # 25%, 50%, 75%, 100%

# 探针从底部向上伸出的长度
probe_lengths = [level + min_immersion for level in target_levels]
# → [60, 110, 160, 210] mm

# 公共电极 — 保证始终浸没(通常比最高探针略长)
com_length = max(probe_lengths) + 20  # 230 mm

4.5 安装方式

方案 A:顶部法兰安装                 方案 B:底部法兰安装
     (探针向下伸入液体)               (探针向上伸入液体)

     ┌──┐法兰                         ═══ 容器底 ═══
  ═══│  │═══ 容器盖                 ┌──┐法兰
     │  │                           │  │
     │  ▌探针                        │  ▌探针 ↑
     │  ▌                           │  ▌
     │  ▌                           │  ▌
     │  ▌                           │  ▌
     │  ▌                           └──┘
     └──┘

     优点:密封在液面以上             优点:密封在容器最低处
     缺点:探针悬臂长,需刚性         (若有泄漏正好排空)
     适用:小容器、密封罐            缺点:密封承受全液位压力
                                     适用:需要排空维护的场景

📌 推荐方案 A。密封件不长期浸泡在液体中,寿命长得多。探针悬臂问题通过选择足够粗的探针解决。


五、信号检测电路设计

5.1 经典直流检测电路

                     VCC (3.3V 或 5V)
                      │
                      ├── R1 ──┬── R2 ──┬── R3 ──┬── R4 ──┐
                      │        │        │        │        │
                    100kΩ    100kΩ     100kΩ     100kΩ    100kΩ (上拉电阻)
                      │        │        │        │        │
                      ├─ ADC0  ├─ ADC1  ├─ ADC2  ├─ ADC3  ├─ ADC4
                      │        │        │        │        │
                   Probe1   Probe2   Probe3   Probe4    COM
                   (最短)             ...    (最长)   (公共)
                      │        │        │        │        │
                      ├────────┼────────┼────────┼────────┤
                      │                                          │
                   ~~~~~~~ 液体 ~~~~~~~
                   (COM 接 GND,各探针通过液体 → COM 形成回路)

上拉电阻取值

液体电导率 推荐上拉电阻 原因
高(盐水/酸碱) 10kΩ ~ 47kΩ 液体电阻小,需较大电流
中(自来水) 100kΩ 平衡灵敏度和功耗
低(纯净水) 470kΩ ~ 1MΩ 液体电阻大,需高阻抗
未知(通用) 100kΩ 折中方案,配合 ADC 自适应

5.2 带保护的工业级电路

实际产品中,需要考虑 ESD 静电防护、浪涌保护:

                    VCC
                     │
                     R1 (100kΩ)
                     │
                     ├─────────────→ MCU ADC
                     │
                     R2 (1kΩ, 限流)
                     │
                     ├── D1 (TVS, 5V) ── GND
                     │
                     ├── C1 (10nF) ── GND   (低通滤波,去噪声)
                     │
                   探针
  • TVS 管(如 SMAJ5.0CA):防静电和浪涌,保护 MCU 引脚
  • 1kΩ 限流电阻:限制 TVS 动作时的电流
  • 10nF 电容:滤除工频 50Hz 干扰(液体充当天线会引入噪声)
  • 100nF 可选:更强的滤波,但响应变慢

5.3 多路复用电路(节省 ADC 通道)

当探针数量很多(如 8~16 档)时,用模拟开关或 IO 扩展:

方案 A:CD4051 模拟开关 (8 选 1)
        优点:1 个 ADC 口 + 3 个控制 IO → 8 路探针
        成本:¥1.5

方案 B:PCF8574 I2C IO 扩展 + 分时上拉
        优点:2 根 I2C 线 → 8 路 GPIO
        每个 GPIO 轮流输出高电平(上拉),其他为输入
        成本:¥2.0

方案 C:HC595 移位寄存器 + 单 ADC
        优点:3 根线 → N 路,可级联
        成本:¥0.8

六、Arduino 驱动开发

6.1 基本四档液位检测

/**
 * 电阻式液位检测 - 4 档基本版
 * 探针: 4 根不锈钢探针 + 1 根公共电极
 * 检测: GPIO + ADC 双重判断
 *
 * 接线:
 *   Probe 1 (25%)  → A0
 *   Probe 2 (50%)  → A1
 *   Probe 3 (75%)  → A2
 *   Probe 4 (100%) → A3
 *   COM            → GND
 */

// ==================== 配置 ====================
#define PROBE_COUNT     4

// 探针对应的 ADC 引脚
const uint8_t probePins[PROBE_COUNT] = {A0, A1, A2, A3};

// 上拉电阻 (Ω)
#define PULLUP_RESISTOR 100000

// 干态基准值(上电初始化时采样)
uint16_t dryBaseline[PROBE_COUNT];

// 导通判断阈值(电压下降百分比,0.0 ~ 1.0)
// 0.30 = 下降 30% 以上判定为导通
#define WET_THRESHOLD   0.30f

// ==================== 初始化 ====================
void setup() {
    Serial.begin(115200);
    Serial.println("=== 电阻式液位检测 (4档) ===");
    Serial.println();

    // 上电时(确保容器为空或探针干燥),采样干态基准
    // 如果无法确定空液,可以用上一次保存的 EEPROM 中的值
    calibrateDryBaseline();

    Serial.println("探针\t干态ADC\t当前ADC\t状态");
    Serial.println("----\t-------\t-------\t----");
}

// ==================== 主循环 ====================
void loop() {
    // 读取每个探针
    for (int i = 0; i < PROBE_COUNT; i++) {
        uint16_t adc = analogRead(probePins[i]);
        bool isWet = isProbeWet(i, adc);

        Serial.print("P");
        Serial.print(i + 1);
        Serial.print("\t");
        Serial.print(dryBaseline[i]);
        Serial.print("\t");
        Serial.print(adc);
        Serial.print("\t");
        Serial.println(isWet ? "● 导通 (湿)" : "○ 断开 (干)");
    }

    // 计算液位档位
    int level = getLevel();
    Serial.print("当前液位档位: ");
    Serial.print(level);
    Serial.print(" / ");
    Serial.println(PROBE_COUNT);

    Serial.println("---");
    delay(1000);
}

// ==================== 核心函数 ====================

/**
 * 采样干态基准值
 * 应在容器为空、探针干燥时调用
 */
void calibrateDryBaseline() {
    Serial.println("正在采样干态基准值...");

    for (int i = 0; i < PROBE_COUNT; i++) {
        pinMode(probePins[i], INPUT);

        // 多次采样取平均(减小噪声)
        uint32_t sum = 0;
        for (int j = 0; j < 32; j++) {
            sum += analogRead(probePins[i]);
            delay(5);
        }
        dryBaseline[i] = (uint16_t)(sum / 32);

        Serial.print("  探针 ");
        Serial.print(i + 1);
        Serial.print(": 干态基准 = ");
        Serial.println(dryBaseline[i]);
    }
    Serial.println("采样完成\n");
}

/**
 * 判断探针是否被液体导通
 * 使用相对变化率而非绝对阈值
 */
bool isProbeWet(int probeIndex, uint16_t currentADC) {
    // 电压下降比例
    float dropRatio = 1.0f - (float)currentADC / dryBaseline[probeIndex];

    // 下降超过阈值 → 导通
    return dropRatio > WET_THRESHOLD;
}

/**
 * 获取液位档位(0~PROBE_COUNT)
 * 0 = 全部干燥(低于最低探针)
 * N = 第 N 根探针导通(液位 ≥ 第 N 档)
 */
int getLevel() {
    int level = 0;
    for (int i = 0; i < PROBE_COUNT; i++) {
        uint16_t adc = analogRead(probePins[i]);
        if (isProbeWet(i, adc)) {
            level = i + 1;  // 记录最高导通的探针
        }
    }
    return level;
}

6.2 带 OLED 和蜂鸣器告警版

/**
 * 电阻式液位计 - OLED 显示 + 告警
 * 探针 4 档 + 最高最低告警
 */

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define PROBE_COUNT   4
#define SCREEN_WIDTH  128
#define SCREEN_HEIGHT 64

const uint8_t probePins[PROBE_COUNT] = {A0, A1, A2, A3};
#define PIN_BUZZER    5
#define PIN_LED_LOW   6   // 低液位告警灯
#define PIN_LED_HIGH  7   // 高液位告警灯

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

uint16_t dryBaseline[PROBE_COUNT];
const float WET_THRESHOLD = 0.30f;

void setup() {
    Serial.begin(115200);
    Wire.begin();

    // GPIO
    pinMode(PIN_BUZZER, OUTPUT);
    pinMode(PIN_LED_LOW, OUTPUT);
    pinMode(PIN_LED_HIGH, OUTPUT);

    // OLED
    display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
    display.clearDisplay();
    display.setTextColor(SSD1306_WHITE);

    // 开机画面
    display.setTextSize(2);
    display.setCursor(15, 10);
    display.println("Resistive");
    display.setCursor(15, 35);
    display.println("Level Meter");
    display.display();
    delay(1500);

    calibrateDryBaseline();
}

void loop() {
    // 读取状态
    bool probeState[PROBE_COUNT];
    int level = 0;

    for (int i = 0; i < PROBE_COUNT; i++) {
        uint16_t adc = analogRead(probePins[i]);
        probeState[i] = isProbeWet(i, adc);
        if (probeState[i]) level = i + 1;
    }

    // 告警判断
    bool alarmLow  = (level == 0);           // 全部干燥 = 没水了
    bool alarmHigh = (level >= PROBE_COUNT); // 全导通 = 满了

    digitalWrite(PIN_LED_LOW,  alarmLow);
    digitalWrite(PIN_LED_HIGH, alarmHigh);

    if (alarmLow || alarmHigh) {
        tone(PIN_BUZZER, alarmHigh ? 2000 : 1000, 200);
    }

    // OLED 显示
    display.clearDisplay();

    // 标题
    display.setTextSize(1);
    display.setCursor(0, 0);
    display.println("Resistive Level");

    // 液位条
    int barY = 12;
    int barH = 30;
    int barW = 20;
    int gap  = 8;
    for (int i = 0; i < PROBE_COUNT; i++) {
        int x = 10 + i * (barW + gap);
        // 从下往上填
        int fillH = probeState[i] ? barH : 0;
        display.drawRect(x, barY, barW, barH, SSD1306_WHITE);
        display.fillRect(x, barY + barH - fillH, barW, fillH, SSD1306_WHITE);
        display.setCursor(x + 5, barY + barH + 2);
        display.print(i + 1);
    }

    // 档位文字
    display.setTextSize(2);
    display.setCursor(15, 48);

    if (level == 0) {
        display.println("EMPTY!");
    } else if (level >= PROBE_COUNT) {
        display.println("FULL!");
    } else {
        display.print("Level: ");
        display.print(level);
        display.print("/");
        display.print(PROBE_COUNT);
    }

    display.display();
    delay(500);
}

void calibrateDryBaseline() {
    for (int i = 0; i < PROBE_COUNT; i++) {
        pinMode(probePins[i], INPUT);
        uint32_t sum = 0;
        for (int j = 0; j < 32; j++) {
            sum += analogRead(probePins[i]);
            delay(5);
        }
        dryBaseline[i] = (uint16_t)(sum / 32);
    }
}

bool isProbeWet(int idx, uint16_t adc) {
    return (1.0f - (float)adc / dryBaseline[idx]) > WET_THRESHOLD;
}

七、STM32 驱动开发

7.1 硬件配置

探针 1 (25%)  → PA0 (ADC1_IN0)
探针 2 (50%)  → PA1 (ADC1_IN1)
探针 3 (75%)  → PA2 (ADC1_IN2)
探针 4 (100%) → PA3 (ADC1_IN3)
COM           → GND

7.2 HAL 库驱动代码

level_probe.h

#ifndef __LEVEL_PROBE_H__
#define __LEVEL_PROBE_H__

#include "stm32f1xx_hal.h"
#include <stdint.h>
#include <stdbool.h>

/* 配置 */
#define PROBE_COUNT      4
#define ADC_SAMPLES      32     // 每次采样次数
#define WET_THRESHOLD    0.30f  // 导通判定阈值 (30%)

/* 探针句柄 */
typedef struct {
    ADC_HandleTypeDef *hadc;
    uint32_t           adc_channels[PROBE_COUNT];
    uint16_t           dry_baseline[PROBE_COUNT];  // 干态基准
    bool               probe_state[PROBE_COUNT];   // 当前状态
    uint8_t            level;                      // 档位 (0~PROBE_COUNT)
} LevelProbe;

/* API */
void LevelProbe_Init(LevelProbe *lp, ADC_HandleTypeDef *hadc);
void LevelProbe_Calibrate(LevelProbe *lp);           // 采样干态基准
void LevelProbe_Update(LevelProbe *lp);               // 更新所有探针状态
uint8_t LevelProbe_GetLevel(LevelProbe *lp);
bool LevelProbe_IsWet(LevelProbe *lp, uint8_t idx);

#endif

level_probe.c

#include "level_probe.h"

/**
 * 读取单个 ADC 通道
 */
static uint16_t readADC(LevelProbe *lp, uint8_t idx) {
    // 选择通道 + 采样时间
    ADC_ChannelConfTypeDef sConfig = {0};
    sConfig.Channel      = lp->adc_channels[idx];
    sConfig.Rank         = 1;
    sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;

    HAL_ADC_ConfigChannel(lp->hadc, &sConfig);

    // 启动 + 等待 + 读取
    HAL_ADC_Start(lp->hadc);
    HAL_ADC_PollForConversion(lp->hadc, 10);
    uint16_t val = HAL_ADC_GetValue(lp->hadc);

    return val;
}

/**
 * 多次采样取平均
 */
static uint16_t readADCAverage(LevelProbe *lp, uint8_t idx) {
    uint32_t sum = 0;
    for (int i = 0; i < ADC_SAMPLES; i++) {
        sum += readADC(lp, idx);
        HAL_Delay(2);
    }
    return (uint16_t)(sum / ADC_SAMPLES);
}

/**
 * 初始化
 */
void LevelProbe_Init(LevelProbe *lp, ADC_HandleTypeDef *hadc) {
    lp->hadc = hadc;

    // 配置 ADC 通道(根据实际引脚设置)
    // PA0→CH0, PA1→CH1, PA2→CH2, PA3→CH3
    for (int i = 0; i < PROBE_COUNT; i++) {
        lp->adc_channels[i] = ADC_CHANNEL_0 + i;  // CH0~CH3
    }
}

/**
 * 标定干态基准值
 * 必须在容器为空且探针干燥时调用
 */
void LevelProbe_Calibrate(LevelProbe *lp) {
    for (int i = 0; i < PROBE_COUNT; i++) {
        lp->dry_baseline[i] = readADCAverage(lp, i);
    }
}

/**
 * 更新所有探针状态
 */
void LevelProbe_Update(LevelProbe *lp) {
    uint8_t highest_wet = 0;

    for (int i = 0; i < PROBE_COUNT; i++) {
        uint16_t adc = readADCAverage(lp, i);

        // 相对变化率判断
        float drop = 1.0f - (float)adc / lp->dry_baseline[i];
        lp->probe_state[i] = (drop > WET_THRESHOLD);

        if (lp->probe_state[i]) {
            highest_wet = i + 1;
        }
    }
    lp->level = highest_wet;
}

/**
 * 获取当前液位档位
 */
uint8_t LevelProbe_GetLevel(LevelProbe *lp) {
    return lp->level;
}

/**
 * 查询某根探针是否导通
 */
bool LevelProbe_IsWet(LevelProbe *lp, uint8_t idx) {
    if (idx >= PROBE_COUNT) return false;
    return lp->probe_state[idx];
}

main.c 使用示例:

#include "level_probe.h"

LevelProbe lp;
char buf[128];

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_ADC1_Init();
    MX_USART1_UART_Init();

    // 初始化液位探针
    LevelProbe_Init(&lp, &hadc1);

    // 上电标定(假设此时容器为空)
    printf("Calibrating dry baseline...\r\n");
    LevelProbe_Calibrate(&lp);
    printf("Calibration done.\r\n\r\n");

    while (1) {
        LevelProbe_Update(&lp);

        sprintf(buf, "Level: %d/%d | States: ",
                LevelProbe_GetLevel(&lp), PROBE_COUNT);

        for (int i = 0; i < PROBE_COUNT; i++) {
            sprintf(buf + strlen(buf), "%c ",
                    LevelProbe_IsWet(&lp, i) ? 'W' : 'D');
        }
        strcat(buf, "\r\n");

        HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 100);
        HAL_Delay(500);
    }
}

八、交流激励:解决电解腐蚀的关键

8.1 直流检测的问题

如果你用前面说的直流方案给一根不锈钢探针通上 5V 直流,把它泡在自来水里,你会发现:

  • 一周后:探针表面开始变暗
  • 一个月后:探针表面有锈斑
  • 三个月后:探针变细了,有些地方出现了凹坑

这就是直流电解腐蚀。直流电压下,探针和水之间形成了一个电解池——探针(阳极)的金属离子持续溶解到水中。

8.2 交流激励原理

解决办法是:不要一直通电。用短暂的正负交替脉冲来检测。

直流(会腐蚀):                   交流(不腐蚀):
    +5V ────────────────               +5V ─┐     ┌─
                         → 电解               │     │
    GND ────────────────               GND ─┘     └─
                                          检测脉冲 (μs~ms 级)
                                          正负交替,平均电压 = 0

交流激励的核心思想:

  1. 正向脉冲:IO 输出高电平(上拉),读 ADC → 得到电压 V+
  2. 反向脉冲:IO 输出低电平(下拉),读 ADC → 得到电压 V-
  3. 差值判断:|V+ - V-| 就是导通程度
  4. 脉冲极短:每次检测只有几百微秒,平均电流接近零
  5. 正负对称:正向和反向时间相等,净电荷转移为零

8.3 Arduino 交流激励实现

/**
 * 交流激励式液位检测 - 防止电解腐蚀
 *
 * 每个检测周期:
 *   Phase 1: IO 输出高电平 → 短暂延时 → 读 ADC (V_high)
 *   Phase 2: IO 输出低电平 → 短暂延时 → 读 ADC (V_low)
 *   Phase 3: IO 设为高阻输入(断开,节省功耗)
 *
 * 导通判定:|V_high - V_low| > 阈值
 */

#define PROBE_COUNT   4
const uint8_t probePins[PROBE_COUNT] = {A0, A1, A2, A3};

// 干态差值基准
uint16_t dryDiff[PROBE_COUNT];
#define WET_THRESHOLD_DIFF  0.25f  // 差值下降 25% → 导通

// 检测间隔(不需要一直检测,间歇采样即可)
#define SAMPLE_INTERVAL_MS  500    // 每 500ms 检测一次

void setup() {
    Serial.begin(115200);
    calibrateAC();
}

void loop() {
    checkAllProbesAC();
    delay(SAMPLE_INTERVAL_MS);
}

/**
 * 交流检测单个探针
 * 返回:正向和反向读数的差值
 */
uint16_t acReadProbe(uint8_t pin) {
    uint16_t vHigh, vLow;

    // Phase 1: 正向充电
    // 内部上拉 + 数字输出高
    pinMode(pin, OUTPUT);
    digitalWrite(pin, HIGH);
    delayMicroseconds(100);  // 等待稳定(液体电容充电)
    vHigh = analogRead(pin);

    // Phase 2: 反向放电
    digitalWrite(pin, LOW);
    delayMicroseconds(100);
    vLow = analogRead(pin);

    // Phase 3: 断开(高阻态,停止电流)
    pinMode(pin, INPUT);

    // 返回差值
    return (vHigh > vLow) ? (vHigh - vLow) : (vLow - vHigh);
}

/**
 * 标定干态差值基准
 */
void calibrateAC() {
    Serial.println("AC 标定中...(确保探针干燥)");
    for (int i = 0; i < PROBE_COUNT; i++) {
        uint32_t sum = 0;
        for (int j = 0; j < 16; j++) {
            sum += acReadProbe(probePins[i]);
            delay(10);
        }
        dryDiff[i] = (uint16_t)(sum / 16);
        Serial.print("  探针 ");
        Serial.print(i + 1);
        Serial.print(": 干态差值 = ");
        Serial.println(dryDiff[i]);
    }
    Serial.println("标定完成\n");
}

/**
 * 交流检测所有探针
 */
void checkAllProbesAC() {
    Serial.print("液位: ");

    int highestWet = 0;
    for (int i = 0; i < PROBE_COUNT; i++) {
        uint16_t diff = acReadProbe(probePins[i]);

        // 差值相对下降(导通后差值变小,因为上拉/下拉能力被液体分流)
        float drop = 1.0f - (float)diff / dryDiff[i];
        bool wet = (drop > WET_THRESHOLD_DIFF);

        if (wet) highestWet = i + 1;

        Serial.print(wet ? "●" : "○");
    }

    Serial.print("  → 档位: ");
    Serial.print(highestWet);
    Serial.print("/");
    Serial.println(PROBE_COUNT);
}

8.4 用 GPIO 推挽输出的纯数字交流方案

/**
 * 极简交流激励 — 纯 GPIO,无需 ADC
 *
 * 原理:
 *   两个相邻 GPIO 组成一对,交替推挽输出
 *   GPIO_A 输出高、GPIO_B 输出低时 → 正向电流
 *   GPIO_A 输出低、GPIO_B 输出高时 → 反向电流
 *   检测电流大小 = 判断是否导通
 *
 * 此法不需要 ADC,用比较器或看 GPIO 上升时间即可
 */

void acDrivePair(uint8_t pinA, uint8_t pinB) {
    // 正半周
    pinMode(pinA, OUTPUT); digitalWrite(pinA, HIGH);
    pinMode(pinB, OUTPUT); digitalWrite(pinB, LOW);
    delayMicroseconds(50);

    // 反半周
    digitalWrite(pinA, LOW);
    digitalWrite(pinB, HIGH);
    delayMicroseconds(50);

    // 断开
    pinMode(pinA, INPUT);
    pinMode(pinB, INPUT);
}

📌 量产建议:对于寿命要求 > 3 年的产品,强烈建议使用交流激励方案。直流方案在验证原型和短期项目中没问题,但长期运行必腐蚀。


九、故障诊断与自检

作为一个负责任的产品,不能只会在正常时读数,还要能在出问题时自己知道出问题了

9.1 常见故障模式

故障 表现 检测方法
探针断线 永不通 干态读数异常(ADC=0 或 4095)
探针短路到 GND 永通 ADC 始终为 0
探针短路到 VCC 永不通 ADC 始终为 4095
密封漏水 外壳内有水 加一个漏水检测电极在壳底
探针表面结垢 时通时不通 读数波动大
线束接触不良 间歇性故障 连续多次采样方差过大

9.2 自检代码

/**
 * 探针自检系统
 */

typedef enum {
    PROBE_OK = 0,
    PROBE_OPEN,       // 断线
    PROBE_SHORT_GND,  // 短路到地
    PROBE_SHORT_VCC,  // 短路到电源
    PROBE_UNSTABLE    // 读数不稳定(可能是接触不良或结垢)
} ProbeFault;

typedef struct {
    ProbeFault fault;
    float      confidence;  // 置信度 0.0~1.0
} ProbeDiagnosis;

ProbeDiagnosis diagnoseProbe(uint8_t pin, uint16_t dryBaseline) {
    ProbeDiagnosis diag = {PROBE_OK, 1.0f};

    // 连续采样,计算均值和方差
    const int N = 32;
    uint16_t samples[N];
    uint32_t sum = 0;

    for (int i = 0; i < N; i++) {
        samples[i] = analogRead(pin);
        sum += samples[i];
        delay(5);
    }

    float mean = (float)sum / N;

    // 计算方差
    float variance = 0;
    for (int i = 0; i < N; i++) {
        variance += (samples[i] - mean) * (samples[i] - mean);
    }
    variance /= N;

    // 判断
    if (mean < 10) {
        // ADC 读数接近 0 → 短路到 GND
        diag.fault = PROBE_SHORT_GND;
        diag.confidence = 0.95f;
    } else if (mean > 4080) {
        // ADC 读数接近 4095
        // 如果干态基准也接近 4095 → 正常(确实干了)
        // 如果干态基准正常但当前值接近 4095 → 可能短路到 VCC
        if (dryBaseline < 3000) {
            diag.fault = PROBE_SHORT_VCC;
            diag.confidence = 0.90f;
        }
    } else if (mean < 200 && dryBaseline > 500) {
        // 读数异常低(不是 GND 短路但比干态低太多)
        diag.fault = PROBE_OPEN;
        diag.confidence = 0.70f;
    } else if (variance > 5000) {
        // 读数波动大
        diag.fault = PROBE_UNSTABLE;
        diag.confidence = 0.60f;
    }

    return diag;
}

void printDiagnosis(ProbeDiagnosis diag, int probeNum) {
    const char *faultNames[] = {"OK", "OPEN", "SHORT_GND", "SHORT_VCC", "UNSTABLE"};

    Serial.print("探针 ");
    Serial.print(probeNum);
    Serial.print(": ");

    if (diag.fault == PROBE_OK) {
        Serial.println("✅ 正常");
    } else {
        Serial.print("⚠️ ");
        Serial.print(faultNames[diag.fault]);
        Serial.print(" (置信度: ");
        Serial.print(diag.confidence * 100, 0);
        Serial.println("%)");
    }
}

十、项目实战:电热水器水位控制器

10.1 需求分析

参数 要求
容器 电热水器内胆,304 不锈钢,高 400mm
液体 自来水(饮用水)
温度 0~100℃
检测档位 5 档(空/低/中/高/满)
控制输出 进水电磁阀 + 加热管互锁 + 蜂鸣告警
安全要求 无水严禁加热(防干烧);满水自停
探针要求 食品级 316L 不锈钢,交流激励防腐蚀

10.2 探针设计

水箱高度 400mm,5 档 + 公共电极:

探针 1:  50mm —  空档(最低水位告警,禁止加热)
探针 2: 120mm —  低档(允许加热最低水位)
探针 3: 200mm —  中档
探针 4: 300mm —  高档
探针 5: 380mm —  满档(停止进水)
COM:    430mm —  公共电极(最长,始终浸到水)

探针材质: 316L 不锈钢棒,直径 3mm
密封: 双 O 型圈 + PTFE 法兰
工作温度: -20 ~ 105℃

10.3 原理图

                    ┌──────────────────────────────────────────┐
                    │           MCU Board (STM32F103)          │
                    │                                          │
    220V AC ────┐   │   ┌─────────┐    ┌──────────┐           │
                │   │   │ 3.3V LDO│    │ 继电器×2 │── 进水阀  │
                ├───┼───┤ AC-DC   │    │          │── 加热管  │
                │   │   │ 5V→3.3V│    └──────────┘           │
                │   │   └─────────┘                           │
                │   │        │                                 │
                │   │   ┌────┴────┐                           │
  ┌──────┐      │   │   │  MCU    │─── OLED (水位 + 温度)     │
  │探针1 │───────────┼───┤  ADC0~4│─── 蜂鸣器                 │
  │探针2 │───────────┼───┤  GPIO  │─── 进水阀控制             │
  │探针3 │───────────┼───┤        │─── 加热管控制 (互锁)      │
  │探针4 │───────────┼───┤  UART  │─── 调试串口               │
  │探针5 │───────────┼───┤        │                           │
  │ COM  │───────────┼───┤  GND   │                           │
  └──────┘      │   │   └─────────┘                           │
                │   └──────────────────────────────────────────┘
                │
                └── 漏电保护器 (必须!)

10.4 安全逻辑

┌─────────────────────────────────────────────────────┐
│                  安全控制逻辑                         │
│                                                      │
│  IF 液位 ≤ 空档 (探针1 干):                          │
│      → 强制关闭加热管(禁止干烧)                     │
│      → 打开进水阀                                    │
│      → 蜂鸣器告警                                    │
│                                                      │
│  IF 空档 < 液位 < 满档:                              │
│      → 允许加热(有水了)                             │
│      → 进水阀状态由用户设定决定                        │
│                                                      │
│  IF 液位 ≥ 满档 (探针5 导通):                        │
│      → 强制关闭进水阀                                 │
│      → 蜂鸣器提示                                    │
│      → 加热可继续                                    │
│                                                      │
│  探针故障诊断:                                        │
│      → 探针 1 和 2 同时异常 → 禁止加热 (保守策略)    │
│      → 探针 5 异常 → 关闭进水阀 (保守策略)            │
│      → 上报故障代码到上位机                           │
└─────────────────────────────────────────────────────┘

10.5 核心控制代码

/**
 * 电热水器水位控制器 - 完整逻辑
 */

// ==================== 状态定义 ====================
enum WaterLevel {
    LEVEL_EMPTY = 0,   // 空(所有探针干)
    LEVEL_LOW   = 1,   // 低(仅探针 1 导通)
    LEVEL_MID   = 2,   // 中(探针 1~2 导通)
    LEVEL_HIGH  = 3,   // 高(探针 1~3 导通)
    LEVEL_FULL  = 4,   // 满(探针 1~4 导通)
    LEVEL_OVER  = 5    // 溢出(探针 5 导通)
};

enum SystemState {
    STATE_IDLE,         // 待机
    STATE_FILLING,      // 正在进水
    STATE_HEATING,      // 正在加热(有水)
    STATE_HEATING_FULL, // 正在加热(水满)
    STATE_ALARM,        // 告警(干烧风险)
    STATE_FAULT         // 故障
};

// ==================== 全局变量 ====================
SystemState sysState = STATE_IDLE;
WaterLevel  waterLevel = LEVEL_EMPTY;
bool valveOpen  = false;
bool heaterOn   = false;
unsigned long lastStateChange = 0;

// ==================== 主控制循环 ====================
void controlLoop() {
    // 1. 读取液位
    waterLevel = readWaterLevel();

    // 2. 诊断探针
    bool probesOK = diagnoseAllProbes();

    // 3. 安全逻辑
    if (!probesOK) {
        // 探针故障 → 安全状态
        setHeater(false);
        setValve(false);
        sysState = STATE_FAULT;
        return;
    }

    switch (waterLevel) {
        case LEVEL_EMPTY:
            // 无水 → 严禁加热
            setHeater(false);
            setValve(true);   // 自动进水
            sysState = STATE_FILLING;
            break;

        case LEVEL_LOW:
        case LEVEL_MID:
            // 有水 → 允许加热
            sysState = STATE_HEATING;
            // 进水阀取决于用户设定(手动/自动)
            break;

        case LEVEL_HIGH:
            // 水量充足 → 加热 + 停进水
            setValve(false);
            sysState = STATE_HEATING;
            break;

        case LEVEL_FULL:
        case LEVEL_OVER:
            // 水满 → 停进水
            setValve(false);
            sysState = STATE_HEATING_FULL;
            break;
    }

    // 4. 用户交互
    updateDisplay();
    handleAlarm();
}

十一、跟其他方案比一比

11.1 横向对比表(扩容版)

维度 电阻探针 电容外贴 电容内置 超声波 压力式(MEMS) 浮球磁簧
精度 分档 连续 连续 连续 连续 连续
最小检测 ~3mm ~1mm ~1mm ~20mm(盲区) ~0.5mm ~5mm
液体导电要求 必须导电 无所谓 无所谓 无所谓 无所谓 无所谓
泡沫影响 ✅严重
挂料/结垢影响 ⚠️ 可能不通 ❌ 不接触 ✅ 会影响 ✅ 严重
温度影响 小(电导率变化) 大(介电常数变) 中(声速变) 中(温漂)
安装难度 ⭐⭐ ⭐⭐⭐ ⭐⭐ ⭐⭐⭐
维护频率 低(交流激励) 极低 极低
寿命 10年+ 10年+ 3~5年 5~8年 10年+ 2~5年
BOM 成本 ¥2~8 ¥8~20 ¥10~30 ¥30~80 ¥12~25 ¥5~20
安全认证难度 中(材料)

11.2 我选方案的真实决策过程

产品经理问你:"我们做个水箱液位检测,精度不需要太高,但要便宜可靠,你说怎么做?"

我的回答顺序是这样的:

1. 先问液体是什么?
   ├─ 自来水/饮用水 → 电阻探针方案,最直接
   ├─ 去离子水       → 不能用电阻式,换电容方案
   ├─ 油类/有机溶剂   → 同上,换电容或超声波
   └─ 酸碱腐蚀液体   → 探针方案需选材(钛合金/石墨)

2. 再问需要连续量还是分档就行?
   ├─ 分档就够 (如 3~5 档) → 电阻探针,完美
   └─ 需要连续量 (0~100% 任意值) → 电容或压力式

3. 问能不能开孔?
   ├─ 能开孔 → 电阻探针或压力式
   └─ 不能开孔 → 电容外贴或超声波

4. 最后问预算?
   ├─ 极低 (< ¥5)  → 电阻探针,没有比这更便宜的了
   ├─ 低 (¥5~20)   → 电阻探针 或 电容外贴
   └─ 中等 (¥20+)  → 超声波 / 压力式

多数饮水机、热水器、加湿器、水处理设备的场景,答案都落在电阻探针上。


十二、量产中踩过的坑

坑一:探针间距太近导致"水桥"误判

现象:两根探针之间距离只有 3mm。液位下降后,探针之间挂着一滴水珠(表面张力),导致本来应该断开的两根探针依然导通——MCU 以为还有水,实际上已经没水了。

教训

  • 探针间距 至少 8mm 以上
  • 探针末端做锥形尖头(减少挂水面积)
  • 软件做去抖动——连续 N 次采样都干才判定为干

坑二:热水导致电导率变化

现象:同一个水箱,水温 20℃ 时液位判断正常,水温升到 80℃ 时探针全部导通了(读数偏低)。

原因:水的电导率随温度升高而增大。80℃ 自来水的电导率大约是 20℃ 时的 3~4 倍。

教训

  • 需要一个温度传感器(NTC 或 DS18B20)放在 COM 电极附近测液温
  • 根据液温动态调整 WET_THRESHOLD:
float getThresholdForTemp(float tempC) {
    // 温度越高,水电导率越高,导通电压降越大
    // 因此阈值要相应调整
    if (tempC < 30) return 0.30f;
    if (tempC < 60) return 0.25f;
    return 0.20f;  // 高温时阈值降低(更容易判断为导通)
}

坑三:水垢是电阻探针的头号杀手

现象:硬水地区(北方为主),用了半年后探针表面结一层白色水垢。水垢是不导电的——明明探针泡在水里,但因为被水垢包裹,测出来是"干"的。

教训

  • 硬水地区用交流激励(交流能一定程度穿透薄水垢层)
  • 探针表面做镜面抛光(光滑表面水垢附着力差)
  • 设计中预留定期维护手段(探针可拆卸清洁)
  • 或者干脆告诉客户:水质硬度 > 300mg/L 的建议选其他方案(如电容式)

坑四:公共电极不能接"真正的地"

现象:有人直接把 COM 电极接到电路板的 GND 上,省了一根 ADC 口。然后有一天,220V 漏电通过水路传过来,把整块板烧了。

教训

  • COM 电极通过一个 100kΩ 电阻接到 GND,不能直连
  • 所有探针和 COM 对 GND 都要加 TVS 保护
  • 交流供电设备必须有漏电保护器
  • 直流低压设备(电池/USB 供电)风险小,但也不建议 COM 直连 GND

坑五:密封胶水的选择

现象:用普通 704 硅橡胶密封探针法兰,三个月后硅胶发白、变脆、漏水。

教训

  • 普通 704 硅橡胶在水中长期浸泡会水解
  • 必须选用食品级 RTV 硅胶氟橡胶 O 型圈
  • O 型圈的寿命远高于胶水密封
  • 不要为了省事用胶水。机械密封(O 型圈 + 法兰压紧)才是正道

十三、总结与金句

核心结论

这种方案的核心竞争力就三个字:简单、可靠

没有复杂的模拟前端,没有 DSP 算法,没有温度补偿的烦恼(交流激励方案下),没有射频干扰的困扰。几根不锈钢棒、几个电阻、一个 ADC,就能做出寿命十年以上的工业级产品。

四条金句

① "最好的技术,是你敢把它放在客户家里十年不用管的那个。"
电阻探针方案就是这样的技术。

② "分档有时候比连续量更有用。"
工业控制里,"没水了""水满了"两个信号比"水位 67.3%"有用得多。不要为了"连续"而牺牲可靠性。

③ "直流通电的探针就是一根牺牲阳极。"
不想让探针慢慢溶解在水里,就老老实实用交流激励。

④ "一个 O 型圈的成本 ¥0.5,密封失效的代价可能是整机报废。"
结构工艺是电阻式方案最容易翻车的地方,马虎不得。

技术要点速查

✅ 原理:水导电 → 探针+COM通过水导通 → ADC测电压下降 → 判断液位档位
✅ 探针材料:304不锈钢(通用)/ 316L(食品级)/ 钛合金(强腐蚀)
✅ 探针直径:家用 2mm / 工业 3~4mm
✅ 探针间距:≥ 8mm(防"水桥"误判)
✅ 密封方案:双 O 型圈 + 法兰压紧(优先于胶水)
✅ 电路配置:100kΩ 上拉 + TVS 保护 + 低通滤波电容
✅ 激励方式:交流激励(长寿命)/ 直流(短期验证)
✅ 判断方法:ADC 相对变化率 > 阈值(不用绝对值)
✅ 必须自检:断线/短路/不稳定诊断
✅ 安全设计:COM 不能直连 GND,需串电阻

适用场景清单

场景 推荐程度 备注
家用饮水机、热水器 ⭐⭐⭐⭐⭐ 完美匹配
加湿器、除湿机水箱 ⭐⭐⭐⭐⭐ 完美匹配
工业冷却塔、水池 ⭐⭐⭐⭐ 注意水垢问题
咖啡机、饮料机 ⭐⭐⭐⭐ 用 316L 食品级
鱼缸、水族箱 ⭐⭐⭐ 用交流激励
污水井、化粪池 ⭐⭐ 结垢和腐蚀严重,寿命受限
去离子水罐 不导电,无能为力
油罐、溶剂罐 不导电,请用电容方案

如果对你有帮助,欢迎点赞、收藏、转发。有问题评论区见,我会回复。🔧

发布于 CSDN | 技术交流欢迎私信

posted on 2026-07-27 03:28  Hello_Damon  阅读(4)  评论(0)    收藏  举报

导航