STM32 环境监测项目笔记(一):DHT11 温湿度传感器原理与驱动实现
本系列笔记是笔者学习 B 站 up 主 “技术探索者” STM32 系列视频所作的记录,不理解的地方推荐观看视频~
目录
- 一、前言
- 二、DHT11 模块核心认知
- 2.1 模块特性与接线
- 2.2 单总线协议时序图解析
- 三、CubeMX 工程配置
- 3.1 基础配置(芯片 / 时钟 / Debug)
- 3.2 TIM1 配置(1μs 高精度延时)
- 3.3 串口 1 配置(数据打印)
- 四、DHT11 驱动代码实现(带详细注释)
- 4.1 头文件(dht11.h):宏定义与结构体
- 4.2 核心函数:延时 / IO 模式切换 / 数据读取
- 4.3 串口重定向(usart.c)
- 五、测试验证与结果分析
- 六、总结
一、前言
大家好,我是 Hello_Embed。上一系列我们完成了智能垃圾桶项目,从模块驱动到功能整合,掌握了嵌入式开发的基础流程。本次开启新系列 ——环境监测项目,核心目标是实现 “温湿度 + 光照强度” 的实时采集与 OLED 显示,既复习定时器、串口等旧知识,也将学习单总线、ADC、IIC 等新协议。
本系列第一篇聚焦 DHT11 温湿度传感器—— 这是嵌入式开发中最常用的入门级温湿度模块,通过单总线协议实现数据传输,接线简单但对时序精度要求高。本次将从模块原理、时序分析、CubeMX 配置到驱动代码,完整实现 DHT11 的温湿度采集功能。
二、DHT11 模块核心认知
2.1 模块特性与接线
2.1.1 核心参数
DHT11 是低成本数字式温湿度传感器,性能满足日常环境监测需求,关键参数如下:
参数 | 规格 | 说明 |
---|---|---|
温度测量 | 范围 0~50℃,精度 ±2℃ | 无负温测量能力,适合常温场景 |
湿度测量 | 范围 20%~80% RH,精度 ±5% RH | 低湿 / 高湿环境精度会下降 |
通信协议 | 单总线(1-Wire) | 仅需 1 根数据线实现双向通信 |
供电电压 | 3.3V~5V | 兼容 STM32 3.3V/5V 供电 |
响应时间 | ≤2s | 每次采集间隔建议 ≥2s |
2.1.2 接线说明
DHT11 共 3 个引脚(部分模块带 4 引脚,其中 1 个为空脚),接线原则如下(本次选用 PA7 作为数据线):
DHT11 引脚 | 功能 | 连接对象(STM32) | 备注 |
---|---|---|---|
VCC | 电源正极 | 3.3V/5V 引脚 | 勿接反,否则可能烧毁模块 |
GND | 电源负极 | GND 引脚 | 必须与 STM32 共地 |
DATA | 单总线数据 | PA7 引脚 | 需配置为双向 IO(输入 / 输出切换) |
2.2 单总线协议时序图解析
DHT11 与 STM32 的通信完全依赖 “时序”,需严格遵循 “起始信号→应答信号→数据传输” 三步流程,时序图如下:
2.2.1 1. 起始信号(STM32 → DHT11)
STM32 主动发送起始信号,告知 DHT11 “准备采集数据”,时序要求:
- 数据线(PA7)拉低 18ms(必须≥18ms,否则 DHT11 不响应);
- 数据线拉高 20~40μs(等待 DHT11 应答);
- 此时 STM32 需将数据线切换为 输入模式,准备接收 DHT11 的应答信号。
2.2.2 2. 应答信号(DHT11 → STM32)
DHT11 检测到起始信号后,主动发送应答信号,时序特征:
- 数据线拉低 80μs(表示 “已收到起始信号”);
- 数据线拉高 80μs(表示 “准备发送数据”);
- 应答信号结束后,进入数据传输阶段。
2.2.3 3. 数据传输(DHT11 → STM32)
DHT11 一次传输 40 位二进制数据(共 5 字节),数据格式与解析规则如下:
- 数据格式:8 位湿度整数 → 8 位湿度小数 → 8 位温度整数 → 8 位温度小数 → 8 位校验和;
- 例:若 5 字节为
0x40, 0x00, 0x19, 0x00, 0x59
,则湿度 = 64% RH,温度 = 25℃,校验和 = 64+0+25+0=89=0x59(校验通过);
- 例:若 5 字节为
- 位数据区分:DHT11 通过 “高电平持续时间” 区分 0 和 1:
- 数据 0:低电平 50μs → 高电平 26~28μs;
- 数据 1:低电平 50μs → 高电平 70μs;
- 校验规则:前 4 字节之和 = 第 5 字节(校验和),若不相等则数据无效。
三、CubeMX 工程配置
本次使用 STM32F103C8T6 最小系统板,新建 CubeMX 工程,配置步骤如下:
3.1 基础配置(芯片 / 时钟 / Debug)
- 芯片选择:搜索并选择
STM32F103C8T6
; - Debug 配置:进入
System Core → SYS
,Debug 选择Serial Wire
(必须设置,否则无法烧录); - 时钟配置:
- 进入
System Core → RCC
,High Speed Clock(HSE)选择Crystal/Ceramic Resonator
(外部晶振); - 进入
Clock Configuration
,将 HCLK 配置为 72MHz(STM32F103 最高主频),配置如下:
- 进入
3.2 TIM1 配置(1μs 高精度延时)
DHT11 时序对时间精度要求到 μs 级,需用定时器实现 1μs 延时,选择 TIM1(16 位定时器,满足延时需求):
- 进入
Timers → TIM1
,模式选择Internal Clock
; - 参数配置:
- Prescaler(预分频值):
72 - 1
(72MHz 时钟 / 72 = 1MHz,即 1 次计数 = 1μs); - Counter Period(ARR):
65535
(16 位定时器最大计数,避免频繁溢出);
- Prescaler(预分频值):
- 配置截图如下:
3.3 串口 1 配置(数据打印)
通过串口 1 打印温湿度数据,配置如下:
- 进入
Connectivity → USART1
,模式选择Asynchronous
(异步通信); - 基本参数:波特率
115200
,数据位8
,停止位1
,校验位None
(默认配置,无需修改); - 引脚:默认 PA9(TX)、PA10(RX),无需手动调整。
工程生成
- 进入
Project Manager → Code Generator
,勾选Generate peripheral initialization as a pair of .c/.h files per peripheral
; - 选择工程路径,Toolchain/IDE 设为
MDK-ARM
,点击Generate Code
生成工程。
四、DHT11 驱动代码实现(带详细注释)
新建 driver
文件夹,创建 dht11.c
和 dht11.h
,并将 driver
文件夹添加到 Keil 工程路径(Options for Target → C/C++ → Include Paths
)。
4.1 头文件(dht11.h):宏定义与结构体
定义数据线引脚、数据类型别名、存储温湿度的结构体,声明核心函数:
#ifndef __DHT11_H
#define __DHT11_H#include "main.h"// 数据类型别名(简化代码)
#define u8 unsigned char
#define u16 unsigned short
#define u32 unsigned int// ------------- DHT11 数据线引脚宏定义 -------------
#define DATA_PIN GPIO_PIN_7 // 数据线对应引脚:PA7
#define DATA_GPIO_Port GPIOA // 数据线对应端口:GPIOA// 数据线电平控制宏(简化代码)
#define DATA_SET() HAL_GPIO_WritePin(DATA_GPIO_Port, DATA_PIN, GPIO_PIN_SET) // 拉高数据线
#define DATA_RESET() HAL_GPIO_WritePin(DATA_GPIO_Port, DATA_PIN, GPIO_PIN_RESET) // 拉低数据线
#define DATA_READ() HAL_GPIO_ReadPin(DATA_GPIO_Port, DATA_PIN) // 读取数据线电平// ------------- 温湿度数据存储结构体 -------------
typedef struct
{u8 Data[5]; // 存储 DHT11 传输的 40 位数据(5 字节)u8 index; // 数据计数标志(可选,用于统计采集次数)u8 temp; // 解析后的温度值(整数部分)u8 humidity; // 解析后的湿度值(整数部分)
} DHT11_DATA;// 全局变量声明(供外部文件调用,如 main.c)
extern DHT11_DATA DHT11_data;// 函数声明
void DHT11_Task(void); // DHT11 采集任务(对外接口)#endif
4.2 核心函数:延时 / IO 模式切换 / 数据读取
在 dht11.c
中实现 5 个核心函数:μs 延时、数据线输入 / 输出模式切换、位数据读取、完整数据采集、采集任务封装。
4.2.1 1μs 高精度延时函数(基于 TIM1)
#include "dht11.h"// 声明 TIM1 句柄(CubeMX 自动生成在 tim.c 中)
extern TIM_HandleTypeDef htim1;
// 定义全局结构体变量(存储温湿度数据)
DHT11_DATA DHT11_data;/*** @brief 1μs 高精度延时函数* @param us:目标延时时间(单位:μs,最大 65530μs,避免溢出)* @retval 无* @note 基于 TIM1 实现,通过设置计数起始值补偿代码执行时间*/
void Delay_us(uint16_t us)
{u16 differ = 0xffff - us - 5; // 计数起始值:-5 用于补偿函数调用耗时__HAL_TIM_SET_COUNTER(&htim1, differ); // 设置 TIM1 计数起始值HAL_TIM_Base_Start(&htim1); // 启动 TIM1 计数// 等待计数到接近 0xffff(避免定时器溢出)while (differ < 0xffff - 5){differ = __HAL_TIM_GET_COUNTER(&htim1); // 实时读取计数值}HAL_TIM_Base_Stop(&htim1); // 停止 TIM1 计数
}
4.2.2 数据线输出模式切换(STM32 发送信号)
/*** @brief 设置数据线为输出模式,并控制电平* @param flag:0=拉低数据线,1=拉高数据线* @retval 无* @note 单总线需频繁切换 IO 模式,此函数封装输出模式配置*/
static void DATA_OUTPUT(u8 flag)
{GPIO_InitTypeDef GPIO_InitStruct = {0}; // GPIO 初始化结构体// 配置 PA7 为推挽输出模式GPIO_InitStruct.Pin = DATA_PIN;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出GPIO_InitStruct.Pull = GPIO_NOPULL; // 无上下拉(输出模式无需)GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;// 高速模式HAL_GPIO_Init(DATA_GPIO_Port, &GPIO_InitStruct);// 根据 flag 控制电平if (flag == 0){DATA_RESET(); // 拉低数据线}else{DATA_SET(); // 拉高数据线}
}
4.2.3 数据线输入模式切换(STM32 接收信号)
/*** @brief 设置数据线为输入模式,并读取电平* @param 无* @retval 0=数据线低电平,1=数据线高电平* @note 配置为上拉输入,避免引脚悬空导致误判*/
static u8 DATA_INPUT(void)
{GPIO_InitTypeDef GPIO_InitStruct = {0};u8 flag = 0; // 存储读取到的电平状态// 配置 PA7 为上拉输入模式GPIO_InitStruct.Pin = DATA_PIN;GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 输入模式GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉电阻(防止悬空)HAL_GPIO_Init(DATA_GPIO_Port, &GPIO_InitStruct);// 读取数据线电平并返回if (DATA_READ() == GPIO_PIN_RESET){flag = 0; // 低电平}else{flag = 1; // 高电平}return flag;
}
4.2.4 读取 1 字节数据(8 位)
/*** @brief 读取 DHT11 发送的 1 字节数据(8 位二进制)* @param 无* @retval 读取到的 1 字节数据* @note 通过高电平持续时间区分 0 和 1,加入超时机制防止程序卡死*/
static u8 DHT11_Read_Byte(void)
{u8 ReadDat = 0; // 存储最终读取的字节数据u8 temp = 0; // 存储每一位的二进制值(0/1)u8 retry = 0; // 超时计数(防止死循环)u8 i = 0; // 循环变量(8 位数据)// 循环 8 次,读取 8 位数据for (i = 0; i < 8; i++){// 1. 等待 DHT11 拉低数据线(每一位数据前先拉低 50μs)while (DATA_READ() == 0 && retry < 100){Delay_us(1);retry++;}retry = 0; // 重置超时计数// 2. 延时 40μs:数据 0 的高电平(26~28μs)会在此期间结束,数据 1 仍为高电平Delay_us(40);// 3. 判断当前电平:高电平则为 1,低电平则为 0if (DATA_READ() == 1){temp = 1;}else{temp = 0;}// 4. 等待 DHT11 拉低数据线(当前位数据传输结束)while (DATA_READ() == 1 && retry < 100){Delay_us(1);retry++;}retry = 0; // 重置超时计数// 5. 数据封装:左移 1 位空出最低位,将当前位值存入ReadDat <<= 1;ReadDat |= temp;}return ReadDat; // 返回读取到的 1 字节数据
}
4.2.5 完整数据采集(起始→应答→读取→校验)
/*** @brief 完整的 DHT11 数据采集函数* @param 无* @retval 1=采集成功,0=采集失败(校验不通过或无应答)* @note 整合起始信号、应答信号、数据读取、校验逻辑*/
static u8 DHT11_Read(void)
{u8 retry = 0; // 超时计数u8 i = 0; // 循环变量(5 字节数据)// 1. 发送起始信号(STM32 → DHT11)DATA_OUTPUT(0); // 数据线输出模式,拉低HAL_Delay(18); // 拉低 18ms(必须≥18ms)DATA_OUTPUT(1); // 拉高数据线Delay_us(20); // 拉高 20μs(等待应答)// 2. 切换为输入模式,接收应答信号(DHT11 → STM32)DATA_INPUT();Delay_us(20); // 等待应答信号稳定// 3. 判断是否收到应答(先低后高)if (DATA_READ() == 0){// 3.1 等待应答低电平结束(80μs)while (DATA_READ() == 0 && retry < 100){Delay_us(1);retry++;}retry = 0;// 3.2 等待应答高电平结束(80μs)while (DATA_READ() == 1 && retry < 100){Delay_us(1);retry++;}retry = 0;// 4. 读取 5 字节数据(40 位)for (i = 0; i < 5; i++){DHT11_data.Data[i] = DHT11_Read_Byte();}Delay_us(50); // 数据读取后延时,确保稳定}// 5. 校验数据(前 4 字节之和 = 第 5 字节)u32 sum = DHT11_data.Data[0] + DHT11_data.Data[1] + DHT11_data.Data[2] + DHT11_data.Data[3];if (sum == DHT11_data.Data[4]){// 校验通过,解析温湿度(仅取整数部分,小数部分通常为 0)DHT11_data.humidity = DHT11_data.Data[0]; // 湿度整数DHT11_data.temp = DHT11_data.Data[2]; // 温度整数return 1; // 采集成功}else{return 0; // 校验失败,数据无效}
}
4.2.6 采集任务封装(对外接口)
/*** @brief DHT11 采集任务(供 main.c 调用)* @param 无* @retval 无* @note 简化外部调用,加入采集次数统计(可选)*/
void DHT11_Task(void)
{if (DHT11_Read()) // 若采集成功{DHT11_data.index++; // 采集次数+1if (DHT11_data.index >= 128) // 防止 index 溢出{DHT11_data.index = 0;}}
}
4.3 串口重定向(usart.c)
在 usart.c
中实现 fputc
函数,让 printf
通过串口 1 打印温湿度数据:
#include <stdio.h> // 包含 printf 所需头文件/*** @brief 串口1 重定向函数,printf 输出到串口* @param ch:要输出的字符* @param f:文件指针(标准输出,无需关注)* @retval 输出的字符*/
int fputc(int ch, FILE *f)
{// 发送 1 个字符到串口1,超时时间 1000msHAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 1000);return ch;
}
Keil 配置(支持 printf)
- 打开工程
Options for Target → Target
,勾选Use MicroLIB
(启用微库); - 进入
Debug → Settings → Flash Download
,勾选Reset and Run
(下载后自动运行)。
五、测试验证与结果分析
在 main.c
中调用 DHT11 采集任务,周期性打印温湿度数据:
#include "dht11.h"int main(void)
{//主循环:每 3 秒采集并打印一次温湿度while (1){DHT11_Task(); // 采集温湿度// 打印数据(仅整数部分,DHT11 小数部分通常为 0)printf("Temp is %d ℃\r\n", DHT11_data.temp);printf("Hum is %d %%RH\r\n", DHT11_data.humidity);printf("------------------------\r\n");HAL_Delay(3000); // 间隔 3 秒(≥DHT11 响应时间 2s)}
}
测试结果
- 硬件接线:DHT11 VCC→3.3V、GND→GND、DATA→PA7;
- 串口工具设置:波特率 115200、数据位 8、停止位 1、校验位 None;
- 预期结果:串口每 3 秒打印一次温湿度,示例如下:
六、总结
本次笔记完整实现了 DHT11 温湿度传感器的驱动开发,核心收获包括:
- 理解单总线协议的时序逻辑:起始信号、应答信号、数据传输的时间要求是采集成功的关键;
- 掌握 IO 模式动态切换:单总线需频繁在 “输出(发信号)” 和 “输入(收信号)” 之间切换;
- 学会数据校验与异常处理:通过校验和判断数据有效性,加入超时机制防止程序卡死。
下一篇我们讲解IIC协议并用OLED屏幕实时显示信息,请关注 Hello_Embed,持续更新环境监测项目!