ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32驱动DHT11温湿度传感器:单总线时序与微秒延时全解析

STM32驱动DHT11温湿度传感器:单总线时序与微秒延时全解析 最近好几个做课设的学弟跑来问我说DHT11这个传感器看着简单——无非就是读个温湿度嘛结果一上手就卡住不是读出来全是0就是数据纹丝不动甚至干脆卡死在延时函数里。我去他们板子上一看问题基本都出在同一个地方对DHT11的单总线时序理解不够透要么直接拿了51的例程往STM32上套要么对HAL库的延时机制想当然。这玩意儿确实便宜、好用、资料满天飞但恰恰因为资料太杂反而容易把人带沟里。这篇教程我不打算照搬数据手册而是把自己调DHT11时踩过的坑、验证过的方法、以及从时序底层到代码实现的全过程捋一遍希望能帮你少走点弯路。1. 为什么DHT11是新手入门的必修课——单总线协议的缩影DHT11这颗传感器在STM32学习路线里的地位有点像练字时的永字。它本身不算什么高性能器件精度也就那样湿度±5%RH温度±2℃但它的通信方式——单总线——几乎涵盖了嵌入式底层开发最核心的几个基本功精确延时、GPIO方向切换、时序解析、数据校验。先说清楚一个概念什么是单总线简单讲就是一根数据线既当发送又当接收主机和设备之间靠严格的时序约定来区分谁在说话、说的什么。DHT11用的就是这种协议和DS18B20类似但比DS18B20更简单没有任何ROM指令、搜索命令之类的花活。这根线上的电平状态在不同时间段有完全不同的含义所以你必须用微秒级的延时去掐时间点。很多初学者在这里犯的第一个错就是拿HAL_Delay()去做微秒延时——HAL库的HAL_Delay()基于SysTick最小单位就是1ms根本掐不了微秒级别的时序。这个坑我后面会专门展开。为什么我强烈建议新手把DHT11调通一遍因为你在调它的过程中会自然地把下面这几件事打通GPIO输出模式与输入模式的切换你要先拉低总线发起起始信号再释放总线等待设备响应中间涉及引脚从输出模式切换到输入模式。很多人就是漏了这一步或者切换方式不对导致永远读不到数据。微秒级延时的实现无论标准库还是HAL库都要搞清楚怎么生成可靠的微秒延时。这是后面驱动任何时序型传感器比如超声波测距、DS18B20、部分LCD屏的通用底子。数据手册的阅读能力DHT11的时序图其实画得很直白但你得学会把datasheet里的时间参数翻译成代码里的延时阈值。这个能力比DHT11本身值钱得多。所以这篇文章的定位不是给你一段能用的代码复制粘贴而是带着你把整个通信过程从头到尾拆一遍。哪怕你用的是国产的APM32、GD32这类STM32兼容芯片或者想移植到K210上去只要理解了这套时序逻辑换平台无非是换个寄存器操作方式而已。2. 硬件接线与选购别看简单这里有三个明显的坑2.1 引脚定义与接线方案DHT11常见的有两种形态裸传感器四根引脚直插和模块集成上拉电阻和去耦电容。引脚定义基本一致区别在模块上已经帮你把外围电路做好了。下面以最常见的裸传感器为例引脚名称接线说明1VDD电源正极接3.3V或5V均可2DATA数据线必须接一个4.7kΩ~10kΩ上拉电阻到VDD3NC悬空不接4GND电源负极STM32的数据引脚建议选择普通GPIO即可比如PA0、PB5之类的不要用JTAG/SWD复用的引脚PA13、PA14、PA15、PB3、PB4否则调试的时候会出各种灵异问题。2.2 坑一上拉电阻不是可有可无的DHT11的数据线是开漏输出结构意思是它只能主动拉低电平不能主动拉高。高电平必须靠外部上拉电阻来实现。模块版本一般已经集成了上拉电阻但如果你买的是裸传感器却忘了加上拉电阻总线就会悬浮不定读出来的数据完全是一堆乱码。我见过一个比较极端的案例有人用裸传感器直接飞线到STM32开发板上没加上拉电阻结果数据时好时坏还以为是代码问题调了两天。最后加上一个10kΩ上拉电阻问题立刻消失。注意STM32的部分GPIO内部有上拉电阻可以配置但内部上拉的阻值较大约30kΩ~50kΩ在一些环境下可能不够稳定。如果你不想外接电阻也可以尝试开启内部上拉但建议手头有模块或电阻的情况下优先用外部上拉可靠性更高。2.3 坑二供电电压的选择并非随意DHT11官方手册写的供电范围是3.3V~5.5V但实际上在3.3V供电和5V供电下上升沿的陡峭程度是有区别的。5V供电时总线的高电平是5V如果你的STM32是3.3V电平系统数据引脚直接灌入5V高电平虽然大多数STM32引脚是5V容忍的FT引脚但为了保险起见最好还是统一用3.3V供电。如果你用的是模块版本一般模块上已经有电平转换电路那就无所谓了。但如果用裸传感器接5V供电然后用一个电阻分压或者电平转换电路接STM32也不是不行只是没必要——DHT11在3.3V下工作完全正常时序参数没有明显变化。2.4 坑三传感器响应速度没有那么快DHT11的采样周期是1秒一次也就是你两次读取间隔至少要大于1秒。我见过有人在主循环里无延时疯狂读取结果发现读出来的数据一直是同一个值还以为传感器坏了。其实不是是DHT11根本还没完成新的采样。实际使用中最好的做法是定时读取比如每隔2秒启动一次采集流程。这样既保证数据有效又避免频繁通信对传感器寿命的影响。3. 单总线时序拆解DHT11协议到底在说什么DHT11的通信过程可以分成两个阶段主机发起起始信号然后DHT11应答并连续输出40位数据。整个过程都在一根线上完成下面我把每一个环节的时间参数都标出来你就明白为什么微秒延时如此重要了。3.1 空闲状态通信开始前总线处于空闲状态由上拉电阻保持高电平。这个高电平是双方通信的基础——谁拉低总线谁就相当于抢占了话语权。3.2 主机发起起始信号主机STM32先把数据引脚配置为输出模式然后拉低总线至少18ms通常建议18~20ms再释放总线拉高。这个低电平就是告诉DHT11我要开始一次通信了你准备一下。注意这个18ms的时间跨度在微秒级时序里算是个漫长的信号所以要求不算苛刻用HAL_Delay(18)也来得及。真正的考验在后面的微秒级环节。3.3 DHT11的应答信号主机释放总线后需要立刻把引脚从输出模式切换为输入模式然后等待DHT11的响应。DHT11收到起始信号后会先拉低总线80μs再拉高80μs这个低80、高80的组合就是应答信号相当于DHT11说我准备好了下面开始发数据。如果你在主机释放总线后的20~40μs内没有检测到低电平说明DHT11没有正确响应。常见原因包括起始信号时间不够长、接线错误、或者传感器故障。3.4 40位数据的输出应答信号结束后DHT11开始连续输出40位数据。这40位的排列顺序是湿度整数部分8bit湿度小数部分8bit温度整数部分8bit温度小数部分8bit校验和8bit每一位数据的传输格式都是先拉低50μs然后根据高电平持续时间的长短来表示该位是0还是1。关键就在这个高电平的持续时间上如果高电平持续26~28μs表示该位是0如果高电平持续70μs左右表示该位是1所以读数据的核心操作是循环40次每次先等待引脚变为低电平然后等待引脚变为高电平最后用延时或超时机制测量这个高电平持续的时间根据时间长短判断该位是0还是1。3.5 通信结束与总线释放40位数据发送完毕后DHT11会拉低总线50μs然后释放总线恢复空闲高电平状态。至此一次通信结束主机等下个周期再发起下一次读取。信号格式里后半部分还有没有更多细节没有了。DHT11的整个通信就是这么简单粗暴——没有地址、没有命令字、没有CRC多项式校验。校验方式是简单的校验和验证接收完40位数据后把前四个字节加起来取低8位如果和第五个字节一致说明数据有效否则丢弃本次数据等待下次读取。4. 驱动代码实现标准库版与HAL库版的完整剖析代码部分我会分别给出标准库和HAL库两个版本的实现思路。不贴完整工程代码但核心函数我会写清楚。这两个版本你理解一个另一个自然就通了。4.1 微秒延时函数绕不过去的第一块基石无论是标准库还是HAL库都得先解决微秒延时问题。我常用的方法有两种方法一SysTick延时裸机环境下直接用SysTick做微秒延时是最通用的做法。核心思路是每次调用时重新加载SysTick的计数值然后轮询COUNTFLAG。但要注意如果你使用了HAL库SysTick已经被HAL_Init()占用了而且HAL库的HAL_Delay()也依赖SysTick中断直接改SysTick配置会和HAL库打架。所以我一般在HAL库工程里采用的微秒延时方案是关闭SysTick中断改用DWTData Watchpoint and Trace核内调试单元。DWT里有一个CYCCNT计数器它随着内核时钟每个周期自增可以做到精确的周期计数。实现方式如下// 使用DWT实现微秒延时适用于Cortex-M3/M4/M7 static volatile uint32_t uwTick_old; void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; // 使能DWT DWT-CYCCNT 0; // 清零计数器 DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; // 使能CYCCNT } void delay_us(uint32_t us) { uint32_t ticks us * (SystemCoreClock / 1000000); // 计算需要的时钟周期数 uint32_t start DWT-CYCCNT; while ((DWT-CYCCNT - start) ticks); // 等待计数器到达目标值 }这个方案的好处是不占用SysTick不影响HAL库的HAL_Delay()并且精度很高。注意SystemCoreClock要根据你实际的系统主频来不然延时会不准。很多人的delay卡死问题根因就是这个值没配对。方法二定时器延时如果你不想碰DWT可以用一个基本定时器比如TIM6、TIM7做微秒延时。定时器配置为1μs计数一次每次延时前清零并启动超时判断。这个方法更通用但代码量稍大我一般在需要多个不同延时通道时才用定时器方案。4.2 标准库版核心代码使用标准库的GPIO操作非常直观直接操作寄存器或标准库函数都行。下面是一个完整的DHT11读取流程// 引脚定义 #define DHT11_GPIO_PORT GPIOA #define DHT11_GPIO_PIN GPIO_Pin_0 #define DHT11_RCC RCC_APB2Periph_GPIOA // 输出高/低电平 #define DHT11_OUT_H() GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN) #define DHT11_OUT_L() GPIO_ResetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN) // 读取引脚电平 #define DHT11_IN_READ() GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) // 引脚方向切换 void DHT11_Pin_Mode(GPIOMode_TypeDef mode) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode mode; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStructure); } // 从总线读取一个位返回0或1 uint8_t DHT11_ReadBit(void) { uint8_t retry 0; // 等待低电平结束50us低电平之后会拉高 while (DHT11_IN_READ() 0) { delay_us(1); if (retry 100) return 0xFF; // 超时保护 } // 低电平结束后延时40us再判断电平 // 如果仍然为高则是1如果已变低则是0 delay_us(40); if (DHT11_IN_READ()) { return 1; } else { return 0; } } // 读取40位数据 uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t buf[5] {0}; uint8_t i, j; // 主机发起起始信号 DHT11_Pin_Mode(GPIO_Mode_Out_PP); DHT11_OUT_L(); delay_us(19000); // 拉低至少18ms DHT11_OUT_H(); // 切换为输入模式释放总线 DHT11_Pin_Mode(GPIO_Mode_IN_FLOATING); delay_us(20); // 等待DHT11响应 // 检测应答信号低80us if (DHT11_IN_READ() 0) { // 等待应答低电平结束 uint8_t retry 0; while (DHT11_IN_READ() 0) { delay_us(1); if (retry 100) return 1; } // 等待应答高电平结束80us retry 0; while (DHT11_IN_READ() 1) { delay_us(1); if (retry 100) return 1; } // 开始读取40位数据 for (i 0; i 5; i) { for (j 0; j 8; j) { uint8_t bit DHT11_ReadBit(); if (bit 0xFF) return 1; // 超时 buf[i] (buf[i] 1) | bit; } } // 校验和验证 uint8_t checksum (buf[0] buf[1] buf[2] buf[3]) 0xFF; if (checksum ! buf[4]) return 2; // 校验失败 *humidity buf[0]; *temperature buf[2]; return 0; } return 1; }注意几个细节GPIO_Mode_IN_FLOATING是浮空输入不加上拉也不下拉。对于DHT11来说总线空闲时由上拉电阻保证高电平所以浮空输入没问题。delay_us(40)是判断0/1的标准做法数据位开始时的50μs低电平结束后延时40μs去采样引脚。如果这时引脚还是高电平说明是170μs高电平还没结束如果已经变低了说明是026~28μs高电平已经结束。超时保护非常重要。如果总线卡死或者传感器没接好没有超时判断会让程序死在循环里——这就是很多人说的delay卡死的来源之一。4.3 HAL库版核心代码HAL库版的本质和标准库完全一样只是GPIO操作的API变了。重点注意HAL_GPIO_ReadPin()的速度比标准库的寄存器操作慢不少在微秒级时序里如果你频繁调用HAL_GPIO_ReadPin去轮询可能引入几个微秒的延迟导致时序偏移。所以我在HAL库工程里读取引脚电平会直接操作寄存器不走HAL封装#define DHT11_GPIO_PORT GPIOA #define DHT11_GPIO_PIN GPIO_PIN_0 // 直接操作IDR寄存器读取电平速度快 #define DHT11_IN_READ() (DHT11_GPIO_PORT-IDR DHT11_GPIO_PIN)引脚模式切换还是得走HAL但只用它来配置模式切换void DHT11_Pin_Mode(uint32_t mode) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_GPIO_PIN; GPIO_InitStruct.Mode mode; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); }读取数据的主流程和标准库版几乎一样我这里就不重复贴了。提示HAL库工程中使用HAL_Delay(18)实现起始信号的18ms低电平完全没问题因为毫秒级延时用HAL是安全的。但一旦进入数据读取阶段微秒级操作务必使用自己的微秒延时函数。4.4 时序逻辑的总结表格过程操作时间参数常见错误起始信号主机拉低总线至少18ms时间不够DHT11无响应释放总线主机切换输入模式20~40μs未切换模式读不到数据应答信号DHT11拉低80μs再拉高80μs各80μs接线错误或传感器损坏数据位050μs低 26~28μs高高电平短误判为1数据位150μs低 70μs高高电平长误判为0校验和前4字节累加低8位—校验失败应丢弃本次数据5. 踩坑实录从现象反推根因的排查链路调试DHT11的过程中我积累了三个比较典型的故障排查经验这里把完整思路写出来比直接告诉你答案更有价值。5.1 现象一读出来的数据固定为0或255排查链路第一步先确认接线。用万用表量一下数据引脚的电平空闲状态下应该是高电平因为有上拉电阻。如果量到低电平说明上拉电阻没接或者传感器已经损坏短路。第二步确认代码是否成功发出了起始信号。可以用示波器或者逻辑分析仪抓数据引脚的波形。没有示波器的话可以在delay_us(19000)前后翻转一个调试引脚用LED或者串口观察时间间隔。第三步确认引脚模式切换。很多人的代码在发起起始信号后忘了把引脚配置为输入模式导致后续的读取操作读到的只是自己输出寄存器的值——永远是0或者1。这就是为什么我一直强调模式切换是单总线通信最关键的一步。5.2 现象二程序卡死在等待应答信号的循环里这个现象对应的就是热搜词里的stm32延时函数delay卡死。通常是两种情况情况A超时保护逻辑没用while循环出不来。如果DHT11_IN_READ() 0这个条件永远为真说明总线被拉低了再也回不到高电平或者引脚模式配置错误。所以每一处while等待都必须加上超时计数器。情况B微秒延时本身就不准导致逻辑判断全乱了。比如delay_us(40)实际延时了400μs时间窗口完全错位本来应该等低电平结束的结果已经跳到了读取数据位阶段时序完全混乱。这种情况要回头检查SystemCoreClock的值和DWT的初始化是否正确。5.3 现象三读出的温湿度数值跳变异常忽高忽低这个一般是两个原因一个原因是校验和没加或者校验失败后没有做数据处理把错误数据直接显示出来了。另一个原因是读取间隔太短DHT11还没完成新采样就被读取数据自然不更新或者照搬上一次的。规范做法是读取后先做校验校验失败就丢弃保留上一次的正确值两次读取间隔不小于500ms建议1~2秒一次。6. 从Demo到实用稳定性和扩展性进阶6.1 在RTOS环境中使用DHT11如果你用的是FreeRTOS这类操作系统需要注意微秒延时在RTOS环境中可能会被任务调度打断导致时序不准确。最简单的办法是在读取DHT11期间用taskENTER_CRITICAL()进入临界区关中断保证时序不受干扰或者把DHT11读取放到一个独立的高优先级任务里。读取DHT11的任务需要设置合理的阻塞延迟比如vTaskDelay(pdMS_TO_TICKS(2000))避免高频读取。涉及多任务共享温湿度数据时建议加上互斥锁或者使用任务通知来保护数据一致性。6.2 典型应用温湿度超限报警与显示做完基础的读取和串口打印之后可以很快扩展出一个实用的小系统把DHT11的数据接到OLED显示再配合一个蜂鸣器实现温湿度超限报警。这个方案在很多课设和毕设里非常常见。下面是一个简化的逻辑框架while (1) { uint8_t humi 0, temp 0; if (DHT11_ReadData(humi, temp) 0) { // 显示到OLED OLED_ShowNum(0, 0, temp, 2); OLED_ShowNum(0, 2, humi, 2); // 超限报警 if (temp 30 || humi 70) { BEEP_ON(); } else { BEEP_OFF(); } } HAL_Delay(2000); // 2秒读一次 }6.3 和K210等平台通信热搜词里有一条k210与stm32通讯这个场景很典型STM32负责读取DHT11数据K210做图像识别或者语音交互两边通过串口或SPI/I2C通信。核心思路是让STM32做传感器数据的稳定采集把处理好的温湿度数值通过串口打包发送K210只需要按协议解析即可。串口数据的简单协议可以这样设计帧头0xAA 温度1字节 湿度1字节 校验1字节。我在实际项目里验证过简单的帧头数据校验和足够满足大部分场景不需要引入复杂的通信协议。6.4 为什么推荐后续换用DHT22或者SHT30DHT11的成本优势很明显但精度确实一般。如果你的项目对温湿度精度有更高要求——比如做一个恒温恒湿的控制系统——可以考虑换DHT22精度更高但时序完全相同或者SHT30I2C接口精度和稳定性都更好。复用性方面有一点值得注意DHT22和DHT11的通信时序几乎是同一套只是数据格式略有不同DHT22是16位湿度16位温度8位校验温度为带符号数。所以如果你已经把DHT11吃透了换DHT22的代码改动量很小只需要修改数据拼接部分即可。7. 写在代码之外给新手的调试工具建议最后聊一个很实际的话题调DHT11用什么工具排障最有效首先示波器或逻辑分析仪是绝对的神器。DHT11的时序频率不高一个几十块钱的入门级逻辑分析仪就够了。把数据线接到逻辑分析仪的通道上抓一段通信波形你就能直观地看到起始信号、应答信号、40位数据的完整形态直接从波形上判断时序是否正确。我调试这类单总线器件时逻辑分析仪几乎是人手必备。其次串口打印是快速验证手段。不用看波形直接打印出错码和数据值根据错误码定位问题在哪一步。我在代码里习惯用返回码区分0正常1通信超时2校验失败。这样排障效率会高很多。再次万用表用于基础电气检查。确认电源电压、上拉电阻是否正常、数据线是否短路或断路。从入手到调通我自己的经验是先看时序图再写代码最后用逻辑分析仪验证。不要急着抄代码DHT11的时序逻辑并不复杂吃透了它你对单总线协议的理解会上一个台阶后面再用任何单总线传感器基本一周内都能搞定。
返回列表