摘要:本文围绕 DHT11 数字温湿度传感器展开,系统介绍其内部结构、工作原理与典型应用场景,并结合数据手册梳理单总线通信协议、40bit 数据格式与校验规则。文章重点对起始信号、响应握手及数据接收等关键时序进行图解分析,随后基于 STM32 HAL 库给出完整的驱动程序设计,涵盖 GPIO 输入输出初始化、起始信号发送、响应检测、逐位与逐字节读取、校验和验证及上电初始化等函数,并补充延时初始化与上电稳定时间的注意事项,帮助读者从原理到代码完整掌握 DHT11 的驱动开发。
1、DHT11简介
DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器。它应用专用的数字模块采集技术和温湿度传感技术,确保产品具有极高的可靠性与卓越的长期稳定性。传感器包括一个电阻式感湿元件和一个NTC测温元件,并与一个高性能8位单片机相连接,因此具有品质卓越、超快响应、抗干扰能力强、性价比极高等优点。每个DHT11传感器都在极为精确的湿度校验室中进行校准,校准系数以程序的形式储存在OTP内存中,传感器内部在检测信号的处理过程中会调用这些校准系数。单线制串行接口使系统集成变得简易快捷,超小的体积、极低的功耗以及可达20米以上的信号传输距离,使其成为各类应用场合的最佳选择。产品为4针单排引脚封装,连接方便,特殊封装形式可根据用户需求提供(以上内容来自数据手册)。

2、原理分析
由简介可知,DHT11内部包含一个电阻式感湿元件、一个NTC测温元件和一个高性能8位单片机。其工作原理是:单片机通过ADC采集感湿电阻和NTC测温元件两端的电压,再将处理后的数据按照一定的协议与其他设备进行通信。
感湿电阻和NTC(温感电阻)的阻值会随温湿度变化而变化,因此采集到的电压值也会随之改变(概念电路图如下)。

封装:

3、应用
暖通空调、测试及检测设备、汽车、数据记录器、消费品、自动控制、气象站、家电、湿度调节器、医疗、除湿器。
4、DHT11数据手册知识点概述
DATA引脚用于微处理器与DHT11之间的通讯和同步,采用单总线数据格式,一次通讯时间约4ms。数据分为小数部分和整数部分,当前小数部分用于以后扩展,现读出为零。一次完整的数据传输为40bit,高位先出。数据格式为:8bit湿度整数数据+8bit湿度小数数据+8bit温度整数数据+8bit温度小数数据+8bit校验和。数据传送正确时,校验和数据等于前四个字节之和。
从上述描述可以归纳出 3 个要点:
- 采用单总线方式进行数据传输(一根线完成数据的收发);
- 一次完整的数据为 40bit(需要连续读取 40 次);
- 明确了数据格式(可确定温湿度数据在哪些位表示)。
5、时序图分析
1)总的通信过程

首先对总的通信过程进行分析:单片机作为主机,先将数据总线由高电平拉低,持续一段时间后再拉高总线。随后总线由输出变为输入,接收DHT11发送的低电平信号。该低电平持续一段时间后,DHT11再次拉高总线,之后开始数据传输。可以看到,数据0和1都是通过高电平实现的,通过高电平持续的时间来区分数据1和数据0。此外,每个数据传输之间有一段低电平作为间隔。
2)起始和响应时序图分析(握手)

后面的时序图都是对总通信过程中各部分的详细展开。根据上图,单片机先将总线电平由高拉低,持续至少18ms,然后再拉高20-40us,这个过程为起始信号。随后总线状态由输出变为输入,如果DHT11正常接收到起始信号,会将总线拉低80us后再拉高80us作为回应。这段就是握手过程:单片机告知DHT11准备接收数据,DHT11回应已准备就绪。之后DHT11便开始发送温湿度数据。
3)数据接收时序图分析


