STM32 从 0 到 1 做智能家居(六):DHT11 温湿度采集

本文详解STM32平台下DHT11单总线温湿度传感器的底层驱动开发,涵盖微秒级时序控制、开漏双向通信、中断关闭策略、40位数据解析与校验机制,并提供可直接复用的完整C代码。重点解决初学者常见的读取失败、校验错误、电平误判等典型问题,强调硬件接口配置(如GPIO开漏模式)、SysTick高精度延时及调试定位方法。

系列第六篇。这是第一个真正意义上的"驱动":一根线、微秒级时序、还要和中断抢时间。

很多人第一次读 DHT11,串口打印出来是 255 255 255 255,然后就卡住了。这一篇会把每一种失败现象对应到具体原因。

一、DHT11 是什么

DHT11 是单总线数字温湿度传感器。所谓单总线,就是只用一根数据线,既传命令也传数据,方向还要来回切换。

一次完整通信,它返回 40 位(5 字节):

字节0:湿度整数部分
字节1:湿度小数部分(DHT11 恒为 0)
字节2:温度整数部分
字节3:温度小数部分(DHT11 恒为 0)
字节4:校验和 = 字节0 + 字节1 + 字节2 + 字节3 的低 8 位

关键参数:

项目数值
供电3.3V ~ 5.5V
湿度范围20% ~ 90% RH,精度 ±5% RH
温度范围0 ~ 50℃,精度 ±2℃
采样间隔≥ 1 秒(手册硬要求)
上电稳定时间≥ 1 秒

几点容易被忽略:

  • DHT11 的小数位基本没用,永远是 0。本项目直接按整数显示,不要拿 字节3 去算小数。
  • 它的精度不高,温度差 1~2℃、湿度差 5% 都算正常范围,不要拿它和空调遥控器上的数字较真。
  • 市面上很多"DHT11 模块"其实是带拉电阻的三针小板,裸传感器是四针。接法不同,但协议一样。

二、时序:记住四个数字

主机:  ──┐                        ┌──────────────────────────
         └──── 拉低 ≥18ms ─────────┘ 释放 20~40us
从机:                             └ 应答低 80us ┐ ┌ 应答高 80us ┐
                                                 │            │
数据位: 每位 = 低电平 50us + 高电平(26~28us = 0,70us = 1)

四个必须记住的数字:

数字含义记错了会怎样
18ms主机起始低电平的最短时间太短从机不响应,读回全 0xFF
80us从机应答的低/高电平检测窗口太窄会误判"无应答"
40us读数据位的采样点取 20us 把 0 判成 1,取 80us 把 1 判成 0
1s两次读取的最小间隔读太快拿到旧数据或直接校验失败

前三个是"通信内"的时序,最后一个常被忽略——它是手册要求,不是建议。

2.1 把整张时序图拆成七段

在这里插入图片描述

详细的时序图看起来线很多,其实从上到下就三块:完整一帧、从机应答放大、单个数据位放大。按顺序读,一共七段:

阶段总线动作时间代码里的对应
① 空闲上拉电阻把总线保持在高电平不限初始化后释放总线
② 主机起始主机把总线拉低≥18ms(代码给 25ms)DHT11_Rst() 拉低 + HAL_Delay(25)
③ 主机释放主机松开,上拉电阻把线拉高20~40us(代码 30us)DHT11_Rst() 置高 + delay_us(30)
④ 从机应答低DHT11 把总线拉低80usDHT11_Check() 第二段等待
⑤ 从机应答高DHT11 松开,上拉拉高80usDHT11_Check() 第三段等待
⑥ 传输 40 位每位:低 50us + 高 26~28us(0)或 70us(1)约 4msReadByte() × 5
⑦ 结束从机释放,总线回到高电平空闲—读完自动回到空闲

对应到图上,就是最上面那条"主机发送起始信号 → 从机响应信号 → 数据"的长波形;中间两张放大图分别是 ④⑤ 和 ⑥ 的细节。

2.2 40 位数据是怎么排的

