DHT11/DHT22温湿度传感器的时序陷阱与精准读取——单总线时序、超时处理

文章目录


每日一句正能量

迈出脚步那一刻,难题往往迎刃而解。

绝大多数困境在头脑中被放大了。一旦你起身去做------哪怕只做最小的一步------焦虑就会让位给具体操作。更重要的是,行动会带来新信息、新反馈,原来的"死局"可能自然松动。

一、前言:为什么DHT传感器是"时序地狱"

在嵌入式开发中,DHT11和DHT22是最常见的温湿度传感器,价格亲民、接口简单。但正是这份"简单",让无数开发者在调试时摔了跟头。DHT系列采用单总线(Single-Wire)协议 ,没有独立的时钟线,所有数据通过一根DATA引脚以微秒级精度的脉冲序列传输。

从STM32F1(72MHz)迁移到F4(168MHz)时,时序问题会被进一步放大:GPIO翻转速度变化、中断延迟差异、定时器时钟源不同......任何一个环节的疏忽,都会导致读取失败、校验错误,甚至系统死锁。

本文将深入剖析DHT单总线协议的时序细节,对比F1与F4的迁移陷阱,并给出生产级的驱动代码和超时处理策略。


二、单总线协议深度解析

2.1 通信时序全景

DHT的通信分为四个阶段:启动信号 → 传感器响应 → 40位数据传输 → 结束。下图展示了完整的时序关系:

阶段一:启动信号(Host → DHT)

MCU将DATA线拉低至少1ms(DHT11建议18-20ms),然后释放(拉高)20-40μs,最后切换为输入模式等待传感器响应。

陷阱1 :F1上GPIO_Mode_IN_FLOATING和F4上GPIO_MODE_INPUT的行为差异。F4的GPIO默认无上拉,必须显式配置GPIO_PULLUP或外接上拉电阻,否则DATA线悬空导致传感器无法拉高总线。

阶段二:传感器响应(DHT → Host)

DHT收到启动信号后,拉低DATA线80μs ,再拉高80μs,表示"我准备好了,开始发送数据"。

陷阱2 :很多教程用固定延时等待响应(如delay_us(40)),但如果传感器未连接或响应超时,程序会死等。必须引入超时计数器

阶段三:40位数据传输

每个数据位以50μs低电平开始,随后是高电平脉冲:

  • 逻辑0 :高电平持续 26-28μs (DHT11)或 22-30μs(DHT22)
  • 逻辑1 :高电平持续 70μs (DHT11)或 68-75μs(DHT22)

40位数据格式为:[湿度高8位][湿度低8位][温度高8位][温度低8位][校验和],MSB先传。

陷阱3:DHT11和DHT22的位宽不同!DHT11的湿度/温度只有整数部分(低8位为0),而DHT22是16位有符号小数(分辨率0.1)。直接复用DHT11代码读取DHT22会得到错误结果。

阶段四:结束

传输完成后,DHT拉低DATA线约50μs,然后释放总线回到高电平空闲状态。


2.2 F1 vs F4 的时序差异

参数 STM32F1 (72MHz) STM32F4 (168MHz) 影响
GPIO翻转延迟 ~0.14μs ~0.06μs F4更快,但配置不当反而引入振铃
定时器时钟源 APB1=72MHz APB1=84MHz (通常/2) 预分频器必须重新计算
GPIO配置 GPIO_Mode单一字段 Mode/Pull/Speed分离 F4需显式设置Speed为VERY_HIGH
中断延迟 12个时钟周期 12个时钟周期 绝对时间更短,相对影响更小

关键迁移点 :F4的GPIO配置必须显式设置GPIO_SPEED_FREQ_VERY_HIGH,否则输出上升沿过缓,可能导致DHT误判启动信号。


三、三种位采样策略对比

策略A:固定延时采样(40μs)------ 简单但脆弱

这是网上最常见的写法:

c 复制代码
while (!HAL_GPIO_ReadPin(DHT_PORT, DHT_PIN));  // 等待上升沿
delay_us(40);                                   // 固定延时
if (HAL_GPIO_ReadPin(DHT_PORT, DHT_PIN))         // 采样
    bit = 1;
