STM32 进阶封神之路(十五):DHT11 单总线实战 —— 温湿度检测从时序解析到代码落地(库函数 + 寄存器)

0 阅读17分钟

STM32 进阶封神之路(十五):DHT11 单总线实战 —— 温湿度检测从时序解析到代码落地(库函数 + 寄存器)

上一篇我们实现了语音模块与传感器的联动,这一篇聚焦嵌入式最常用的 “环境感知模块”——DHT11 温湿度传感器。DHT11 采用独特的单总线通信方式,仅需 1 根 GPIO 引脚即可实现温湿度数据采集,广泛应用于智能家居、气象监测、工业控制等场景,是入门单总线协议的最佳选择。

本文基于实战资料,从 DHT11 核心认知、单总线协议时序、硬件连接,到 STM32 代码实现(时序模拟 + 数据解析 + 校验),手把手带你吃透单总线通信的底层逻辑,让你彻底掌握温湿度检测的全流程!

一、复习回顾:传感器通信核心基础衔接

在深入 DHT11 前,先衔接关键通信知识,避免知识断层:

  1. 传感器通信分类:按接口可分为串口(KQM6600)、I2C、SPI、单总线(DHT11),单总线的核心优势是 “引脚占用少”(仅需 1 根数据线);
  2. 单总线特点:主从结构(STM32 为主机,DHT11 为从机)、半双工通信、无时钟线(靠时序同步),需严格遵循协议时序才能获取数据;
  3. 核心逻辑:STM32 主动发送起始信号→DHT11 响应→DHT11 发送 40 位数据(温湿度 + 校验)→STM32 解析数据→校验有效性→输出结果。

二、DHT11 核心认知:从传感器特性到单总线协议

1. DHT11 传感器核心参数与优势

(1)关键参数(实战必知)
  • 测量范围:温度 - 20℃60℃(精度 ±2℃),湿度 5% RH95% RH(精度 ±5% RH);
  • 供电电压:3.3V~5.5V(宽电压兼容,STM32 可直接供电);
  • 通信接口:单总线(DATA 引脚);
  • 响应时间:≤2 秒(每次测量间隔需≥2 秒,避免数据失真);
  • 输出数据格式:40 位数字信号(湿度整数 8 位 + 湿度小数 8 位 + 温度整数 8 位 + 温度小数 8 位 + 校验 8 位);
  • 引脚定义:VCC(电源)、DATA(单总线数据)、NC(空脚)、GND(地)。
(2)核心优势
  • 接线简单:仅需 1 根 GPIO 引脚 + VCC+GND,节省 STM32 引脚资源;
  • 成本低廉:性价比极高,适合批量应用;
  • 集成度高:内置校准芯片,直接输出数字信号,无需 ADC 采集;
  • 稳定性强:工业级设计,适应恶劣环境。

2. 单总线协议深度解析(核心!时序是关键)

DHT11 的单总线协议是 “时序驱动” 的通信方式,所有数据交互都通过严格的时序信号实现,任何时序偏差都会导致通信失败。

(1)单总线通信的核心特性
  • 总线结构:1 根 DATA 线,外接 4.7KΩ 上拉电阻(总线闲置时为高电平);
  • 主从关系:STM32 为主机(主动发起通信),DHT11 为从机(被动响应);
  • 数据传输:高位先出,一次通信传输 40 位数据,含校验位(确保数据可靠);
  • 时序要求:通信的起始、响应、数据位(0/1)都有固定的电平变化和时间要求,需精准模拟。
(2)完整通信时序流程(四步走)
  1. 主机发送起始信号:STM32 拉低 DATA 引脚≥18ms(最长不超过 30ms),然后释放总线(DATA 由低变高),等待 DHT11 响应;
  2. 从机响应信号:DHT11 检测到起始信号后,拉低 DATA 引脚 83μs,再拉高 87μs,完成响应(主机需检测到这两个电平跳变,确认 DHT11 在线);
  3. 从机发送数据:响应后,DHT11 连续发送 40 位数据,每位数据由 “低电平 + 高电平” 组成,通过高电平持续时间区分 bit0 和 bit1;
  4. 通信结束:40 位数据发送完成后,DHT11 释放总线,DATA 恢复高电平,一次通信结束。
(3)关键时序细节(必记!通信成功的核心)
① 起始信号时序
  • 主机拉低时间:T≥18ms(推荐 20ms,兼顾稳定性和效率);
  • 主机释放后等待时间:约 13μs(DHT11 准备响应)。
② 响应信号时序
  • 从机拉低时间:78μs~88μs(典型 83μs);
  • 从机拉高时间:80μs~92μs(典型 87μs);
  • 主机检测:需连续检测到 “低电平→高电平” 跳变,否则判定为通信失败。
