DHT11实验_定时器实现us延时

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-08-29 08:09:36 Views: 0 Likes: 0 Comments: 0

143.5.15DHT11实验(定时器实现us延时)

/* main.c */
#include "main.h"  // 提供 HAL 类型与错误处理函数
#include "tim.h"   // 声明 TIM1 句柄和微秒延时所需的定时器初始化函数
#include "usart.h" // 声明 USART1 初始化函数和课程使用的 printf 宏
#include "gpio.h"  // 声明 GPIO 时钟初始化函数
#include "dht11.h" // 声明 DHT11 数据数组与读取接口

void SystemClock_Config(void); // 声明 72 MHz 系统时钟配置函数

int main(void) // 程序入口:初始化外设并每秒读取一次 DHT11
{
    HAL_Init();             // 初始化 HAL 库、SysTick 和中断优先级基础配置
    SystemClock_Config();   // 使用外部晶振和 PLL 配置系统时钟
    MX_GPIO_Init();         // 开启 DHT11 所在 GPIOA 的端口时钟
    MX_TIM1_Init();         // 将 TIM1 配成 1 MHz 计数,供 Delay_us() 使用
    MX_USART1_UART_Init();  // 初始化 USART1,供温湿度数据输出

    while (1) // 周期性读取并显示 DHT11 数据
    {
        if (DHT_Read()) // 返回 1 时表示课程代码的校验通过
        {
            printf(" H:%u  T:%u \r\n", Data[0], Data[2]); // 输出整数湿度和整数温度
        }
        HAL_Delay(1000); // 两次读取之间等待 1 s
    }
}

void SystemClock_Config(void) // 配置 72 MHz 系统时钟及总线分频
{
    RCC_OscInitTypeDef RCC_OscInitStruct = {0}; // 保存 HSE、HSI 和 PLL 振荡器配置
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; // 保存系统总线时钟与分频配置

    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; // 选择外部高速晶振
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;                   // 开启 HSE
    RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;    // HSE 不预分频
    RCC_OscInitStruct.HSIState = RCC_HSI_ON;                   // 保持内部高速时钟开启
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;                // 开启 PLL
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;        // PLL 输入选择 HSE
    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;                // 8 MHz 乘 9 得到 72 MHz
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)        // 写入振荡器配置
    {
        Error_Handler(); // 振荡器配置失败
    }

    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | // 指定要配置的时钟域
                                  RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;  // 同时配置 APB1 与 APB2
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 系统时钟取自 PLL
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;         // HCLK = 72 MHz
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;          // PCLK1 = 36 MHz
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;          // PCLK2 = 72 MHz,TIM1 也取 72 MHz
    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) // 设置总线时钟
    {
        Error_Handler(); // 总线时钟配置失败
    }
}

void Error_Handler(void) // HAL 初始化失败时统一进入的错误处理函数
{
    __disable_irq(); // 发生不可恢复错误时关闭全局中断
    while (1)       // 停留在错误状态,便于调试定位
    {
    }
}

/* dht11.c */
#include "stm32f1xx.h" // 提供 STM32F1 寄存器与基础类型定义
#include "dht11.h"     // 提供 DHT11 接口和 TIM1 句柄声明

uint8_t Data[5] = {0x00, 0x00, 0x00, 0x00, 0x00}; // 保存湿度、温度和校验字节

void Delay_us(uint16_t us) // 使用 1 MHz 的 TIM1 产生约 us 微秒阻塞延时
{
    uint16_t differ = 0xffff - us; // 计算倒数 us 个计数位置作为起点

    __HAL_TIM_SET_COUNTER(&htim1, differ); // 将 TIM1 计数器写入起始值
    HAL_TIM_Base_Start(&htim1);           // 启动 TIM1 向上计数
    while (differ < 0xffff)               // 轮询等待计数器到达 0xFFFF
    {
        differ = __HAL_TIM_GET_COUNTER(&htim1); // 读取当前计数值
    }
    HAL_TIM_Base_Stop(&htim1); // 到达目标计数后停止 TIM1
}