else
    bit = 0;
while (HAL_GPIO_ReadPin(DHT_PORT, DHT_PIN));    // 等待下降沿

问题

  • 无超时保护,传感器脱落时死循环
  • 40μs是DHT11和DHT22的"中间值",对温度漂移、电源波动无适应性
  • 中断抢占可能导致延时偏差

策略B:脉宽测量------ 鲁棒但阻塞CPU

测量高电平持续时间,>40μs判为1,<40μs判为0:

c 复制代码
while (!HAL_GPIO_ReadPin(DHT_PORT, DHT_PIN));  // 等待上升沿
uint32_t start = __HAL_TIM_GET_COUNTER(&htim);
while (HAL_GPIO_ReadPin(DHT_PORT, DHT_PIN));   // 等待下降沿
uint32_t width = __HAL_TIM_GET_COUNTER(&htim) - start;
bit = (width > 40) ? 1 : 0;

问题

  • 两个while循环都无超时,任一死锁
  • CPU被完全占用,RTOS环境下会导致任务饥饿
  • 定时器溢出未处理(16位定时器在84MHz APB1下约780μs溢出)

策略C:双采样+超时保护(推荐)------ 生产级方案

结合策略A和B的优点,在关键时间点多次采样并引入超时:

c 复制代码
/**
 * @brief  读取单个位(带超时保护)
 * @param  timeout_us: 最大等待时间(微秒)
 * @retval 0/1 或 DHT_ERR_TIMEOUT
 */
static int8_t DHT_ReadBit(uint16_t timeout_us)
{
    uint16_t retry = 0;
    
    /* 等待低电平结束(50μs低脉冲) */
    while (HAL_GPIO_ReadPin(DHT_PORT, DHT_PIN) == GPIO_PIN_RESET) {
        if (++retry >= timeout_us) return DHT_ERR_TIMEOUT;
        delay_us(1);
    }
    
    /* 等待上升沿后的采样窗口 */
    delay_us(30);  // 第一个采样点:避开上升沿毛刺
    
    uint8_t sample1 = HAL_GPIO_ReadPin(DHT_PORT, DHT_PIN);
    
    delay_us(20);  // 第二个采样点:50μs处
    
    uint8_t sample2 = HAL_GPIO_ReadPin(DHT_PORT, DHT_PIN);
    
    /* 多数表决:两个样本中至少一个为1则判1 */
    uint8_t bit = (sample1 || sample2) ? 1 : 0;
    
    /* 等待当前位结束,防止影响下一位 */
    retry = 0;
    while (HAL_GPIO_ReadPin(DHT_PORT, DHT_PIN) == GPIO_PIN_SET) {
        if (++retry >= 100) return DHT_ERR_TIMEOUT;  // 80μs上限
        delay_us(1);
    }
    
    return bit;
}

优势

  • 噪声免疫:双采样+多数表决,抗毛刺
  • 超时保护:每个等待循环都有上限
  • 兼容DHT11/DHT22:30μs和50μs采样点覆盖两种传感器的0/1分界
  • RTOS友好:单次位读取最长阻塞约120μs,可接受

四、完整驱动代码(F1/F4通用)

4.1 头文件 dht_driver.h

c 复制代码
#ifndef __DHT_DRIVER_H
#define __DHT_DRIVER_H

#include "stm32f1xx_hal.h"  /* 迁移到F4时改为 stm32f4xx_hal.h */

/* 传感器类型选择 */
typedef enum {
    DHT11 = 0,
    DHT22 = 1
} DHT_TypeDef;

/* 错误码 */
typedef enum {
    DHT_OK = 0,
    DHT_ERR_TIMEOUT = -1,
    DHT_ERR_CHECKSUM = -2,
    DHT_ERR_NO_RESPONSE = -3,
    DHT_ERR_BUSY = -4
} DHT_StatusTypeDef;

/* 数据结构 */
typedef struct {
    float temperature;   /* 温度,单位°C */
    float humidity;      /* 湿度,单位%RH */
    uint8_t valid;       /* 数据有效标志 */
} DHT_DataTypeDef;