③ 数据位时序(区分 bit0 和 bit1)

DHT11 通过 “高电平持续时间” 区分数据 0 和 1,这是解析数据的关键:

  • bit0(数据 0) :低电平持续 5058μs(典型 54μs),高电平持续 2327μs(典型 24μs);
  • bit1(数据 1) :低电平持续 5058μs(典型 54μs),高电平持续 6874μs(典型 71μs);
  • 核心区别:低电平时间一致,高电平时间不同(bit1 是 bit0 的 3 倍左右)。
(4)数据格式与校验规则
① 40 位数据格式(高位先出)

plaintext

bit0~bit7:湿度整数部分(H_int)
bit8~bit15:湿度小数部分(H_dec,DHT11该部分恒为0)
bit16~bit23:温度整数部分(T_int)
bit24~bit31:温度小数部分(T_dec,精度0.1℃)
bit32~bit39:校验位(Check
② 校验规则(确保数据可靠)

校验位 = 湿度整数 + 湿度小数 + 温度整数 + 温度小数(结果取低 8 位);

  • 若校验位与接收的第 39~32 位数据一致,则数据有效;
  • 若不一致,则数据失真,需重新采集。
示例解析

假设接收 40 位数据(十六进制):0x35 0x00 0x18 0x04 0x51

  • 湿度整数:0x35 = 53 → 53% RH;
  • 湿度小数:0x00 = 0 → 0.0% RH;
  • 温度整数:0x18 = 24 → 24℃;
  • 温度小数:0x04 = 4 → 0.4℃;
  • 校验位:0x35 + 0x00 + 0x18 + 0x04 = 0x51,与接收校验位一致→数据有效;
  • 最终结果:温度 24.4℃,湿度 53.0% RH。

三、DHT11 硬件连接与 GPIO 配置原则

1. 硬件连接(STM32F103C8T6 + DHT11)

表格

STM32 引脚DHT11 引脚连接说明
3.3V/VCCVCC供电(推荐 3.3V,避免 5V 烧毁 GPIO)
GNDGND共地(通信稳定的前提)
PB0(任意 GPIO)DATA单总线数据线(需外接 4.7KΩ 上拉电阻)
关键注意事项
  • 上拉电阻不可省略:DATA 引脚需串联 4.7KΩ 上拉电阻到 3.3V,确保总线闲置时为高电平,避免信号干扰;
  • 接线长度:推荐接线长度≤5 米,超过 5 米需降低上拉电阻阻值(如 2.2KΩ),减少信号衰减;
  • 供电稳定性:若使用开关电源,需添加 0.1μF 滤波电容(靠近 DHT11 VCC 引脚),避免电源纹波导致数据跳变。

2. GPIO 配置原则(单总线通信核心)

DHT11 的 DATA 引脚需在 “输出模式”(发送起始信号)和 “输入模式”(接收响应 / 数据)之间切换,配置要点:

  • 输出模式:推挽输出(GPIO_Mode_Out_PP),速度 50MHz,用于拉低总线发送起始信号;
  • 输入模式:浮空输入(GPIO_Mode_IN_FLOATING),用于检测 DHT11 的响应和数据信号;
  • 切换时机:发送起始信号后,立即切换为输入模式,等待 DHT11 响应。

四、STM32 实战代码:DHT11 单总线驱动实现

核心流程:GPIO 初始化→发送起始信号→检测响应→接收 40 位数据→数据解析→校验→输出结果,以下是库函数与寄存器双版本实现(以 PB0 为 DATA 引脚)。

1. 核心头文件与数据结构定义

c

运行

#include "stm32f10x.h"
#include <stdio.h>

// DHT11数据结构体(存储解析后的数据)
typedef struct {
    uint8_t humidity_int;    // 湿度整数部分(%RH)
    uint8_t humidity_dec;    // 湿度小数部分(%RH,DHT11恒为0)
    uint8_t temp_int;       // 温度整数部分(℃)
    uint8_t temp_dec;       // 温度小数部分(℃)
    uint8_t check_sum;      // 校验位
    uint8_t data_valid;     // 数据有效性标志(1=有效,0=无效)
} DHT11_Data_TypeDef;

DHT11_Data_TypeDef dht11_data; // 全局温湿度数据变量
#define DHT11_DATA_PIN    GPIO_Pin_0  // 数据引脚PB0
#define DHT11_DATA_PORT   GPIOB       // 数据端口GPIOB

2. GPIO 模式切换函数(单总线核心)

单总线需频繁切换 GPIO 输入 / 输出模式,封装专用切换函数:

(1)库函数版

c

运行

// 配置DHT11 DATA引脚为输出模式
void DHT11_SetOutputMode(void) {
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Pin = DHT11_DATA_PIN;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(DHT11_DATA_PORT, &GPIO_InitStruct);
}

// 配置DHT11 DATA引脚为输入模式
void DHT11_SetInputMode(void) {
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Pin = DHT11_DATA_PIN;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
    GPIO_Init(DHT11_DATA_PORT, &GPIO_InitStruct);
}

// 拉低DATA引脚
void DHT11_PinLow(void) {
    GPIO_ResetBits(DHT11_DATA_PORT, DHT11_DATA_PIN);
}

// 拉高DATA引脚(释放总线)
void DHT11_PinHigh(void) {
    GPIO_SetBits(DHT11_DATA_PORT, DHT11_DATA_PIN);
}

// 读取DATA引脚电平
uint8_t DHT11_ReadPin(void) {
    return GPIO_ReadInputDataBit(DHT11_DATA_PORT, DHT11_DATA_PIN);
}
(2)寄存器版

c

运行

// 配置为输出模式(推挽输出)
void DHT11_SetOutputMode(void) {
    DHT11_DATA_PORT->CRL &= ~(0x0F << (DHT11_DATA_PIN * 4));
    DHT11_DATA_PORT->CRL |= (0x03 << (DHT11_DATA_PIN * 4)); // MODE=11(50MHz),CNF=00(推挽)
}

// 配置为输入模式(浮空输入)
void DHT11_SetInputMode(void) {
    DHT11_DATA_PORT->CRL &= ~(0x0F << (DHT11_DATA_PIN * 4));
    DHT11_DATA_PORT->CRL |= (0x04 << (DHT11_DATA_PIN * 4)); // MODE=00(输入),CNF=01(浮空)
}

// 拉低引脚
void DHT11_PinLow(void) {
    DHT11_DATA_PORT->ODR &= ~(1 << DHT11_DATA_PIN);
}

// 拉高引脚
void DHT11_PinHigh(void) {
    DHT11_DATA_PORT->ODR |= (1 << DHT11_DATA_PIN);
}

// 读取引脚电平
uint8_t DHT11_ReadPin(void) {
    return (DHT11_DATA_PORT->IDR & (1 << DHT11_DATA_PIN)) ? 1 : 0;
}

3. 延时函数(时序精准控制关键)

单总线对延时精度要求极高,需实现微秒级精准延时(通过空循环模拟,需根据实际主频校准):

c

运行

// 微秒级延时(STM32F103 72MHz主频校准)
void DHT11_DelayUs(uint32_t us) {
    uint32_t i;
    for(i = 0; i < us * 9; i++); // 72MHz下,约1us对应9个空循环(需根据实际调整)
}

// 毫秒级延时
void DHT11_DelayMs(uint32_t ms) {
    uint32_t i, j;
    for(i = 0; i < ms; i++) {
        for(j = 0; j < 1000; j++);
    }
}

4. 单总线时序模拟与数据读取函数

(1)发送起始信号 + 检测响应

c

运行

// 发送起始信号并检测DHT11响应,返回1=响应成功,0=响应失败
uint8_t DHT11_SendStartAndWaitAck(void) {
    uint8_t ack_flag = 0;
    uint32_t timeout = 0;

    // 1. 配置为输出模式,发送起始信号(拉低≥18ms)
    DHT11_SetOutputMode();
    DHT11_PinLow();
    DHT11_DelayMs(20); // 拉低20ms,满足≥18ms要求

    // 2. 释放总线(拉高),等待DHT11响应(10~35us)
    DHT11_PinHigh();
    DHT11_SetInputMode(); // 切换为输入模式
    DHT11_DelayUs(15); // 等待15us,给DHT11准备时间

    // 3. 检测DHT11拉低响应(低电平83us左右)
    timeout = 0;
    while(DHT11_ReadPin() == 1) {
        timeout++;
        DHT11_DelayUs(1);
        if(timeout > 100) { // 超时100us未检测到低电平,响应失败
            return 0;
        }
    }
    ack_flag = 1; // 检测到低电平,响应第一步成功

    // 4. 检测DHT11拉高响应(高电平87us左右)
    timeout = 0;
    while(DHT11_ReadPin() == 0) {
        timeout++;
        DHT11_DelayUs(1);
        if(timeout > 100) { // 超时100us未检测到高电平,响应失败
            return 0;
        }
    }

    // 5. 等待高电平结束(响应完成)
    timeout = 0;
    while(DHT11_ReadPin() == 1) {
        timeout++;
        DHT11_DelayUs(1);
        if(timeout > 100) { // 超时100us,响应失败
            return 0;
        }
    }

    return ack_flag; // 响应成功
}
(2)读取 1 位数据(区分 bit0 和 bit1)

c

运行

// 读取1位数据,返回0或1
uint8_t DHT11_ReadBit(void) {
    uint8_t bit_val = 0;
    uint32_t timeout = 0;

    // 1. 等待低电平结束(DHT11拉低54us左右)
    timeout = 0;
    while(DHT11_ReadPin() == 0) {
        timeout++;
        DHT11_DelayUs(1);
        if(timeout > 60) { // 超时60us,读取失败
            return 0;
        }
    }

    // 2. 延时30us,区分bit0和bit1(bit0高电平≤27us,bit1≥68us)
    DHT11_DelayUs(30);

    // 3. 读取引脚电平:高电平则为bit1,低电平则为bit0
    if(DHT11_ReadPin() == 1) {
        bit_val = 1;
        // 等待bit1的高电平结束
        while(DHT11_ReadPin() == 1);
    } else {
        bit_val = 0;
    }

    return bit_val;
}
(3)读取 1 字节数据(8 位,高位先出)

c

运行

// 读取1字节数据,返回读取结果(高位先出)
uint8_t DHT11_ReadByte(void) {
    uint8_t byte_val = 0;
    uint8_t i;

    for(i = 0; i < 8; i++) {
        byte_val <<= 1; // 高位先出,左移一位
        byte_val |= DHT11_ReadBit(); // 读取当前位并写入最低位
    }

    return byte_val;
}
(4)读取 40 位完整数据 + 校验

c

运行

// 读取DHT11温湿度数据,返回1=成功,0=失败
uint8_t DHT11_ReadData(void) {
    uint8_t humidity_int, humidity_dec, temp_int, temp_dec, check_sum;

    // 1. 发送起始信号并检测响应
    if(DHT11_SendStartAndWaitAck() == 0) {
        dht11_data.data_valid = 0;
        return 0;
    }

    // 2. 读取40位数据(高位先出)
    humidity_int = DHT11_ReadByte(); // 湿度整数
    humidity_dec = DHT11_ReadByte(); // 湿度小数(DHT11为0)
    temp_int = DHT11_ReadByte();    // 温度整数
    temp_dec = DHT11_ReadByte();    // 温度小数
    check_sum = DHT11_ReadByte();   // 校验位

    // 3. 数据校验
    if(check_sum == (humidity_int + humidity_dec + temp_int + temp_dec)) {
        // 校验成功,更新数据
        dht11_data.humidity_int = humidity_int;
        dht11_data.humidity_dec = humidity_dec;
        dht11_data.temp_int = temp_int;
        dht11_data.temp_dec = temp_dec;
        dht11_data.check_sum = check_sum;
        dht11_data.data_valid = 1;
        return 1;
    } else {
        // 校验失败
        dht11_data.data_valid = 0;
        return 0;
    }
}

5. 初始化与主函数实战

c

运行

// DHT11初始化(使能GPIO时钟)
void DHT11_Init(void) {
    // 使能GPIOB时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    // 初始配置为输出模式,拉高总线
    DHT11_SetOutputMode();
    DHT11_PinHigh();

    // 初始化数据结构体
    memset(&dht11_data, 0, sizeof(DHT11_Data_TypeDef));
    dht11_data.data_valid = 0;

    DHT11_DelayMs(100); // 上电延时100ms,等待DHT11稳定
}

// printf重定向(串口1,115200bps 8N1,复用之前代码)
int fputc(int ch, FILE *f);

int main(void) {
    // 初始化串口1(用于打印数据)
    USART1_Init(); // 串口初始化函数复用之前的实现
    // 初始化DHT11
    DHT11_Init();

    printf("DHT11温湿度检测系统初始化成功!\r\n");
    printf("每隔2秒采集一次数据...\r\n\r\n");

    while(1) {
        // 读取温湿度数据(间隔≥2秒)
        if(DHT11_ReadData() == 1) {
            // 数据有效,打印结果
            printf("=== DHT11温湿度数据 ===\r\n");
            printf("温度:%d.%d ℃\r\n", dht11_data.temp_int, dht11_data.temp_dec);
            printf("湿度:%d.%d %%RH\r\n", dht11_data.humidity_int, dht11_data.humidity_dec);
            printf("校验位:0x%02X(校验成功)\r\n", dht11_data.check_sum);
            printf("=======================\r\n\r\n");
        } else {
            // 数据无效,打印错误信息
            printf("数据采集失败,请检查接线或时序!\r\n\r\n");
        }

        DHT11_DelayMs(2000); // 间隔2秒采集一次
    }
}

五、实验现象与数据验证

1. 硬件验证

  • DHT11 上电后,电源指示灯亮起,STM32 与 DHT11 共地良好,上拉电阻连接正确;
  • 串口助手配置 115200bps 8N1,接收 STM32 发送的温湿度数据。

2. 串口打印结果

plaintext

DHT11温湿度检测系统初始化成功!
每隔2秒采集一次数据...

=== DHT11温湿度数据 ===
温度:25.3 ℃
湿度:58.0 %RH
校验位:0x7B(校验成功)
=======================

=== DHT11温湿度数据 ===
温度:25.4 ℃
湿度:58.0 %RH
校验位:0x7C(校验成功)
=======================

3. 特殊情况验证(负温度)

当温度低于 0℃时,DHT11 的温度小数部分最高位(bit24)会置 1,示例数据解析:

  • 接收数据:湿度整数 = 50,湿度小数 = 0,温度整数 = 10,温度小数 = 0x81(bit24=1),校验位 = 0x5B;
  • 解析:温度 =-(10 + 0.1)℃ = -10.1℃;
  • 校验:50+0+10+0x81=0xDB?不,实际校验时按二进制加法,温度小数 0x81=129,50+0+10+129=189=0xBD,需确保校验逻辑正确。

六、DHT11 实战避坑指南(10 + 高频错误)

1. 通信失败(数据采集失败)

  • 原因 1:未接 4.7KΩ 上拉电阻→总线闲置时为低电平,DHT11 无法识别起始信号;解决:在 DATA 引脚与 3.3V 之间串联 4.7KΩ 上拉电阻;
  • 原因 2:起始信号拉低时间不足 18ms→DHT11 未检测到起始指令;解决:延长起始信号拉低时间至 20ms,确保满足协议要求;
  • 原因 3:GPIO 模式切换时机错误→发送起始信号后未及时切换为输入模式;解决:释放总线(拉高)后立即调用DHT11_SetInputMode切换为输入;
  • 原因 4:延时函数不精准→微秒级延时偏差导致时序错乱;解决:根据 STM32 主频校准DHT11_DelayUs函数(如 72MHz 下 1us≈9 个空循环)。

2. 数据校验失败

  • 原因 1:数据读取不完整→40 位数据丢失某几位;解决:检查延时精度,确保每个 bit 的读取时序符合要求;
  • 原因 2:电源纹波过大→DHT11 供电不稳定导致数据失真;解决:在 DHT11 VCC 引脚旁并联 0.1μF 滤波电容;
  • 原因 3:采集间隔过短→未满足 DHT11≥2 秒的测量间隔要求;解决:两次采集之间延时≥2 秒。

3. 数据跳变过大

  • 原因 1:传感器未预热→上电后立即采集数据;解决:上电后延时 100ms 再开始采集;
  • 原因 2:传感器靠近热源 / 水源→环境干扰;解决:将 DHT11 放置在通风、无干扰的环境中;
  • 原因 3:接线过长→信号衰减导致数据失真;解决:缩短接线长度(≤5 米),或降低上拉电阻阻值。

七、总结:DHT11 单总线核心要点与进阶方向

1. 核心要点回顾

  • 单总线通信核心:时序驱动,起始信号、响应信号、数据位的时序必须严格遵循协议;
  • DHT11 数据流程:发送起始信号→检测响应→读取 40 位数据→校验→输出结果;
  • 代码实现关键:GPIO 模式切换、精准延时、数据位区分(高电平时间)、校验机制;
  • 避坑核心:上拉电阻不可少、延时精准、采集间隔≥2 秒、供电稳定。

2. 进阶学习方向

  • 数据滤波:通过滑动平均算法(如连续采集 5 次取平均值)优化数据跳变;
  • 中断方式采集:使用定时器中断替代空循环延时,避免阻塞主程序;
  • 低功耗优化:DHT11 闲置时进入低功耗模式,STM32 定时唤醒采集;
  • 多传感器组网:通过单总线地址扩展(如 DHT22 支持地址配置),实现多个传感器共用 1 根 GPIO;
  • 无线传输:结合蓝牙 / WiFi 模块,将温湿度数据上传至手机 APP 或云平台。

掌握 DHT11 单总线实战后,你已具备 “单总线协议解析” 的核心能力,可轻松迁移至其他单总线传感器(如 DS18B20 温度传感器)。下一篇我们将聚焦定时器的高级应用 ——PWM 波输出,实现 LED 调光、电机调速等功能,进一步拓展 STM32 的控制边界!