图上"数据格式"那一行写得很清楚:

40 bit = 湿度整数(8) + 湿度小数(8) + 温度整数(8) + 温度小数(8) + 校验和(8)
         buf[0]        buf[1]        buf[2]        buf[3]        buf[4]

两个关键规则:

  • 高位先出(MSB first):每字节先读到的是 bit7,所以 ReadByte() 里是"先左移再或";
  • 校验和 = 前四字节相加取低 8 位:对不上就说明中间有位读错了。

DHT11 的 buf[1]、buf[3](两个小数位)实际恒为 0,所以图上虽然画了 8 位,真正有意义的只有湿度和温度的整数。

2.3 数字 0 和 1 在波形上怎么区分

图上最下面那组"数字 0 / 数字 1"的放大波形,是整张图的核心:

数字 0: ──┐ 50us ┌──── 26~28us ────┐
          └──────┘                 └──

数字 1: ──┐ 50us ┌─────── 70us ────────┐
          └──────┘                     └──

两种数据的低电平都是 50us,区别只在后面高电平的长短:

高电平宽度代表
26~28us0
70us1

所以采样点不能落在"高电平刚开始"或"快结束"的位置,必须取中间:低电平结束后 40us。这就是 ReadBit() 里 delay_us(40) 的来历。

2.4 图上编号 ①~⑱ 对应哪段代码

原图左侧列了 18 个步骤,和本工程代码是一一对应的:

图上编号动作代码位置
①~⑦切输出、置高、延时 10us、拉低、延时 25ms、释放、延时 30usDHT11_Rst()
⑧~⑫切输入、读电平、等从机拉低、等 80us 低结束、等 80us 高结束(超时 100us 判失败)DHT11_Check()
⑬~⑯读一位、等 50us 低结束、延时 40us、采样判 0/1ReadBit()
⑰循环 40 次,读 5 个字节ReadData() 里的 for(i=0;i<5;i++)
⑱前四字节求和,与第五字节比较ReadData() 末尾的校验

图里还画了"主机信号"和"DHT11 信号"两个框,标着"主机上拉 / DHT11 上拉"。这两块其实描述的是同一根线:

  • 空闲时,两边都不驱动,上拉电阻把线拉高;
  • 谁把线拉低,就相当于谁在"说话";
  • 所以主机和从机都必须是开漏,否则一边拉低、一边推挽输出高,就会对拉。

看懂这一点,就明白为什么第四篇里 PB1 必须配 Output Open Drain,而不是普通的推挽输出。

2.5 为什么整帧读完要关中断

一次完整通信约 4ms(40 位 × 约 100us)。图上从"起始信号"到"最后一位"的整段,都要求微秒级精度。

如果这 4ms 里被按键中断或串口中断插进来几十微秒,某一位的高电平就可能被采错,校验和随即失败。所以 ReadData() 在读完应答后、读 40 位之前关中断,读完立刻开中断。

这也是为什么前面反复强调:DHT11 的读取频率不能高。每 2 秒关一次 4ms 中断可以接受,每秒读十次就会明显影响串口接收。

三、为什么必须自己写 delay_us

HAL 只给了 HAL_Delay(),最小单位是毫秒。DHT11 需要 40µs、10µs 这种精度,只能自己数。

STM32 的 SysTick 是一个 24 位递减计数器,HAL 初始化后每 1ms 重装一次。1ms 内它数了 100000 次(100MHz),所以:

1µs = 100 个 SysTick 计数

代码:

#include "DHT11.h"

static uint32_t fac_us = 0;

void delay_init(uint8_t SYSCLK)
{
    fac_us = SYSCLK;            // 100MHz 时传 100
}

void delay_us(uint32_t nus)
{
    uint32_t ticks  = nus * fac_us;       // 需要累计的计数
    uint32_t reload = SysTick->LOAD;      // 重装值
    uint32_t told   = SysTick->VAL;       // 上一次读到的计数值
    uint32_t tnow, tcnt = 0;

    while(1)
    {
        tnow = SysTick->VAL;
        if(tnow != told)
        {
            if(tnow < told)
            {
                tcnt += told - tnow;                // 正常递减
            }
            else
            {
                tcnt += reload - tnow + told;       // 发生了重装(回绕)
            }
            told = tnow;
            if(tcnt >= ticks) break;
        }
    }
}

