十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32读取DHT11温湿度:单总线时序与GPIO模拟实现详解

STM32读取DHT11温湿度:单总线时序与GPIO模拟实现详解 简介在嵌入式系统开发中传感器数据采集是常见却容易踩坑的环节尤其是单总线类器件。DHT11作为典型的温湿度传感器其数据读取并不依赖复杂的协议栈而是依赖对微秒级时序的精确把控。掌握其工作原理需要理解单总线的物理层电平状态、起始信号、应答窗口以及40位数据帧的校验规则。通过GPIO模拟单总线时序可以避免专用外设资源占用提升代码可移植性这在资源受限的MCU上具有重要工程价值。实践中标准库与HAL库在引脚方向切换、延时实现和中断处理上存在显著差异而编译优化等级、上拉电阻配置、采样点位置等因素都会直接影响读取的稳定性。串口输出与逻辑分析仪是验证时序正确性的有效工具。本文围绕DHT11的读取过程系统梳理时序参数、代码实现和调试技巧帮助开发者从原理层面彻底解决温湿度采集中常见的误读、卡死和随机跳变等问题。1. DHT11温湿度采集不是把数据读对而是把时序抠准DHT11 这类单总线传感器在 STM32 上跑起来的难点从来不在协议本身而在微秒级延时是否准确、GPIO 方向切换有没有引入额外开销。项目里同时给出了标准库与 HAL 库两套实现并在 USART1 上以文本帧输出温度与湿度适合已经能点灯、但还没系统整理过单总线时序的开发者。初看工程文件时容易困惑里面混着iar_cortexM3b_math.a、libarm_cortexM3l_math.a和arm_common_tables.c这些其实是模板工程自带的 DSP 相关文件与 DHT11 数据采集没有关系后面单独解释。把主频、延时、引脚模式这三件事理清楚标准库和 HAL 库两个版本都能稳定读到 0.1 级温湿度。2. DHT11单总线协议起始信号、应答窗口与40位数据帧校验2.1 单总线物理层与总线空闲状态DHT11 只有一根 DATA 线既做输入又做输出主机和传感器之间通过拉低、释放总线来传递信息。总线空闲时必须保持高电平所以 STM32 的 GPIO 要配置成开漏输出带上拉或者推挽输出加外部 4.7kΩ 上拉电阻。很多人在标准库工程里把引脚配成推挽输出后读不到数据就是因为释放总线时引脚被强制拉高DHT11 的应答低电平根本拉不动这根线。总线上所有时序都以低电平脉冲的宽度作为信息载体。主机先发出一个大于 18ms 的起始低电平然后释放总线DHT11 检测到这个下降沿后开始应答。应答信号由一段 80μs 的低电平和一段 80μs 的高电平组成主机在应答高电平结束后开始读取 40 位数据。时序段最短典型最长主机起始低电平18 ms20 ms30 ms主机释放到探测应答20 μs30 μs40 μsDHT11 应答低电平78 μs80 μs84 μsDHT11 应答高电平78 μs80 μs84 μs数据位“0”高电平宽度22 μs26 μs30 μs数据位“1”高电平宽度68 μs70 μs75 μs每一位起始低电平宽度48 μs50 μs54 μs这张表是后面调代码的对照基准。DHT11 手册上没有给全这些数值但实际抓波形时不同批次的传感器会有几微秒偏差表中典型值就是代码里延时的设计依据。2.2 主机起始信号与应答窗口的判定主机的起始信号不能太短。如果只拉低 5msDHT11 可能还在上电初始化阶段不会响应。我一般控制在 20ms 左右误差大一点没关系因为这个阶段只需要保证大于 18ms。起始信号结束后释放总线等待 DHT11 把总线拉低。需要特别注意主机释放总线到 DHT11 拉低总线之间存在一个盲区。常见实现是这样写的static void DHT11_Start(void) { DHT11_Pin_Output(); /* 切到输出模式 */ GPIO_ResetBits(DHT11_GPIO, DHT11_PIN); Delay_Ms(20); /* 拉低 20ms必须大于 18ms */ GPIO_SetBits(DHT11_GPIO, DHT11_PIN); Delay_Us(30); /* 释放总线等待传感器拉低 */ DHT11_Pin_Input(); /* 切回输入才能检测电平 */ }这段代码里DHT11_Pin_Output()和DHT11_Pin_Input()承担了模式切换标准库和 HAL 库的差异也集中在这两个函数上。Delay_Us(30)不能省也不能太长超过 40μs 会错过应答窗口的低电平起点导致后面采样到的是应答高电平而不是数据位。2.3 数据位“0”和“1”的判别点在 40μsDHT11 发送的每一位都以 50μs 低电平开头之后是 26μs 或 70μs 的高电平。问题在于主机怎么知道当前是高电平的哪一段。常见做法是等总线被拉低后延时跳过 50μs 低电平窗口然后再采样引脚。如果此时读到高电平说明高电平还没结束这一位是“1”如果读到低电平说明高电平已结束这一位是“0”。uint8_t DHT11_ReadByte(void) { uint8_t i, byte 0; for (i 0; i 8; i) { while (GPIO_ReadInputDataBit(DHT11_GPIO, DHT11_PIN) RESET); Delay_Us(40); /* 跳过 50us 低电平窗口 */ if (GPIO_ReadInputDataBit(DHT11_GPIO, DHT11_PIN) SET) { byte | (uint8_t)(0x01 (7 - i)); while (GPIO_ReadInputDataBit(DHT11_GPIO, DHT11_PIN) SET); } } return byte; }while等待低电平的作用是同步每一位的起始点因为前一位结束后的高电平时长不确定不能靠固定延时对齐位边界。Delay_Us(40)之后才采样避开了低电平的 4854μs 波动范围。读到高电平时把它判为“1”并把对应位置 1然后等总线回到低电平进入下一位。这个循环里若不等待位结束下一轮while会立即退出导致连续误读。2.4 校验位前四个字节相加的末 8 位40 位数据依次是湿度整数、湿度小数、温度整数、温度小数、校验字节每个部分 8 位高位在前。DHT11 的小数位在大部分批次里固定输出 0但代码必须按完整帧解析不能直接丢弃小数位。校验规则是把前四个字节相加取低 8 位与校验字节比较。uint8_t DHT11_ReadFrame(uint8_t *temp, uint8_t *humi) { uint8_t buf[5] {0}; uint8_t i; for (i 0; i 5; i) { buf[i] DHT11_ReadByte(); } if ((uint8_t)(buf[0] buf[1] buf[2] buf[3]) ! buf[4]) { return 1; /* 校验失败 */ } *humi buf[0]; *temp buf[2]; return 0; }校验失败时整帧丢弃不要输出半帧数据。DHT11 本身是慢传感器两次采集间隔至少保持 1 秒所以偶尔丢一帧不影响显示连续性。工程里如果没有做帧校验环境串口上就会看到温湿度偶尔整体跳变一个随机值定位起来很费劲。3. 标准库版本寄存器级GPIO模拟单总线读写3.1 延时函数校准与微秒级精度标准库工程里最常用的延时方式是软件循环但Delay_Us(40)的实际执行时间受编译器优化级别影响很大。Keil 的 -O0 和 -O2 编译出的循环耗时可能差 30% 以上。一个比较稳的做法是用 SysTick 做基准配置成 1μs 中断一次再用全局变量计数但中断会打断 GPIO 采样读位时反而可能踩到跳变沿。更简单的方案是保持软件循环但把延时函数和读位函数放在同一个 C 文件里关闭该文件的优化或者用逻辑分析仪实测后修正循环次数。标准库 V3.5 的模板里如果直接抄例程的Delay_Us要注意原例程主频可能是 8MHz而 STM32F103 的晶振倍频后是 72MHz延时差接近 9 倍。编译优化等级实测 40μs 延时误差读位稳定性-O0偏大 24μs稳定-O2偏小 812μs数据位偶尔错位-O2 __no_optimize接近理论值稳定优化等级对普通外设操作无感对 DHT11 这种微秒级单总线来说就是能不能稳定读帧的区别。我一般给延时函数加上__attribute__((optimize(O0)))这样即使整个工程开了 -O2采样时序部分仍然不受编译器影响。3.2 引脚初始化和总线复位标准库下 GPIO 初始化要把引脚先配置成推挽输出用于发送起始信号然后通过修改 CRL/CRH 寄存器切换到浮空输入或上拉输入。初始化时不能只配一种模式因为在一次完整采集任务里引脚要在输出和输入之间切换至少 41 次。void DHT11_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStruct.GPIO_Pin DHT11_PIN; GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DHT11_GPIO, GPIO_InitStruct); GPIO_SetBits(DHT11_GPIO, DHT11_PIN); }这里的GPIO_Mode_Out_PP只是初始状态DHT11_Pin_Input()内部要把模式改成GPIO_Mode_IPU也就是上拉输入。上拉输入能保证总线空闲时为高电平省掉外接上拉电阻。如果板子上已经接了 4.7kΩ 上拉改成GPIO_Mode_IN_FLOATING也可以两者表现差距不大。复位总线时GPIO_ResetBits拉低引脚只是一个寄存器写操作速度很快真正耗时的是后面的Delay_Ms(20)。有些资料写成Delay_Ms(18)但考虑到温度变化影响 RC 延时我习惯留出 20% 余量。3.3 GPIO方向切换的两种写法和读时序容差标准库里切换方向的正规做法是重新调用GPIO_Init但这种方式内部有几十条语句切换一次要 1μs 以上对位时序影响明显。读位函数里每读一位要切换两次方向累计误差不可忽略。更好的做法是直接操作 CRL 寄存器。void DHT11_Pin_Output(void) { GPIOA-CRL ~(0x0F (4 * 6)); /* 清除 PA6 配置 */ GPIOA-CRL | (0x03 (4 * 6)); /* 推挽输出 50MHz */ } void DHT11_Pin_Input(void) { GPIOA-CRL ~(0x0F (4 * 6)); /* 清除 PA6 配置 */ GPIOA-CRL | (0x08 (4 * 6)); /* 上拉/下拉输入配合设置 ODR */ GPIOA-ODR | (1 6); /* 选择上拉 */ }CRL 寄存器每 4 位控制一个引脚PA6 对应的偏移是4 * 6。0x03表示推挽输出 50MHz0x08表示输入模式。改为输入后还要设置 ODR 对应位置 1硬件上相当于内部上拉电阻接入。这套写法的执行时间只有几条汇编指令对时序影响可以忽略。3.4 标准库下的数据组包与错误丢弃策略一次完整的 DHT11 读取如果在中途失败比如读到的字节全是 0xFF总线状态可能已经异常。处理策略是读取失败后把本次数据标记为无效同时拉低总线复位一次并等待下次轮询周期再重试。不要在失败后立即连续重试DHT11 连续唤醒会拉高功耗而且大约需要 1 秒恢复时间。if (DHT11_ReadFrame(temp, humi) 0) { printf(TEMP:%d.%d HUMI:%d.%d\r\n, temp 4, temp 0x0F, humi 4, humi 0x0F); } else { printf(DHT11 frame error\r\n); }温度值的整数部分是buf[2]的高四位因为 DHT11 的温度范围到 50℃8 位有符号数够用所以高四位实际只用了 09 的值区间。小数部分在低四位多数器件恒为 0。temp 4得到整数temp 0x0F得到小数这样打印出来就是带一位小数的文本帧。4. HAL库版本CubeMX工程下的移植与代码生成4.1 CubeMX配置项与工程模板选择HAL 库版本的项目是从 STM32CubeMX 生成的工程文件里面保留了.ioc配置。CubeMX 里需要确认几个关键项缺一个都会让代码在运行时行为异常。SYS 的 Debug 必须选Serial Wire否则首次烧写后 SWD 引脚被复用成 GPIO再想下载程序会提示error: no stm32 target found这个问题出现在很多 STM32F103C8T6 最小系统板上。配置项推荐值说明SYS - DebugSerial Wire保留 SWD 下载能力RCC - HSECrystal/Ceramic Resonator使用外部 8MHz 晶振USART1 - ModeAsynchronous115200-8-N-1GPIO - PA6Output Push Pull初始输出态代码内动态切方向Clock ConfigurationHCLK 72MHz必须确认 PLL 倍频为 9PA6 在 CubeMX 里只能选一种初始模式HAL 库代码运行时再改方向。选Output Push Pull更合适因为上电瞬间引脚保持默认低电平概率小一点不会在传感器初始化时产生意外起始信号。4.2 引脚方向切换的两种写法HAL 库对 GPIO 方向切换没有提供专门的 API常规做法是重新调用HAL_GPIO_Init但它在内部做了延迟和时钟检测每次调用耗时在 3μs 以上对位时序是个隐患。另一种做法是访问 GPIO 的 MODER 寄存器HAL 库封装了结构体可以直接操作底层硬件。#define DHT11_PORT GPIOA #define DHT11_PIN GPIO_PIN_6 void DHT11_Set_Output(void) { GPIOA-MODER ~(GPIO_MODER_MODER6); /* 清空 PA6 模式位 */ GPIOA-MODER | (1U (2 * 6)); /* 01 输出模式 */ } void DHT11_Set_Input(void) { GPIOA-MODER ~(GPIO_MODER_MODER6); /* 00 输入模式 */ GPIOA-PUPDR | (1U (2 * 6)); /* 01 上拉 */ }GPIO_MODER_MODER6是 HAL 库头文件里预定义的掩码宏展开后就是寄存器操作。1U (2 * 6)表示 MODER 寄存器里每 2 位控制一个引脚PA6 对应第 1213 位。切输入模式时把 PUPDR 对应的两位设为01等效于标准库的GPIO_Mode_IPU。这两组宏定义放在dht11.h里读位函数和起始信号函数共用避免重复初始化。4.3 HAL库读时序与超时保护HAL 库版本的读位函数与标准库逻辑相同但读引脚用的函数是HAL_GPIO_ReadPin等待电平的while循环里必须加超时退出。DHT11 如果损坏或接线松动总线会一直保持高电平while循环会卡死整个采集线程。uint8_t DHT11_ReadBit(void) { uint16_t timeout 10000; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET) { if (--timeout 0) { return 0xFF; /* 超时返回异常值 */ } } Delay_Us(40); /* 跳过低电平窗口 */ timeout 10000; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { if (--timeout 0) { return 0xFF; } } return HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET ? 1 : 0; }超时值 10000 在 72MHz 主频下折算成时间远小于 DHT11 的响应窗口它只起保险作用正常工作时不会触发。注意第二个while等的是高电平结束这样可以确保下一位起始前总线回到低电平时序不会累积偏移。把超时返回值和校验失败都归为一次无效采集调用方只依赖最终校验结果不单独判断超时。4.4 工程中DSP库文件与采集无关不要删除源码包里混着iar_cortexM3b_math.a、iar_cortexM3l_math.a、libarm_cortexM3l_math.a、arm_common_tables.c、arm_linear_interp_data.c这些是 CMSIS-DSP 的库和查表文件。DHT11 采集过程只用 GPIO 翻转和延时完全不涉及浮点运算或 FFT所以这些文件对最终程序大小和运行效率没有影响。它们存在的原因是创建工程模板时勾选了 DSP 库选项Keil 或 IAR 把相应源文件拷贝到了工程目录。删除它们不会影响 DHT11 功能但如果工程里其他模块引用了arm_math.h链接阶段会报告找不到符号。我的建议是放在工程里不动反正编译时只链接被引用的部分不产生额外 Flash 占用。5. 串口显示与验证技巧从串口调试助手到逻辑分析仪5.1 printf重定向到USART1两个版本都用串口打印温湿度重定向方式不同。标准库工程在 Keil 里勾选MicroLIB后用fputc重定向HAL 库工程直接调用HAL_UART_Transmit。标准库的printf自带缓存和格式化逻辑代码更简洁HAL 库的printf重定向要处理半主机模式的问题。int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; }0xFFFF是发送超时时间单位是毫秒这里表示无限等待。串口调试助手要设置成 115200-8-N-1不要开流控。如果用的是 CH340 或 FTDI 芯片的 USB 转串口模块先确认 Windows 设备管理器里枚举出的 COM 口号下载完程序后重新拔插一次模块避免串口被旧驱动占用。5.2 常见异常现象与排查顺序异常现象可能原因排查方法温湿度输出为 0起始信号未生效引脚模式始终为输入检查DHT11_Set_Output是否被调用输出 255 或 0xFF总线被拉低后无法恢复确认外部上拉电阻焊接ODR 是否置 1数据偶尔跳变延时偏短采样点落在低电平窗口用逻辑分析仪测量延时实际值串口无任何输出串口助手波特率错误或引脚接反短接 TX/RX 自测发送第一次烧写后无法再下载SWD 引脚被复用CubeMX 里开启 Serial Wire按住复位烧写这些现象里最隐蔽的是“温湿度都显示 255”因为校验字节也变成了 0xFF帧校验反而能通过。遇到这种情况优先检查上拉电阻而不是调延时。DHT11 模块上如果已经集成了上拉电阻代码里就不需要开内部上拉两个上拉并联也不会产生问题。5.3 用逻辑分析仪抓时序波形定位问题软件调参解决不了的时序问题最终要靠逻辑分析仪。采样率设置在 10MHz 以上将探头接到 PA6 和 GND触发方式选下降沿抓取从起始信号到 40 位数据结束的完整波形。先看主机起始低电平宽度是否在 18ms 以上再看 DHT11 应答的低电平和 80μs 高电平是否完整最后逐位测量高电平宽度。波形上每一位都呈现“50μs 低 26μs 高”或“50μs 低 70μs 高”的模式。把光标放在高电平中段读出宽度26μs 附近是数据“0”70μs 附近是数据“1”如果看到高低电平交替杂乱说明延时函数实际执行时间和理论值偏差太大。这时优先把延时函数改成__no_optimize或检查系统主频是否在 72MHz而不是怀疑 DHT11 本身。本文还有配套的精品资源点击获取
返回列表