STM32 环境监测项目笔记(一):DHT11 温湿度传感器原理与驱动实现

本系列笔记是笔者学习 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 "准备采集数据",时序要求:

  1. 数据线(PA7)拉低 18ms(必须≥18ms,否则 DHT11 不响应);
  2. 数据线拉高 20~40μs(等待 DHT11 应答);
  3. 此时 STM32 需将数据线切换为 输入模式,准备接收 DHT11 的应答信号。
2.2.2 2. 应答信号(DHT11 → STM32)

DHT11 检测到起始信号后,主动发送应答信号,时序特征:

  1. 数据线拉低 80μs(表示 "已收到起始信号");
  2. 数据线拉高 80μs(表示 "准备发送数据");
  3. 应答信号结束后,进入数据传输阶段。
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(校验通过);
  • 位数据区分 :DHT11 通过 "高电平持续时间" 区分 0 和 1:
    • 数据 0:低电平 50μs → 高电平 26~28μs;
    • 数据 1:低电平 50μs → 高电平 70μs;
  • 校验规则:前 4 字节之和 = 第 5 字节(校验和),若不相等则数据无效。

三、CubeMX 工程配置

本次使用 STM32F103C8T6 最小系统板,新建 CubeMX 工程,配置步骤如下:

3.1 基础配置(芯片 / 时钟 / Debug)

  1. 芯片选择 :搜索并选择 STM32F103C8T6
  2. Debug 配置 :进入 System Core → SYS,Debug 选择 Serial Wire(必须设置,否则无法烧录);
  3. 时钟配置
    • 进入 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 位定时器,满足延时需求):

  1. 进入 Timers → TIM1,模式选择 Internal Clock
  2. 参数配置:
    • Prescaler(预分频值):72 - 1(72MHz 时钟 / 72 = 1MHz,即 1 次计数 = 1μs);
    • Counter Period(ARR):65535(16 位定时器最大计数,避免频繁溢出);
  3. 配置截图如下:

3.3 串口 1 配置(数据打印)

通过串口 1 打印温湿度数据,配置如下:

  1. 进入 Connectivity → USART1,模式选择 Asynchronous(异步通信);
  2. 基本参数:波特率 115200,数据位 8,停止位 1,校验位 None(默认配置,无需修改);
  3. 引脚:默认 PA9(TX)、PA10(RX),无需手动调整。
工程生成
  1. 进入 Project Manager → Code Generator,勾选 Generate peripheral initialization as a pair of .c/.h files per peripheral
  2. 选择工程路径,Toolchain/IDE 设为 MDK-ARM,点击 Generate Code 生成工程。

四、DHT11 驱动代码实现(带详细注释)

新建 driver 文件夹,创建 dht11.cdht11.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)
  1. 打开工程 Options for Target → Target,勾选 Use MicroLIB(启用微库);
  2. 进入 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)
    }
}
测试结果
  1. 硬件接线:DHT11 VCC→3.3V、GND→GND、DATA→PA7;
  2. 串口工具设置:波特率 115200、数据位 8、停止位 1、校验位 None;
  3. 预期结果 :串口每 3 秒打印一次温湿度,示例如下:

六、总结

本次笔记完整实现了 DHT11 温湿度传感器的驱动开发,核心收获包括:

  1. 理解单总线协议的时序逻辑:起始信号、应答信号、数据传输的时间要求是采集成功的关键;
  2. 掌握 IO 模式动态切换:单总线需频繁在 "输出(发信号)" 和 "输入(收信号)" 之间切换;
  3. 学会数据校验与异常处理:通过校验和判断数据有效性,加入超时机制防止程序卡死。

下一篇我们讲解IIC协议并用OLED屏幕实时显示信息,请关注 Hello_Embed,持续更新环境监测项目!

相关推荐
程序员大雄学编程4 小时前
「深度学习笔记4」深度学习优化算法完全指南:从梯度下降到Adam的实战详解
笔记·深度学习·算法·机器学习
新子y5 小时前
【小白笔记】PyTorch 和 Python 基础的这些问题
pytorch·笔记·python
三佛科技-134163842125 小时前
便携式榨汁机方案开发,榨汁机果汁机MCU控制方案设计
单片机·嵌入式硬件·智能家居·pcb工艺
逐步前行5 小时前
C标准库--浮点<float.h>
c语言·开发语言
yongui478345 小时前
基于TMS320F28027实现光伏MPPT控制
单片机·嵌入式硬件
rechol5 小时前
类与对象(中)笔记整理
java·javascript·笔记
水饺编程5 小时前
第3章,[标签 Win32] :窗口类03,窗口过程函数字段
c语言·c++·windows·visual studio
新子y6 小时前
【小白笔记】KNN 核心预测函数 _predict_one 的过程
笔记
橘子是码猴子6 小时前
LangExtract:基于LLM的信息抽取框架 学习笔记
笔记·学习