两个细节:

① delay_init 必须在时钟配置之后调用。

main() 里的顺序是:

HAL_Init();
SystemClock_Config();                       // 先把主频配到 100MHz
delay_init(SystemCoreClock / 1000000);      // 再传 100

如果放在 SystemClock_Config() 之前,SystemCoreClock 还是 16MHz,fac_us 变成 16,所有微秒延时被拉长 6 倍多,DHT11 必然读失败。

② 为什么判断回绕要补 reload - tnow + told。

SysTick 是减到 0 后重装成 LOAD 再继续减。如果两次采样之间刚好跨过重装点,tnow 会比 told 大,直接相减会得到负数(无符号数会变成巨大值)。补上 reload 才是真实经过的计数。

这段代码不需要你背,但要知道它只适合在主循环里调用。在中断里调用时,如果中断优先级高于 SysTick,计数可能不准。

四、一根线双向:开漏 + 方向切换

主机要发起始信号时,线是"输出";从机回数据时,线是"输入"。所以运行时要来回改 GPIO 模式。

#define DHT11_DQ_IN   HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1)

static void DHT11_IO_OUT(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin   = GPIO_PIN_1;
    GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_OD;   // ★ 开漏,不是推挽
    GPIO_InitStruct.Pull  = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

static void DHT11_IO_IN(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin  = GPIO_PIN_1;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

为什么必须开漏:

  • 开漏输出只有两种状态:拉低,或者"释放"(高阻,由外部上拉拉高);
  • 主机释放总线后,从机才能把它拉低;
  • 推挽输出会主动输出高电平,从机拉低时就是两个 MOS 管对顶,轻则数据错,重则发热。

这就是为什么第四篇里 PB1 一定要配成 Output Open Drain。配错的话,DHT11_Check() 永远等不到应答。

五、起始信号与应答检测

void DHT11_Rst(void)
{
    DHT11_IO_OUT();                                  // 主机控制总线
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET);    // 先释放
    delay_us(10);

    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET);  // 拉低
    HAL_Delay(25);                                   // ≥18ms,这里留余量
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET);    // 释放
    delay_us(30);                                    // 给从机应答时间
}

为什么给 25ms 而不是 18ms?

HAL_Delay(18) 名义上是 18ms,但函数进入、退出、中断打断都会吃掉一点时间。手册要求"至少 18ms",所以给到 25ms 留足余量——多等几毫秒没有副作用。

应答检测:

static uint8_t DHT11_Check(void)
{
    uint8_t retry = 0;

    while(DHT11_DQ_IN && retry < 100) { retry++; delay_us(1); }  // 等从机拉低
    if(retry >= 100) return 1;

    retry = 0;
    while(!DHT11_DQ_IN && retry < 100) { retry++; delay_us(1); } // 等 80us 低电平结束
    if(retry >= 100) return 1;

    retry = 0;
    while(DHT11_DQ_IN && retry < 100) { retry++; delay_us(1); }  // 等 80us 高电平结束
    if(retry >= 100) return 1;

    return 0;
}

每一步最多等 100µs,超时就返回失败。

为什么是 100µs?因为应答电平是 80µs,留一点余量。如果这里写成 while(...) 不带超时,只要传感器没接好,程序就永久卡死在这里——这是新手最常写出的死循环。

六、读一位:40µs 采样点是核心

每一位的结构是:

低电平 50us  +  高电平(0: 26~28us  /  1: 70us)

所以读完 50µs 低电平后,在高电平期间采样:

