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

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

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

/*
 * 整体目的:使用 TIM1 构造微秒级阻塞延时,按 DHT11 单总线时序读取 5 字节温湿度数据。
 * 时钟依据:TIM1CLK=72 MHz,PSC=71 后计数频率为 1 MHz,因此 CNT 每增加 1 约经过 1 μs。
 * 引脚依据:DHT11 DATA 接 PA5;主机发送起始信号时 PA5 为输出,读取应答和数据时切为输入。
 * 协议流程:主机拉低至少 18 ms → 释放总线 → 传感器应答 → 读取 40 位
 *           → 前 4 字节求和的低 8 位与第 5 字节校验。
 * 代码范围:摘自 CubeIDE/09DHT11 的 main.c、tim.c 和 dht11.c;
 *           HAL 定时器/GPIO/串口库内部不逐行展开。
 */

/* ==================== Core/Src/tim.c:1 MHz TIM1 自由计数基础 ==================== */

TIM_HandleTypeDef htim1;                    /* 定义 TIM1 句柄,供初始化和 Delay_us() 共同访问。 */

void MX_TIM1_Init(void)                    /* 把 TIM1 配置成每拍约 1 μs 的向上计数器。 */
{                                           /* TIM1 初始化函数体开始。 */
    TIM_ClockConfigTypeDef clock_cfg = {0}; /* 清零时钟源配置结构体。 */
    htim1.Instance = TIM1;                 /* 选择 APB2 上的高级定时器 TIM1。 */
    htim1.Init.Prescaler = 71U;            /* 实际 72 分频:72 MHz/72=1 MHz。 */
    htim1.Init.CounterMode = TIM_COUNTERMODE_UP; /* CNT 向上递增。 */
    htim1.Init.Period = 0xFFFFU;           /* ARR 设为 16 位最大值,一圈为 65536 μs。 */
    htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; /* 不额外分频数字采样时钟。 */
    htim1.Init.RepetitionCounter = 0U;     /* 每次溢出都产生普通更新事件。 */
    htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; /* ARR 写入立即生效。 */
    if (HAL_TIM_Base_Init(&htim1) != HAL_OK) /* 把基础配置应用到 TIM1。 */
    {                                       /* 初始化失败分支开始。 */
        Error_Handler();                    /* 失败时进入统一错误处理。 */
    }                                       /* 初始化结果判断结束。 */
    clock_cfg.ClockSource = TIM_CLOCKSOURCE_INTERNAL; /* 使用芯片内部定时器时钟。 */
    if (HAL_TIM_ConfigClockSource(&htim1, &clock_cfg) != HAL_OK) /* 应用时钟源。 */
    {                                       /* 时钟源配置失败分支开始。 */
        Error_Handler();                    /* 配置失败时停止正常业务。 */
    }                                       /* 时钟源配置结果判断结束。 */
}                                           /* TIM1 初始化函数结束;计数器尚未启动。 */

/* ==================== Core/Src/dht11.c:微秒延时 ==================== */

uint8_t Data[5] = {0U};                    /* 依次保存湿度整数/小数、温度整数/小数和校验字节。 */

void Delay_us(uint16_t us)                 /* 使用 TIM1 轮询产生约 us 微秒的阻塞延时。 */
{                                           /* 微秒延时函数体开始。 */
    uint16_t counter =                     /* 计算本次计数器的起始值。 */
        (uint16_t)(0xFFFFU - us - 25U);    /* 起点与终点都避开溢出点 25 拍,差值仍为 us。 */
    __HAL_TIM_SET_COUNTER(&htim1, counter); /* 把 TIM1_CNT 写成计算出的起始值。 */
    HAL_TIM_Base_Start(&htim1);            /* 启动 TIM1 普通计数,不开启中断。 */
    while (counter < (uint16_t)(0xFFFFU - 25U)) /* 未到固定终点 65510 就继续等待。 */
    {                                       /* 忙等循环开始,期间 CPU 不能处理其他前台任务。 */
        counter = (uint16_t)__HAL_TIM_GET_COUNTER(&htim1); /* 反复读取当前 CNT。 */
    }                                       /* 到达目标计数后退出忙等。 */
    HAL_TIM_Base_Stop(&htim1);             /* 停止 TIM1,避免函数返回后继续计数。 */
}                                           /* 微秒延时函数结束;调用和轮询开销会带来一定误差。 */

