跳转到内容

液位传感器集成

液位传感器集成

本节介绍如何使用 HC-SR04 超声波传感器测量容器液位,并将数据整合到自动投料系统中。学习完成后,您将能够:

  • 理解超声波测距原理和 HC-SR04 的使用方法
  • 实现超声波传感器的驱动代码
  • 将液位数据通过 MQTT 发送到 Node-RED
  • 扩展多路液位监测
┌────────────────────────────────────┐
│ HC-SR04 超声波传感器 │
├────────────────────────────────────┤
│ │
│ ┌──────────┐ │
│ │ 发射器 │ ←── 发出 40KHz 声波│
│ └──────────┘ │
│ ╲ ╱ │
│ ╳ 声波遇到水面反射 │
│ ╱ ╲ │
│ ┌──────────┐ │
│ │ 接收器 │ ←── 接收回波 │
│ └──────────┘ │
│ │
│ VCC ─── 5V │
│ GND ─── GND │
│ Trig ─── GPIO 2 (ESP32 发脉冲) │
│ Echo ─── GPIO 3 (ESP32 接收) │
└────────────────────────────────────┘
工作原理:
1. ESP32 向 Trig 引脚发送 10μs 高电平脉冲
2. HC-SR04 发射 8 个 40KHz 超声波脉冲
3. 声波遇到水面反射
4. HC-SR04 接收回波,Echo 引脚输出高电平
5. Echo 高电平持续时间 = 声波往返时间
6. 距离 = 时间 × 声速 ÷ 2
声速: 340 m/s (20°C)
最大量程: 4m
最小盲区: 2cm
精度: ±3mm
// Ultrasonic.h — 超声波传感器驱动
#ifndef ULTRASONIC_H
#define ULTRASONIC_H
// 测量距离 (单位: cm)
// triggerPin: Trig 引脚
// echoPin: Echo 引脚
// 返回值: 距离值 (cm),超时返回 -1
long measureDistance(int triggerPin, int echoPin) {
// 确保 Trig 为低电平
digitalWrite(triggerPin, LOW);
delayMicroseconds(2);
// 发送 10μs 高电平脉冲
digitalWrite(triggerPin, HIGH);
delayMicroseconds(10);
digitalWrite(triggerPin, LOW);
// 读取 Echo 高电平持续时间 (微秒)
long duration = pulseIn(echoPin, HIGH, 30000); // 30ms 超时 (~5m)
if (duration == 0) {
Serial.println("Ultrasonic: No echo received (timeout)");
return -1; // 超时
}
// 计算距离 (cm)
// 声速 340m/s = 0.034 cm/μs
// 往返时间 → 距离 = duration × 0.034 / 2
// 简化为: distance = duration / 58
long distance = duration / 58;
return distance;
}
// 多次测量取平均 (减少噪声)
long measureDistanceAverage(int triggerPin, int echoPin, int samples = 5) {
long total = 0;
int validSamples = 0;
for (int i = 0; i < samples; i++) {
long d = measureDistance(triggerPin, echoPin);
if (d > 0) { // 只累加有效值
total += d;
validSamples++;
}
delay(10); // 测量间隔
}
if (validSamples == 0) return -1;
return total / validSamples;
}
#endif
// 液位计算
// 容器总高度 (从传感器到容器底部)
const int CONTAINER_HEIGHT = 24; // cm
// 将测量距离转换为液位百分比
int distanceToLevelPercent(long distance, int containerHeight) {
if (distance < 0) return -1; // 测量失败
if (distance >= containerHeight) return 0; // 空
if (distance <= 2) return 100; // 满 (传感器盲区)
int level = ((containerHeight - distance) * 100) / containerHeight;
return constrain(level, 0, 100);
}
// 引脚定义
#define TRIG_PIN 2
#define ECHO_PIN 3
#define RELAY_PUMP_PIN 20
#define CONTAINER_HEIGHT 24 // cm
const int TRIG_PIN = 2;
const int ECHO_PIN = 3;
long distance1 = 0; // 1 号容器液位
long distance2 = 0; // 2 号容器液位 (可选)
void handleGetData() {
// 测量 1 号容器液位 (取平均)
distance1 = measureDistanceAverage(TRIG_PIN, ECHO_PIN);
if (distance1 < 0) {
// 测量失败
client.publish("esp32/dosing/status",
"{\"error\":\"sensor_failure\"}");
currentState = STATE_TIME_FOR_SLEEP;
return;
}
// 计算液位百分比
int levelPercent = distanceToLevelPercent(distance1, CONTAINER_HEIGHT);
Serial.printf("Distance: %ld cm, Level: %d%%\n",
distance1, levelPercent);
// 构建 JSON 发送到 Node-RED
String payload = "{\"distance\":" + String(distance1) +
",\"level\":" + String(levelPercent) +
",\"container_height\":" + String(CONTAINER_HEIGHT) +
",\"state\":\"get_data\"}";
client.publish("esp32/dosing/info", payload.c_str());
// 等待 Node-RED 返回指令
currentState = STATE_WAIT;
lastCommandTime = millis();
}
// 扩展多路液位监测
// 引脚定义 (多路)
#define TRIG1_PIN 2 // 容器 1 Trig
#define ECHO1_PIN 3 // 容器 1 Echo
#define TRIG2_PIN 4 // 容器 2 Trig
#define ECHO2_PIN 5 // 容器 2 Echo
// 容器参数
const int CONTAINER1_HEIGHT = 24;
const int CONTAINER2_HEIGHT = 30;
void handleGetData() {
// 同时读取多个传感器
distance1 = measureDistanceAverage(TRIG1_PIN, ECHO1_PIN, 3);
distance2 = measureDistanceAverage(TRIG2_PIN, ECHO2_PIN, 3);
// 构建多容器 JSON
String payload = "{\"distance1\":" + String(distance1) +
",\"distance2\":" + String(distance2) +
",\"level1\":" + String(distanceToLevelPercent(distance1, CONTAINER1_HEIGHT)) +
",\"level2\":" + String(distanceToLevelPercent(distance2, CONTAINER2_HEIGHT)) +
",\"state\":\"get_data\"}";
client.publish("esp32/dosing/info", payload.c_str());
currentState = STATE_WAIT;
}
// Node-RED Function: 解析 ESP32 液位数据
// 从 esp32/dosing/info Topic 接收
var data = msg.payload;
// data 格式: {"distance":16,"level":33,"container_height":24}
var distance = data.distance;
var levelPercent = data.level;
// 计算剩余量 (升)
// 假设容器截面为 20cm × 20cm
var containerVolume = 20 * 20 * data.container_height / 1000; // 升
var remainingVolume = containerVolume * levelPercent / 100;
msg.payload = {
distance: distance,
level: levelPercent,
remaining: remainingVolume.toFixed(1),
unit: "liters",
timestamp: Date.now()
};
return msg;
Terminal window
# 1. 测试传感器
mosquitto_pub -t "esp32/dosing/command" -m "get_data"
# 2. 检查 MQTT 数据
mosquitto_sub -t "esp32/dosing/info" -v
# 输出: esp32/dosing/info {"distance":16,"level":33,...}
# 3. 用手遮挡传感器测试
# 距离变小 → level 增加
# 移开手 → level 减小
# 4. 传感器故障测试
# 拔掉 Echo 线 → 应看到 {"error":"sensor_failure"}