static uint8_t ReadBit(void)
{
    uint8_t retry = 0;

    while(DHT11_DQ_IN && retry < 100) { retry++; delay_us(1); }   // 等低电平开始
    retry = 0;
    while(!DHT11_DQ_IN && retry < 100) { retry++; delay_us(1); }  // 等 50us 低电平结束

    delay_us(40);                                   // ★ 采样点
    return DHT11_DQ_IN ? 1 : 0;
}

采样点为什么是 40µs:

采样点结果
20µs数字 0 的高电平还没结束(26~28µs),会被误判成 1
40µs落在 28µs 之后、70µs 之前,0 和 1 都能正确区分
80µs数字 1 的高电平已经结束(70µs),会被误判成 0

40µs 正好在"0 的最长高电平"和"1 的最短高电平"中间,是最稳的选择。

七、读 5 字节与校验

static uint8_t ReadByte(void)
{
    uint8_t i, dat = 0;
    for(i = 0; i < 8; i++)
    {
        dat <<= 1;
        dat |= ReadBit();
    }
    return dat;
}

uint8_t ReadData(uint8_t *HumiH, uint8_t *HumiL,
                 uint8_t *TempH, uint8_t *TempL)
{
    uint8_t buf[5], i;

    DHT11_Rst();
    if(DHT11_Check() != 0) return HAL_ERROR;    // 没有应答,直接失败

    __disable_irq();                            // ★ 40 位时序期间不被打断
    for(i = 0; i < 5; i++) buf[i] = ReadByte();
    __enable_irq();

    if((uint8_t)(buf[0] + buf[1] + buf[2] + buf[3]) == buf[4])
    {
        *HumiH = buf[0];  *HumiL = buf[1];
        *TempH = buf[2];  *TempL = buf[3];
        return HAL_OK;
    }
    return HAL_ERROR;                           // 校验和不对
}

7.1 为什么读 40 位要关中断

40 位大约需要 4ms。这 4ms 里如果来了按键中断、USART2 接收中断,CPU 会跳去执行几十微秒,DHT11 的时序就被拉偏了,可能错一位,校验和直接失败。

关中断的代价是:这 4ms 内如果正好有串口数据到达,可能丢字节。

这个取舍在本工程里可以接受,因为:

  • 读取安排在每 2 秒一次,不是高频;
  • ESP 的返回数据按行解析,偶发丢一两个字节最多坏一行,不会让协议整体错位;
  • 按键有独立时间戳消抖,丢一次边沿不至于误动作。

7.2 校验和为什么能发现错误

buf[0] 到 buf[3] 相加,取低 8 位,应该等于 buf[4]。

只要时序错一位,8 位里某一位翻转,校验和几乎必然对不上。所以校验失败基本等于"时序或接线有问题",不要怀疑传感器"算错了"。

八、接到主循环:2 秒一次,不重试

#define DHT11_RETRY 1     // 不重试

static void Sensors_Update(void)
{
    uint8_t ok = 0;

    for(uint8_t r = 0; r < DHT11_RETRY; r++)
    {
        if(ReadData(&g_HumiH, &g_HumiL, &g_TempH, &g_TempL) == HAL_OK)
        {
            ok = 1;
            break;
        }
        HAL_Delay(10);
    }
    g_dht11_ok = ok;

    Read_ENS160();
    Mqtt_Set_Env((int8_t)g_TempH, g_HumiH, g_AQI, g_ECO2, g_TVOC);
}

主循环里每 2 秒调用一次:

if(HAL_GetTick() - last_sensor >= 2000)
{
    last_sensor = HAL_GetTick();
    Sensors_Update();
}

为什么不重试:

  • DHT11 手册要求两次通信间隔 ≥1 秒;
  • for 循环里的"重试"间隔只有 10ms,重读拿到的还是上一帧的残留,只会白等;
  • 本项目用 2 秒周期,本身已经给了足够间隔,失败就等下一轮。

为什么风扇转的时候容易失败:

风扇是电机,启动和换向时会产生电源尖峰和电磁干扰。表现就是:风扇不转时读数正常,风扇一转,校验和就偶尔失败。