/* ==================== Core/Src/dht11.c:PA5 输入/输出切换 ==================== */

void DHT_GPIO_SET_OUTPUT(void)             /* 将 PA5 交给主机主动驱动起始信号。 */
{                                           /* 输出模式切换函数体开始。 */
    GPIO_InitTypeDef gpio = {0};           /* 清零 GPIO 配置结构体。 */
    gpio.Pin = GPIO_PIN_5;                 /* 选择连接 DHT11 DATA 的 PA5。 */
    gpio.Mode = GPIO_MODE_OUTPUT_PP;       /* 按课程工程配置为推挽输出。 */
    gpio.Speed = GPIO_SPEED_FREQ_HIGH;     /* 选择高速 GPIO 边沿能力。 */
    HAL_GPIO_Init(GPIOA, &gpio);           /* 把输出模式配置写入 PA5。 */
}                                           /* 输出模式切换函数结束。 */

void DHT_GPIO_SET_INPUT(void)              /* 释放 PA5,由传感器驱动总线并供 MCU 读取。 */
{                                           /* 输入模式切换函数体开始。 */
    GPIO_InitTypeDef gpio = {0};           /* 清零 GPIO 配置结构体。 */
    gpio.Pin = GPIO_PIN_5;                 /* 仍选择 DHT11 DATA 所在 PA5。 */
    gpio.Mode = GPIO_MODE_INPUT;           /* 将 PA5 改为输入,停止主机主动输出。 */
    gpio.Pull = GPIO_NOPULL;               /* 不启用内部上下拉,外部数据线应有合适上拉。 */
    HAL_GPIO_Init(GPIOA, &gpio);           /* 把输入模式配置写入 PA5。 */
}                                           /* 输入模式切换函数结束。 */

/* ==================== Core/Src/dht11.c:读取一个字节 ==================== */

uint8_t DHT_Read_Byte(void)                /* 按高位先发顺序读取连续 8 个 DHT11 数据位。 */
{                                           /* 单字节读取函数体开始。 */
    uint8_t value = 0U;                    /* 累积 8 位结果,初始清零。 */
    uint8_t retry = 0U;                    /* 限制等待电平变化的循环次数。 */
    uint8_t i = 0U;                        /* 保存当前正在读取的位序号。 */

    for (i = 0U; i < 8U; i++)             /* 每个 DHT11 数据字节固定包含 8 位。 */
    {                                       /* 一位数据的读取流程开始。 */
        while ((HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_RESET) && /* 等待位起始低电平结束。 */
               (retry < 100U))             /* 最多等待约 100 次,避免总线异常时永久卡死。 */
        {                                   /* 低电平等待循环开始。 */
            Delay_us(1U);                  /* 每次轮询间隔约 1 μs。 */
            retry++;                       /* 累计等待次数。 */
        }                                   /* 低电平等待循环结束。 */
        retry = 0U;                        /* 为本位后续等待重新清零。 */
        Delay_us(40U);                     /* 高电平开始约 40 μs 后采样,区分短 0 与长 1。 */
        value <<= 1U;                      /* 先左移,为当前位腾出最低位。 */
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_SET) /* 40 μs 后仍为高则判为 1。 */
        {                                   /* 当前位为 1 的分支开始。 */
            value |= 1U;                   /* 把结果最低位置 1。 */
        }                                   /* 当前位为 1 的分支结束;为 0 时无需额外写入。 */

        while ((HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_SET) && /* 等待当前高电平结束。 */
               (retry < 100U))             /* 同样设置等待上限。 */
        {                                   /* 高电平等待循环开始。 */
            Delay_us(1U);                  /* 每次等待约 1 μs。 */
            retry++;                       /* 累计等待次数。 */
        }                                   /* 高电平等待循环结束。 */
        retry = 0U;                        /* 为下一位清除等待计数。 */
    }                                       /* 8 位循环结束。 */
    return value;                          /* 返回按发送顺序组合完成的字节。 */
}                                           /* 单字节读取函数结束。 */