void DHT_GPIO_SET_OUTPUT(void) // 将 PA5 切换为主机发送起始信号所需的输出模式
{
    GPIO_InitTypeDef GPIO_InitStructure; // 保存 PA5 输出配置

    GPIO_InitStructure.Pin = GPIO_PIN_5;             // DHT11 DATA 连接 PA5
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;   // 主机发送起始信号时使用推挽输出
    GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH; // 使用高速 GPIO 输出
    HAL_GPIO_Init(GPIOA, &GPIO_InitStructure);        // 将输出配置写入 GPIOA
}

void DHT_GPIO_SET_INPUT(void) // 将 PA5 切换为读取传感器应答和数据的输入模式
{
    GPIO_InitTypeDef GPIO_InitStructure; // 保存 PA5 输入配置

    GPIO_InitStructure.Pin = GPIO_PIN_5;             // DHT11 DATA 仍使用 PA5
    GPIO_InitStructure.Mode = GPIO_MODE_INPUT;       // 释放总线并读取传感器数据
    GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH; // 保留课程工程中的速度配置
    HAL_GPIO_Init(GPIOA, &GPIO_InitStructure);        // 将输入配置写入 GPIOA
}

uint8_t DHT_Read_Byte(void) // 按 DHT11 高位先发的时序读取一个字节
{
    uint8_t ReadData = 0; // 累积本次接收的 8 个数据位
    uint8_t temp;         // 暂存当前位判断结果 0 或 1
    uint8_t retry = 0;    // 等待电平变化时的超时计数
    uint8_t i;            // 8 位数据循环下标

    for (i = 0; i < 8; i++) // DHT11 每个字节固定发送 8 位
    {
        while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == 0 && retry < 100) // 等待每一位起始低电平结束
        {
            Delay_us(1); // 每次等待约 1 us
            retry++;     // 防止信号异常时永久卡在循环中
        }
        retry = 0;   // 为等待本位高电平结束重新计数
        Delay_us(40); // 在高电平开始约 40 us 后读取电平,用脉宽区分 0 和 1
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == 1) // 40 us 后仍为高电平则判为 1
        {
            temp = 1; // 保存逻辑 1
        }
        else // 40 us 后已经为低电平,当前位为 0
        {
            temp = 0; // 40 us 前已经变低则判为逻辑 0
        }

        while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == 1 && retry < 100) // 等待当前位高电平结束
        {
            Delay_us(1); // 每次等待约 1 us
            retry++;     // 累加等待时间并限制最大循环次数
        }
        retry = 0; // 为下一位重新计数

        ReadData <<= 1; // 为当前位腾出最低位位置,DHT11 高位先发
        ReadData |= temp; // 把当前位写入结果最低位
    }

    return ReadData; // 返回组合完成的一个字节
}

uint8_t DHT_Read(void) // 发送起始信号、接收五字节并执行课程中的校验
{
    uint8_t retry = 0; // 等待 DHT11 应答电平时的超时计数
    uint8_t i;         // 五个数据字节的循环下标

    DHT_GPIO_SET_OUTPUT();                                    // PA5 切换为输出,准备发送起始信号
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);      // 主机拉低 DATA
    HAL_Delay(18);                                             // 低电平保持 18 ms
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);        // 主机先把 DATA 拉高
    Delay_us(20);                                              // 高电平保持约 20 us

    DHT_GPIO_SET_INPUT();                                      // PA5 切换为输入,把总线交给 DHT11
    Delay_us(20);                                              // 等待 DHT11 开始应答
    if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_RESET) // 检测到应答低电平后继续读取
    {
        while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_RESET && retry < 100) // 等应答低电平结束
        {
            Delay_us(1); // 每次等待约 1 us
            retry++;     // 限制低电平等待次数
        }
        retry = 0; // 为应答高电平等待重新计数

        while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_SET && retry < 100) // 等应答高电平结束
        {
            Delay_us(1); // 每次等待约 1 us
            retry++;     // 限制高电平等待次数
        }
        retry = 0; // 应答完成,清零计数

        for (i = 0; i < 5; i++) // 连续读取湿度、温度和校验共 5 字节
        {
            Data[i] = DHT_Read_Byte(); // 按发送顺序保存当前字节
        }
        Delay_us(50); // 按课程代码在一帧读取后等待 50 us
    }

    uint32_t sum = Data[0] + Data[1] + Data[2] + Data[3]; // 计算前四个数据字节之和
    if (sum == Data[4])                                  // 与第五个校验字节比较
    {
        return 1; // 校验相等,通知主循环打印数据
    }
    else // 前四字节之和与校验字节不相等
    {
        return 0; // 校验不相等,本次数据不打印
    }
}