这是硬件问题,不是代码问题。本工程的应对是:2 秒采样 + 成功才更新缓存,避免把错误值刷到显示和 MQTT 上。

九、第一次调试:按现象定位

不要一上来就怀疑代码,先看现象。

9.1 打印原始 5 字节

在 ReadData() 里临时加一行:

for(i = 0; i < 5; i++) printf("%02X ", buf[i]);
printf("\r\n");

%02X 会把每个字节打成两位十六进制,比十进制更容易看出"全 FF"这种特征。

9.2 现象对照表

现象最可能的原因
5 个字节全是 FF没有应答:开漏没配、上拉没接、线没接对、传感器坏了
5 个字节全是 00总线一直被拉低,或读的时候方向没切回来
校验和错误(buf[4] 对不上)时序偏了:采样点、延时精度、中断打断
偶尔校验错误电源干扰(风扇/加热)、线太长、采样间隔太短
温湿度都是 0,但校验通过传感器刚上电没稳定,或读到了无效帧
湿度对、温度明显偏高传感器贴着发热器件(比如加热片、稳压芯片)
第一次读失败,后面正常上电 1 秒内就读取了,没等稳定

9.3 三个快速排查动作

  1. 换一根杜邦线。杜邦线内部断芯太常见了,万用表通断档一量就知道;
  2. 量 PB1 空闲电压。释放状态应该是 3.3V 左右;如果是 0V,说明线被拉死或方向没切回来;
  3. 把读取周期临时改成 5 秒。如果就不失败了,说明是间隔或干扰问题,不是时序算法问题。

十、本章易错点总结

现象真正原因处理
读出来永远 FF FF FF FF FF没配开漏,主机一直输出高,从机拉不低PB1 改 OUTPUT_OD + 上拉
读出来全是 00总线被持续拉低,或释放后没切输入检查方向切换与上拉
所有微秒延时都明显变长delay_init 在时钟配置前调用,传了 16放到 SystemClock_Config() 之后,传 100
微秒延时"瞬间返回"忘了调用 delay_init,fac_us = 0初始化时调用一次
校验和总是失败采样点不是 40µs固定 40µs,别用 20/80
校验和偶发失败读 40 位时没关中断__disable_irq/__enable_irq 包住
程序卡住不出结果应答/位检测的 while 没写超时每个等待都加 100µs 上限
10ms 后就重读违反 DHT11 ≥1s 间隔要求周期 2 秒,DHT11_RETRY=1
风扇一转就读失败电机干扰电源与信号2 秒采样、成功才更新缓存、必要时加电容
上电第一次读失败没等传感器稳定上电延时 ≥1s 再读
温度明显偏高传感器靠近发热源移开加热片/稳压芯片
读数据时按键变迟钝关中断 4ms 的正常代价可接受;不要提高读取频率
用 HAL_Delay(18) 做起始电平函数开销导致实际不足 18ms给到 25ms
换了根线就好了杜邦线断芯/接触不良通断档逐根检查

十一、小结与下一篇

这一篇的核心,其实就四句话:

  • 一根线双向,所以必须开漏 + 动态切方向;
  • 四个数字:18ms 起始、80µs 应答、40µs 采样、1s 间隔;
  • 读 40 位要关中断,用 4ms 的代价换时序稳定;
  • 校验和是最好用的自检,失败先查时序和接线,不要怀疑传感器。

下一篇进入 I2C:ENS160 空气质量传感器。你会看到 I2C 的地址为什么要左移、寄存器的读写时序长什么样、以及 AQI / eCO2 / TVOC 三个数据怎么拼出来。

如果这篇文章对你有帮助,欢迎点赞收藏。系列更新中,下一篇见。

附录:DHT11 完整代码(可直接复制)

下面是本工程实际使用的完整代码,按 App\DHT11.h 和 App\DHT11.c 两个文件放好即可。

A.1 DHT11.h

#ifndef __DHT11_H          // 防止头文件被重复包含
#define __DHT11_H

#include "main.h"          // 引入 HAL 与引脚宏定义

