电阻式液位检测从原理到实战
电阻式液位检测:用几根不锈钢探针实现可靠的分档液位测量
一位 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 | 技术交流欢迎私信
posted on 2026-07-27 03:28 Hello_Damon 阅读(4) 评论(0) 收藏 举报
浙公网安备 33010602011771号