/* dht11.h */
#ifndef __DHT11_H // 防止 dht11.h 被重复包含
#define __DHT11_H // 定义 dht11.h 的头文件保护宏

#include "stm32f1xx_hal.h" // 获取 HAL GPIO 和定时器接口
#include "stm32f1xx.h"     // 获取 STM32F1 基础定义

extern uint8_t Data[5];          // 引用 dht11.c 中的五字节接收数组
extern TIM_HandleTypeDef htim1;  // 引用 tim.c 中用于微秒延时的 TIM1 句柄

void Delay_us(uint16_t us);          // 声明微秒级阻塞延时函数
void DHT_GPIO_SET_OUTPUT(void);      // 声明 PA5 输出模式切换函数
void DHT_GPIO_SET_INPUT(void);       // 声明 PA5 输入模式切换函数
uint8_t DHT_Read_Byte(void);         // 声明单字节读取函数
uint8_t DHT_Read(void);              // 声明一帧 DHT11 数据读取函数

#endif // dht11.h 头文件保护结束

/* tim.c */
#include "tim.h" // 引入 TIM1 句柄声明和 HAL 定时器接口

TIM_HandleTypeDef htim1; // 保存 TIM1 的实例和初始化参数

void MX_TIM1_Init(void) // 将 TIM1 配成 1 MHz 向上计数的基本定时器
{
    TIM_ClockConfigTypeDef sClockSourceConfig = {0}; // 配置 TIM1 的计数时钟源
    TIM_MasterConfigTypeDef sMasterConfig = {0};     // 配置主从同步与触发输出

    htim1.Instance = TIM1;                                        // 选择高级定时器 TIM1
    htim1.Init.Prescaler = 71;                                    // 72 MHz 除以 72,计数周期为 1 us
    htim1.Init.CounterMode = TIM_COUNTERMODE_UP;                   // 使用向上计数
    htim1.Init.Period = 65535;                                    // 自动重装值设为 16 位最大值
    htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;             // 不额外分频定时器采样时钟
    htim1.Init.RepetitionCounter = 0;                              // 不使用高级定时器重复计数
    htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; // 禁用 ARR 预装载
    if (HAL_TIM_Base_Init(&htim1) != HAL_OK)                       // 初始化 TIM1 基本计数单元
    {
        Error_Handler(); // TIM1 基本初始化失败
    }

    sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; // 使用内部定时器时钟
    if (HAL_TIM_ConfigClockSource(&htim1, &sClockSourceConfig) != HAL_OK) // 写入时钟源配置
    {
        Error_Handler(); // TIM1 时钟源配置失败
    }

    sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;          // 不向其他外设输出特殊触发
    sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; // 不使用定时器主从同步
    if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK) // 写入主模式配置
    {
        Error_Handler(); // 主模式配置失败
    }
}

void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *tim_baseHandle) // 为 TIM1 开启外设时钟
{
    if (tim_baseHandle->Instance == TIM1) // 只为 TIM1 执行底层初始化
    {
        __HAL_RCC_TIM1_CLK_ENABLE(); // 开启 TIM1 外设时钟
    }
}

void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef *tim_baseHandle) // 反初始化时关闭 TIM1 时钟
{
    if (tim_baseHandle->Instance == TIM1) // 只为 TIM1 执行底层反初始化
    {
        __HAL_RCC_TIM1_CLK_DISABLE(); // 关闭 TIM1 外设时钟
    }
}

/* tim.h */
#ifndef __TIM_H__ // 防止 tim.h 被重复包含
#define __TIM_H__ // 定义 tim.h 的头文件保护宏

