4、传感器数据采集:DHT11/DHT22 温湿度传感器、BH1750 光照传感器、ADC 模拟量读取、传感器数据滤波算法

做智能家居,传感器就是系统的眼睛和耳朵。没有准确的数据,再漂亮的自动化逻辑都是空中楼阁。这一章,我们来聊聊几种最常用的传感器,以及怎么让它们的数据变得可靠。

4.1 DHT11 与 DHT22:温湿度传感器的选择与实战

温湿度传感器里,DHT11 和 DHT22 是绝对的「入门级明星」。我最早做环境监测项目时,用的就是 DHT11,便宜、简单,但坑也不少。

4.1.1 选型对比:别买错了

这两兄弟长得几乎一样,引脚定义也相同,但性能差距挺大。我个人习惯直接看这张表:

参数 DHT11 DHT22 (AM2302)
温度范围 0~50°C -40~80°C
温度精度 ±2°C ±0.5°C
湿度范围 20~90% RH 0~100% RH
湿度精度 ±5% RH ±2% RH
采样周期 1 秒 2 秒
价格 约 3-5 元 约 15-25 元

说白了,DHT11 只能告诉你「大概热不热」,DHT22 才能告诉你「精确到几度」。做室内恒温控制,我建议直接上 DHT22,省得后期数据不准来回折腾。

4.1.2 接线与代码:单总线通信

DHT 系列用的是单总线协议,一根数据线搞定通信。接线很简单:

  • VCC → 3.3V
  • GND → GND
  • DATA → GPIO4(示例)
注意: 数据线到 VCC 之间需要接一个 4.7kΩ~10kΩ 的上拉电阻。有些模块板载了电阻,但裸片版本必须自己加。我曾经偷懒没加,结果数据全是 0,排查了半天。

代码方面,我推荐用 Adafruit 的库,封装得很好:

#include <DHT.h>

#define DHTPIN 4
#define DHTTYPE DHT22   // 如果是 DHT11,改成 DHT11

DHT dht(DHTPIN, DHTTYPE);

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

void loop() {
  delay(2000);  // DHT22 采样周期至少 2 秒

  float h = dht.readHumidity();
  float t = dht.readTemperature();

  if (isnan(h) || isnan(t)) {
    Serial.println("读取失败!检查接线");
    return;
  }

  Serial.print("湿度: ");
  Serial.print(h);
  Serial.print(" %\t");
  Serial.print("温度: ");
  Serial.print(t);
  Serial.println(" °C");
}
小技巧: 读取间隔不要小于采样周期。DHT11 至少等 1 秒,DHT22 至少等 2 秒。读太快,传感器会罢工,返回 NAN。

4.2 BH1750 光照传感器:数字化的光感方案

光照传感器里,BH1750 是我的首选。为什么?因为它直接输出数字量,精度高,而且不受光源角度影响。你想想看,用光敏电阻还得自己算公式、校准,多麻烦。

4.2.1 硬件连接与 I2C 地址

BH1750 走 I2C 协议,接线就四根线:

  • VCC → 3.3V
  • GND → GND
  • SCL → GPIO22 (ESP32 默认)
  • SDA → GPIO21 (ESP32 默认)

它的 I2C 地址由 ADDR 引脚决定:

ADDR 电平 I2C 地址
GND (低电平) 0x23
VCC (高电平) 0x5C

嗯,这里要注意:如果你同时挂多个 BH1750,每个必须设置不同的 ADDR 电平,否则地址冲突。

4.2.2 读取光照强度

用库就两行代码的事:

#include <BH1750.h>
#include <Wire.h>

BH1750 lightMeter;

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

void loop() {
  float lux = lightMeter.readLightLevel();
  Serial.print("光照强度: ");
  Serial.print(lux);
  Serial.println(" lx");
  delay(500);
}
重点: BH1750 有几种测量模式。默认是连续高分辨率模式,精度 1 lx,适合室内。如果做户外强光检测,可以切换到高分辨率模式 2,精度 0.5 lx,但量程会变小。

4.3 ADC 模拟量读取:ESP32 的「模拟耳朵」

很多传感器输出的是模拟电压,比如土壤湿度传感器、MQ 系列气体传感器。这时候就得靠 ADC(模数转换器)了。ESP32 内置了两个 12 位 ADC,共 18 个通道。

4.3.1 基本读取方法

12 位 ADC 意味着返回值范围是 0~4095,对应 0~3.3V。代码很简单:

#define ADC_PIN 34   // ADC1 通道 6

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);  // 设置分辨率 12 位
}