/* ==================== Core/Src/dht11.c:读取完整 40 位帧 ==================== */

uint8_t DHT_Read(void)                     /* 发送起始信号、接收 5 字节并校验。 */
{                                           /* 完整读取函数体开始。 */
    uint8_t retry = 0U;                    /* 限制等待传感器应答的时间。 */
    uint8_t i = 0U;                        /* 五字节读取循环下标。 */
    uint8_t checksum = 0U;                 /* 保存前四字节和的低 8 位。 */

    DHT_GPIO_SET_OUTPUT();                 /* PA5 切为输出,主机准备发起通信。 */
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); /* 主机把 DATA 拉低。 */
    HAL_Delay(18U);                        /* 低电平保持 18 ms,满足课程使用的起始时序。 */
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); /* 主机暂时拉高 DATA。 */
    Delay_us(20U);                         /* 等待约 20 μs 后释放并监听传感器。 */
    DHT_GPIO_SET_INPUT();                  /* PA5 切为输入,把总线控制权交给 DHT11。 */
    Delay_us(20U);                         /* 等待传感器进入应答低电平。 */

    if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) != GPIO_PIN_RESET) /* 未检测到预期应答低电平。 */
    {                                       /* 无响应错误分支开始。 */
        return 0U;                         /* 立即报告失败,不把旧 Data 当作新数据。 */
    }                                       /* 无响应错误分支结束。 */

    while ((HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_RESET) && /* 等应答低电平结束。 */
           (retry < 100U))                 /* 限制最长等待次数。 */
    {                                       /* 应答低电平等待开始。 */
        Delay_us(1U);                      /* 每次轮询约等待 1 μs。 */
        retry++;                           /* 累计低电平等待时间。 */
    }                                       /* 应答低电平等待结束。 */
    retry = 0U;                            /* 为应答高电平等待清零。 */

    while ((HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5) == GPIO_PIN_SET) && /* 等应答高电平结束。 */
           (retry < 100U))                 /* 限制最长等待次数。 */
    {                                       /* 应答高电平等待开始。 */
        Delay_us(1U);                      /* 每次轮询约等待 1 μs。 */
        retry++;                           /* 累计高电平等待时间。 */
    }                                       /* 应答高电平等待结束。 */

    for (i = 0U; i < 5U; i++)             /* 顺序读取湿度、温度和校验共 5 字节。 */
    {                                       /* 五字节读取循环开始。 */
        Data[i] = DHT_Read_Byte();         /* 调用单字节函数并保存当前字节。 */
    }                                       /* 五字节读取循环结束。 */

    checksum = (uint8_t)(Data[0] + Data[1] + Data[2] + Data[3]); /* 只保留和的低 8 位。 */
    return (checksum == Data[4]) ? 1U : 0U; /* 校验相等返回成功,否则返回失败。 */
}                                           /* 完整 DHT11 读取函数结束。 */

/* ==================== Core/Src/main.c:周期读取与串口输出 ==================== */

