本系列笔记是笔者学习 B 站 up 主 "技术探索者" STM32 系列视频所作的记录,不理解的地方推荐观看视频~
目录
- 一、前言
- [二、DHT11 模块核心认知](#二、DHT11 模块核心认知)
- [2.1 模块特性与接线](#2.1 模块特性与接线)
- [2.2 单总线协议时序图解析](#2.2 单总线协议时序图解析)
- [三、CubeMX 工程配置](#三、CubeMX 工程配置)
- [3.1 基础配置(芯片 / 时钟 / Debug)](#3.1 基础配置(芯片 / 时钟 / Debug))
- [3.2 TIM1 配置(1μs 高精度延时)](#3.2 TIM1 配置(1μs 高精度延时))
- [3.3 串口 1 配置(数据打印)](#3.3 串口 1 配置(数据打印))
- [四、DHT11 驱动代码实现(带详细注释)](#四、DHT11 驱动代码实现(带详细注释))
- [4.1 头文件(dht11.h):宏定义与结构体](#4.1 头文件(dht11.h):宏定义与结构体)
- [4.2 核心函数:延时 / IO 模式切换 / 数据读取](#4.2 核心函数:延时 / IO 模式切换 / 数据读取)
- [4.3 串口重定向(usart.c)](#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):宏定义与结构体
定义数据线引脚、数据类型别名、存储温湿度的结构体,声明核心函数:
c
#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)
c
#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 发送信号)
c
/**
* @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 接收信号)
c
/**
* @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 位)
c
/**
* @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,低电平则为 0
if (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 完整数据采集(起始→应答→读取→校验)
c
/**
* @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 采集任务封装(对外接口)
c
/**
* @brief DHT11 采集任务(供 main.c 调用)
* @param 无
* @retval 无
* @note 简化外部调用,加入采集次数统计(可选)
*/
void DHT11_Task(void)
{
if (DHT11_Read()) // 若采集成功
{
DHT11_data.index++; // 采集次数+1
if (DHT11_data.index >= 128) // 防止 index 溢出
{
DHT11_data.index = 0;
}
}
}
4.3 串口重定向(usart.c)
在 usart.c
中实现 fputc
函数,让 printf
通过串口 1 打印温湿度数据:
c
#include <stdio.h> // 包含 printf 所需头文件
/**
* @brief 串口1 重定向函数,printf 输出到串口
* @param ch:要输出的字符
* @param f:文件指针(标准输出,无需关注)
* @retval 输出的字符
*/
int fputc(int ch, FILE *f)
{
// 发送 1 个字符到串口1,超时时间 1000ms
HAL_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 采集任务,周期性打印温湿度数据:
c
#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,持续更新环境监测项目!