#include "main.h" // 获取 HAL 定时器句柄类型

extern TIM_HandleTypeDef htim1; // 引用 tim.c 中唯一的 TIM1 句柄

void MX_TIM1_Init(void); // 声明 TIM1 初始化函数

#endif /* __TIM_H__ */ // tim.h 头文件保护结束

/* usart.c */
#include "usart.h" // 引入 USART1 句柄、缓冲区和初始化声明

uint8_t u_buf[256];        // 保存 printf 格式化后的待发送文本
UART_HandleTypeDef huart1; // 保存 USART1 的实例和配置参数

void MX_USART1_UART_Init(void) // 配置 USART1 为 115200-8-N-1
{
    huart1.Instance = USART1;                        // 选择 USART1 外设
    huart1.Init.BaudRate = 115200;                   // 波特率设为 115200 bit/s
    huart1.Init.WordLength = UART_WORDLENGTH_8B;     // 每帧 8 位数据
    huart1.Init.StopBits = UART_STOPBITS_1;          // 使用 1 位停止位
    huart1.Init.Parity = UART_PARITY_NONE;           // 不使用奇偶校验
    huart1.Init.Mode = UART_MODE_TX_RX;              // 同时使能发送和接收
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;     // 不使用 RTS/CTS 硬件流控
    huart1.Init.OverSampling = UART_OVERSAMPLING_16; // 使用 16 倍过采样
    if (HAL_UART_Init(&huart1) != HAL_OK)            // 初始化 USART1 并检查返回值
    {
        Error_Handler(); // USART1 初始化失败
    }
}

void HAL_UART_MspInit(UART_HandleTypeDef *uartHandle) // 配置 USART1 时钟与 PA9/PA10
{
    GPIO_InitTypeDef GPIO_InitStruct = {0}; // 保存 USART1 引脚配置

    if (uartHandle->Instance == USART1) // 只为 USART1 执行底层初始化
    {
        __HAL_RCC_USART1_CLK_ENABLE(); // 开启 USART1 外设时钟
        __HAL_RCC_GPIOA_CLK_ENABLE();  // 开启 PA9、PA10 所在 GPIOA 时钟

        GPIO_InitStruct.Pin = GPIO_PIN_9;             // PA9 对应 USART1_TX
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;       // TX 配置为复用推挽输出
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 使用高速输出模式
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);        // 应用 PA9 配置

        GPIO_InitStruct.Pin = GPIO_PIN_10;      // PA10 对应 USART1_RX
        GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // RX 配置为输入
        GPIO_InitStruct.Pull = GPIO_NOPULL;     // 不启用内部上下拉
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);  // 应用 PA10 配置
    }
}

void HAL_UART_MspDeInit(UART_HandleTypeDef *uartHandle) // 关闭 USART1 时钟并复位引脚
{
    if (uartHandle->Instance == USART1) // 只为 USART1 执行底层反初始化
    {
        __HAL_RCC_USART1_CLK_DISABLE();                     // 关闭 USART1 外设时钟
        HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9 | GPIO_PIN_10);   // 复位 PA9 与 PA10 配置
    }
}

/* usart.h */
#ifndef __USART_H__ // 防止 usart.h 被重复包含
#define __USART_H__ // 定义 usart.h 的头文件保护宏

#include "main.h" // 获取 HAL 串口句柄类型

extern UART_HandleTypeDef huart1; // 引用 usart.c 中的 USART1 句柄
extern uint8_t u_buf[256];        // 引用 usart.c 中的格式化缓冲区

// 将可变参数先格式化到 u_buf,再通过 USART1 阻塞发送。
#define printf(...) /* 接收与标准 printf 相同的可变参数 */ \
    HAL_UART_Transmit(&huart1, (uint8_t *)u_buf, /* 用 USART1 发送 u_buf */ \
                      sprintf((char *)u_buf, __VA_ARGS__), 0xffff) /* sprintf 返回发送长度 */

void MX_USART1_UART_Init(void); // 声明 USART1 初始化函数

#endif /* __USART_H__ */ // usart.h 头文件保护结束

/* gpio.c */
#include "gpio.h" // 引入 GPIO 初始化声明