根据数据接收时序图,每次数据发送前会有一段50us的低电平信号,之后进行1bit的数据传输。若传输数据0,高电平持续26us-28us;若传输数据1,高电平持续70us。
6、程序设计
1)GPIO 输入与输出初始化
由于需要对同一个 GPIO 口实现收发两种功能,首先编写输入初始化和输出初始化两个函数。
/**
* @brief 将DHT11数据引脚配置为输入模式
* @note 对应时序图"握手阶段":主机发送起始信号后需释放总线,
* 将IO切换为输入以接收DHT11的响应低电平(80us)。
* @param 无
* @retval 无
*/
static void DHT11_IO_IN(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DHT11_GPIO_PIN; // DHT11与单片机连接的GPIO引脚
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 输入模式
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉输入(总线空闲时为高电平)
HAL_GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct);
}
/**
@brief 将DHT11数据引脚配置为推挽输出模式
@note 对应时序图"起始信号阶段":主机需主动拉低总线至少18ms,
因此必须先切换为输出模式才能控制总线电平。
@param 无
@retval 无
*/
static void DHT11_IO_OUT(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DHT11_GPIO_PIN; // DHT11与单片机连接的GPIO引脚
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉(输出模式下无实际影响)
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速输出,保证电平跳变沿陡峭
HAL_GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct);
}
2)接下来按照通信时序图逐步编写函数。
a、单片机发送起始信号函数
/**
* @brief 主机发送起始信号(对应时序图"起始信号阶段")
* @note 时序要求:
* 1. 总线默认高电平(上拉),主机拉低至少18ms(代码延时20ms);
* 2. 再拉高20~40us(代码延时30us),表示起始信号结束;
* 3. 之后总线交由DHT11控制,等待其响应。
* @param 无
* @retval 无
*/
void DHT11_Rst(void)
{
DHT11_IO_OUT(); // 切换为输出模式,准备控制总线电平
// —— 起始信号第1步:拉低总线(对应时序图红色部分,低电平≥18ms)——
HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_RESET); // 总线由高变低
HAL_Delay(20); // 延时20ms,满足"至少18ms"的时序要求
// —— 起始信号第2步:拉高总线(对应时序图黄色部分,高电平20~40us)——
HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET); // 总线拉高
delay_us(30); // 延时30us,处于20~40us允许范围内
}

起始信号函数中,GPIO配置为输出模式。由于DHT11数据总线默认连接了上拉电阻,总线默认是高电平,因此直接输出低电平即可实现由高变低。低电平持续时间最少18ms,这里延时20ms以保证持续时间足够(对应上图红色部分)。之后拉高电平20到40us,通过输出高电平并延时30us实现(对应上图黄色部分)。
b、检测DHT11响应函数
/**
* @brief 读取DHT11数据引脚电平的宏定义
* @note 等价于 HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN)
*/
#define DHT11_DQ_IN HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN)
/**
@brief 检测DHT11是否响应(对应时序图"响应阶段/握手过程")
@note 时序要求:
1. DHT11收到起始信号后,会先拉低总线80us(响应低电平);
2. 随后再拉高80us(响应高电平),表示准备就绪。
@param 无
@retval 0 - 检测到DHT11响应(存在);1 - 未检测到DHT11(超时/不存在)
*/
uint8_t DHT11_Check(void)
{
uint8_t retry = 0;
DHT11_IO_IN(); // 切换为输入模式,准备接收DHT11的响应信号
// —— 等待DHT11拉低总线(对应时序图红色部分:响应低电平80us)——
// 若总线一直为高且超时(>100us),说明DHT11未拉低,判定无响应
while (DHT11_DQ_IN && retry < 100)
{
retry++;
delay_us(1);
}
if (retry >= 100) return 1; // 超时未检测到低电平,返回"不存在"
// —— 等待DHT11再次拉高总线(对应时序图黄色部分:响应高电平80us)——
// 若总线一直为低且超时(>100us),说明DHT11未拉高,判定无响应
retry = 0;
while (!DHT11_DQ_IN && retry < 100)
{
retry++;
delay_us(1);
}
if (retry >= 100) return 1; // 超时未检测到高电平,返回"不存在"
return 0; // 成功检测到完整的响应握手,返回"存在"
}