/* 外部定时器句柄声明(在main.c或tim.c中定义) */
extern TIM_HandleTypeDef htim6;  /* F1用TIM3,F4用TIM6 */

/* 函数声明 */
DHT_StatusTypeDef DHT_Init(DHT_TypeDef type, GPIO_TypeDef* port, uint16_t pin);
DHT_StatusTypeDef DHT_Read(DHT_DataTypeDef* data);
const char* DHT_GetErrorString(DHT_StatusTypeDef status);

#endif /* __DHT_DRIVER_H */

4.2 核心实现 dht_driver.c

c 复制代码
#include "dht_driver.h"
#include <string.h>

/* 私有宏 */
#define DHT_START_LOW_US        18000   /* DHT11: 18ms, DHT22: 1-10ms */
#define DHT_START_HIGH_US       30      /* 释放后等待时间 */
#define DHT_RESPONSE_TIMEOUT    100     /* 响应超时:100μs */
#define DHT_BIT_TIMEOUT_LOW     60      /* 位低电平超时 */
#define DHT_BIT_TIMEOUT_HIGH    80      /* 位高电平超时 */
#define DHT_READ_INTERVAL_MS    2000    /* 最小读取间隔:2s */

/* 私有变量 */
static DHT_TypeDef dht_type;
static GPIO_TypeDef* dht_port;
static uint16_t dht_pin;
static uint32_t last_read_tick = 0;

/**
 * @brief  微秒级延时(基于硬件定时器)
 * @note   F1: TIM3@72MHz, Prescaler=71 -> 1μs/tick
 *         F4: TIM6@84MHz, Prescaler=83 -> 1μs/tick
 */
static void DHT_DelayUs(uint16_t us)
{
    __HAL_TIM_SET_COUNTER(&htim6, 0);
    while (__HAL_TIM_GET_COUNTER(&htim6) < us) {
        /* 空循环,可被中断抢占 */
    }
}

/**
 * @brief  设置GPIO为输出模式
 */
static void DHT_SetOutput(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOA_CLK_ENABLE();  /* 根据实际端口修改 */
    
    GPIO_InitStruct.Pin = dht_pin;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;  /* F4改为 GPIO_SPEED_FREQ_VERY_HIGH */
    GPIO_InitStruct.Pull = GPIO_NOPULL;            /* F4建议改为 GPIO_PULLUP */
    
    HAL_GPIO_Init(dht_port, &GPIO_InitStruct);
}

/**
 * @brief  设置GPIO为输入模式(带上拉)
 */
static void DHT_SetInput(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    GPIO_InitStruct.Pin = dht_pin;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;  /* 关键:必须上拉,否则总线悬空 */
    
    HAL_GPIO_Init(dht_port, &GPIO_InitStruct);
}

/**
 * @brief  发送启动信号
 */
static DHT_StatusTypeDef DHT_SendStart(void)
{
    DHT_SetOutput();
    
    /* 拉低至少18ms(DHT11)或1ms(DHT22)*/
    HAL_GPIO_WritePin(dht_port, dht_pin, GPIO_PIN_RESET);
    if (dht_type == DHT11) {
        DHT_DelayUs(18000);  /* 18ms */
    } else {
        DHT_DelayUs(1000);   /* 1ms,DHT22要求更低 */
    }
    
    /* 释放总线 */
    HAL_GPIO_WritePin(dht_port, dht_pin, GPIO_PIN_SET);
    DHT_DelayUs(DHT_START_HIGH_US);
    
    /* 切换为输入,等待响应 */
    DHT_SetInput();
    
    return DHT_OK;
}

/**
 * @brief  等待传感器响应(带超时)
 */