void MX_GPIO_Init(void) // 开启 DHT11 与 USART1 所在的 GPIOA 时钟
{
    __HAL_RCC_GPIOA_CLK_ENABLE(); // 开启 PA5、PA9 和 PA10 所在 GPIOA 时钟
}

/* gpio.h */
#ifndef __GPIO_H__ // 防止 gpio.h 被重复包含
#define __GPIO_H__ // 定义 gpio.h 的头文件保护宏

#include "main.h" // 获取 HAL 基础类型

void MX_GPIO_Init(void); // 声明 GPIO 时钟初始化函数

#endif /* __GPIO_H__ */ // gpio.h 头文件保护结束

/* main.h */
#ifndef __MAIN_H // 防止 main.h 被重复包含
#define __MAIN_H // 定义 main.h 的头文件保护宏

#include "stm32f1xx_hal.h" // 引入 STM32F1 HAL 库类型与接口

void Error_Handler(void); // 声明统一错误处理函数

#endif /* __MAIN_H */ // main.h 头文件保护结束

一、DHT11温湿度传感器实验

1.1 任务资源图

  • 本实验使用 STM32F103ZET6 读取 DHT11 温湿度传感器,并经 USART1 输出温度和湿度。
  • HAL_Delay() 的单位是毫秒,而 DHT11 的应答和数据位判别需要几十微秒的时序,因此使用 TIM1 实现 Delay_us()
  • TIM1 在本实验中只作为由程序启动、读取和停止的计数器,不使用 PWM、输入捕获、外部通道或定时器中断。
  • DHT11 的数据线使用 PA5,源码中的 GPIOAGPIO_PIN_5 是实际依据。

1.2 实验演示

  • DHT11 三针模块与 STM32 的连接如下:
STM32F103ZET6                 DHT11 三针模块
-------------                ----------------
3.3V  ---------------------> VCC
GND   ---------------------> GND
PA5   <--------------------> DATA
  • DATA 是双向数据线。STM32 发起通信时将 PA5 配为推挽输出并拉低;随后将 PA5 切为输入,释放总线,由 DHT11 输出应答和 40 位数据。双方不能同时驱动该线。
  • 视频画面能确认本课使用的是带小电路板的三针 DHT11 模块,但看不清板上 DATA 是否已接上拉电阻,因此不能直接断言“该模块一定自带上拉”。实际接线前应查看所购模块原理图,或断电后测量 DATAVCC 之间的电阻;若没有上拉,再按模块手册连接外部上拉电阻。
  • 课程说明该模块的 VCC 可接 3.3 V 或 5 V。供电电压和 DATA 上拉电压是两件事:若模块把 DATA 上拉到 VCC,使用 5 V 供电时还必须核对 STM32 对应引脚的输入电压规格;没有模块原理图时,不应凭外观判断数据线电平。
  • 串口连接为 PA9 -> USB 转串口模块 RXPA10 <- USB 转串口模块 TXGND -> GND,参数为 115200 bit/s、8 位数据位、1 位停止位、无校验。
  • 用手靠近传感器或改变周围空气状态时,串口中的温湿度会缓慢变化。DHT11 的采样间隔应不小于 1 s。

1.3 代码分析

1.3.1 系统时钟配置
  • 系统时钟为 72 MHz。TIM1 位于 APB2,APB2 未分频时 TIM1 输入时钟也是 72 MHz。
  • MX_GPIO_Init() 只需要开启 GPIOA 时钟;TIM1 没有配置外部通道,因此不需要为 TIM1 分配输入捕获或 PWM 引脚。
  • MX_TIM1_Init()MX_USART1_UART_Init() 必须在第一次调用 DHT_Read() 前完成。
1.3.2 定时器一初始化
  • TIM1 使用内部时钟,向上计数,Prescaler=71Period=65535,不启用中断。
  • 预分频器的实际分频系数是 71 + 1 = 72,因此计数频率为:

$ f_{CNT}=\frac{72\ \text{MHz}}{71+1}=1\ \text{MHz} $

  • 计数器每增加 1 就经过 $1\ \mu s$。16 位计数器的范围是 0 到 65535,单次延时上限约为 65.535 ms。