此函数用于读取DHT11的响应信号。首先将IO配置为输入模式。DHT11_DQ_IN是读取IO电平的宏定义,等价于HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN)。第一个while循环中,若IO电平为高且retry小于100,则延时1us并递增retry。若超过100us电平仍为高,则返回1,表示未检测到DHT11——因为根据时序图,DHT11正常响应时会拉低总线80us(上图红色部分)。第二个while循环判断上图黄色部分:将读取值取反,即检测总线是否被拉高。若超过100us仍未检测到高电平响应,同样返回1;正常检测到响应则返回0。
c、读取数据
读取数据需要编写3个函数:读取1位、读取8位(1个字节)、读取40位(DHT11发送的一次完整数据)。
/**
* @brief 从DHT11读取1位数据(对应时序图"数据接收阶段")
* @note 时序要求:
* 1. 每位数据前有50us低电平作为起始标志;
* 2. 随后高电平持续26~28us表示数据"0",持续70us表示数据"1";
* 3. 采样点选在高电平开始后40us处:此时数据0已结束(低电平),
* 数据1仍在持续(高电平),据此区分0和1。
* @param 无
* @retval 1 - 读到数据"1";0 - 读到数据"0"
*/
uint8_t DHT11_Read_Bit(void)
{
uint8_t retry = 0;
// —— 等待50us低电平结束(对应时序图红色部分:位起始低电平)——
// 总线被DHT11拉低,等待其释放
while (DHT11_DQ_IN && retry < 100)
{
retry++;
delay_us(1);
}
// —— 等待高电平到来(数据位开始传输)——
retry = 0;
while (!DHT11_DQ_IN && retry < 100)
{
retry++;
delay_us(1);
}
// —— 在高电平持续40us处采样(对应时序图黄色部分:数据位判断点)——
// 数据0高电平仅26~28us,此时已结束(总线为低);
// 数据1高电平持续70us,此时仍在持续(总线为高)
delay_us(40);
if (DHT11_DQ_IN) return 1; // 采样点仍为高电平 → 数据"1"
else return 0; // 采样点已为低电平 → 数据"0"
}

