1. 概述
1.1 实验目的
本实验旨在利用 DHT11 温湿度传感器,每隔 5 秒采集一次环境的温度与湿度数据,并通过串口将数据循环打印输出。所使用的 DHT11 模块硬件结构简单,包含三个接口引脚:电源正极(VCC)、电源负极(GND)以及一根用于数据通信的单总线信号线。
DHT11 采用的是一种单总线双向串行通信协议传输。这种通信方式仅仅需要一个I/O 口,就能同时完成数据的发送和接收,在短距离、低速率的数据采集应用中具有成本低、接线简洁的优势,非常适合初学者进行嵌入式系统开发和温湿度监测实验。
1.2 DHT11指标介绍
指标类别 | DHT11 | DHT22 |
---|---|---|
温度测量范围 | 0°C ~ 50°C | -40°C ~ 80°C |
温度精度 | ±2°C | ±0.5°C |
湿度测量范围 | 20% ~ 90% RH | 0% ~ 100% RH |
湿度精度 | ±5% RH | ±2% RH |
响应时间 | ≈ 1 秒 | ≈ 0.5 秒 |
采样间隔 | ≥ 1 秒(低频) | ≥ 0.5 秒(更快) |
工作电压 | 3V ~ 5V | 3.3V ~ 6V |
适用场景 | 常规室内监控,成本敏感应用 | 精度要求高、适用于室内外及极端环境 |
价格(通常) | 较便宜 | 相对较贵 |
1.3 硬件连接
DATA接微控制器GPIO引脚,需通过4.7kΩ上拉电阻连接至VCC(确保总线空闲时为高电平),这种模块已经做好了上拉电阻和工作指示灯的电路集成,只需把正负极通过杜邦线连接到单片机的正负极上,并把DATA 连接到单片机其中的一个GPIO引脚上即可,本文连接的引脚是PE0引脚。
如果买的不是模块,仅仅是DHT11传感器,是不带上拉电阻的,而且一共有4个引脚,其中一个是NC(空脚),需要自己进行上拉电阻的焊接,方便在实际工程中使用,集成到已有的板子上。如果是学习或工程研发调试阶段,建议买上面那种模块化的。
1.4 DHT11数据结构
原始数据 5字节(40bit)固定格式(高位优先输出):
00101101 (Byte4) | 00000000 (Byte3) | 00011100 (Byte2) | 00000000 (Byte1) | 01001001 (Byte0)
字节序 | 字段名称 | 数据类型 | 说明 |
---|---|---|---|
Byte4 | 湿度整数部分 | 8位无符号 | 范围:0~99%RH |
Byte3 | 湿度小数部分 | 8位无符号 | 固定补零(实际分辨率1%RH) |
Byte2 | 温度整数部分 | 8位有符号 | 范围:-40~+80℃ |
Byte1 | 温度小数部分 | 8位无符号 | 固定补零(实际分辨率1℃) |
Byte0 | 校验和 | 8位无符号 | Byte4+Byte3+Byte2+Byte1 |
1.5 DHT11时序图
主机发送开始信号后,延时等待 20us-40us 后读取 DH11T 的回应信号,读取总线为低电平,说明DHT11 发送响应信号,DHT11 发送响应信号后,再把总线拉高,准备发送数据,每一 bit 数据都以低电平开始,格式见下面图示。如果读取响应信号为高电平,则 DHT11 没有响应,请检查线路是否连接正常。
1.5.1 请求数据时序图
上图是一个总图,描述了数据请求阶段和数据接收阶段,当为数据请求阶段时,与HDT11的DATA口连接的单片机I/O口需要设置为推挽输出模式,因为要写数据。下图为数据请求阶段时序图。
1.5.2 接收数据时序图
当单片机向DHT11请求数据后在数据接收阶段,需要把单片机I/O设置为浮空输入模式,读取数据时序图如下所示:
数据位为 0 的时序图
数据位为 1 的时序图
2. STM32CubeMX配置
2.1 SYS配置
本文使用JLink下载代码,如果读者用STLink同理,都是如下图选择
2.2 GPIO配置
2.3 USART1配置
选择异步通信,波特率115200常用波特率,其他配置默认即可,影响不大,主要是为了打印程序内部信息到控制台查看。
2.4 RCC配置
选择外部晶振作为时钟源,然后如下图配置时钟树,万年不变,记住配置即可。
最终目的就是要求最后时钟树的末端,高速时钟和低速时钟分别是72MHz和36MHz
2.5 PROJECT配置
3. keil MDK配置
完成上述配置后,点击右上角的GENERATE CODE生成代码,并打开工程,便进入到keil MDK界面。
3.1 创建驱动文件
3.1.1 创建目录和文件
首先在工程目录core下新建hardware文件夹,方便我们存放一些外部模块的驱动代码文件,并新建两个文件在此文件夹下,分别是dht11.h 和 dht11.c,注意这两个文件的后缀,不要隐藏文件后缀名,防止后缀错误。一般会在这个文件中写一些模块的驱动或者封装功能函数,在主函数中只需调用这些驱动文件的函数即可使用模块功能为我们的业务服务了。
3.1.2 配置路径
3.1.3 配置文件
3.2 引入包配置
为了让串口能够重定向更方便的打印输出内容,需要引入这个包
3.3 调试配置
根据自己的调试下载工具选择,作者使用的JLink V8 所以选择下图所示选项,如果是STLink 则选择对应选项即可。
防止每次下载按重启按键,可选择烧录后自动跑新程序这个选项
配置完后一定要记得关闭keil MDK软件,再打开VSCode进行编码工作,否则刚刚设置的内容并没有生成配置文件,以上配置工作白搞了。
4. VSCode编码
打开对应工程后,一定要先编译下,这样才能在对应.c文件下找到对应.h文件
4.1 USART1编写
重定向代码编写,这是固定的代码,找到usart.h / usart.c文件放到对应的位置上即可
/* USER CODE BEGIN 1 */
int fputc(int ch, FILE * file){
HAL_UART_Transmit(&huart1,(uint8_t *)&ch, 1, 1000);
return ch;
}
/* USER CODE END 1 */
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */
完成以上两个地方的代码编写后,就可以在业务程序中使用printf(" your context"); 进行串口打印了。
4.2 驱动文件编写
#ifndef __DHT11_H
#define __DHT11_H
#include "stm32f1xx_hal.h" // 根据实际MCU型号修改
#include <stdint.h>
#include <stdbool.h>
// 错误码定义
typedef enum {
DHT11_OK = 0,
DHT11_ERROR_TIMEOUT,
DHT11_ERROR_CHECKSUM,
DHT11_ERROR_NO_RESPONSE
} DHT11_Status;
// 温湿度数据结构体
typedef struct {
float temperature; // 温度(℃)
float humidity; // 湿度(%RH)
} DHT11_Data;
// 函数声明
DHT11_Status DHT11_Write(void);
DHT11_Status DHT11_Read(DHT11_Data *data);
void DHT11_DelayUs(uint32_t us);
#endif
#include "dht11.h"
#include "gpio.h"
#include "usart.h"
// 微秒级延时
void DHT11_DelayUs(uint32_t us)
{
uint32_t delay = (HAL_RCC_GetHCLKFreq() / 4000000 * us);
while (delay--)
{
;
}
}
// 初始化DHT11(配置GPIO为开漏输出)
DHT11_Status DHT11_Write(void) {
// 1. 配置引脚为推挽输出(发送起始信号)
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DHT11_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(DHT11_GPIO_Port, &GPIO_InitStruct);
// 1. 主机发送起始信号(拉低18ms)
HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET);
HAL_Delay(18); // 实际需精确到18ms
// 2. 拉高20-40us,切换为输入模式
HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_SET);
DHT11_DelayUs(30);
return DHT11_OK;
}
// 读取DHT11数据
DHT11_Status DHT11_Read(DHT11_Data *data) {
uint8_t buffer[5] = {0}; // 40位数据:湿度整数/小数,温度整数/小数,校验和
uint8_t checksum = 0;
uint16_t timeout = 100; // 最多等待 100us
// 1. 配置引脚为上拉输入(等待DHT11响应)
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DHT11_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(DHT11_GPIO_Port, &GPIO_InitStruct);
// 2. 检测DHT11响应(40-50us低电平 + 40-50us高电平)
timeout = 100; // 超时时间约100us
while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {
DHT11_DelayUs(1);
if (--timeout == 0) {
return DHT11_ERROR_NO_RESPONSE; // 等待低电平响应超时
}
}
// 检测DHT11的响应低电平结束(40-50us低电平)
timeout = 60;// 适当增加超时容限
while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) {
DHT11_DelayUs(1);
if (--timeout == 0) {
return DHT11_ERROR_TIMEOUT; // 低电平持续时间过长
}
}
// 检测DHT11的响应高电平结束(40-50us高电平)
timeout = 60;// 适当增加超时容限
while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {
DHT11_DelayUs(1);
if (--timeout == 0) {
return DHT11_ERROR_TIMEOUT; // 高电平持续时间过长
}
}
// 3. 读取40位数据(每位:12-14us低电平 + 高电平长度决定0/1: 26-28us / 116-118us)
for (int i = 0; i < 40; i++) {
// 等待每位开始的12-14us低电平
timeout = 30; // 适当增加超时容限
while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) {
if (--timeout == 0) {
return DHT11_ERROR_TIMEOUT;
}
DHT11_DelayUs(1);
}
// 测量高电平持续时间
DHT11_DelayUs(30); // 等待30us后采样
if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {
buffer[i / 8] |= (1 << (7 - (i % 8))); // 高电平持续时间长,表示1
// 等待高电平结束
timeout = 100;
while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {
if (--timeout == 0) {
return DHT11_ERROR_TIMEOUT;
}
DHT11_DelayUs(1);
}
}
// 如果是0,高电平已经结束(26-28us),不需要额外等待
}
// 4. 校验数据(前4字节之和 = 校验和)
checksum = buffer[0] + buffer[1] + buffer[2] + buffer[3];
if (checksum != buffer[4]) {
return DHT11_ERROR_CHECKSUM;
}
// 5. 填充结果(忽略小数部分)
data->humidity = buffer[0];
data->temperature = buffer[2];
return DHT11_OK;
}
4.2.1 延时函数
因为HAL库只提供毫秒级别的延时函数,而单总线双向串行通讯协议的时序图中有用到微妙级别的延时,所以需要自定义微妙延时函数。
// 微秒级延时
void DHT11_DelayUs(uint32_t us)
{
uint32_t delay = (HAL_RCC_GetHCLKFreq() / 4000000 * us);
while (delay--)
{
;
}
}
4.2.2 数据请求函数
// 初始化DHT11(配置GPIO为开漏输出)
DHT11_Status DHT11_Write(void) {
// 1. 配置引脚为推挽输出(发送起始信号)
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DHT11_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(DHT11_GPIO_Port, &GPIO_InitStruct);
// 1. 主机发送起始信号(拉低18ms)
HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET);
HAL_Delay(18); // 实际需精确到18ms
// 2. 拉高20-40us,切换为输入模式
HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_SET);
DHT11_DelayUs(30);
return DHT11_OK;
}
4.2.3 数据接收函数
// 读取DHT11数据
DHT11_Status DHT11_Read(DHT11_Data *data) {
uint8_t buffer[5] = {0}; // 40位数据:湿度整数/小数,温度整数/小数,校验和
uint8_t checksum = 0;
uint16_t timeout = 100; // 最多等待 100us
// 1. 配置引脚为上拉输入(等待DHT11响应)
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DHT11_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(DHT11_GPIO_Port, &GPIO_InitStruct);
// 2. 检测DHT11响应(40-50us低电平 + 40-50us高电平)
timeout = 100; // 超时时间约100us
while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {
DHT11_DelayUs(1);
if (--timeout == 0) {
return DHT11_ERROR_NO_RESPONSE; // 等待低电平响应超时
}
}
// 检测DHT11的响应低电平结束(40-50us低电平)
timeout = 60;// 适当增加超时容限
while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) {
DHT11_DelayUs(1);
if (--timeout == 0) {
return DHT11_ERROR_TIMEOUT; // 低电平持续时间过长
}
}
// 检测DHT11的响应高电平结束(40-50us高电平)
timeout = 60;// 适当增加超时容限
while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {
DHT11_DelayUs(1);
if (--timeout == 0) {
return DHT11_ERROR_TIMEOUT; // 高电平持续时间过长
}
}
// 3. 读取40位数据(每位:12-14us低电平 + 高电平长度决定0/1: 26-28us / 116-118us)
for (int i = 0; i < 40; i++) {
// 等待每位开始的12-14us低电平
timeout = 30; // 适当增加超时容限
while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) {
if (--timeout == 0) {
return DHT11_ERROR_TIMEOUT;
}
DHT11_DelayUs(1);
}
// 测量高电平持续时间
DHT11_DelayUs(30); // 等待30us后采样
if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {
buffer[i / 8] |= (1 << (7 - (i % 8))); // 高电平持续时间长,表示1
// 等待高电平结束
timeout = 100;
while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {
if (--timeout == 0) {
return DHT11_ERROR_TIMEOUT;
}
DHT11_DelayUs(1);
}
}
// 如果是0,高电平已经结束(26-28us),不需要额外等待
}
// 4. 校验数据(前4字节之和 = 校验和)
checksum = buffer[0] + buffer[1] + buffer[2] + buffer[3];
if (checksum != buffer[4]) {
return DHT11_ERROR_CHECKSUM;
}
// 5. 填充结果(忽略小数部分)
data->humidity = buffer[0];
data->temperature = buffer[2];
return DHT11_OK;
}
4.3 主函数业务代码
/* USER CODE BEGIN Includes */
#include "dht11.h"
/* USER CODE END Includes */
/* USER CODE BEGIN 2 */
DHT11_Data sensor_data;
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
printf("hello world!!! \r\t\n");
if(DHT11_Write() == DHT11_OK){
if (DHT11_Read(&sensor_data) == DHT11_OK) {
printf("Temperature: %.1f C, Humidity: %.1f%%RH\r\n",
sensor_data.temperature, sensor_data.humidity);
} else {
printf("DHT11 Read Error! \r\n");
}
}else {
printf("DHT11 Write Error!\r\n");
}
HAL_Delay(5000);
}
/* USER CODE END 3 */
5. 结果验证
对代码进行编译烧录后,通过连接串口,能够看到对应的数据,因为DHT11温湿度传感器精度不高,所以没有小数点,尽管程序有读取,均为0,如果想要更高精度的测量,建议使用DHT22