3、湿度传感器集成:DHT11/DHT22温湿度传感器原理、时序分析与数据读取、代码实现与校准

好,咱们进入第三章。这一章聊湿度传感器,具体点说,就是DHT11和DHT22这两兄弟。

说实话,温湿度测量在嵌入式里太常见了。智能药盒要是不知道环境湿度,药品受潮了都不知道。我自己做过一个项目,药盒放在浴室旁边,用户洗完澡湿度飙到90%,里面的胶囊全黏在一起了。从那以后,我对湿度采集就特别上心。

3.1 DHT11与DHT22:选谁更合适?

先看个对比表,心里有个底:

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

我个人习惯:如果只是做室内环境监测,DHT11够用。但智能药盒嘛,我建议上DHT22。为什么?药盒里可能存放胰岛素或者某些对湿度敏感的抗生素,±5%的误差有时候会误判。你想想看,明明湿度已经超标了,DHT11还报个55%,你就错过了预警时机。

3.2 单总线协议:一根线搞定通信

DHT系列用的是一根数据线完成双向通信,叫单总线协议。说白了,就是主机和从机共用一根线,靠时序来区分谁在说话。

嗯,这里要注意:单总线对时序要求比较严格。我刚开始玩的时候,直接用delay_ms()去读,结果数据全是0xFF。后来用逻辑分析仪一看,原来是延时不够准。

3.2.1 通信流程

  1. 主机发起信号:拉低总线至少18ms,然后释放(拉高)20~40μs。
  2. 从机响应:DHT拉低总线80μs,再拉高80μs,表示“我准备好了”。
  3. 数据传输:每1位数据,先拉低50μs,然后拉高。拉高时间长短决定是0还是1。

为什么会这样设计?因为单总线没有时钟线,只能靠脉冲宽度来编码。0的拉高时间是26~28μs,1的拉高时间是70μs左右。你只要在拉高后等40μs采样,电平高就是1,低就是0。

关键时序参数(来自数据手册)

  • 起始信号低电平时间:≥18ms
  • 从机响应低电平:80μs
  • 从机响应高电平:80μs
  • 数据位“0”高电平时间:26~28μs
  • 数据位“1”高电平时间:70μs

3.3 代码实现:手写单总线驱动

我不喜欢用现成的库。自己写驱动,出了问题才知道怎么调。下面是我在STM32上写的DHT22读取代码,核心部分就这些:

// 宏定义
#define DHT_GPIO_PORT   GPIOB
#define DHT_PIN         GPIO_PIN_0

// 拉低总线
void DHT_Start(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    // 配置为输出
    GPIO_InitStruct.Pin = DHT_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(DHT_GPIO_PORT, &GPIO_InitStruct);
    
    // 拉低至少18ms
    HAL_GPIO_WritePin(DHT_GPIO_PORT, DHT_PIN, GPIO_PIN_RESET);
    delay_ms(20);
    
    // 拉高20~40μs
    HAL_GPIO_WritePin(DHT_GPIO_PORT, DHT_PIN, GPIO_PIN_SET);
    delay_us(30);
}

// 读取1位数据
uint8_t DHT_ReadBit(void) {
    // 等待低电平结束(50μs)
    while(HAL_GPIO_ReadPin(DHT_GPIO_PORT, DHT_PIN) == GPIO_PIN_RESET);
    
    // 延时40μs后采样
    delay_us(40);
    
    if(HAL_GPIO_ReadPin(DHT_GPIO_PORT, DHT_PIN) == GPIO_PIN_SET) {
        // 等待高电平结束
        while(HAL_GPIO_ReadPin(DHT_GPIO_PORT, DHT_PIN) == GPIO_PIN_SET);
        return 1;
    } else {
        return 0;
    }
}

// 读取1字节
uint8_t DHT_ReadByte(void) {
    uint8_t data = 0;
    for(int i = 0; i < 8; i++) {
        data <<= 1;
        data |= DHT_ReadBit();
    }
    return data;
}

// 读取温湿度
uint8_t DHT_ReadData(float *temp, float *humi) {
    uint8_t data[5] = {0};
    
    DHT_Start();
    
    // 等待从机响应
    if(HAL_GPIO_ReadPin(DHT_GPIO_PORT, DHT_PIN) == GPIO_PIN_RESET) {
        // 等待80μs低电平结束
        while(HAL_GPIO_ReadPin(DHT_GPIO_PORT, DHT_PIN) == GPIO_PIN_RESET);
        // 等待80μs高电平结束
        while(HAL_GPIO_ReadPin(DHT_GPIO_PORT, DHT_PIN) == GPIO_PIN_SET);
        
        // 读取40位数据
        for(int i = 0; i < 5; i++) {
            data[i] = DHT_ReadByte();
        }
        
        // 校验和
        if((data[0] + data[1] + data[2] + data[3]) == data[4]) {
            // DHT22: 湿度高8位+低8位,除以10
            *humi = ((uint16_t)data[0] << 8 | data[1]) / 10.0;
            *temp = ((uint16_t)data[2] << 8 | data[3]) / 10.0;
            return 1; // 成功
        }
    }
    return 0; // 失败
}

个人经验:delay_us()一定要用定时器或者硬件延时,别用软件循环。我在一个项目里用for循环做微秒延时,结果编译器优化等级一改,时序全乱了。后来换了定时器,稳如老狗。

3.4 数据校准:别信出厂值

DHT系列出厂前做过校准,但说实话,那个校准系数是批量做的,个体差异还是有的。我曾经拿10个DHT22放在同一个恒温恒湿箱里,读数最大差了1.5°C和3% RH。

所以,我建议做两点校准:

  1. 单点偏移校准:在25°C、50% RH的标准环境下,记录偏差值,软件里减掉。
  2. 线性度修正:如果要求高,做两点校准。比如在20%和80% RH下分别记录,算出斜率。

代码里加个校准函数就行:

// 校准参数(示例值,实际需标定)
#define TEMP_OFFSET  -0.3   // 温度偏移
#define HUMI_OFFSET   1.2   // 湿度偏移
#define HUMI_SLOPE    0.98  // 湿度斜率

void DHT_Calibrate(float *temp, float *humi) {
    *temp += TEMP_OFFSET;
    *humi = (*humi - HUMI_OFFSET) * HUMI_SLOPE;
    
    // 限幅
    if(*humi < 0) *humi = 0;
    if(*humi > 100) *humi = 100;
}

避坑指南:我曾经遇到过一个问题——DHT22在湿度超过90%时读数跳变到0。后来查资料才知道,传感器结露了。所以药盒如果放在高湿环境,记得加防结露处理,或者干脆用DHT22的加热版本。

3.5 实际项目中的注意事项

  • 上拉电阻:数据线必须接4.7kΩ~10kΩ上拉到VCC。我见过有人忘了接,通信时好时坏。
  • 电源去耦:DHT对电源噪声敏感,靠近传感器放一个100nF电容。
  • 采样间隔:DHT11至少1秒,DHT22至少2秒。读太频繁会出错。
  • 错误重试:如果校验和失败,等1秒再读。我一般重试3次,还失败就报错。

嗯,这一章就到这里。DHT系列虽然简单,但想读得稳、读得准,还是得下点功夫。下一章咱们聊气压传感器,那个更有意思——BMP280的I2C通信和高度计算,到时候见。