第三十二章 DHT11实验
本章,我们将介绍数字温湿度传感器DHT11的使用,与前一章的温度传感器相比,该传感器不但能测温度,还能测湿度。我们将学习如何获取DHT11传感器的温湿度数据,并把数据显示在LCD上。
本章分为如下几个小节:
32.1 DHT11介绍
32.2 硬件设计
32.3 程序设计
32.4 下载验证
32.1 DHT11介绍
32.1.1 DHT11简介
DHT11是一款温湿度一体化的数字传感器,实物图如下图所示。

图32.1.1.1 DHT11实物图
该传感器包括一个电容式测湿元件和一个NTC测温元件,并与一个高性能8位单片机相连接。通过单片机等微处理器简单的电路连接就能够实时的采集本地湿度和温度。DHT11与单片机之间能采用简单的单总线进行通信,仅仅需要一个I/O口。传感器内部湿度和温度数据40Bit的数据一次性传给单片机,数据采用校验和方式进行校验,有效的保证数据传输的准确性。DHT11功耗很低,5V电源电压下,工作平均最大电流0.5mA。
DHT11的技术参数如下:
l 工作电压范围:3.3V ~ 5.5V
l 工作电流:平均0.5mA
l 输出:单总线数字信号
l 测量范围:湿度5 ~ 95%RH,温度-20 ~ 60℃
l 精度:湿度±5%,温度±2℃
l 分辨率:湿度1%,温度0.1℃
DHT11的管脚排列如下图所示:

图32.1.1.2 DHT11管脚排列图
32.1.2 DHT11时序介绍
虽然DHT11与DS18B20类似,都是单总线访问,但是DHT11的访问,相对DS18B20来说简单很多。下面我们先来看看DHT11的数据结构。
DHT11数字温湿度传感器采用单总线数据格式。即,单个数据引脚端口完成输入输出双向传输。其数据包由5byte(40bit)组成。数据分小数部分和整数部分,一次完整的数据传输为40bit,高位先处。DHT11的数据格式为:8bit湿度整数数据+8bit湿度小数数据+8bit温度整数数据+8bit温度小数部分+8bit校验和。其中校验和数据为前面四个字节相加。
传感器数据输出的是未编码的二进制数据。数据(湿度、温度、整数、小数)之间应该分开处理。例如,某次从DHT11读到的数据如下图所示:

图32.1.2.1 某次读取到DHT11数据
由以上数据就可得到湿度和温度的值,计算方法:
湿度 = byte4 . byte3 = 45.0(%RH)
温度 = byte2 . byte1 = 28.0(℃)
校验 = byte4 + byte3 + byte2 + byte1 = 73 (= 湿度 + 温度) (校验正确)
可以看出,DHT11的数据格式十分简单的,DHT11和MCU的一次通信最大为34ms左右,建议主机连续读取时间间隔不要小于2s。
下面,我们介绍一下DHT11的传输时序。DHT11的数据发送流程如下图所示。

图32.1.2.2 DHT11数据发送流程图
首先主机发送开始信号,即:拉低数据线,保持t1(至少18ms)时间,然后拉高数据线t2(10~35us)时间,然后读取DHT11的响应,正常的话,DHT11会拉低数据线,保持t3(78~88us)时间,作为响应信号,然后DHT11拉高数据线,保持t4(80~92us)时间后,开始输出数据。
DHT11输出数字‘0’时序如下图所示。

图32.1.2.3 DHT11数字‘0’时序图
DHT11输出数字‘1’的时序如下图所示:

图32.1.2.4 DHT11输出数字‘1’时序图
DHT11输出数字‘0’和‘1’时序,一开始都是DHT11拉低数据线54us,后面拉高数据线保持的时间就不一样,数字‘0’就是23~27us,而数字‘1’就是68~74us。
通过以上了解,我们就可以通过ESP32-S3来实现对DHT11的读取了。
DHT11的介绍就到这里,更详细的介绍,请参考DHT11的数据手册。
32.2 硬件设计
32.2.1 例程功能
DHT11每隔100ms左右读取一次数据,并把温度和湿度显示在LCD上。LED0闪烁用于提示程序正在运行。
32.2.2 硬件资源
1)LED灯
LED 0 - IO51
2)RGBLCD / MIPILCD(引脚太多,不罗列出来)
3)DHT11
1WIRE_DQ - IO23
32.2.3 原理图
DHT11相关原理图和DS18B20原理图是一样的,这里就不再列出来了。
DHT11和DS18B20的接口是共用一个的,不过DHT11有4条腿,需要把U4的4个接口都用上,将DHT11传感器插入到这个上面就可以通过ESP32-P4来读取温湿度值了。连接示意图如图32.2.1所示:

