欢迎光临
我们一直在努力

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

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

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


目录

  • 一、老工程师的开场白
  • 二、基本原理:水就是一根导线
  • 三、为什么这个方案值得认真对待?
  • 四、探针设计与结构工艺
  • 五、信号检测电路设计
  • 六、Arduino 驱动开发
  • 七、STM32 驱动开发
  • 八、交流激励:解决电解腐蚀的关键
  • 九、故障诊断与自检
  • 十、项目实战:电热水器水位控制器
  • 十一、跟其他方案比一比
  • 十二、量产中踩过的坑
  • 十三、总结与金句

一、老工程师的开场白

在嵌入式这行摸爬滚打了 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

交流激励的核心思想:

  • 正向脉冲:IO 输出高电平(上拉),读 ADC → 得到电压 V+
  • 反向脉冲:IO 输出低电平(下拉),读 ADC → 得到电压 V-
  • 差值判断:|V+ – V-| 就是导通程度
  • 脉冲极短:每次检测只有几百微秒,平均电流接近零
  • 正负对称:正向和反向时间相等,净电荷转移为零
  • 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 | 技术交流欢迎私信

    赞(0)
    未经允许不得转载:171主机测评 » 电阻式液位检测从原理到实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址