1.3.3 DHT11分析
  • DHT_GPIO_SET_OUTPUT() 将 PA5 配置为推挽输出,用于发送开始信号;DHT_GPIO_SET_INPUT() 将 PA5 配置为输入,用于接收传感器应答和数据。
  • DHT_Read() 的流程是:PA5 拉低至少 18 ms,拉高约 20 us,切换为输入,等待 DHT11 的低电平和高电平应答,再读取 5 个字节。
  • DHT11 发送的数据依次为湿度整数、湿度小数、温度整数、温度小数和校验和。本实验打印 Data[0]Data[2],即整数湿度和整数温度。
1.3.4 DHT11_Read_Byte分析
  • DHT_Read_Byte() 连续读取 8 位。每一位先等待约 50 us 的低电平结束,再等待 40 us 后读取电平。
  • 高电平约 26 到 28 us 表示 0,约 70 us 表示 1;在高电平开始 40 us 后采样,短高电平已经结束,长高电平仍然存在。
  • 每读取一位执行一次左移并写入当前位,最终按照高位在前的顺序组成一个字节。
1.3.5 Delay_us分析
  • Delay_us() 为 DHT11 的应答等待和位判别提供微秒级延时;毫秒级的开始信号仍使用 HAL_Delay(18)
  • DHT11 协议规定,校验字节等于前四个数据字节之和的低 8 位:

$ \text{checksum}=(Data[0]+Data[1]+Data[2]+Data[3])\ &\ 0xFF $

  • 课程源码实际使用 uint32_t sum,并直接比较 sum == Data[4],没有显式截取低 8 位。本课正常温湿度数据之和未超过一个字节时,两种写法结果相同;这里按课程源码呈现,不擅自改写其判断逻辑。
  • retry < 100 的作用是避免等待电平时永久卡死,但读取函数没有单独返回“位超时”状态,最终仍依靠校验结果判断本帧是否可用。

1.4 Delay_us分析

  • Delay_us(us) 先把 TIM1 的计数器写为 0xFFFF-us,启动 TIM1,再轮询当前计数值。计数到 0xFFFF 时停止定时器并返回。
Delay_us(20)

CNT: 65515 -> 65516 -> ... -> 65535
       |<--------- 20 个 1 us 计数周期 --------->|
  • 该函数是忙等延时,CPU 在等待期间不能处理其他任务,适合 DHT11 所需的几十微秒短时序;长时间延时应使用 SysTick、定时器中断或操作系统延时机制。
  • 这种方法的核心是先将定时器配置成已知计数单位,再通过写入计数器起点、启动、读取计数值和停止得到可控短延时。

1.5 配置界面配置定时器

  • 在 CubeMX 中将 TIM1 的 Clock Source 设为 Internal Clock,Prescaler 设为 71,Counter Mode 设为 Up,Counter Period 设为 65535。
  • 不配置 PWM、输入捕获通道,也不启用 TIM1 中断;Auto-reload preload 可保持 Disable。
  • dht11.cdht11.h 加入工程,dht11.h 中使用正确的 htim1 句柄。使用其他定时器时,需要同步修改句柄和初始化函数。

1.6 头文件透析

  • dht11.h 应声明 Data[5]Delay_us()、GPIO 模式切换函数和读取函数;tim.h 应声明 htim1
  • 复用驱动时,将 GPIOAGPIO_PIN_5 改为实际接入的端口和引脚,并保证数据线有可靠上拉、传感器供电与 MCU 数据引脚电平兼容。
  • 主循环在每次读取后执行 HAL_Delay(1000)。校验失败时不会输出本次数据;但课程源码在未检测到新应答时不会清空 Data[],若数组中仍保存着上一帧有效数据,仍可能再次打印旧值。这是阅读当前代码时需要知道的实际行为,不代表本节还缺少了一段源码。
  • 排查顺序:检查 VCC、GND、DATA 与 PA5 的连接;检查上拉与供电;检查 TIM1 的 1 MHz 计数配置;最后检查串口、源文件是否加入工程及初始化顺序。