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

资讯详情

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

DHT11温湿度传感器实战:从单总线原理到STM32代码详解

DHT11温湿度传感器实战:从单总线原理到STM32代码详解 DHT11这颗传感器可以说是我见过的最“亲民”的温湿度采集方案了。玩STM32的几乎没有不认识它的不管是江科大视频入门的新手还是做智能鱼缸、宿舍灯控这类小项目的资深玩家DHT11总是那个最省心的起点。市面上的教程一抓一大把但多数只告诉你“这么接、这么读”却没人告诉你为什么要接上拉电阻、为什么读到的数据全是0、为什么程序跑着跑着就卡死在延时函数里。这篇教程我不打算只给你贴一段能跑的代码而是把DHT11从原理到实战整个捋一遍。包括单总线时序协议、硬件原理图设计、HAL库和标准库两种写法、以及我这些年踩过的坑和排查思路。无论你是刚点亮LED想更进一步的新手还是被DHT11时序折磨到怀疑人生的老玩家这篇文章都应该能帮到你。1. DHT11到底是什么——先搞清楚这传感器的工作底细1.1 官方参数先放出来省得你到处查手册先看一张参数速查表这可比你满网翻PDF方便多了参数数值说明供电电压3.3V ~ 5.5V建议5V供电3.3V时序上略有差异测量范围湿度20% ~ 90% RH超出范围精度无法保证测量范围温度0°C ~ 50°C超过50°C不建议长期使用湿度精度±5% RH差不多就是“大约”级别温度精度±2°C跟DS18B20比差不少分辨率1% RH1°C整数输出别指望小数采样周期1秒每次读取间隔必须大于1秒单总线协议单根数据线双向通信时序要求严格看完这个表你应该心里有数了DHT11的精度确实一般但它便宜、简单、够用这才是它经久不衰的真正原因。室内环境监测、智能鱼缸、实验室温湿度记录这些场景对精度的要求其实没那么苛刻DHT11完全扛得住。1.2 单总线协议是怎么工作的DHT11用的是维基百科上叫“单总线1-Wire”的通信协议但跟DS18B20那种不同DHT11是自己的一套私有时序更简单也更脆弱。硬件上只需要一根数据线既当输入又当输出主机STM32和从机DHT11轮流占用这根线。整个通信过程分三个阶段主机发起起始信号主机把总线拉低18~30ms然后释放总线。这个低电平要足够长DHT11才能“听到”你在叫它。注意太短了不行DHT11来不及响应太长了也有隐患容易超出它的容忍范围。DHT11响应信号DHT11检测到起始信号后会主动拉低总线80us再释放80us相当于打了个招呼“我在准备发数据了”。数据传输响应信号之后DHT11连续输出40位数据每一位都用“低电平50us 高电平Xus”来表示。关键在高电平的持续时间高电平持续26~28us代表逻辑0持续70us代表逻辑1。所以读DHT11的核心工作就是精确测量这40位数据里每一比特高电平的宽度。这也是为什么延时函数的质量直接决定了读取的成败。1.3 为什么DHT11适合入门项目我见过太多人一上来就想上SHT30、BME280这种I2C接口的高精度传感器结果卡在I2C地址配置和寄存器读写上半天调不通。DHT11虽然精度不如它们但对于入门学习来说它的价值恰恰在于“麻烦”——要手写时序、要精确延时、要处理校验和做完这一整套你对GPIO的理解、对时序的敏感度、对数字逻辑的把握都会上一个台阶。而且从项目落地的角度看DHT11做环境监测完全够用。我做过的智能鱼缸项目里水温监测用DS18B20空气温湿度就用DHT11。鱼缸上方空气的温湿度变化本来就不是突变型的DHT11的1Hz采样频率和整数精度完全够用成本还低坏了换一颗也不心疼。2. 硬件准备与原理图设计2.1 接线只有三根线但别小看它们DHT11常见的有两种封装四针直插式蓝壳和三针模块式。先说直插式它长这样引脚顺序从正面看从左到右1脚VCC供电2脚DATA数据3脚NC空脚不用接4脚GND地模块式就更简单了板上已经帮你把上拉电阻、滤波电容都做进去了引出三个引脚VCC、DATA、GND。你只需要连三根杜邦线就完事。这里有个关键点无论直插式还是模块式数据线DATA都必须接一个上拉电阻到VCC阻值4.7kΩ~10kΩ。模块式的板子上已经有了直插式的你必须自己加。为什么必须上拉因为DHT11的数据线是开漏输出它只能拉低总线、不能主动拉高总线。高电平完全依赖外部上拉电阻。你要是忘了接这个电阻读回来的数据一定是一堆乱码或者干脆全是0。这个坑我见过太多人踩了甚至包括一些号称“照着教程接线”的博主。2.2 上拉电阻与去耦电容如何选上拉电阻的取值不是随便选的。阻值太小电流消耗大信号边沿更陡峭但会增加DHT11的驱动负担阻值太大上升沿会变缓在长线传输时波形容易畸变导致时序判断出错。实际项目中4.7kΩ是首选其次10kΩ也能用尤其在3.3V供电时10kΩ更稳妥。另外强烈建议在VCC和GND之间加一个100nF的陶瓷去耦电容紧贴DHT11的电源引脚放置。别觉得这是多余操作DHT11工作时电流变化虽然不大但STM32的GPIO翻转、板上其他外设的开关都会造成电源纹波这个电容能有效稳住供电电压减少读数的随机抖动。如果供电电压是5V而STM32是3.3V系统电平兼容性问题不大因为DHT11的高电平输出接近VCC读GPIO时只要高于STM32的VIH阈值一般1.8V左右就能识别为1。但反过来如果STM32用开漏输出加外部上拉到3.3VDHT11的VCC却接了5V这时电平匹配也没问题。真正要注意的是不要让DHT11的数据线直接承受5V电压而不做任何处理模块式板子都考虑到了这个问题直插式得你自己确认。2.3 嘉立创画原理图时的几个坑现在大家画原理图基本都用嘉立创EDADHT11这个元件库也比较全。我分享几个画图时的经验元件选型时注意封装DHT11有两种主流封装一种是常见的4针直插外形像蓝盒子一种是贴片封装。原理图符号看起来差不多但PCB封装一定要选对否则焊盘对不上打样回来只能飞线。标注一下引脚序号DHT11的引脚排列不对称1脚VCC、4脚GND中间是DATA和NC。画原理图时如果你不标清楚Layout的时候很容易把电源和数据线搞反。我建议在原理图里直接写上“VCC 1”这样的注释省得后面还得查手册。上拉电阻的位置上拉电阻要尽量靠近DHT11的数据引脚而不是靠近STM32的GPIO引脚。这样做的原因是让上拉效应对DHT11的输出路径更直接减少长线传输时的串扰。线长尽量短DHT11的单总线协议对线长敏感PCB上数据线走线不要超过10cm如果要用杜邦线连接外部探头尽量控制在20cm以内。超过这个长度波形畸变会导致读取出错这时候加一个1kΩ的串联电阻在源端可以做简单的阻抗匹配能改善不少。3. 代码实现——HAL库和标准库两手抓3.1 微秒级延时函数DWT大法读DHT11时序最关键的是要能精确延时几十微秒。很多人直接用“空循环for循环”来延时这在标准库时代勉强能用但代码优化等级一改延时时间就变了非常不可靠。我推荐用DWTData Watchpoint and Trace来实现微秒延时。DWT是Cortex-M内核自带的调试外设里面有一个CYCCNT周期计数器可以直接数CPU时钟周期精度高、不依赖SysTick、不受中断影响。实现代码如下// dwt_delay.h #ifndef __DWT_DELAY_H #define __DWT_DELAY_H #include stm32f1xx_hal.h void DWT_Delay_Init(void); void DWT_Delay_us(uint32_t us); #endif// dwt_delay.c #include dwt_delay.h void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } void DWT_Delay_us(uint32_t us) { uint32_t start_cnt DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start_cnt) ticks); }这段代码的核心是SystemCoreClock它代表当前系统主频。如果你的STM32跑在72MHz那1us就是72个时钟周期。DWT_Delay_Init必须在系统时钟配置完成后调用一次之后就可以到处用DWT_Delay_us了。为什么不用HAL自带的HAL_Delay因为它只能精确到毫秒而且依赖SysTick中断。如果你在中断里调用DWT_Delay_us或者在SysTick被重定义、暂停的情况下使用就会卡死。3.2 GPIO输入输出模式切换DHT11的通信过程要求数据线在“主机输出”和“主机输入”之间来回切换。HAL库中每次切换模式都需要重新初始化GPIO这里有个常见的性能陷阱HAL_GPIO_Init()内部会做很多检查工作开销比较大。但你不用太担心因为DHT11数据量很小这点开销完全可忽略。标准库的写法是用GPIO_InitStructure重新初始化同一个端口本质上跟HAL库差不多。我个人的建议是用宏封装好切换函数代码看起来更干净#define DHT11_GPIO_PORT GPIOC #define DHT11_GPIO_PIN GPIO_PIN_8 void DHT11_Set_Output(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_GPIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); } void DHT11_Set_Input(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_GPIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); }注意切到输入模式时我设置了内部上拉GPIO_PULLUP。虽然外部已经有4.7kΩ上拉但内部上拉等于双保险即使在外部上拉失效的情况下读回来的电平也不会浮空乱跳。3.3 完整读取时序代码有了延时函数和GPIO切换函数下面就是核心的读取时序了。完整代码如下uint8_t DHT11_Read_Data(uint8_t *humidity_int, uint8_t *temperature_int) { uint8_t data[5] {0}; uint32_t timeout 0; // 主机发送起始信号 DHT11_Set_Output(); HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_RESET); DWT_Delay_us(20000); // 拉低至少18ms用20ms更稳 HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET); DWT_Delay_us(30); // 释放20~40us // 切换为输入模式 DHT11_Set_Input(); // 等待DHT11拉低响应信号 timeout 1000; while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { if (--timeout 0) return 1; // 超时退出 } // 响应信号80us低 80us高 timeout 1000; while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_RESET) { if (--timeout 0) return 1; } timeout 1000; while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { if (--timeout 0) return 1; } // 读取40位数据 for (int i 0; i 40; i) { // 每一位开始必然是先拉低50us timeout 1000; while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_RESET) { if (--timeout 0) return 1; } // 高电平持续时间决定0或1 DWT_Delay_us(40); if (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { data[i / 8] | (0x80 (i % 8)); // 这个bit是1 } // 否则是0不需要额外操作 // 等待该位结束高电平变低为下一位做准备 timeout 1000; while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { if (--timeout 0) return 1; } } // 校验和验证 if ((uint8_t)(data[0] data[1] data[2] data[3]) ! data[4]) { return 2; // 校验失败 } *humidity_int data[0]; *temperature_int data[2]; return 0; // 成功 }这段代码的核心逻辑是每一位数据低电平必然持续50us我不管这个低电平有多长只要检测到低电平结束线变高就开始计时。等40us后去读引脚状态如果还是高说明高电平时长超过了40us这是逻辑1的特征70us如果是低说明高电平只有26~28us早就结束了这是逻辑0。用40us作为阈值完美区分0和1。“等待该位结束”那一步很多人会忽略导致连续读位时错位。必须等到当前数据位的高电平完全结束才能开始读取下一位否则会把同一个高电平当成新的一位来读结果全乱。3.4 HAL库 vs 标准库选型心得现在的教程基本分两派江科大经典的标准库教程和ST官方主推的HAL库。我两个都写过说说感受对比项标准库HAL库代码量直接操作寄存器简洁封装多初始化代码长学习曲线需要理解寄存器细节上手快CubeMX自动生成可移植性换芯片型号要改不少换同系列芯片基本不用改调试便利需要自己查寄存器手册HAL内置很多断言和错误处理社区资源老教程多江科大系列官方生态完善CubeMX配合好我的建议是如果你刚入门直接用HAL库省下的时间足够你多调几个功能模块。但前提是你要看懂HAL库生成的代码背后的寄存器操作不要只做“复制-粘贴-编译-下载”的搬运工。如果你做的是量产项目需要精细控制代码大小和功耗标准库或者直接寄存器操作更合适。不管用哪个库DHT11的读取逻辑本身是一样的核心都在那40位数据的时序读取上。你只要吃透时序换库就是换个API名字的事。4. 常见问题排查与避坑指南4.1 数据全是0或全是1先别怀疑传感器坏了这是DHT11最经典的故障现象。95%的情况不是传感器坏了而是下面几个原因之一上拉电阻没接或没接对数据线悬空电平不确定读取结果自然是乱码或全0。GPIO模式配错了延时期间还保持在输出模式导致DHT11无法拉低总线整个时序全部推倒。起始信号拉低时间太短有些代码里写的是HAL_Delay(20)但毫秒级延时在微控制器里经过库函数封装后可能不准导致DHT11根本没启动。建议直接用DWT做20ms精确延时或者干脆用HAL_Delay(25)多给点余量。供电电压不稳DHT11的工作电压范围是3.3V~5.5V但如果供电电压恰好处于临界值且电源纹波大DHT11可能无法正常响应。这种情况把供电改到5V试试。排查顺序我建议这样来先用万用表测VCC是否为标称值再测上拉电阻两端电压是否正常数据线空闲时应为高电平然后跑一个最简单的读引脚程序看空闲时读回来的电平状态。4.2 delay函数卡死真相很多人在跑DHT11代码时发现程序卡死在延时函数里出不来。这个我太有经验了最常见的几个原因使用了HAL_Delay但SysTick被其他代码重定向比如你在初始化FreeRTOS或者重新配置了SysTickHAL_Delay就会失效甚至卡死。DWT延时能完美规避这个问题。中断里调用延时函数HAL_Delay依赖SysTick中断如果在更高优先级的中断里调用SysTick中断进不来就会死等。用DWT就没有这个限制。时钟配置问题如果你用CubeMX配置了PLL但DWT延时里用的是SystemCoreClock注意CubeMX生成的代码里SystemCoreClock是自动更新的应该没问题。但如果你手动改了时钟配置忘记更新SystemCoreClock这个变量那DWT延时的实际时间就会完全不对读时序自然全部失败。排查方法很简单下载代码后先跑一个LED闪烁程序确认SysTick和时钟正常再逐个加外设。如果LED不闪了说明是时钟配置本身的问题与DHT11无关。4.3 error: no stm32 target found怎么破这个报错经常出现在下载调试阶段虽然跟DHT11本身无关但我搜热词时发现很多人卡在这一步。核心原因就是ST-Link连不上芯片排查步骤从简单到复杂排列检查接线SWDIO、SWCLK、GND三条线必须连对。ST-Link的SWD接口不供电你还得单独给板子供3.3V或5V电源。检查BOOT0引脚如果BOOT0被拉高芯片启动后进入系统存储器模式核心可能没跑用户程序SWD连接会异常。正常下载时BOOT0应该接低电平。检查复位电路如果复位引脚被拉死芯片一直处于复位状态SWD自然连不上。测一下NRST引脚的电平正常应该是高电平。降低SWD时钟频率有些ST-Link默认速率太高在长线或杜邦线连接时信号衰减严重。可以在Keil的调试设置里把SWD时钟从4MHz降到1MHz甚至更低。先按住复位键连接如果上述都不行可以先按住板子复位键点Keil下载图标在开始擦除瞬间松开复位键这个“野路子”在芯片内部程序跑飞导致SWD被禁用时特别管用。4.4 温度湿度长期不变大概率是采样频率问题还有一个特别隐蔽的坑DHT11的采样周期是1秒也就是说你每秒最多只能发起一次读取如果读得太频繁DHT11根本不响应你的起始信号读回来的数据永远是上一次缓存的值。这就导致一个现象你的LCD屏上湿度一直显示55%温度一直显示26°C感觉“传感器坏了”实际上是你主循环里读取频率太高DHT11罢工了。解决办法很简单在读取函数外面加一个时间判断确保两次读取间隔至少1.2秒uint32_t last_read_time 0; uint32_t now HAL_GetTick(); if (now - last_read_time 1200) { if (DHT11_Read_Data(hum, temp) 0) { // 更新显示 } last_read_time now; }这个做法既保证了数据实时性又不会浪费CPU时间反复调用读取函数避免影响系统其他任务的执行。5. 从模块到项目——几个实际应用思路5.1 智能鱼缸不只是测水温DHT11在智能鱼缸项目里被用得很多。鱼缸环境监测通常需要两个传感器水里放DS18B20测水温鱼缸上方空气用DHT11测温湿度两个数据共同决定加热棒和雾化器的工作状态。我做过一个完整的项目空气湿度低于60%时自动开启加湿雾化器温度高于30°C时启动风扇通风同时数据通过ESP8266上传到手机端。整套系统核心就是STM32 DHT11 DS18B20 ESP8266DHT11在其中扮演的角色是“环境感知基础层”代码完全可以用上面那套读取逻辑。有一点要注意鱼缸上方蒸汽大、湿度高DHT11长时间工作在90%RH以上环境精度会漂移甚至内部电路受潮损坏。所以传感器不要直接挂在鱼缸上方要挂在稍远离水面且空气流通的位置。5.2 宿舍环境监测与联动控制另一个热门的DHT11应用场景是宿舍环境监测常跟灯控、窗帘联动。比如温湿度传感器检测到室内温度高于28°C且湿度高于70%自动触发风扇或空调插座检测到温度低于18°C自动开启加热设备。这种宿舍小项目DHT11的精度完全够用成本又低一个宿舍组网下来传感器成本不超过20块钱。这种项目里DHT11的位置就非常重要了别放在窗户旁边或者直接暴露在空调风口下否则读数跳变很厉害。放在室内靠中间的墙面离地1.5米左右是最有代表性的位置。我还见过有人拿DHT11做实验室环境监控把数据通过串口打印到电脑上用Python脚本做曲线绘制。这个思路我很推荐STM32负责采集串口输出格式化数据上位机做展示双向分离调起来也方便。写在最后的一点经验DHT11玩了这么多年我的体会是它的价值不在于性能而在于“让你真正理解什么是时序”。每一个微秒级的高低电平变化背后都是MCU和设备之间实实在在的物理交互。把DHT11调通的那天你对STM32的GPIO、时钟、延时的理解会比之前所有LED闪烁加起来都要深刻。最后分享一个小技巧调试DHT11时不要一上来就接系统、看LCD、跑整个程序。先写一个最简单的“读原始数据串口打印”的程序用USB转TTL模块连接STM32的串口1打开串口助手看输出。如果数据能稳定打印出来再接LCD、接WiFi模块。这样一层一层往上搭出了任何问题都能快速定位到某一层省下的调试时间非常可观。
返回列表