读取1位数据时,首先通过while循环等待50us的低电平结束(上图红色部分),再等待高电平到来。高电平即我们要接收的数据0或1。检测到高电平后,延时40us——该时长远大于数据0的高电平持续时间28us(图中黄色部分),又远小于数据1的70us。延时后判断IO电平:若仍为高电平,说明是数据1,返回1;若已变为低电平,则为数据0,返回0。
/**
* @brief 从DHT11读取1个字节(8位数据)
* @note 高位先出:每次将已读数据左移1位,再并入新读取的1位。
* 连续调用8次DHT11_Read_Bit()完成一个字节的接收。
* @param 无
* @retval 读到的8位数据(uint8_t)
*/
uint8_t DHT11_Read_Byte(void)
{
uint8_t i, dat;
dat = 0;
for (i = 0; i < 8; i++) // 循环8次,逐位读取
{
dat <<= 1; // 左移1位,为新的数据位腾出最低位
dat |= DHT11_Read_Bit(); // 读取1位并拼接到最低位
}
return dat; // 返回完整字节
}
读取1个字节的实现较为简单,直接调用读取1位的函数并循环8次即可。
/**
* @brief 从DHT11读取一次完整数据(40bit = 5字节)
* @note 数据格式:湿度整数 + 湿度小数 + 温度整数 + 温度小数 + 校验和
* 校验规则:前4字节之和 == 第5字节(校验和)
* @param humiH - 输出:湿度整数部分指针
* @param humiL - 输出:湿度小数部分指针
* @param tempH - 输出:温度整数部分指针
* @param tempL - 输出:温度小数部分指针
* @retval HAL_OK - 读取成功且校验通过
* HAL_ERROR - DHT11未响应或读取失败
*/
uint8_t DHT11_Read_Data(uint8_t *humiH, uint8_t *humiL, uint8_t *tempH, uint8_t *tempL)
{
uint8_t buf[5]; // 缓存5个字节:4字节数据 + 1字节校验和
uint8_t i;
DHT11_Rst(); // 发送起始信号,启动一次通信
// —— 握手检测:确认DHT11已响应 ——
if (DHT11_Check() == 0) // 返回0表示DHT11正常响应
{
// —— 数据接收阶段:连续读取5个字节(40bit)——
for (i = 0; i < 5; i++)
{
buf[i] = DHT11_Read_Byte(); // 逐字节读取
}
// —— 校验和验证:前4字节之和应等于第5字节 ——
if ((buf[0] + buf[1] + buf[2] + buf[3]) == buf[4])
{
// 校验通过,将温湿度数据写入输出指针
*humiH = buf[0]; // 湿度整数部分
*humiL = buf[1]; // 湿度小数部分
*tempH = buf[2]; // 温度整数部分
*tempL = buf[3]; // 温度小数部分
}
// 校验失败时不更新输出值(保持原值)
}
else
{
return HAL_ERROR; // DHT11未响应起始信号,返回错误
}
return HAL_OK; // 读取成功
}
读取一次完整数据(40bit)时,首先判断DHT11_Check()的返回值。若返回1,说明DHT11未响应起始信号,直接返回HAL_ERROR;若返回0,说明DHT11正确响应,进入IF分支:先循环5次读出完整数据,再验证校验和——若前4个字节之和等于第5个字节(校验和),则数据正确,将温湿度数据写入对应指针变量并返回HAL_OK。
d、上电初始化
/**
* @brief 初始化DHT11的IO口并检测DHT11是否存在
* @note 上电流程:
* 1. 将IO配置为推挽输出并输出高电平,确保总线空闲时为高;
* 2. 调用DHT11_Rst()发送起始信号;
* 3. 调用DHT11_Check()等待并检测DHT11的响应握手。
* @param 无
* @retval 0 - DHT11存在且响应正常;1 - DHT11不存在或未响应
*/
uint8_t FS_DHT11_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// —— 配置GPIO为推挽输出模式 ——
GPIO_InitStruct.Pin = DHT11_GPIO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速
HAL_GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct);
// —— 输出高电平:确保总线空闲状态为高(对应时序图起始前状态)——
HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET);
// —— 发送起始信号,启动与DHT11的通信握手 ——
DHT11_Rst();
// —— 检测DHT11响应:返回0表示存在,返回1表示不存在 ——
return DHT11_Check();
}
上电初始化函数先将IO配置为推挽输出模式并输出高电平,从软件层面确保DHT11上电后总线默认为高电平。随后调用DHT11_Rst()发送起始信号,最后通过return DHT11_Check()等待并检测DHT11的响应:正确响应返回0,未响应返回1。
至此,DHT11的程序编写已全部完成。使用时,上电初始化调用FS_DHT11_Init()函数,需要读取温湿度数据时调用DHT11_Read_Data()函数即可。
7、工程实现步骤与源码

