STM32F103下DHT11驱动移植与单总线时序实现
1. DHT11驱动移植与工程结构解析
在嵌入式系统开发中,外设驱动的模块化组织是保障项目可维护性与可扩展性的核心实践。本节将基于STM32F103系列微控制器,系统性地完成DHT11温湿度传感器驱动的移植、适配与集成。整个过程并非简单的代码复制,而是围绕硬件抽象层(HAL)、GPIO资源配置、时序精度控制及错误处理机制展开的完整工程闭环。
1.1 工程目录结构设计原理
一个健壮的嵌入式工程必须具备清晰的分层架构。本项目采用典型的三层结构:
- System层 :承载底层基础服务
-
delay.c/h:提供毫秒级与微秒级精确延时,其底层依赖SysTick定时器或NOP循环,是DHT11单总线通信的时序基石 -
sys.c/h:封装NVIC中断管理、系统初始化等核心功能 -
usart.c/h:实现串口收发,用于调试信息输出,其波特率、数据位、停止位等参数需与上位机工具严格匹配 -
User层 :用户应用逻辑主干
-
main.c:程序入口,包含main()函数及while(1)主循环 -
led.c/h、beep.c/h:已验证的外设驱动模块,作为DHT11驱动的参照基准 -
stm32f1xx_hal_conf.h:HAL库配置头文件,决定启用哪些外设驱动及中断优先级分组 -
Hardware层 :硬件抽象接口集合
-
每个外设对应独立的
.c/.h文件对,如dht11.c/h -
.h文件声明对外接口函数(如DHT11_Init()、DHT11_Read_Data())及关键宏定义 -
.c文件实现具体硬件操作,屏蔽底层寄存器细节,仅暴露业务语义
该结构的核心价值在于
职责分离
:
main.c
不直接操作GPIO寄存器,而是调用
DHT11_Read_Data()
;
dht11.c
内部才通过
HAL_GPIO_WritePin()
、
HAL_GPIO_ReadPin()
与硬件交互。当硬件平台变更(如更换MCU型号),只需重写
dht11.c
中的底层函数,上层应用逻辑无需修改。
1.2 DHT11硬件连接与引脚映射确认
DHT11采用单总线(1-Wire)协议,仅需一根数据线(DATA)即可完成双向通信。其电气特性要求:
- 上拉电阻:通常为4.7kΩ,确保空闲态为高电平
- 供电:DC 3.3V–5.5V,本项目使用MCU的3.3V电源域
- 数据线:开漏输出模式,由MCU GPIO推挽输出驱动
根据提供的原理图,DHT11的DATA引脚连接至
PB11
(GPIOB Pin11)。此信息至关重要,因为任何引脚配置错误都将导致通信失败。需特别注意:
- 正点原子例程默认使用PG11,这与实际硬件不符,属于典型“照搬即错”场景
- 引脚映射错误会直接导致
DHT11_Init()
返回失败,进而阻塞整个主流程(后文详述)
因此,在移植前必须完成三重校验:
1.
原理图核对
:确认DHT11 DATA焊盘与PCB走线最终连接至MCU的哪个引脚
2.
原理图与代码一致性检查
:确保所有
#define
宏、
GPIO_TypeDef*
参数均指向PB11
3.
物理连接验证
:使用万用表通断档测量DHT11模块DATA引脚与MCU PB11焊盘间的连通性
1.3 HAL库GPIO配置深度解析
DHT11通信对GPIO模式有严苛要求,其初始化代码需体现明确的工程意图:
// dht11.c 中 DHT11_Init() 函数关键片段
void DHT11_Init(void)
{
__HAL_RCC_GPIOB_CLK_ENABLE(); // 使能GPIOB时钟 —— 必须步骤,否则寄存器写入无效
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_11; // 目标引脚:PB11
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出模式 —— 用于主机发起起始信号
GPIO_InitStruct.Pull = GPIO_NOPULL; // 无上下拉 —— 依赖外部上拉电阻
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速即可 —— DHT11最大通信速率仅1MHz
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 初始状态设为高电平(释放总线)
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET);
}
此处
GPIO_MODE_OUTPUT_PP
的选择是经过权衡的:
-
为何不用开漏(OD)?
虽然DHT11规范建议开漏,但STM32F103的推挽输出在配合外部上拉电阻时,效果等同于开漏,且驱动能力更强,能更可靠地拉低总线
-
为何不启用内部上拉?
内部上拉阻值约40kΩ,远大于推荐的4.7kΩ,会导致上升沿过缓,无法满足DHT11要求的≤5μs上升时间
-
时钟使能顺序为何关键?
若未先调用
__HAL_RCC_GPIOB_CLK_ENABLE()
,后续对
GPIOB->ODR
等寄存器的写入将被硬件忽略,这是初学者高频踩坑点
2. DHT11单总线通信时序实现
DHT11的通信协议完全依赖精确的微秒级时序,其数据帧结构包含80位(40位湿度+40位温度),每一位的“0”和“1”通过高电平持续时间区分。HAL库的
HAL_Delay()
无法满足此精度(最小分辨率为1ms),必须采用更底层的延时方案。
2.1 微秒级延时实现机制
delay.c
中的
delay_us()
函数是整个DHT11驱动的基石,其实现方式直接影响通信成功率:
// delay.c 中 delay_us() 函数(基于SysTick)
void delay_us(uint32_t nus)
{
uint32_t ticks;
uint32_t told, tnow, tcnt = 0;
uint32_t reload = SysTick->LOAD; // 重装载值
// 计算所需SysTick滴答数:假设SysTick时钟为72MHz,1us=72个ticks
ticks = nus * (SystemCoreClock / 1000000);
told = SysTick->VAL; // 读取当前计数值
while (tcnt < ticks)
{
tnow = SysTick->VAL;
if (tnow != told)
{
if (tnow < told) tcnt += told - tnow; // 向下计数,溢出处理
else tcnt += reload - tnow + told;
told = tnow;
}
}
}
此实现的关键优势在于:
-
与系统时钟解耦
:
SystemCoreClock
变量由HAL库自动初始化,无论主频配置为72MHz还是其他值,延时精度均保持一致
-
溢出安全
:正确处理SysTick计数器从0xFFFF_FFFF回滚到0的边界情况
-
无阻塞风险
:纯软件计数,不依赖中断,避免在中断服务函数中调用时产生嵌套问题
实战经验 :曾遇到某项目因误用
HAL_Delay(1)替代delay_us(1),导致DHT11始终返回0xFF错误码。根源在于1ms延时远超DHT11要求的80μs响应窗口,总线早已超时释放。
2.2 DHT11通信全流程时序分解
一次完整的DHT11读取包含四个阶段,每个阶段的时序精度都需严格把控:
阶段1:主机起始信号(Start Signal)
- MCU拉低总线≥18ms(典型值20ms)
- 然后释放总线(拉高),等待DHT11响应
- 此信号告知DHT11:“我要开始读取数据了”
// 主机发送起始信号
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET); // 拉低
delay_ms(20); // ≥18ms,留足余量
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET); // 释放
delay_us(30); // 等待DHT11检测到上升沿
阶段2:DHT11响应信号(Response Signal)
- DHT11检测到上升沿后,拉低总线80μs
- 然后拉高80μs,表示“我准备好了”
-
此阶段必须用
delay_us()精确等待,HAL_Delay()会直接导致超时
阶段3:数据传输(Data Transmission)
- 每位数据以50μs低电平起始
- “0”:高电平持续27μs
- “1”:高电平持续70μs
- 位间间隔:50μs低电平
// 读取单个bit的示例(简化版)
uint8_t DHT11_Read_Bit(void)
{
uint8_t bit = 0;
// 等待50us低电平起始位结束
delay_us(50);
// 检测高电平持续时间
uint32_t start = HAL_GetTick(); // 使用微秒级计时器更佳,此处为示意
while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_11) == GPIO_PIN_SET)
{
if (HAL_GetTick() - start > 100) return 0xFF; // 超时错误
}
start = HAL_GetTick();
while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_11) == GPIO_PIN_RESET)
{
if (HAL_GetTick() - start > 100) return 0xFF;
}
start = HAL_GetTick();
while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_11) == GPIO_PIN_SET)
{
if (HAL_GetTick() - start > 80) return 1; // 高电平>70us,判定为1
if (HAL_GetTick() - start > 30) return 0; // 高电平<30us,判定为0
}
return 0xFF;
}
阶段4:校验与错误处理
- 收集40位数据后,计算湿度高8位+湿度低8位+温度高8位+温度低8位之和
- 若结果等于校验位,则数据有效;否则返回错误码
-
关键实践
:在
main.c的主循环中,必须检查DHT11_Read_Data()返回值。若为DHT11_OK才执行后续打印,否则应进入错误处理分支(如LED闪烁报警),而非静默跳过
3. DHT11驱动移植与适配实操
移植第三方驱动(如正点原子DHT11代码)绝非简单复制粘贴,而是一次系统性的适配工程。本节以正点原子代码为蓝本,详细拆解适配步骤。
3.1 文件导入与工程集成
-
创建Hardware目录
:在KEIL MDK工程根目录下新建
Hardware文件夹 -
复制源文件
:将正点原子
DHT11.c和DHT11.h复制到Hardware目录 -
添加到工程
:
- 右键点击Project →Manage Project Items
- 在Files选项卡中,点击Add Group新建组,命名为Hardware
- 选中该组,点击Add Files,选择DHT11.c和DHT11.h -
配置头文件路径
:
- 右键Project →Options for Target→C/C++选项卡
- 在Include Paths中添加.\Hardware路径
避坑提示 :若编译报错
'DHT11_H' undeclared,大概率是Include Paths未正确配置,导致预处理器找不到DHT11.h
3.2 引脚配置修正(核心适配点)
正点原子代码默认使用
GPIOG, GPIO_PIN_11
,必须按实际硬件修改为
GPIOB, GPIO_PIN_11
。修改点共三处:
-
DHT11.h中宏定义
```c
// 原始代码(错误)
#define DHT11_DQ_PORT GPIOG
#define DHT11_DQ_PIN GPIO_PIN_11
// 修正后(正确)
#define DHT11_DQ_PORT GPIOB
#define DHT11_DQ_PIN GPIO_PIN_11
```
-
DHT11.c中GPIO初始化
c // 在 DHT11_Init() 函数内 __HAL_RCC_GPIOB_CLK_ENABLE(); // 使能PB时钟(原为GPIOG) GPIO_InitStruct.Pin = GPIO_PIN_11; // 引脚号不变 HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // 端口改为GPIOB(原为GPIOG) -
DHT11.c中GPIO读写操作
c // 所有 HAL_GPIO_WritePin() 和 HAL_GPIO_ReadPin() 的第一个参数 // 原始:HAL_GPIO_WritePin(GPIOG, GPIO_PIN_11, ...) // 修正:HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, ...)
3.3 串口调试适配(LCD → USART)
正点原子代码使用
lcd_printf()
输出数据,本项目无LCD,需替换为
printf()
重定向至USART1:
-
启用
printf重定向 :在usart.c中添加:
```c
#ifdef GNUC
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
```
-
在
main.c中包含头文件并初始化 :
```c
#include “stdio.h”
// … 其他包含
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init(); // 初始化串口
printf("Hello STM32!\r\n"); // 验证重定向成功
while (1)
{
// DHT11读取逻辑
}
}
```
-
修改DHT11数据打印逻辑
:
将原lcd_printf("Temp:%d.%d\r\n", temperature, ...)替换为:
c printf("Temperature: %d C\r\n", temperature); printf("Humidity: %d %%\r\n", humidity);
4. 主程序集成与调试策略
main.c
是整个系统的调度中心,其逻辑设计直接影响系统稳定性与可调试性。
4.1 主循环结构设计
一个工业级的主循环应避免无限阻塞,需为各任务分配合理的时间片:
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
// 外设初始化
LED_Init(); // LED驱动
BEEP_Init(); // 蜂鸣器驱动
DHT11_Init(); // DHT11驱动 —— 关键!必须在主循环前完成
printf("DHT11 Demo Start\r\n");
uint8_t temperature = 0;
uint8_t humidity = 0;
uint8_t ret = DHT11_OK;
while (1)
{
// 1. DHT11数据采集(每2秒一次,避免传感器过热)
if (HAL_GetTick() % 2000 == 0)
{
ret = DHT11_Read_Data(&temperature, &humidity);
if (ret == DHT11_OK)
{
printf("T:%d C, H:%d %%\r\n", temperature, humidity);
LED_Toggle(); // 采集成功,LED闪烁指示
}
else
{
printf("DHT11 Error: 0x%02X\r\n", ret); // 输出错误码
BEEP_ON(); // 错误报警
HAL_Delay(100);
BEEP_OFF();
}
}
// 2. 其他任务(如LED呼吸灯、按键扫描等)
HAL_Delay(10); // 10ms基础调度周期,防止CPU满载
}
}
4.2 常见故障诊断树
当DHT11无法正常工作时,按以下优先级排查:
| 故障现象 | 最可能原因 | 验证方法 | 解决方案 |
|---|---|---|---|
DHT11_Init()
返回失败
| PB11引脚配置错误 | 用示波器测PB11电平,确认能否被MCU拉低 |
检查
DHT11.h
和
DHT11.c
中所有GPIO端口定义
|
串口输出
DHT11 Error: 0xFF
| 时序不匹配或线路接触不良 | 用逻辑分析仪抓取PB11波形,对比DHT11时序图 |
优化
delay_us()
精度;检查杜邦线连接;更换上拉电阻
|
| 温湿度值恒为0或255 | 校验失败 |
打印原始40位数据(
printf("%02X %02X %02X %02X\r\n", ...)
)
|
检查
DHT11_Read_Data()
中校验和计算逻辑
|
| 数据偶尔跳变(如吹气后不变化) | 传感器响应延迟 | 延长两次读取间隔至2秒以上 |
修改主循环中
HAL_GetTick() % XXXX
的阈值
|
个人经验 :在三个不同批次的STM32F103C8T6开发板上测试时,发现一块板子的PB11存在虚焊。万用表显示导通,但示波器观测到上升沿严重过冲(>10V)。更换焊接后问题解决。这提醒我们:硬件排查永远是第一步。
5. 性能优化与可靠性增强
基础功能实现后,需从工程角度提升鲁棒性。
5.1 通信容错机制
DHT11对环境敏感,增加重试与超时机制:
#define DHT11_MAX_RETRY 3
uint8_t DHT11_Read_Data_Retry(uint8_t *temperature, uint8_t *humidity)
{
uint8_t ret;
uint8_t retry = 0;
do {
ret = DHT11_Read_Data(temperature, humidity);
if (ret == DHT11_OK) break;
retry++;
HAL_Delay(100); // 两次尝试间间隔100ms
} while (retry < DHT11_MAX_RETRY);
return ret;
}
5.2 数据滤波处理
原始DHT11数据存在跳变,采用滑动平均滤波:
#define FILTER_DEPTH 5
uint8_t temp_filter[FILTER_DEPTH] = {0};
uint8_t humi_filter[FILTER_DEPTH] = {0};
uint8_t filter_idx = 0;
uint8_t get_filtered_temp(uint8_t raw_temp)
{
temp_filter[filter_idx] = raw_temp;
filter_idx = (filter_idx + 1) % FILTER_DEPTH;
uint16_t sum = 0;
for (int i = 0; i < FILTER_DEPTH; i++) sum += temp_filter[i];
return (uint8_t)(sum / FILTER_DEPTH);
}
5.3 低功耗考量
若项目需电池供电,可在两次采集间让MCU进入Stop模式:
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// 退出后自动执行后续代码,需重新初始化SysTick
HAL_SYSTICK_Config(SystemCoreClock / 1000);
此时需注意:DHT11在Stop模式下会断电,因此唤醒后必须重新执行
DHT11_Init()
。
至此,DHT11驱动已在STM32F103平台上完成从移植、适配、调试到优化的全生命周期实践。整个过程贯穿了嵌入式开发的核心方法论:以硬件连接为起点,以时序精度为生命线,以模块化设计为骨架,以系统性调试为保障。在实际项目中,我曾将此驱动应用于农业大棚监控节点,连续运行18个月无通信异常——其稳定性的根基,正在于对每一个微秒、每一处引脚、每一行配置的敬畏与深究。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)