前言

在上一篇文章中,我们深入理解了STM32定时器的原理。今天我们将“趁热打铁”,利用定时器实现微秒级延时 (delay_us),驱动经典的 DHT11 温湿度传感器,并将采集到的数据通过 串口 (UART) 打印到电脑上。

这个项目虽小,但涵盖了嵌入式开发的三大核心:通信协议 (单总线)、时序控制、调试输出


一、 硬件原理:DHT11 怎么说话?

DHT11 是一款数字温湿度传感器,它只有 3个引脚VCCGNDDATA
这就意味着,所有的数据(湿度整数、小数、温度整数、小数、校验和)都要通过这一根 DATA 线传输。

1. 单总线协议 (One-Wire)

这就像两个人用一根电话线通话:

  • 平时电话线挂起(高电平)。
  • MCU 想问数据,就把线拉低一段时间(起始信号),然后松手。
  • DHT11 看到信号后,也把线拉低响应一下。
  • 接着,DHT11 开始“发电报”,发送 40 位(5个字节)的数据。

2. 0 和 1 的区别

DHT11 传输 0 和 1 不靠电压高低,而是靠高电平持续的时间长短

  • 0:低电平 50us + 高电平 26-28us
  • :低电平 50us + 高电平 70us

二、 难点攻克:微秒级延时

HAL 库只提供了 HAL_Delay(),这是毫秒 (ms) 级别的。而 DHT11 的时序要求是微秒 (us) 级别的。如果延时不准,读出来的数据全是乱码。

解决方案:利用定时器 (TIM)!
还记得上一篇的公式吗?
 

我们只需要配置一个定时器,让它每走一步就是 1us

  • 假设时钟 72MHz。
  • 设置 PSC=72−1=71。
  • 那么计数器频率 = 1MHz,即 1us 数一次

三、 CubeMX 配置步骤

1. 时钟与调试

  • RCC: High Speed Clock (HSE) -> Crystal/Ceramic Resonator.
  • SYS: Debug -> Serial Wire.
  • Clock Configuration: 确保主频是 72MHz。

2. 串口配置 (USART1)

  • Connectivity -> USART1
  • Mode: Asynchronous (异步)。
  • Baud Rate: 115200。
  • Word Length: 8 Bits (包含校验位则选9,这里默认无校验选8)。

3. 微秒定时器配置 (TIM1)

  • Timers -> TIM1
  • Clock Source: Internal Clock。
  • Prescaler (PSC)71 (即72分频,1us跳一次)。
  • Counter Period (ARR)65535 (设为最大值,防止溢出太快)。

4. DHT11 引脚配置

  • 选择一个引脚(例如 PA1)。
  • 设置为 GPIO_Output (初始状态)。
  • GPIO Pull-up/Pull-downNo pull-up and no pull-down (通常模块自带上拉电阻,如果是裸芯片需要外接上拉)。
  • User Label: 改名为 DHT11_PIN


四、 代码实战

1. 串口重定向 (printf)

为了方便打印,我们需要重定向 printf。在 main.c 或 usart.c 中添加

#include <stdio.h>

// 重定向 printf 到串口1
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

注意:在Keil中需要勾选 "Use MicroLIB"

2. 微秒延时函数 (利用TIM1)

在 main.c 中添加基于定时器的延时函数:

// 简单的微秒延时
void delay_us(uint16_t us)
{
    __HAL_TIM_SET_COUNTER(&htim1, 0); // 计数器清零
    __HAL_TIM_ENABLE(&htim1);         // 启动定时器
    while (__HAL_TIM_GET_COUNTER(&htim1) < us); // 等待计数到 us
    __HAL_TIM_DISABLE(&htim1);        // 关闭定时器
}

3. GPIO 方向切换

DHT11 是半双工的,一会 MCU 发(输出),一会 MCU 收(输入)。我们需要动态切换 GPIO 模式:

// 切换为输出模式
void DHT11_Mode_Out(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = DHT11_PIN_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(DHT11_PIN_GPIO_Port, &GPIO_InitStruct);
}

// 切换为输入模式
void DHT11_Mode_In(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = DHT11_PIN_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;     // 浮空输入
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(DHT11_PIN_GPIO_Port, &GPIO_InitStruct);
}

4. 读取数据的核心逻辑

这是最关键的部分,严格按照时序图来写:

// 读取一个字节
uint8_t DHT11_Read_Byte(void)
{
    uint8_t i, dat = 0;
    for (i = 0; i < 8; i++)
    {
        // 等待低电平过去(开头是50us低电平)
        while (HAL_GPIO_ReadPin(DHT11_PIN_GPIO_Port, DHT11_PIN_Pin) == GPIO_PIN_RESET);
        
        // 现在是高电平了,延时40us看看
        delay_us(40);
        
        // 如果延时40us后还是高电平,说明是数据 '1' (因为'0'的高电平只有26-28us)
        if (HAL_GPIO_ReadPin(DHT11_PIN_GPIO_Port, DHT11_PIN_Pin) == GPIO_PIN_SET)
        {
            dat |= (1 << (7 - i)); // 高位在前
            // 等待高电平结束
            while (HAL_GPIO_ReadPin(DHT11_PIN_GPIO_Port, DHT11_PIN_Pin) == GPIO_PIN_SET);
        }
    }
    return dat;
}

// 读取温湿度数据
// 返回值:0-成功,1-失败
uint8_t DHT11_Read_Data(uint8_t *temp, uint8_t *humi)
{
    uint8_t buf[5];
    uint8_t i;

    // 1. 主机发送起始信号
    DHT11_Mode_Out();
    HAL_GPIO_WritePin(DHT11_PIN_GPIO_Port, DHT11_PIN_Pin, GPIO_PIN_RESET);
    HAL_Delay(20); // 拉低至少18ms
    HAL_GPIO_WritePin(DHT11_PIN_GPIO_Port, DHT11_PIN_Pin, GPIO_PIN_SET);
    delay_us(30);  // 拉高20-40us

    // 2. 主机切换为输入,判断DHT11是否响应
    DHT11_Mode_In();
    // 检查DHT11是否拉低了电平
    if (HAL_GPIO_ReadPin(DHT11_PIN_GPIO_Port, DHT11_PIN_Pin) == GPIO_PIN_RESET)
    {
        // 等待DHT11拉低结束(80us)
        while (HAL_GPIO_ReadPin(DHT11_PIN_GPIO_Port, DHT11_PIN_Pin) == GPIO_PIN_RESET);
        // 等待DHT11拉高结束(80us)
        while (HAL_GPIO_ReadPin(DHT11_PIN_GPIO_Port, DHT11_PIN_Pin) == GPIO_PIN_SET);

        // 3. 开始接收40位数据
        for (i = 0; i < 5; i++)
        {
            buf[i] = DHT11_Read_Byte();
        }

        // 4. 校验计算 (前4个字节之和 == 第5个字节)
        if (buf[0] + buf[1] + buf[2] + buf[3] == buf[4])
        {
            *humi = buf[0]; // 湿度整数
            *temp = buf[2]; // 温度整数
            return 0; // 成功
        }
    }
    return 1; // 失败
}

5. 主函数 Main

别忘了在初始化部分开启 TIM1 的时钟(虽然不需要开启中断,但 HAL_TIM_Base_Init 已经配置好了,只要不 Start_IT 就行)。

  /* USER CODE BEGIN 2 */
  printf("DHT11 Test Start...\r\n");
  uint8_t temperature = 0;
  uint8_t humidity = 0;
  /* USER CODE END 2 */

  /* USER CODE BEGIN WHILE */
  while (1)
  {
      if (DHT11_Read_Data(&temperature, &humidity) == 0)
      {
          printf("Temp: %d C, Humi: %d %%\r\n", temperature, humidity);
      }
      else
      {
          printf("DHT11 Error!\r\n");
      }
      
      // DHT11 采样周期建议大于1秒,太快会读不到
      HAL_Delay(1500);

    /* USER CODE END WHILE */

五、 避坑指南(含泪总结)

  1. printf 无法打印浮点数/无输出:
    • 检查 Keil 工程选项 -> Target -> 勾选 Use MicroLIB
    • 检查串口线 TX/RX(互相反接) 也就是 GND 是否接好。
  2. 数据一直是 0 或 Error:
    • 时序问题:这就是为什么我们要用定时器做 delay_us 而不是简单的 for 循环空转。
    • GPIO模式:确保 DHT11_Mode_Out 和 DHT11_Mode_In 切换正确。
    • 硬件连接:有些 DHT11 模块引脚排列是 VCC-DATA-GND,有些是 VCC-GND-DATA,一定要看模块丝印,接反必烧!
  3. 读数太快:DHT11 这种老式传感器反应很慢,循环里 HAL_Delay 至少要在 1000ms 以上,否则第二次读取会失败。


六、 总结

在线串口助手:串口助手

通过这个实战,我们不仅学会了驱动 DHT11,更重要的是:

  1. 验证了定时器的实用性:用硬件定时器做微秒延时,精准可靠。
  2. 掌握了 GPIO 的灵活运用:在输入和输出之间反复横跳。
  3. 调试技巧:利用串口打印实时观察数据,是嵌入式开发最基本的技能。

如果你对定时器配置还不熟悉,可以回顾我的上一篇文章:《【STM32专题】深入理解定时器原理与配置》

源码已上传,欢迎点赞收藏!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