Q1: 超声波传感器在泡沫或粉尘环境中可靠吗?

Section titled “Q1: 超声波传感器在泡沫或粉尘环境中可靠吗?”

不推荐。泡沫会吸收超声波,粉尘会散射声波。推荐使用:

  • 泡沫环境: 电容式液位传感器
  • 粉尘环境: 雷达液位计
  • 防腐蚀要求: 非接触式超声波(但需定期清洁传感器表面)

Q2: 容器形状不规则怎么计算液位?

Section titled “Q2: 容器形状不规则怎么计算液位?”

对于不规则容器,可以在 Node-RED 中使用查表法:

// Node-RED Function: 不规则容器液位换算
// 预定义的容器形状 → 液位映射表
var lookupTable = [
{distance: 0, volume: 100},
{distance: 5, volume: 85},
{distance: 10, volume: 65},
{distance: 15, volume: 40},
{distance: 20, volume: 15},
{distance: 24, volume: 0}
];
// 通过插值计算实际剩余量
function interpolate(distance, table) {
for (var i = 1; i < table.length; i++) {
if (distance <= table[i].distance) {
var ratio = (distance - table[i-1].distance) /
(table[i].distance - table[i-1].distance);
return table[i-1].volume -
ratio * (table[i-1].volume - table[i].volume);
}
}
return 0;
}

Q3: 如何减少液位波动引起的误判?

Section titled “Q3: 如何减少液位波动引起的误判?”
  1. 软件: 多次采样取平均 (measureDistanceAverage)
  2. 软件: 移动平均滤波 (Node-RED 端)
  3. 硬件: 传感器安装固定支架,减少晃动
  4. 硬件: 在容器内加装导波管

推荐做法:

  • 传感器距离液面至少 2cm(避免盲区)
  • 多次采样取平均值(建议 5 次)
  • 传感器表面保持清洁
  • 液位数据附加上次投料时间一并存储
  • 实现传感器故障检测和报警

避免做法:

  • 传感器表面有冷凝水或结露时忽略校准
  • 单次测量就作为最终液位值
  • 容器内壁有附着物影响回波
  • 忽略温度对声速的影响
  1. HC-SR04: Trig + 10μs 脉冲 → Echo 测脉宽 → 距离 = 脉宽/58
  2. 液位计算: 容器高度 - 测量距离 = 液位高度
  3. 多次采样: 5 次取平均减少噪声
  4. 故障检测: 超时返回 -1,触发告警
  5. 多路扩展: 最多支持 8+ 超声波传感器