第四节:读取温湿度与气体数据
好,咱们进入第四节课。这节课要解决一个核心问题——怎么让ESP32准确读取环境数据。
你想想看,一个空气净化器,如果连传感器数据都读不准,那后面的联动控制全是瞎扯。我见过不少项目,代码写得花里胡哨,结果一上电,温度跳了5度,气体浓度忽高忽低——这种数据你敢用吗?
这节课,咱们就踏踏实实把DHT22和MQ-135这两个传感器搞定。我会把我在实际项目中踩过的坑、总结的经验,全部分享给你。
4.1 DHT22单总线协议驱动编写
DHT22这个传感器,我用了不下十年。它最大的特点就是——便宜、稳定、精度够用。但它的通信协议有点特殊,叫「单总线」。
说白了,就是一根数据线,既要发信号又要收信号。时序要求很严格,差个几微秒就读不出来。
4.1.1 单总线协议时序分析
DHT22的通信过程,其实就三步:
- 主机发起信号:拉低总线至少18ms,然后拉高20-40μs
- 传感器响应:拉低80μs,再拉高80μs
- 数据传输:40位数据,每位以50μs低电平开始
这里有个关键点——怎么区分0和1?
很简单:
- 高电平持续26-28μs → 表示0
- 高电平持续70μs → 表示1
嗯,这里要注意。ESP32的主频是240MHz,但Arduino框架下的delayMicroseconds()函数精度有限。我建议用esp_timer_get_time()来做微秒级计时,比delay靠谱得多。
4.1.2 驱动代码实现
好,直接上代码。这是我个人习惯的写法——把DHT22封装成一个类,方便复用。
class DHT22 {
private:
uint8_t pin;
int16_t temperature;
int16_t humidity;
bool waitForLevel(uint8_t level, uint32_t timeout_us) {
uint32_t start = esp_timer_get_time();
while (digitalRead(pin) != level) {
if (esp_timer_get_time() - start > timeout_us) return false;
}
return true;
}
uint8_t readByte() {
uint8_t data = 0;
for (int i = 0; i < 8; i++) {
// 等待50μs低电平结束
waitForLevel(HIGH, 100);
uint32_t start = esp_timer_get_time();
waitForLevel(LOW, 100);
uint32_t duration = esp_timer_get_time() - start;
// 判断是0还是1
data <<= 1;
if (duration > 40) data |= 1;
}
return data;
}
public:
DHT22(uint8_t gpio_pin) : pin(gpio_pin) {}
bool read() {
// 1. 主机发起信号
pinMode(pin, OUTPUT);
digitalWrite(pin, LOW);
delay(20); // 至少18ms
digitalWrite(pin, HIGH);
delayMicroseconds(30);
pinMode(pin, INPUT_PULLUP);
// 2. 等待响应
if (!waitForLevel(LOW, 100)) return false;
if (!waitForLevel(HIGH, 100)) return false;
// 3. 读取40位数据
uint8_t data[5];
for (int i = 0; i < 5; i++) {
data[i] = readByte();
}
// 4. 校验和验证
uint8_t checksum = data[0] + data[1] + data[2] + data[3];
if (checksum != data[4]) return false;
// 5. 解析数据
humidity = (data[0] << 8) | data[1];
temperature = (data[2] << 8) | data[3];
return true;
}
float getTemperature() { return temperature / 10.0f; }
float getHumidity() { return humidity / 10.0f; }
};
4.2 MQ-135模拟量读取(ADC)
MQ-135这个气体传感器,说白了就是个可变电阻。它检测到有害气体时,内部电阻会变化。我们通过ADC读取电压值,就能反推出气体浓度。
但这里有个问题——MQ-135的模拟输出是非线性的。你不能直接用电压值当浓度用,得做校准。
4.2.1 ADC配置与读取
ESP32的ADC有12位精度,也就是0-4095。但它的线性度嘛...说实话,不太行。尤其是靠近0V和3.3V两端,误差比较大。
我个人习惯用ADC1的通道,因为ADC2在WiFi开启时会受影响。
#include "driver/adc.h"
#include "esp_adc_cal.h"
#define MQ135_PIN GPIO_NUM_34 // ADC1_CH6
static esp_adc_cal_characteristics_t adc_chars;
void initMQ135() {
// 配置ADC
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);
// 校准ADC
esp_adc_cal_characterize(
ADC_UNIT_1,
ADC_ATTEN_DB_11,
ADC_WIDTH_BIT_12,
1100, // 参考电压,单位mV
&adc_chars
);
}
float readMQ135Raw() {
uint32_t adc_reading = adc1_get_raw(ADC1_CHANNEL_6);
uint32_t voltage = esp_adc_cal_raw_to_voltage(adc_reading, &adc_chars);
return voltage / 1000.0f; // 转换为伏特
}
4.2.2 气体浓度换算
MQ-135的数据手册里有个曲线图,展示了不同气体浓度下Rs/R0的比值。我们需要把这个曲线拟合成数学公式。
我实测下来,用指数函数拟合效果最好:
// 以NH3为例的浓度换算
float calculateNH3(float voltage) {
// Rs = (Vc - Vout) / Vout * RL
float Rs = (3.3f - voltage) / voltage * 10.0f; // RL=10kΩ
// R0:在洁净空气中的阻值
float ratio = Rs / R0;
// 指数拟合:ppm = a * (ratio)^b
// 参数来自数据手册曲线拟合
float ppm = 10.0f * pow(ratio, -1.5f);
return ppm;
}
4.3 数据校准与滤波
说实话,原始数据直接拿来用,那是不行的。DHT22还好,MQ-135的噪声特别大。我见过有人用原始数据做控制,结果净化器一会儿开一会儿关,跟抽风似的。
4.3.1 滑动平均滤波
这是我最常用的滤波方法。说白了,就是取最近N次数据的平均值。N越大越平滑,但响应也越慢。
class MovingAverage {
private:
float* buffer;
int size;
int index;
float sum;
bool filled;
public:
MovingAverage(int n) : size(n), index(0), sum(0), filled(false) {
buffer = new float[n];
memset(buffer, 0, n * sizeof(float));
}
float addValue(float value) {
sum -= buffer[index];
buffer[index] = value;
sum += value;
index = (index + 1) % size;
if (index == 0) filled = true;
int count = filled ? size : index;
return sum / count;
}
};
4.3.2 中值滤波
对于突发噪声(比如有人走过带来的气流扰动),中值滤波效果更好。我一般取5个数据的中位数。
float medianFilter(float* data, int len) {
// 复制数组
float temp[len];
memcpy(temp, data, len * sizeof(float));
// 冒泡排序
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < len - i - 1; j++) {
if (temp[j] > temp[j + 1]) {
float t = temp[j];
temp[j] = temp[j + 1];
temp[j + 1] = t;
}
}
}
return temp[len / 2];
}
4.4 多传感器数据融合显示
好,现在我们有温度、湿度、气体浓度三个数据了。怎么把它们整合到一起显示?
我习惯用一个结构体来打包所有数据:
typedef struct {
float temperature; // 温度,单位℃
float humidity; // 湿度,单位%
float nh3_ppm; // 氨气浓度,单位ppm
float co2_ppm; // CO2浓度,单位ppm
uint32_t timestamp; // 时间戳
uint8_t quality; // 数据质量:0-100
} SensorData_t;
SensorData_t currentData;
void updateSensorData() {
// 读取DHT22
if (dht22.read()) {
currentData.temperature = dht22.getTemperature();
currentData.humidity = dht22.getHumidity();
}
// 读取MQ-135
float voltage = readMQ135Raw();
currentData.nh3_ppm = calculateNH3(voltage);
// 数据质量评估
currentData.quality = evaluateDataQuality();
currentData.timestamp = millis();
}
显示方面,我推荐用OLED屏幕。128x64的分辨率,I2C接口,占两个引脚就够了。
void displaySensorData() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
// 第一行:温度
display.printf("Temp: %.1f C\n", currentData.temperature);
// 第二行:湿度
display.printf("Humi: %.1f %%\n", currentData.humidity);
// 第三行:气体
display.printf("NH3: %.1f ppm\n", currentData.nh3_ppm);
// 第四行:数据质量
display.printf("Qual: %d%%\n", currentData.quality);
display.display();
}
好了,这节课的内容就到这里。总结一下:
- DHT22的时序要精确,用硬件定时器比delay靠谱
- MQ-135需要预热和校准,R0值最好自动获取
- 滤波是必须的,中值+滑动平均组合效果最好
- 数据融合用结构体打包,显示时加上质量指示
下节课,咱们要讲怎么用这些数据做联动控制。到时候,我会分享一个我实际做过的净化器控制算法——不是简单的阈值开关,而是PID调节。敬请期待。