图32.2.3.2 DHT11与开发板连接图
32.3 程序设计
本例程主要用到的是GPIO函数,GPIO函数已在11.3.1章节中详细阐述,为避免重复,此处不再赘述。建议读者查阅第十一章的函数解析章节,以获取更多关于GPIO函数的信息。
32.3.1 程序流程图

图32.3.1.1 DHT11实验程序流程图
32.3.2 程序解析
在23_dht11例程中,作者在23_dht11\components\BSP路径下新增了1个文件夹DHT11,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。
1. DHT11驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。DHT11驱动源码包括两个文件:dht11.c和dht11.h。
下面先解析dht11.h的程序。对DHT11的数据引脚做了相关定义。
#define DHT11_DQ_GPIO_PIN GPIO_NUM_23
由于数据线会存在获取高低电平以及设置高低电平,所以为DHT11_DQ_GPIO_PIN 做了相关宏函数供单总线时序函数调用。
#define DHT11_DQ_IN gpio_get_level(DHT11_DQ_GPIO_PIN) /* IO操作 */
#define DHT11_DQ_OUT(x) do { x ? \
gpio_set_level(DHT11_DQ_GPIO_PIN, 1): \
gpio_set_level(DHT11_DQ_GPIO_PIN, 0); \
} while(0) /* DHT11端口定义 */
接下来,解析一下dht11.c的程序,首先来看一下DHT11初始化函数dht11_init,代码如下:
/**
* @brief 初始化DHT11
* @param 无
* @retval 0, 正常; 1, 不存在/不正常
*/
uint8_t dht11_init(void)
{
gpio_config_t gpio_init_struct;
gpio_init_struct.intr_type = GPIO_INTR_DISABLE; /* 失能引脚中断 */
gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT_OD; /* 开漏输入和输出 */
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; /* 使能上拉 */
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; /* 失能下拉 */
gpio_init_struct.pin_bit_mask = 1ull << DHT11_DQ_GPIO_PIN; /* 设置位掩码 */
gpio_config(&gpio_init_struct); /* 配置DHT11引脚 */
dht11_reset(); /* 复位DHT11 */
return dht11_check(); /* 等待DHT11的响应 */
}
在DHT11初始化函数中,首先对数据引脚初始化,然后通过调用复位函数和自检函数,可以知道DHT11是否正常。
下面介绍一下在前面提及的几个信号类型。
/**
* @brief 复位DHT11
* @param 无
* @retval 无
*/
void dht11_reset(void)
{
DHT11_DQ_OUT(0); /* 拉低DQ */
esp_rom_delay_us(25000); /* 拉低至少18ms */
DHT11_DQ_OUT(1); /* DQ=1 */
esp_rom_delay_us(30); /* 主机拉高10~35us */
}
/**
* @brief 等待DHT11的回应
* @param 无
* @retval 0,DHT11正常; 1,DHT11异常/不存在
*/
uint8_t dht11_check(void)
{
uint8_t retry = 0;
uint8_t rval = 0;
while (DHT11_DQ_IN && retry < 100) /* DHT11会拉低40~80us */
{
retry++;
esp_rom_delay_us(1);
}
if (retry >= 100)
{
rval = 1;
}
else
{
retry = 0;
while (!DHT11_DQ_IN && retry < 100) /* DHT11拉低后会再次拉高87us */
{
retry++;
esp_rom_delay_us(1);
}
if (retry >= 100)
{
rval = 1;
}
}
return rval;
}
以上两个函数分别代表着前面所说的复位脉冲与应答信号,大家可以对比前面的时序图进行理解。那么在上一章DS18B20的实验中,也对复位脉冲以及应答信号进行了详细的解释,大家也可以对比理解。
DHT11与DS18B20有所不同,DHT11是不需要写函数,只需要读函数即可,下面我们看一下读函数:
/**
* @brief 从DHT11读取一个位
* @param 无
* @retval 读取到的位值: 0 / 1
*/
uint8_t dht11_read_bit(void)
{
uint8_t retry = 0;
while (DHT11_DQ_IN && retry < 100) /* 等待变为低电平 */
{
retry++;
esp_rom_delay_us(1);
}
retry = 0;
while (!DHT11_DQ_IN && retry < 100) /* 等待变高电平 */
{
retry++;
esp_rom_delay_us(1);
}
esp_rom_delay_us(40); /* 等待40us */
if (DHT11_DQ_IN) /* 根据引脚状态返回 bit */
{
return 1;
}
else
{
return 0;
}
}
/**
* @brief 从DHT11读取一个字节
* @param 无
* @retval data:读到的数据
*/
static uint8_t dht11_read_byte(void)
{
uint8_t i, data = 0;
for (i = 0; i < 8; i++) /* 循环读取8位数据 */
{
data <<= 1; /* 高位数据先输出, 先左移一位 */
data |= dht11_read_bit(); /* 读取1bit数据 */
}
return data;
}
在这里dht11_read_bit函数从DHT11处读取1位数据,大家可以对照前面的读时序图进行分析,读数字0和1的不同,在于高电平的持续时间,所以这个作为判断的依据。dht11_read_byte函数就是调用一位数据读取函数进行实现。
下面介绍读取温湿度函数,其定义如下:
/**
* @brief 从DHT11读取一次数据(温度和湿度的整数部分)
* @param temp: 温度值(范围:-20~60°)(放大10倍)
* @param humi: 湿度值(范围:5%~95%)(放大10倍)
* @retval 0, 正常; 1, 失败
*/
uint8_t dht11_read_data(short *temp, short *humi)
{
uint8_t buf[5];
uint8_t i;
short raw_temp = 0;
short raw_humi = 0;
dht11_reset();
if (dht11_check() == 0)
{
/* 读取40位数据 buf[0]湿度高8位 buf[1]湿度低8位 buf[2]温度高8位 buf[3]温度低8位 */
for (i = 0; i < 5; i++)
{
buf[i] = dht11_read_byte();
}
if ((buf[0] + buf[1] + buf[2] + buf[3]) == buf[4]) /* 数据校验正确 */
{
raw_humi = buf[0] * 10 + buf[1]; /* 获取湿度数据 */
if (buf[3] & 0x80) /* 温度为负值 */
{
raw_temp = buf[2] * 10 + (buf[3] & 0x7F);
raw_temp = -raw_temp;
}
else
{
raw_temp = buf[2] * 10 + buf[3]; /* 温度数据 */
}
*humi = raw_humi;
*temp = raw_temp;
}
}
else
{
return 1;
}
return 0;
}
读取温湿度函数也是根据时序图进行实现的,在发送复位信号以及应答信号产生后,即可以读取5Byte数据进行处理,校验成功即读取数据有效成功。
2. CMakeLists.txt文件
本例程的功能实现主要依靠DHT11驱动。要在main函数中,成功调用DHT11文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件,修改如下:
set(src_dirs
LED
LCD
DHT11)
set(include_dirs
LED
LCD
DHT11)
set(requires
driver
esp_lcd
esp_common)
idf_component_register( SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
3. main.c驱动代码
在main.c里面编写如下代码。
void app_main(void)
{
esp_err_t ret;
uint8_t t = 0;
short temperature = 0;
short humidity = 0;
ret = nvs_flash_init(); /* 初始化NVS */
if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
led_init(); /* LED初始化 */
lcd_init(); /* LCD屏初始化 */
lcd_show_string(30, 50, 200, 16, 16, "ESP32-P4", RED);
lcd_show_string(30, 70, 200, 16, 16, "DHT11 TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
while (dht11_init()) /* 初始化DHT11数字温湿度传感器 */
{
lcd_show_string(30, 110, 200, 16, 16, "DHT11 Error", RED);
vTaskDelay(pdMS_TO_TICKS(200));
lcd_fill(30, 110, 239, 126, WHITE);
vTaskDelay(pdMS_TO_TICKS(200));
}
lcd_show_string(30, 110, 200, 16, 16, "DHT11 OK", RED);
lcd_show_string(30, 130, 200, 16, 16, "Temp:00.0C", BLUE);
lcd_show_string(30, 150, 200, 16, 16, "Humi:00.0%", BLUE);
while (1)
{
if (t % 10 == 0) /* 每100ms读取一次 */
{
dht11_read_data(&temperature, &humidity); /* 读取温湿度值 */
lcd_show_num(70, 130, temperature / 10, 2, 16, BLUE); /* 显示整数 */
lcd_show_num(94, 130, temperature % 10, 1, 16, BLUE); /* 显示小数 */
lcd_show_num(70, 150, humidity / 10, 2, 16, BLUE); /* 显示湿度 */
}
vTaskDelay(pdMS_TO_TICKS(10));
t++;
if (t == 20)
{
t = 0;
LED0_TOGGLE();
}
}
}
在app_main函数中,设置一个while循环去初始化DHT11,若DHT11成功被初始化,程序便可往下执行。在while循环中每隔100毫秒调用一次dht11_get_temperature函数获取一下温湿度数据,并在LCD上显示。LED灯每隔200毫秒状态翻转,实现闪烁效果。
32.4 下载验证
假定DHT11传感器已经接上去正确的位置,将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示当前的温度值的内容如下图所示:

图32.4.1 DHT11实验程序运行效果图
至此,本章实验结束。大家可以将本章通过DHT11读取到的温度值,和前一章的通过DS18B20读取到的温度值对比一下,看看哪个更准确?。