【STM32实战】串口打印 DHT11 温湿度数据 (HAL库 + CubeMX)
本文介绍了如何利用STM32定时器实现微秒级延时(delay_us)来驱动DHT11温湿度传感器。通过CubeMX配置定时器、串口和GPIO引脚,详细讲解了单总线通信协议、时序控制等核心内容。重点包括:1) 使用定时器实现精确的微秒延时;2) GPIO输入输出模式的动态切换;3) DHT11数据读取逻辑的实现;4) 串口打印调试技巧。文章还总结了常见问题解决方案,如printf无法输出、数据读取错
前言
在上一篇文章中,我们深入理解了STM32定时器的原理。今天我们将“趁热打铁”,利用定时器实现微秒级延时 (delay_us),驱动经典的 DHT11 温湿度传感器,并将采集到的数据通过 串口 (UART) 打印到电脑上。
这个项目虽小,但涵盖了嵌入式开发的三大核心:通信协议 (单总线)、时序控制、调试输出。
一、 硬件原理:DHT11 怎么说话?
DHT11 是一款数字温湿度传感器,它只有 3个引脚:VCC、GND、DATA。
这就意味着,所有的数据(湿度整数、小数、温度整数、小数、校验和)都要通过这一根 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-down:
No 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 */
五、 避坑指南(含泪总结)
- printf 无法打印浮点数/无输出:
- 检查 Keil 工程选项 ->
Target-> 勾选Use MicroLIB。 - 检查串口线 TX/RX(互相反接) 也就是 GND 是否接好。
- 检查 Keil 工程选项 ->
- 数据一直是 0 或 Error:
- 时序问题:这就是为什么我们要用定时器做
delay_us而不是简单的 for 循环空转。 - GPIO模式:确保
DHT11_Mode_Out和DHT11_Mode_In切换正确。 - 硬件连接:有些 DHT11 模块引脚排列是 VCC-DATA-GND,有些是 VCC-GND-DATA,一定要看模块丝印,接反必烧!
- 时序问题:这就是为什么我们要用定时器做
- 读数太快:DHT11 这种老式传感器反应很慢,循环里
HAL_Delay至少要在 1000ms 以上,否则第二次读取会失败。

六、 总结
在线串口助手:串口助手
通过这个实战,我们不仅学会了驱动 DHT11,更重要的是:
- 验证了定时器的实用性:用硬件定时器做微秒延时,精准可靠。
- 掌握了 GPIO 的灵活运用:在输入和输出之间反复横跳。
- 调试技巧:利用串口打印实时观察数据,是嵌入式开发最基本的技能。
如果你对定时器配置还不熟悉,可以回顾我的上一篇文章:《【STM32专题】深入理解定时器原理与配置》。
源码已上传,欢迎点赞收藏!
更多推荐





所有评论(0)