static DHT_StatusTypeDef DHT_WaitResponse(void)
{
    uint16_t retry = 0;
    
    /* 等待DHT拉低(80μs响应低电平)*/
    while (HAL_GPIO_ReadPin(dht_port, dht_pin) == GPIO_PIN_SET) {
        if (++retry >= DHT_RESPONSE_TIMEOUT) {
            return DHT_ERR_NO_RESPONSE;
        }
        DHT_DelayUs(1);
    }
    
    retry = 0;
    /* 等待DHT拉高(80μs响应高电平)*/
    while (HAL_GPIO_ReadPin(dht_port, dht_pin) == GPIO_PIN_RESET) {
        if (++retry >= DHT_RESPONSE_TIMEOUT) {
            return DHT_ERR_NO_RESPONSE;
        }
        DHT_DelayUs(1);
    }
    
    retry = 0;
    /* 等待响应高电平结束,进入数据阶段 */
    while (HAL_GPIO_ReadPin(dht_port, dht_pin) == GPIO_PIN_SET) {
        if (++retry >= DHT_RESPONSE_TIMEOUT) {
            return DHT_ERR_NO_RESPONSE;
        }
        DHT_DelayUs(1);
    }
    
    return DHT_OK;
}

/**
 * @brief  读取单个位(双采样+超时)
 */
static int8_t DHT_ReadBit(void)
{
    uint16_t retry = 0;
    uint8_t sample1, sample2;
    
    /* 等待低电平开始(50μs低脉冲)*/
    while (HAL_GPIO_ReadPin(dht_port, dht_pin) == GPIO_PIN_SET) {
        if (++retry >= DHT_BIT_TIMEOUT_LOW) return DHT_ERR_TIMEOUT;
        DHT_DelayUs(1);
    }
    
    retry = 0;
    /* 等待低电平结束,上升沿到来 */
    while (HAL_GPIO_ReadPin(dht_port, dht_pin) == GPIO_PIN_RESET) {
        if (++retry >= DHT_BIT_TIMEOUT_LOW) return DHT_ERR_TIMEOUT;
        DHT_DelayUs(1);
    }
    
    /* 双采样策略:30μs和50μs */
    DHT_DelayUs(30);
    sample1 = HAL_GPIO_ReadPin(dht_port, dht_pin);
    
    DHT_DelayUs(20);
    sample2 = HAL_GPIO_ReadPin(dht_port, dht_pin);
    
    /* 多数表决 */
    uint8_t bit = (sample1 && sample2) ? 1 : 0;
    
    /* 等待当前位高电平结束 */
    retry = 0;
    while (HAL_GPIO_ReadPin(dht_port, dht_pin) == GPIO_PIN_SET) {
        if (++retry >= DHT_BIT_TIMEOUT_HIGH) return DHT_ERR_TIMEOUT;
        DHT_DelayUs(1);
    }
    
    return bit;
}

/**
 * @brief  读取一个字节(MSB优先)
 */
static DHT_StatusTypeDef DHT_ReadByte(uint8_t* byte)
{
    uint8_t data = 0;
    
    for (int i = 0; i < 8; i++) {
        int8_t bit = DHT_ReadBit();
        if (bit < 0) return (DHT_StatusTypeDef)bit;  /* 错误码传递 */
        
        data = (data << 1) | bit;
    }
    
    *byte = data;
    return DHT_OK;
}

/**
 * @brief  初始化DHT驱动
 */
DHT_StatusTypeDef DHT_Init(DHT_TypeDef type, GPIO_TypeDef* port, uint16_t pin)
{
    dht_type = type;
    dht_port = port;
    dht_pin = pin;
    
    /* 确保定时器已启动 */
    if (HAL_TIM_Base_GetState(&htim6) != HAL_TIM_STATE_BUSY) {
        HAL_TIM_Base_Start(&htim6);
    }
    
    /* 初始状态:输出高电平 */
    DHT_SetOutput();
    HAL_GPIO_WritePin(dht_port, dht_pin, GPIO_PIN_SET);
    
    HAL_Delay(100);  /* 传感器上电稳定时间 */
    
    return DHT_OK;
}

/**
 * @brief  读取温湿度数据(带完整超时和校验)
 */