int main(void)                             /* 程序入口,初始化后每秒读取一次 DHT11。 */
{                                           /* main() 函数体开始。 */
    HAL_Init();                            /* 初始化 HAL 与 SysTick。 */
    SystemClock_Config();                  /* 配置 72 MHz 系统时钟。 */
    MX_GPIO_Init();                        /* 开启 DHT11 所在 GPIOA 等工程 GPIO 时钟。 */
    MX_TIM1_Init();                        /* 初始化 Delay_us() 使用的 1 MHz TIM1。 */
    MX_USART1_UART_Init();                 /* 初始化温湿度输出使用的 USART1。 */

    while (1)                              /* 周期性采样循环开始。 */
    {                                       /* 一次 DHT11 采样周期开始。 */
        if (DHT_Read() != 0U)             /* 只有收到应答且校验正确时才打印。 */
        {                                   /* 有效数据分支开始。 */
            printf(" H:%u T:%u\r\n",      /* 输出整数湿度和整数温度。 */
                   Data[0],                 /* 第 1 字节是湿度整数部分。 */
                   Data[2]);                /* 第 3 字节是温度整数部分。 */
        }                                   /* 有效数据分支结束。 */
        HAL_Delay(1000U);                  /* 两次读取之间等待约 1 秒。 */
    }                                       /* 返回循环顶部开始下一次读取。 */
}                                           /* main() 语法边界。 */

一、DHT11温湿度传感器实验

1.1 任务资源图

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

1.2 实验演示

  • DHT11 三针模块与 STM32 的连接如下:
STM32F103ZET6                 DHT11 三针模块
-------------                ----------------
3.3V  ---------------------> VCC
GND   ---------------------> GND
PA5   <--------------------> DATA
  • DATA 是双向数据线。STM32 发起通信时将 PA5 配为推挽输出并拉低;随后将 PA5 切为输入,释放总线,由 DHT11 输出应答和 40 位数据。双方不能同时驱动该线。
  • 视频画面能确认本课使用的是带小电路板的三针 DHT11 模块,但看不清板上 DATA 是否已接上拉电阻,因此不能直接断言“该模块一定自带上拉”。实际接线前应查看所购模块原理图,或断电后测量 DATA 与 VCC 之间的电阻;若没有上拉,再按模块手册连接外部上拉电阻。
  • 课程说明该模块的 VCC 可接 3.3 V 或 5 V。供电电压和 DATA 上拉电压是两件事:若模块把 DATA 上拉到 VCC,使用 5 V 供电时还必须核对 STM32 对应引脚的输入电压规格;没有模块原理图时,不应凭外观判断数据线电平。
  • 串口连接为 PA9 -> USB 转串口模块 RX、PA10 <- USB 转串口模块 TX、GND -> 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=71、Period=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],当总和超过 255 时会与协议的“低 8 位校验”不一致。顶部教学代码已显式转换为 uint8_t checksum,保证按协议只比较低 8 位。
  • retry < 100 的作用是避免等待电平时永久卡死,但读取函数没有单独返回“位超时”状态,最终仍依靠校验结果判断本帧是否可用。

1.4 Delay_us分析

  • 配套源码把起点写成 0xFFFF-us-25,终点写成 0xFFFF-25。两端同时减 25 不会改变它们之间的 us 个计数差,而是让轮询终点避开 16 位计数器即将溢出的 0xFFFF,降低程序恰好错过终点后又等待一整圈的风险。
Delay_us(20)

CNT: 65490 -> 65491 -> ... -> 65510
       |<--------- 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.c、dht11.h 加入工程,dht11.h 中使用正确的 htim1 句柄。使用其他定时器时,需要同步修改句柄和初始化函数。

1.6 头文件透析

  • dht11.h 应声明 Data[5]、Delay_us()、GPIO 模式切换函数和读取函数;tim.h 应声明 htim1。
  • 复用驱动时,将 GPIOA、GPIO_PIN_5 改为实际接入的端口和引脚,并保证数据线有可靠上拉、传感器供电与 MCU 数据引脚电平兼容。
  • 主循环在每次读取后执行 HAL_Delay(1000)。顶部教学代码在未检测到应答时直接返回失败,并按低 8 位校验,因此不会把“没有新应答”误判为一次成功读取;更完整的驱动还应把各个等待超时分别向上传递。
  • 排查顺序:检查 VCC、GND、DATA 与 PA5 的连接;检查上拉与供电;检查 TIM1 的 1 MHz 计数配置;最后检查串口、源文件是否加入工程及初始化顺序。