
手头项目要做一个环境监测的小板子第一时间想到的就是STM32配DHT11温湿度传感器。DHT11这颗芯片在嵌入式圈子里算是最经典的入门级传感器了价格便宜、接线简单、单总线协议几乎每块开发板的配件清单里都有它。网上教程多如牛毛但真正到自己动手时各种问题还是扑面而来读出来全是0、数据偶尔跳变、甚至烧录时报“No STM32 Target Found”直接卡死在第一步。这篇文章就以DHT11温湿度传感器为主线从硬件原理到单总线时序再到STM32的代码实现和实际调试踩坑完整梳理一遍希望对正在调DHT11的你有实际帮助。1. 先搞明白DHT11便宜但不简单的温湿度传感器1.1 DHT11的内部到底有什么DHT11看着像三极管其实内部集成了一个电阻式湿度传感元件和一个NTC热敏电阻测温元件再加上一个8位单片机做信号处理和单总线通信。它输出的不是模拟电压而是经过内部校准后的数字信号直接通过一根数据线发出来。也就是说我们不需要ADC采样也不需要自己算温湿度曲线只要按协议把数据读回来就行。它的供电范围是3.3V到5.5V所以不管是STM32F103这种3.3V系统还是51单片机那种5V系统都能直接供电。测量范围上是湿度20%到90%RH、温度0到50摄氏度精度为湿度正负5%RH、温度正负2摄氏度分辨率只有1。这个精度确实谈不上好但胜在稳定和便宜像室内环境监测、智能台灯、鱼缸温度提醒这类对精度要求不高的场景完全够用。有一点很多人容易忽略DHT11的采样周期是1秒也就是说两次读取之间至少要间隔1秒以上否则拿到的是旧数据。实际编写代码时建议把读取周期放在1.5秒到2秒之间既能拿到新数据又不会因为太频繁而把通信时序搞乱。1.2 接线和原理图设计需要注意的点DHT11常见的封装有两种一种是四脚直插引脚间距较大另一种是模块化小板子板载上拉电阻和滤波电容。不管哪种核心信号都只有一根DATA线但这一根线是有讲究的。DHT11的DATA引脚属于开漏输出需要在外部接一个上拉电阻到VCC阻值通常是4.7k到10k。如果你买的是模块板上已经集成好上拉电阻直接接杜邦线就行。如果是裸芯片千万记得加电阻不然数据线没有默认高电平通信直接废掉。我遇到过好几个用裸芯片但没加上拉电阻的案例表现就是主机发完起始信号后DHT11完全没有响应读上来的数据始终是0xFF。引脚名称接法1VCC接3.3V或5V2DATA接STM32 GPIO需外接4.7k-10k上拉3NC悬空4GND接地原理图上还有个容易忽视的细节DATA线上可以加一个小电容滤波容值不建议太大一般100pF到1nF就够。加太大的电容会导致信号边沿变缓反而影响时序判断尤其在快速读位时会读到错误电平。此外如果板子空间允许尽量把DHT11靠近MCU放置走线不要太长。用开发板加杜邦线调试时线的长度尽量控制在20厘米以内长了之后分布电容和干扰都会上来读数就会不稳定。GPIO的选择上建议避开SWD调试用的PA13、PA14和PB3、PB4不然调试器可能受影响。我习惯放在PA0或PB0这类普通IO上方便接线也方便排查问题。2. 单总线协议读懂DHT11的时序是成功的一半2.1 40位数据怎么拆DHT11一次完整传输会发送40位数据顺序是湿度整数8位、湿度小数8位、温度整数8位、温度小数8位、校验和8位。要注意的是DHT11的精度是1所以小数部分恒为0但协议格式里依然保留了两个小数字节。读取的时候不要忽略它们要做完整解析万一以后换DHT22代码也能复用。校验和的计算方式是把前四个字节相加取低8位。如果这个值等于第五个字节说明传输正确如果不等于这一帧数据直接丢弃。这相当于一个简单的CRC8能过滤掉很大一部分干扰导致的错误数据所以代码里一定要做校验不要嫌麻烦直接信任数据。举个例子某次读到湿度整数30、湿度小数0、温度整数25、温度小数0那么校验和应该等于30加0加25加0等于55。如果收到55帧有效湿度就是30%RH温度就是25摄氏度。2.2 从起始信号到每一位数据逐个时间点说明DHT11的单总线时序是整个驱动的核心也是新手最容易出问题的地方。先把流程理一遍主机先把数据线拉低保持至少18毫秒然后释放总线并拉高等待20到40微秒。这个低电平信号就是起始信号作用是唤醒DHT11。注意18毫秒不是随便写的太短了DHT11不会响应太长了也没必要反而会拖慢整体读取速度。DHT11接收到起始信号后会先拉低总线80微秒作为响应信号然后再拉高80微秒告诉主机“我准备好了准备发数据”。如果主机等不到这80微秒的低电平基本可以判断DHT11没工作或者接线有问题。接下来就是40位数据逐位输出。每一位的传输格式都是先拉低50微秒表示“我要开始发一位了”然后拉高高电平持续26到28微秒表示数据0高电平持续70微秒表示数据1。换句话说区分0和1的关键不是低电平时间而是高电平的持续时间。对于STM32来说标准且通用的读取方式是检测到引脚变高后延时40到45微秒然后读引脚电平。如果此时引脚仍为高说明这一位是1如果已经是低则说明是0。因为0的高电平只有26到28微秒延时40微秒后早就变成低电平了而1的高电平有70微秒延时40微秒后依然是高。这个思路避免了精确计算高电平持续时间代码写起来非常简洁。两条重要的时间底线要记住起始信号的低电平时间不能少于18毫秒。每一位数据的50微秒低电平大概是起始位读出时不能把它误判成数据。2.3 为什么读时序时不能开中断DHT11的时序都是微秒级的比如数据0和数据1之间的差别就在几十微秒。如果读取过程中来了一个中断CPU跳去执行中断服务函数很可能等回来后这一位的高电平已经结束了读到的就是错误电平整个数据帧直接废掉。所以在DHT11的读取函数里建议在起始信号发出后到数据全部读完之前临时关闭全局中断读完再恢复。这在裸机开发中非常简单调用PRIMASK相关的指令或者在进入读取函数前关中断即可。如果用的是FreeRTOS等系统还要注意至少读数据的临界区不能被打断可以用taskENTER_CRITICAL包裹读取过程。这里必须提醒一句关中断的时间不宜过长。DHT11一帧40位数据每一位约50到70微秒整体读下来大约3到5毫秒这个时间去关中断完全能接受。反而是一些新手为了图省事在读取前后加了长延时或串口打印导致关中断时间过长影响系统实时性这种做法要尽量避免。3. STM32代码实现标准库和HAL库两条路线3.1 微秒级延时先搞定DHT11驱动对延时的精度要求不高但要有微秒级控制能力。最可靠的做法是用SysTick做us延时或者用DWT数据观察点定时器。很多开发板例程自带delay_us和delay_ms函数直接复用即可。如果没有现成延时函数提供一个基于SysTick的简单实现思路。先配置SysTick的时钟源为HCLK这样在72MHz主频下SysTick每计数72次就是1微秒。延时函数里先计算需要的计数值然后不断读取当前计数值直到递减到0。要注意的是如果工程里同时用了HAL_Delay它也是基于SysTick实现的两者共用SysTick时会互相干扰建议DHT11的延时单独用一个基本定时器或者用DWT来实现这样最干净。如果只是想快速验证DHT11能不能工作临时用一个简单的空循环延时也可以凑合但不同编译优化级别下循环延时差异很大开O2优化后延时时间会明显缩短所以不建议长期这么用。实际项目里还是老老实实做个精准延时函数比较省心。3.2 标准库版DHT11读取标准外设库在STM32F1系列里用得很多代码直接操作GPIO结构体和方法宏比较直观。下面是一份完整的DHT11读取实现可以直接抄到自己的工程里只要把GPIO和RCC宏改成实际所用引脚即可。#include stm32f10x.h #include delay.h #define DHT11_GPIO_PORT GPIOA #define DHT11_GPIO_PIN GPIO_Pin_0 #define DHT11_RCC RCC_APB2Periph_GPIOA static void DHT11_SetOutputMode(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStructure); } static void DHT11_SetInputMode(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStructure); } static uint8_t DHT11_ReadByte(void) { uint8_t i, data_byte 0; for (i 0; i 8; i) { while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) RESET); delay_us(40); if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) SET) { data_byte | (0x80 i); } while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) SET); } return data_byte; } uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t buf[5] {0}; uint8_t i 0; DHT11_SetOutputMode(); GPIO_WriteBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN, Bit_RESET); delay_ms(20); GPIO_WriteBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN, Bit_SET); delay_us(30); DHT11_SetInputMode(); if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) SET) { return 0; } while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) RESET); while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) SET); for (i 0; i 5; i) { buf[i] DHT11_ReadByte(); } DHT11_SetOutputMode(); GPIO_WriteBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN, Bit_SET); if (buf[4] ((buf[0] buf[1] buf[2] buf[3]) 0xFF)) { *humidity buf[0]; *temperature buf[2]; return 1; } return 0; }这个代码的思路是读取之前把GPIO配成推挽输出拉低20毫秒再拉高然后立刻改成浮空输入模式。之后先等DHT11的80微秒低电平响应信号再等80微秒高电平随后按字节读取40位数据。读完数据后再把GPIO切回输出模式并拉高释放总线。如果你想让它更稳定可以在进入读数据前临时关中断读完再开。比如用__disable_irq()和__enable_irq()包裹中间部分或者用更细粒度的临界区保护。实测下来加了关中断后连续读取100帧的出错率会明显下降。3.3 HAL库版实现要点HAL库下GPIO的初始化可以直接用CubeMX生成但读取DHT11时的方向切换如果频繁调用HAL_GPIO_Init会有比较大的函数开销有可能影响时序精度。折中的做法是初始化时用HAL函数完成配置读取过程中切换方向使用寄存器操作。比如PA0要切为输出模式可以直接修改CRL寄存器的低4位。PA0对应CRL寄存器的bit0到bit3把这4位设为模式通用推挽输出0011再配合ODR寄存器控制输出电平。切换成输入模式时把这4位设为浮空输入0100。下面是一段配合CubeMX生成的HAL工程使用的读取切换代码片段#define DHT11_PORT GPIOA #define DHT11_PIN GPIO_PIN_0 static void DHT11_SetOutput(void) { GPIOA-CRL ~(0xF 0); GPIOA-CRL | (0x3 0); } static void DHT11_SetInput(void) { GPIOA-CRL ~(0xF 0); GPIOA-CRL | (0x4 0); }如果DHT11接在PA15、PB3等其他端口移位计算要对应调整。HAL库工程里也可以用HAL_GPIO_ReadPin和HAL_GPIO_WritePin来读写引脚但考虑到读取是按微秒级判断的HAL函数的宏封装会在不影响功能的前提下多几条指令一般也能跑只是在超频或时序很紧时会出问题。我的建议是读写引脚用寄存器操作或直接调用底层宏方向切换用上述方式这样既保留HAL工程的初始化框架又保证了读取时序。4. 调试实录让无数人翻车的报错和数据异常4.1 烧录时报“No STM32 Target Found”怎么查很多人在调DHT11时遇到烧录失败第一反应是代码写崩了其实这个报错往往和硬件连接有关。error: no stm32 target found! if your product embeds debug authentication, please...这个提示的大致意思是找不到STM32目标芯片。排查顺序建议是先看ST-LINK和板子的接线SWDIO接SWDIOSWCLK接SWCLKGND保证共地这是最基础也最容易被忽略的问题。接着看板子的供电如果目标板是由ST-LINK供电要确保ST-LINK的3.3V输出能力足够最好把板子单独供电再共地这样最稳。再看BOOT0引脚正常情况下BOOT0要拉低如果误拉高到3.3V芯片会进入系统存储器Boot模式调试接口可能无法正常访问。还有一个和DHT11直接相关的情况如果把DHT11的数据线接到了PA13或PA14上而这两个引脚同时是SWD调试接口外部传感器的寄生电容和信号变化可能干扰调试口通信造成偶尔连接不上。调DHT11项目时我习惯把传感器放在非复用引脚上至少不要占用SWD引脚。如果实在只有这两个引脚可用可以试试降低SWD时钟频率或者把传感器先断开再烧录烧完再接回去。另外如果电脑设备管理器里ST-LINK出现黄色感叹号多半是驱动问题重新安装最新版ST-LINK驱动即可。开发板上的虚拟串口出现感叹号也类似属于驱动没有正确安装不影响芯片烧录。4.2 读出来的数据全是0或255这是DHT11调试中最常见的症状。读出来全是0通常意味着程序认为DHT11一直在输出低电平常见原因有三个一是DATA线没有接上拉电阻数据线保持低电平二是GPIO方向切换没做好读取时引脚仍然是推挽输出模式把DHT11的信号短路了三是DHT11和STM32之间没有共地电平参考不一致。读出来全是255则往往是总线一直保持高电平说明DHT11压根没有拉低总线。可能原因是供电没接好或者起始信号的低电平时间不够DHT11没被唤醒。还有一个很容易犯的错起始信号延时用的是delay_ms(20)但如果delay_ms实现本身有问题实际延时远远不够18毫秒DHT11无法识别起始命令自然不会有任何应答。排查这类问题时我建议先用逻辑分析仪或示波器看DATA脚的波形。没有仪器的情况下可以把读取频率放慢比如每次读取间隔2秒在初始化和读取之间加几个延时再串口打印原始数据帧观察应答部分是否有低电平出现。很多时候打印原始40位数据比直接打印温湿度更有排查价值。4.3 数据偶尔跳变、温度和湿度对不上数据偶发跳变的问题绝大多数和硬件环境有关。杜邦线太长、供电纹波大、附近有电机或继电器之类的干扰源都可能导致时序误判。解决办法有几种尽可能缩短传感器到MCU的接线距离在电源两端加100nF去耦电容DHT11的VCC和GND之间用10uF电解电容稳压数据线上加上拉电阻的同时和GND之间加一个1nF电容滤除高频干扰。代码层面也能做一定程度的容错。比如连续读三次取至少两次相同的结果或者把校验失败的数据直接丢弃下次再读。DHT11本身温度精度正负2度、湿度正负5度如果发现读到的温度和湿度值在正常范围外——比如温度超过50度或湿度超过90%RH可以直接判定为异常帧。另外要特别注意读取间隔。DHT11每次转换需要1秒如果你在1秒内连续读两次第二次拿到的很可能是上一次缓存的旧数据看起来就是“跳变”。建议在主循环里维护一个时间戳只有距离上次成功读取超过1.5秒才发起下一次读取。4.4 Keil里delay卡死是咋回事DHT11例程里通常依赖延时函数但很多人把传感器代码移植到自己的工程后发现程序跑死在delay_us或delay_ms里面。这种情况往往是因为SysTick配置冲突。比如HAL库的HAL_Delay在初始化时会设置SysTick的中断优先级如果你自己写的延时函数也基于SysTick且初始化和中断处理互相覆盖就会导致计数失效看起来像卡死。解决思路是给DHT11单独分配一个基本定时器做微秒延时或者用DWT循环计数器。DWT是Cortex-M3/M4内核自带的调试组件不需要占用定时器用起来非常方便。以STM32F103为例使能DWT的步骤如下CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk;之后延时n微秒就可以通过读取DWT-CYCCNT来计算。72MHz主频下DWT-CYCCNT每增加72就表示1微秒。这种延时方式不受SysTick和中断上下文影响很适合在DHT11这类对时序敏感的驱动中使用。如果你不想写DWT也可以用TIM2做us延时不过基本定时器资源在复杂项目中比较珍贵DWT是个更轻量的选择。5. 实战扩展和个人心得5.1 把DHT11接到实际项目里DHT11最常见的落地方案是配合STM32做环境数据采集然后通过OLED屏显示或者通过ESP8266等无线模块上传到云平台。其实你不需要一开始就做很复杂的架构先把传感器数据读稳再逐步加显示和联网功能。我个人比较推荐的最小系统是STM32F103C8T6最小系统板、DHT11模块、0.96寸OLED、一个按键。按键用来切换显示模式OLED第一行显示温度第二行显示湿度。整个工程在标准库下大约300行代码很适合练习单总线协议和状态机思维。如果你做毕业设计或者课设用这个组合扩展一个智能台灯或者鱼缸控制只需要再补一路继电器控制加热棒代码结构保持不变只是在读取温湿度后加判断逻辑就行。网上搜“STM32 DHT11 OLED”能找到大量现成方案但建议不要直接照抄最好自己按这篇教程的时序重新写一遍真正遇到问题才会知道怎么排查。照着别人代码跑通不算本事能在跑不通时定位问题是真功夫。5.2 读取策略和代码架构建议很多新手喜欢在主循环里while套延时读一次DHT11就阻塞好几个毫秒然后一直串口打印。这种做法在简单Demo里没问题一旦加了OLED刷新、按键扫描、无线通信就会感觉系统特别卡。更好的方式是做一个状态机把DHT11读取放到一个定时调度环节里例如每2秒触发一次读取读取完成后置一个数据更新标志主循环检测到标志后再处理显示和上传。整个读取过程可以拆成几个状态IDLE、发送起始信号、等待响应、读取数据、校验和释放总线。每个状态在定时器中断或调度器里执行避免长时间阻塞。如果不想写状态机也可以用RTOS把DHT11读取放在一个优先级较低的线程里用信号量每隔2秒触发一次读取完直接发布温度湿度数据。这样主线程不会因为传感器读取而暂停太久。无论用哪种方式都要把校验失败的情况考虑进去。DHT11因为价格便宜偶尔传输出错很正常不要因为一帧校验失败就重启设备连续失败3次再报错也不迟。我在实际项目中遇到比较多的情况是传感器在刚上电的第一帧数据不稳定所以初始化后可以先延迟500毫秒再开始第一次读取。5.3 什么时候应该换DHT22DHT11最让人吐槽的就是精度和量程。温度正负2度、湿度正负5度小数点还是0做精度要求高的环境监测确实不合适。如果你的项目对数据精度有要求或者需要放到户外场景DHT22也就是AM2302是更合适的选择。DHT22的通信协议和DHT11几乎一致同样是一根单总线同样40位数据但湿度和温度的小数位是真实有效的。湿度精度从5%RH提升到2%RH温度精度从2度提升到0.5度量程也扩展到湿度0到100%RH、温度零下40到80摄氏度。把DHT11驱动改成DHT22驱动只需要调整数据解析方式时序部分基本不变。所以在写DHT11驱动时建议在解析层面就按“整数位和小数位分开”的框架来做不要直接把小数位丢掉。这样以后硬件升级到DHT22软件改动会非常小就是你写驱动时留下的后路。最后分享一个实际经验DHT11这颗传感器本身容错能力有限但它的单总线协议特别适合练手能让你对GPIO方向切换、微秒级延时、时序容错有非常直观的理解。我在带新人时都会让他们先用DHT11把裸机驱动完整写一遍再上RTOS和复杂业务基础会扎实很多。如果你现在正卡在某个DHT11的坑里按上面这些排查步骤走一遍大概率能解决。实在还不行先量一下DATA线上的静态电平——高电平正常低电平就是没上拉就这么简单。