/* 读取一次温湿度:5 字节数据 + 校验和
 * 参数:HumiH 湿度整数、HumiL 湿度小数
 *       TempH 温度整数、TempL 温度小数
 * 返回:HAL_OK 成功;HAL_ERROR 无应答或校验失败
 */
uint8_t ReadData(uint8_t *HumiH, uint8_t *HumiL,
                 uint8_t *TempH, uint8_t *TempL);

/* 微秒延时初始化:传入 SystemCoreClock / 1000000
 * 100MHz 主频传 100;必须在 SystemClock_Config() 之后调用
 */
void delay_init(uint8_t SYSCLK);

/* 毫秒延时:内部循环调用 delay_us(1000)
 * 只在主循环使用,不要在中断里调用
 */
void delay_ms(uint16_t nms);

#endif

A.2 DHT11.c

#include "DHT11.h"

/* ---------------- 引脚定义 ---------------- */
#define DHT11_GPIO_PORT   GPIOB              // DHT11 接在 GPIOB
#define DHT11_GPIO_PIN    GPIO_PIN_1         // 具体是 PB1
#define DHT11_DQ_IN       HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN)

/* ---------------- 微秒延时 ---------------- */
static uint32_t fac_us = 0;   // 1us 需要多少个 SysTick 计数(100MHz 时为 100)

void delay_init(uint8_t SYSCLK)
{
    fac_us = SYSCLK;          // 主频 100MHz → fac_us = 100
}

void delay_us(uint32_t nus)
{
    uint32_t ticks;
    uint32_t told, tnow, tcnt = 0;
    uint32_t reload = SysTick->LOAD;   // HAL 已按 1ms 配好,100MHz 时为 99999

    ticks = nus * fac_us;              // 本次延时需要累计的计数个数
    told  = SysTick->VAL;              // 进入时的计数值(24 位,递减)

    while(1)
    {
        tnow = SysTick->VAL;           // 当前计数值

        if(tnow != told)
        {
            if(tnow < told)
            {
                tcnt += told - tnow;   // 正常递减,直接累加差值
            }
            else
            {
                tcnt += reload - tnow + told;   // 跨过重装点:补上 reload
            }

            told = tnow;               // 更新基准值

            if(tcnt >= ticks) break;   // 累计够了就退出
        }
    }
}

void delay_ms(uint16_t nms)
{
    uint32_t i;
    for(i = 0; i < nms; i++)
    {
        delay_us(1000);            // 1ms = 1000us
    }
}

/* ---------------- 总线方向切换 ---------------- */
/* 主机说话:开漏输出
 * 开漏只能“拉低”或“释放”,释放后由外部上拉拉高,
 * 这样从机才有机会把总线拉低。推挽会与从机对拉。
 */
static void DHT11_IO_OUT(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    GPIO_InitStruct.Pin   = DHT11_GPIO_PIN;
    GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_OD;   // 开漏输出
    GPIO_InitStruct.Pull  = GPIO_PULLUP;           // 内部上拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct);
}

/* 从机说话:输入模式
 * 外部再并一个 4.7k~10k 上拉,边沿会更干净
 */
static void DHT11_IO_IN(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    GPIO_InitStruct.Pin  = DHT11_GPIO_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct);
}

/* ---------------- 主机起始信号 ---------------- */
void DHT11_Rst(void)
{
    DHT11_IO_OUT();     // 切到输出方向

    HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET);   // 先释放,保证总线为高
    delay_us(10);                                                        // 等总线稳定

    HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_RESET); // 拉低,发起始信号
    HAL_Delay(25);                                                       // 保持 ≥18ms,这里给 25ms 余量

    HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET);   // 释放总线
    delay_us(30);                                                        // 给从机应答时间
}

/* ---------------- 应答检测 ---------------- */
/* 返回 0:检测到正常的“低 80us + 高 80us”应答
 * 返回 1:超时,说明传感器没接好、没上拉或方向不对
 */