DHT_StatusTypeDef DHT_Read(DHT_DataTypeDef* data)
{
    uint8_t buf[5] = {0};
    DHT_StatusTypeDef status;
    
    /* 检查读取间隔(DHT要求≥2s)*/
    uint32_t now = HAL_GetTick();
    if (now - last_read_tick < DHT_READ_INTERVAL_MS) {
        return DHT_ERR_BUSY;
    }
    
    /* 关中断,防止时序被抢占(关键!)*/
    uint32_t primask = __get_PRIMASK();
    __disable_irq();
    
    /* 发送启动信号 */
    status = DHT_SendStart();
    if (status != DHT_OK) goto cleanup;
    
    /* 等待响应 */
    status = DHT_WaitResponse();
    if (status != DHT_OK) goto cleanup;
    
    /* 读取40位数据(5字节)*/
    for (int i = 0; i < 5; i++) {
        status = DHT_ReadByte(&buf[i]);
        if (status != DHT_OK) goto cleanup;
    }
    
    /* 恢复中断 */
    __set_PRIMASK(primask);
    
    /* 校验和检查 */
    uint8_t checksum = buf[0] + buf[1] + buf[2] + buf[3];
    if (checksum != buf[4]) {
        return DHT_ERR_CHECKSUM;
    }
    
    /* 解析数据 */
    if (dht_type == DHT11) {
        data->humidity = (float)buf[0];
        data->temperature = (float)buf[2];
    } else {  /* DHT22 */
        uint16_t hum_raw = (buf[0] << 8) | buf[1];
        uint16_t temp_raw = (buf[2] << 8) | buf[3];
        
        data->humidity = (float)hum_raw / 10.0f;
        
        /* 温度最高位为符号位 */
        if (temp_raw & 0x8000) {
            data->temperature = -((float)(temp_raw & 0x7FFF) / 10.0f);
        } else {
            data->temperature = (float)temp_raw / 10.0f;
        }
    }
    
    data->valid = 1;
    last_read_tick = HAL_GetTick();
    
    return DHT_OK;
    
cleanup:
    __set_PRIMASK(primask);
    
    /* 错误恢复:强制输出高电平,复位总线 */
    DHT_SetOutput();
    HAL_GPIO_WritePin(dht_port, dht_pin, GPIO_PIN_SET);
    
    return status;
}

/**
 * @brief  错误码转字符串
 */
const char* DHT_GetErrorString(DHT_StatusTypeDef status)
{
    switch (status) {
        case DHT_OK: return "OK";
        case DHT_ERR_TIMEOUT: return "TIMEOUT";
        case DHT_ERR_CHECKSUM: return "CHECKSUM ERROR";
        case DHT_ERR_NO_RESPONSE: return "NO RESPONSE";
        case DHT_ERR_BUSY: return "BUSY (read too fast)";
        default: return "UNKNOWN";
    }
}

4.3 使用示例 main.c

c 复制代码
#include "main.h"
#include "dht_driver.h"

TIM_HandleTypeDef htim6;  /* F4用TIM6,F1用TIM3 */

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    
    /* 初始化定时器(1μs/tick)*/
    MX_TIM6_Init();  /* CubeMX生成 */
    HAL_TIM_Base_Start(&htim6);
    
    /* 初始化DHT22,接PA1 */
    DHT_DataTypeDef dht_data = {0};
    DHT_StatusTypeDef status = DHT_Init(DHT22, GPIOA, GPIO_PIN_1);
    
    if (status != DHT_OK) {
        printf("DHT init failed: %s\n", DHT_GetErrorString(status));
    }
    
    while (1) {
        status = DHT_Read(&dht_data);
        
        if (status == DHT_OK) {
            printf("Temp: %.1f°C, Hum: %.1f%%RH\n", 
                   dht_data.temperature, dht_data.humidity);
        } else {
            printf("DHT read error: %s\n", DHT_GetErrorString(status));
        }
        
        HAL_Delay(2500);  /* 必须≥2s */
    }
}

五、超时状态机与错误恢复

5.1 状态机设计

上图右侧展示了完整的超时状态机。每个状态都有明确的超时阈值:

状态 超时阈值 超时原因
RESP_WAIT 100μs 传感器未连接/未供电
RESP_LOW 100μs 传感器拉低后未释放
RESP_HIGH 100μs 传感器响应异常
BIT_LOW 60μs 位起始低电平过长(总线被占用)
BIT_HIGH 80μs 位高电平过长(噪声/传感器故障)