void loop() {
  int raw = analogRead(ADC_PIN);
  float voltage = raw * (3.3 / 4095.0);
  Serial.print("原始值: ");
  Serial.print(raw);
  Serial.print("  电压: ");
  Serial.print(voltage);
  Serial.println(" V");
  delay(100);
}
避坑指南: ESP32 的 ADC 线性度不太好,尤其是靠近 0V 和 3.3V 两端。我曾经做一个水位检测项目,发现低电压段读数跳得厉害。后来加了校准曲线才解决。如果要求高精度,建议外挂 ADS1115 这样的独立 ADC 芯片。

4.3.2 电压衰减与量程

默认 ADC 量程是 0~3.3V。如果你要测更高电压,得用分压电阻。比如测 12V 电池,用 10kΩ 和 3.3kΩ 分压,把 12V 降到约 3V。公式很简单:

Vout = Vin * R2 / (R1 + R2)

选电阻时注意功率,别冒烟了。

4.4 传感器数据滤波算法:让数据「稳」下来

原始传感器数据往往噪声很大。你想想看,一个光照传感器,读数在 300 lx 到 350 lx 之间乱跳,这自动化怎么触发?所以滤波是必须的。

4.4.1 限幅滤波:剔除野值

最简单的方法。设定一个最大允许偏差,如果当前值和上次值差太多,就丢掉。

#define MAX_GAP 50

int lastValue = 0;

int limitFilter(int newValue) {
  if (abs(newValue - lastValue) <= MAX_GAP) {
    lastValue = newValue;
    return newValue;
  } else {
    return lastValue;  // 超出范围,用旧值
  }
}

我在做烟雾传感器时用过这招,有效滤掉了偶尔的尖峰干扰。

4.4.2 中位值滤波:抗脉冲干扰

连续采样 N 次,排序后取中间值。对脉冲干扰效果极好。

#define N 5

int medianFilter() {
  int buf[N];
  for (int i = 0; i < N; i++) {
    buf[i] = analogRead(ADC_PIN);
    delay(10);
  }
  // 冒泡排序
  for (int i = 0; i < N - 1; i++) {
    for (int j = 0; j < N - i - 1; j++) {
      if (buf[j] > buf[j + 1]) {
        int temp = buf[j];
        buf[j] = buf[j + 1];
        buf[j + 1] = temp;
      }
    }
  }
  return buf[N / 2];
}
建议: N 取奇数,方便取中位。N 越大越平滑,但响应变慢。一般 5~11 之间比较合适。

4.4.3 滑动平均滤波:平滑曲线

维护一个固定长度的队列,每次取平均值。这是我最常用的方法,适合温湿度这种变化缓慢的信号。

#define WINDOW_SIZE 10

int buffer[WINDOW_SIZE];
int index = 0;
int sum = 0;

int movingAverageFilter(int newValue) {
  sum -= buffer[index];
  buffer[index] = newValue;
  sum += buffer[index];
  index = (index + 1) % WINDOW_SIZE;
  return sum / WINDOW_SIZE;
}

说白了,这就是一个「最近 N 次读数的平均」。窗口越大越平滑,但滞后也越大。做恒温控制时,我一般用 5~10 的窗口。

4.4.4 一阶低通滤波:轻量级平滑

不需要数组,只用一个公式:结果 = 上次结果 * (1 - α) + 当前值 * α。α 越小越平滑。

#define ALPHA 0.3

int lastFiltered = 0;

int lowPassFilter(int newValue) {
  lastFiltered = (int)(lastFiltered * (1 - ALPHA) + newValue * ALPHA);
  return lastFiltered;
}

这个算法内存占用极小,适合资源受限的场景。我曾在 ESP01 上用这个,效果不错。

4.5 综合实战:一个可靠的环境监测节点

把上面所有东西串起来。一个典型的做法是:

  1. 用 DHT22 读温湿度
  2. 用 BH1750 读光照
  3. 用 ADC 读一个模拟土壤湿度传感器
  4. 对每个数据流分别应用滑动平均滤波
  5. 每 5 秒上报一次到 HomeAssistant

嗯,这里要注意:不同传感器的采样周期不同。DHT22 最快 2 秒一次,BH1750 可以 100ms 一次,ADC 可以更快。我习惯用定时器统一调度,而不是在 loop 里用 delay 死等。

核心原则: 传感器数据采集,准确比快速更重要。宁可慢一点,也要保证数据可靠。一个错误的读数,可能触发错误的自动化动作——比如大冬天把空调开成制冷。

好了,这一章的内容就到这里。下一章我们聊聊怎么把这些数据优雅地传给 HomeAssistant,让它们真正派上用场。