uint8_t DHT11_Check(void)
{
    uint8_t retry = 0;

    DHT11_IO_IN();      // 切到输入,准备听从机

    /* ① 等从机把总线拉低(应答低电平开始),最长 100us */
    while(DHT11_DQ_IN && retry < 100)
    {
        retry++;
        delay_us(1);
    }
    if(retry >= 100) return 1;

    retry = 0;

    /* ② 等这 80us 低电平结束,退出时正好是应答高电平开始 */
    while(!DHT11_DQ_IN && retry < 100)
    {
        retry++;
        delay_us(1);
    }
    if(retry >= 100) return 1;

    return 0;
}

/* ---------------- 读一位 ---------------- */
/* 每位 = 低 50us + 高电平(0:26~28us,1:70us)
 * 在低电平结束后延时 40us 采样:
 *   0 的高电平已经结束 → 读到低 → 0
 *   1 的高电平还没结束 → 读到高 → 1
 */
uint8_t ReadBit(void)
{
    uint8_t relay = 0;

    /* 等这一位的低电平开始(下降沿) */
    while(DHT11_DQ_IN && relay < 100)
    {
        relay++;
        delay_us(1);
    }

    relay = 0;

    /* 等 50us 低电平结束(上升沿),从这里开始计时 */
    while(!DHT11_DQ_IN && relay < 100)
    {
        relay++;
        delay_us(1);
    }

    delay_us(40);       // ★ 采样点,不能改成 20 或 80

    return DHT11_DQ_IN ? 1 : 0;
}

/* ---------------- 读一字节 ---------------- */
/* DHT11 高位在前:先读到的是 MSB */
uint8_t ReadByte(void)
{
    uint8_t i, dat = 0;

    for(i = 0; i < 8; i++)
    {
        dat <<= 1;          // 左移,给新的一位腾位置
        dat |= ReadBit();   // 把读到的位放到最低位
    }

    return dat;
}

/* ---------------- 读 5 字节 + 校验 ---------------- */
uint8_t ReadData(uint8_t *HumiH, uint8_t *HumiL,
                 uint8_t *TempH, uint8_t *TempL)
{
    uint8_t buf[5], i;

    DHT11_Rst();                        // 1. 发起始信号

    if(DHT11_Check() != 0)              // 2. 没有应答,直接失败
    {
        return HAL_ERROR;
    }

    __disable_irq();                    // 3. 关中断,保护 40bit 微秒时序(约 4ms)

    for(i = 0; i < 5; i++)
    {
        buf[i] = ReadByte();
    }

    __enable_irq();                     // 4. 读完立刻恢复中断

    /* 5. 校验:前 4 字节之和的低 8 位 == 第 5 字节 */
    if((uint8_t)(buf[0] + buf[1] + buf[2] + buf[3]) == buf[4])
    {
        *HumiH = buf[0];                // 湿度整数
        *HumiL = buf[1];                // 湿度小数(DHT11 恒为 0)
        *TempH = buf[2];                // 温度整数
        *TempL = buf[3];                // 温度小数(DHT11 恒为 0)
        return HAL_OK;
    }

    return HAL_ERROR;                   // 校验失败,交给上层决定是否用缓存
}

A.3 在 main.c 中怎么调用

/* 初始化:必须放在 SystemClock_Config() 之后 */
delay_init(SystemCoreClock / 1000000);   // 100MHz → 100

HAL_Delay(1000);                         // 上电等 1s,让传感器稳定

uint8_t HumiH, HumiL, TempH, TempL;

if(ReadData(&HumiH, &HumiL, &TempH, &TempL) == HAL_OK)
{
    printf("温度:%d.%d ℃,湿度:%d.%d %%RH\r\n",
           TempH, TempL, HumiH, HumiL);
}
else
{
    printf("DHT11 读取失败\r\n");
}

最后提醒一句:DHT11.c 里用到了 HAL_Delay()、GPIO_InitTypeDef、SysTick、__disable_irq(),这些都通过 DHT11.h 里的 #include "main.h" 带进来。如果你把 main.h 换成别的头文件,记得确认这些定义还在。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值