1. 从零搭建STM32与DHT11的通信桥梁

第一次接触DHT11温湿度传感器时,我被它简单的三线接口吸引住了——只需要电源、地线和一根数据线就能工作。但真正上手时才发现,这个看似简单的传感器藏着不少门道。记得当时为了搞清单总线时序,我用示波器抓波形抓到凌晨三点。

单总线协议是DHT11的核心技术特点。与I2C、SPI等多线协议不同,它仅用一根数据线完成双向通信。这种设计节省了宝贵的IO资源,特别适合STM32这类引脚有限的微控制器。但代价是需要严格遵循时序要求——误差超过20us就可能导致通信失败。

在实际项目中,我推荐使用STM32F103ZET6的PB11引脚连接DHT11。这个引脚既支持5V容忍(DHT11工作电压4.5-5.5V),又方便与开发板上的USART1复用。硬件连接非常简单:

  • VCC接3.3V或5V(实测5V时数据更稳定)
  • GND接地
  • DATA接PB11,建议加个4.7K上拉电阻

2. 深入解析DHT11通信协议

2.1 数据格式揭秘

DHT11每次传输40bit数据,包含:

  • 湿度整数(8bit) + 湿度小数(8bit)
  • 温度整数(8bit) + 温度小数(8bit)
  • 校验和(8bit)

校验和是前四个字节的和,这个设计我在多个项目中验证过其可靠性。例如收到数据:0x01 0x00 0x1A 0x00 0x1B,校验计算0x01+0x00+0x1A+0x00=0x1B,验证通过。

2.2 关键时序控制

最让我头疼的是起始信号时序。主机(STM32)需要先拉低总线至少18ms(实测20ms最稳妥),然后拉高20-40us等待DHT11响应。这里有个坑:拉高时间超过40us可能导致DHT11无响应。

用代码实现是这样的:

void DHT11_Start()
{
    GPIO_InitTypeDef GPIO_InitStruct;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    
    // 配置为推挽输出
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_11;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    GPIO_ResetBits(GPIOB, GPIO_Pin_11); // 拉低
    Delay_ms(20); // 保持20ms
    GPIO_SetBits(GPIOB, GPIO_Pin_11); // 释放总线
    Delay_us(30); // 等待30us
}

2.3 数据位识别技巧

DHT11的0和1不是用电平高低区分,而是用高电平持续时间:

  • 0:26-28us高电平
  • 1:116-118us高电平

我的经验是在低电平结束后延时40us采样,此时电平状态即代表数据值。这个延时很关键,太早会采样到起始低电平,太晚可能错过数据。

3. STM32硬件配置实战

3.1 GPIO模式切换技巧

DHT11通信需要动态切换GPIO输入输出模式。我推荐使用库函数快速切换:

void DHT11_IO_IN(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_11;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
    GPIO_Init(GPIOB, &GPIO_InitStruct);
}

void DHT11_IO_OUT(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_11;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);
}

3.2 精准延时实现

单总线对延时要求苛刻,我常用的两种方案:

  1. 使用SysTick定时器(精度高但不便调试)
  2. 基于循环的简易延时(适合快速验证)

这里给出循环延时的实现:

void Delay_us(uint32_t us)
{
    uint32_t delay = (HCLK_FREQ/8000000)*us;
    while(delay--);
}

注意要根据实际时钟频率调整系数,我用72MHz主频时这个参数刚好。

4. 数据采集与串口输出

4.1 完整数据读取流程

经过多次优化,我的数据读取函数稳定如下:

uint8_t DHT11_Read_Data(uint8_t *temp, uint8_t *humi)
{
    uint8_t buf[5];
    uint8_t i;
    
    DHT11_Start();
    if(DHT11_Check() == 0)
    {
        for(i=0;i<5;i++)
        {
            buf[i] = DHT11_Read_Byte();
        }
        if((buf[0]+buf[1]+buf[2]+buf[3])==buf[4])
        {
            *humi = buf[0];
            *temp = buf[2];
            return 0; // 成功
        }
    }
    return 1; // 失败
}

4.2 串口调试技巧

建议使用115200波特率,格式设置为8N1。在串口助手中添加时间戳可以观察采样间隔:

void USART1_Init(void)
{
    USART_InitTypeDef USART_InitStruct;
    GPIO_InitTypeDef GPIO_InitStruct;
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);
    
    // TX(PA9)
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // RX(PA10)
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    USART_InitStruct.USART_BaudRate = 115200;
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;
    USART_InitStruct.USART_StopBits = USART_StopBits_1;
    USART_InitStruct.USART_Parity = USART_Parity_No;
    USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_Init(USART1, &USART_InitStruct);
    
    USART_Cmd(USART1, ENABLE);
}

5. 常见问题与性能优化

5.1 典型故障排查

  • 无响应:检查上拉电阻,确保起始信号时长足够
  • 数据错误:降低GPIO速度,或增加时序延时裕量
  • 校验失败:尝试降低采样频率(DHT11需>1s间隔)

5.2 提升稳定性技巧

  1. 在数据线加100nF电容滤波
  2. 使用屏蔽线缆长距离传输
  3. 添加看门狗防止程序卡死

有次在工业现场遇到干扰问题,最后是通过在电源端加磁珠解决的。这也提醒我们,看似简单的传感器在实际环境中可能遇到各种意外情况。

6. 项目扩展与进阶应用

6.1 多传感器组网

通过切换GPIO可以驱动多个DHT11,我做过最多同时接8个的温湿度监测系统。关键点是:

  • 每个传感器独立供电
  • 设置不同的采样时间点
  • 添加防冲突机制

6.2 低功耗设计

使用STM32的睡眠模式+DHT11的间歇工作,可使整体功耗降至50uA以下。我的实现方案:

void Enter_StopMode(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
    PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);
    SystemInit(); // 唤醒后重新初始化时钟
}

把这段代码放在主循环中,配合RTC定时唤醒,非常适合电池供电的场景。

更多推荐