static uint32_t fac_us = 0; //us延时倍乘数
void delay_init(uint8_t SYSCLK)
{
fac_us = SYSCLK;
}
void delay_us(uint32_t nus)//100 6800
{
uint32_t ticks;
uint32_t told, tnow, tcnt = 0;
uint32_t reload = SysTick->LOAD; //LOAD的值
ticks = nus * fac_us; //需要的节拍数
told = SysTick->VAL; // 24 刚进入时的计数器值
while (1)
{
tnow = SysTick->VAL;//22 20 0
if (tnow != told)
{
if (tnow < told)
tcnt += told - tnow; //这里注意一下SYSTICK是一个递减的计数器就可以了.
else
tcnt += reload - tnow + told;
told = tnow;
if (tcnt >= ticks)
break; //时间超过/等于要延迟的时间,则退出.
}
};
}
void delay_ms(uint16_t nms)
{
uint32_t i;
for (i = 0; i < nms; i++)
delay_us(1000);
}
delay_init(16); —— 延时函数的初始化
这是延时功能的初始化函数(常见于STM32等单片机的HAL库或自定义驱动中),参数16对应系统时钟频率(16MHz)。其作用是:根据系统时钟配置内部定时器/计数器的参数,确保后续调用HAL_Delay等延时函数时能产生准确的时间延迟。
若缺少此步骤,HAL_Delay可能因时钟配置错误导致延时偏差,直接影响DHT11通信时序的准确性。
HAL_Delay(2500); —— 满足DHT11上电稳定时间
DHT11数据手册明确要求:上电后需等待至少1秒(1000ms)才能开始通信,这段时间用于传感器内部电路稳定和校准。
代码中HAL_Delay(2500)延时2500毫秒(2.5秒),目的是:
- 给DHT11足够的“上电稳定时间”,确保传感器内部完成初始化、进入可通信状态;
- 避免因传感器未稳定就发起通信,导致读取失败或数据错误。
总结
这两行代码的核心作用是“为DHT11通信做前置准备”:
delay_init(16)保障延时功能的精度;HAL_Delay(2500)满足DHT11的上电稳定时序要求。
只有先完成这些准备,后续才能按照DHT11的通信协议(起始信号、数据读取等)正确获取温湿度数据。

主函数代码:
if(ReadData(&HumiH,&HumiL,&TempH,&TempL) == HAL_OK)
{
printf("Humi:%d.%d Temp:%d.%d C\n\r",HumiH,HumiL,TempH,TempL);
}
else
{
printf("DHT11_ERR\n\r");
}
HAL_Delay(2000);
8、参考资料
为便于读者进一步查阅与深入学习,下面列出本文撰写过程中参考的主要资料。这些资料分别对应 DHT11 传感器本身、STM32 HAL 库开发环境以及相关应用实践,可帮助理解本文涉及的通信协议、时序分析与驱动代码。
- DHT11 数据手册(DHT11 Humidity & Temperature Sensor Datasheet):由 Aosong(奥松)电子提供,是理解 DHT11 内部结构、单总线通信协议、40bit 数据格式、校验规则以及上电稳定时间等关键参数的第一手权威来源。本文第 4 节「DHT11数据手册知识点概述」和第 5 节「时序图分析」中的时序要求均以此手册为准。
- STM32 HAL 库参考手册(STM32F1xx HAL and Low-layer Driver User Manual,UM1850):由 STMicroelectronics 提供,系统介绍了 HAL 库中 GPIO 初始化、读写引脚电平(HAL_GPIO_Init、HAL_GPIO_ReadPin、HAL_GPIO_WritePin)等 API 的用法与参数含义。本文第 6 节「程序设计」中的 GPIO 输入输出初始化、起始信号发送与响应检测代码均基于该库编写。
- STM32 参考手册(STM32F10xxx Reference Manual,RM0008):由 STMicroelectronics 提供,详细说明了 SysTick 定时器的工作原理与寄存器配置。本文第 7 节「工程实现步骤与源码」中 delay_init、delay_us 等延时函数正是基于 SysTick 实现,查阅该手册有助于理解延时精度的来源。
- 应用笔记:DHT11 与 STM32 单总线通信实践(AN 类应用笔记,常见于各开发板厂商与社区):这类资料通常结合具体开发板给出完整的工程示例与调试经验,对理解本文第 7 节「工程实现步骤与源码」中的主函数调用流程、上电稳定时间处理以及实际调试中可能遇到的时序问题具有直接参考价值。
3万+



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