5.2 错误恢复策略

当发生超时时,驱动执行以下恢复:

  1. 强制总线复位:将DATA线切回输出模式,拉高电平
  2. 等待2秒:让传感器内部状态机复位
  3. 返回错误码:上层应用可选择重试(建议最多3次)
  4. 记录错误计数:连续失败超过阈值可触发硬件复位(如重启传感器电源)
c 复制代码
/* 上层应用的重试逻辑 */
DHT_StatusTypeDef DHT_ReadWithRetry(DHT_DataTypeDef* data, uint8_t max_retry)
{
    DHT_StatusTypeDef status;
    
    for (int i = 0; i < max_retry; i++) {
        status = DHT_Read(data);
        if (status == DHT_OK) return DHT_OK;
        
        HAL_Delay(2000);  /* 每次重试间隔2s */
    }
    
    return status;  /* 返回最后一次错误 */
}

六、硬件设计要点

6.1 上拉电阻

DATA线必须外接4.7kΩ~10kΩ上拉电阻至VCC。虽然F4可配置内部上拉(约40kΩ),但阻值过大,在长线缆或高速翻转时无法提供足够的上升沿驱动能力。

6.2 线缆长度

  • <30cm:直连即可
  • 30cm~1m:建议使用屏蔽线,DATA与GND双绞
  • >1m:必须降低上拉电阻(至3.3kΩ),并在传感器端加100nF退耦电容
  • >3m:不推荐,建议改用I2C/SPI传感器(如SHT30)

6.3 电源退耦

DHT传感器在数据发送期间有突发电流(约1mA/位),若电源纹波过大,会导致时序漂移。建议在传感器VCC引脚就近放置100nF陶瓷电容


七、调试技巧

7.1 逻辑分析仪抓包

使用Saleae/DSLogic等逻辑分析仪捕获实际波形,对比 datasheet 规格:

  1. 检查启动信号低电平是否≥1ms
  2. 测量响应低电平/高电平是否约80μs
  3. 测量位0高电平是否在22-30μs(DHT22)或26-28μs(DHT11)
  4. 测量位1高电平是否在68-75μs(DHT22)或70μs(DHT11)

7.2 软件调试

DHT_ReadBit()中加入调试输出,打印每个位的采样值:

c 复制代码
/* 调试模式宏 */
#ifdef DHT_DEBUG
    printf("Bit[%d]: s1=%d, s2=%d, result=%d\n", i, sample1, sample2, bit);
#endif

7.3 常见故障排查

现象 可能原因 解决方案
"NO RESPONSE" 上拉电阻缺失/松动 检查4.7kΩ电阻,F4配置GPIO_PULLUP
"CHECKSUM ERROR" 定时器精度不足 用示波器校准DHT_DelayUs()
温度跳变±85°C 数据解析错误(DHT11代码读DHT22) 确认dht_type配置正确
随机返回0 读取间隔<2s 检查HAL_GetTick()间隔
RTOS下频繁超时 中断关闭时间太长 改用信号量+中断方式,或提高任务优先级

八、总结

从STM32F1迁移到F4驱动DHT传感器,核心挑战不在于代码量,而在于对微秒级时序的精确把控:

  1. GPIO配置 :F4必须显式设置GPIO_SPEED_FREQ_VERY_HIGHGPIO_PULLUP
  2. 定时器校准:APB1时钟变化导致预分频器需重新计算(F1: 71, F4: 83)
  3. 超时保护:每个等待循环必须有上限,防止死锁
  4. 双采样策略:30μs+50μs双点采样,兼容DHT11/DHT22且抗噪声
  5. 中断管理:数据读取期间关中断,但超时后必须恢复,避免系统瘫痪

DHT传感器虽然"古老",但在成本敏感的场景中仍有不可替代的地位。掌握其单总线时序的底层原理,不仅能解决当下的驱动问题,更能培养对时序临界代码的敏感度------这是每一位嵌入式工程师的必修课。


转载自:https://blog.csdn.net/u014727709/article/details/162279385

欢迎 👍点赞✍评论⭐收藏,欢迎指正