文章目录
系列第六篇。这是第一个真正意义上的"驱动":一根线、微秒级时序、还要和中断抢时间。
很多人第一次读 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 把总线拉低 | 80us | DHT11_Check() 第二段等待 |
| ⑤ 从机应答高 | DHT11 松开,上拉拉高 | 80us | DHT11_Check() 第三段等待 |
| ⑥ 传输 40 位 | 每位:低 50us + 高 26~28us(0)或 70us(1) | 约 4ms | ReadByte() × 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~28us | 0 |
| 70us | 1 |
所以采样点不能落在"高电平刚开始"或"快结束"的位置,必须取中间:低电平结束后 40us。这就是 ReadBit() 里 delay_us(40) 的来历。
2.4 图上编号 ①~⑱ 对应哪段代码
原图左侧列了 18 个步骤,和本工程代码是一一对应的:
| 图上编号 | 动作 | 代码位置 |
|---|---|---|
| ①~⑦ | 切输出、置高、延时 10us、拉低、延时 25ms、释放、延时 30us | DHT11_Rst() |
| ⑧~⑫ | 切输入、读电平、等从机拉低、等 80us 低结束、等 80us 高结束(超时 100us 判失败) | DHT11_Check() |
| ⑬~⑯ | 读一位、等 50us 低结束、延时 40us、采样判 0/1 | ReadBit() |
| ⑰ | 循环 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 三个快速排查动作
- 换一根杜邦线。杜邦线内部断芯太常见了,万用表通断档一量就知道;
- 量 PB1 空闲电压。释放状态应该是 3.3V 左右;如果是 0V,说明线被拉死或方向没切回来;
- 把读取周期临时改成 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 换成别的头文件,记得确认这些定义还在。
本文详解STM32平台下DHT11单总线温湿度传感器的底层驱动开发,涵盖微秒级时序控制、开漏双向通信、中断关闭策略、40位数据解析与校验机制,并提供可直接复用的完整C代码。重点解决初学者常见的读取失败、校验错误、电平误判等典型问题,强调硬件接口配置(如GPIO开漏模式)、SysTick高精度延时及调试定位方法。
1万+

被折叠的 条评论
为什么被折叠?



