资讯详情

资讯详情

STM32驱动DHT11温湿度传感器:单总线时序与延时函数深度解析

做嵌入式开发这些年如果要我推荐一个最“短平快”的入门传感器DHT11温湿度传感器一定排在前三。它便宜、接线简单、代码量小却能完整地教会你单总线时序控制、数据校验、微秒级延时这些单片机开发里最核心的基本功。再加上STM32这颗国民级MCU两者组合就是新手向项目的黄金搭档从智能家居小终端到农业大棚监测到处都能看到它的身影。这篇文章我会把DHT11的底层协议、STM32的驱动写法、实际调试中遇到的坑一次性讲透。不管你是刚拿到开发板想跑通第一个传感器的萌新还是已经写了不少代码但总在时序上翻车的老同学这篇都能给你一份可以直接抄作业的参考方案。1. 整体设计与思路拆解1.1 DHT11到底是个什么东西DHT11是一款数字温湿度传感器内部集成了一个电阻式湿度感应元件和一个NTC热敏电阻再加上一个8位的MCU做信号处理。它把模拟量的采集、校准、转换全部封装在内部外部只需要一根数据线就能把温湿度结果读出来。所以你不需要关心ADC怎么采集、电阻怎么标定你只需要根据协议把数据“读”回来就行。我经常跟刚入门的同学说DHT11就像一个小型气象站的前端采集模块只不过它把最复杂的模拟电路部分替你藏起来了留给你的只有一根线的通信问题。这根线用的是单总线协议和I2C、SPI这类多引脚的通信方式完全不同。1.2 为什么单总线方案值得学单总线1-Wire最大的特点是省引脚整个通信过程只占用MCU的一个GPIO口。对于STM32这种引脚资源宝贵的场景省一个口就多一分灵活。但省引脚是有代价的单总线的时序要求非常严格所有数据的传输都依赖主机精确控制高低电平的持续时间一旦延时不准读回来的数据就是垃圾。DHT11的数据位判断依赖50us左右的低电平后高电平的持续时间高电平持续26-28us表示“0”持续70us表示“1”这个窗口非常小。写驱动的时候主控芯片的时钟频率、延时函数的实现方式都会直接影响读取结果。这也是为什么同样一份代码在72MHz的STM32F103上能跑通换到别的板子上就莫名其妙出错。理解了这一层你就知道学DHT11的重点不是“会用”而是“会调时序”。1.3 标准库还是HAL库环境怎么选STM32的开发环境目前主流是两种标准外设库和HAL库。标准库代码更直白寄存器操作看得见摸得着适合学习底层原理HAL库封装更完善配合STM32CubeMX可以快速生成工程适合做项目、做产品原型。我的建议是如果你是第一次接触DHT11想把它彻底搞懂直接用标准库或者寄存器操作如果你是为了快速完成一个功能模块直接上HAL库CubeMX里把引脚一配写驱动的时候用HAL_GPIO_WritePin和HAL_GPIO_ReadPin就行。我自己调试DHT11时用的是标准库代码更精简逻辑更直观踩坑排查也更容易定位问题。2. 硬件接线与原理细节解析2.1 引脚接线与上拉电阻DHT11通常有3个引脚也有4脚的其中一脚悬空VCC、DATA、GND。供电范围是3.3V到5.5V也就是说STM32的3.3V可以直接供电不需要额外电平转换。DATA引脚接在STM32的一个普通GPIO上我用的是PA0你可以根据自己板子随意换。这里有一个必须注意的硬件细节DATA引脚需要外接一个上拉电阻典型值是4.7kΩ到10kΩ。上拉电阻的作用是在总线空闲时把电平拉高因为单总线协议规定空闲状态是高电平。如果没有上拉电阻通信时会经常出现数据错乱或者完全读不到数据的情况。注意市面上便宜的DHT11模块通常已经把上拉电阻焊好了但如果是买裸传感器自己搭电路千万别省这颗电阻。我也见过一些模块把上拉做成4.7k电源端再并一个100nF的滤波电容这样供电更稳定。2.2 40位数据帧格式与校验算法DHT11一次完整通信会返回40位数据排列顺序如下数据位内容举例25.3℃, 60.0%bit39 - bit32湿度整数部分0x3C60bit31 - bit24湿度小数部分0x000bit23 - bit16温度整数部分0x1925bit15 - bit8温度小数部分0x033bit7 - bit0校验和0x58校验和的算法是湿度整数 湿度小数 温度整数 温度小数结果的低8位如果等于校验字节则数据有效。举个例子60 0 25 3 8888的十六进制是0x58正好等于校验字节。写代码的时候就是把这前四个字节相加取低8位和第五个字节比一下。我实际测试过很多次校验出错多数不是传感器坏了而是读取时序有问题导致某一位被误判。所以校验失败本身就是一个信号你的驱动时序需要回炉检查了。2.3 完整通信时序拆解DHT11的通信时序可以分成四个阶段。第一阶段是主机发送起始信号主机把总线拉低持续至少18ms然后释放总线。这18ms的低电平相当于把DHT11从休眠中唤醒。我测试时用了20ms留了一点余量。第二阶段是DHT11响应主机释放总线后DHT11会主动把总线拉低约80us表示“收到”然后再拉高约80us表示“准备发送数据”。第三阶段是40位数据的传输。每一位数据的传输格式是先拉低50us然后拉高拉高的时间长短决定了这一位是0还是1。高电平持续26-28us是“0”高电平持续70us是“1”。写代码的时候就需要测量这个高电平时长来判断当前位是0还是1。第四阶段是结束DHT11发送完40位数据后再次拉低50us然后释放总线整个过程结束。阶段电平行为时间参数说明起始信号MCU拉低≥18ms唤醒DHT11应答信号DHT11拉低80us表示已接收指令应答后拉高DHT11拉高80us准备发送数据数据位“0”DHT11拉高26-28us短高电平数据位“1”DHT11拉高70us长高电平结束信号DHT11拉低50us通信结束2.4 驱动代码的逐行讲解初始化GPIO时PA0先配置为推挽输出用于拉低总线发送起始信号之后需要切换为输入模式读取DHT11的数据。标准库写法如下void DHT11_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStruct.GPIO_Pin GPIO_Pin_0; GPIO_InitStruct.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_SetBits(GPIOA, GPIO_Pin_0); }注意初始化完成后要把引脚拉高让总线处于空闲状态。发送起始信号的函数要用到微秒级延时延时的实现方式决定了驱动能不能稳定工作这部分我放到实操环节详细讲。3. 实操过程与核心环节实现3.1 精确微秒延时的三种实现方式DHT11驱动里最核心的基础设施就是微秒延时。标准库自带的Delay通常只是毫秒级直接用毫秒延时去凑微秒是行不通的。我常用的是三种方案。最简单的是使用SysTick定时器做微秒延时。SysTick是内核自带的24位递减计数器通过它做延时精度很高也不需要额外占用定时器资源。基本思路是配置SysTick的时钟源为HCLK然后加载计数值轮询COUNTFLAG标志。void delay_us(uint32_t us) { uint32_t count us * 72; // 72MHz主频下1us等于72个时钟周期 SysTick-LOAD count - 1; SysTick-VAL 0; SysTick-CTRL | SysTick_CTRL_ENABLE_Msk; while (!(SysTick-CTRL SysTick_CTRL_COUNTFLAG_Msk)); SysTick-CTRL ~SysTick_CTRL_ENABLE_Msk; }第二种是用DWTData Watchpoint and Trace模块这是Cortex-M3/M4内核里的一个调试计数器也可以用来做高精度延时。它的好处是不占用SysTickSysTick可以留给操作系统或者别的功能用。void delay_us_dwt(uint32_t us) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; uint32_t count us * 72; while (DWT-CYCCNT count); DWT-CTRL 0; }第三种是直接用定时器比如TIM2或TIM3做微秒延时精度更高但代价是占用一个外设定时器。DHT11的时序对延时的要求其实不高用SysTick实现就足够了我用SysTick测过稳定性很好。注意不要用空循环for(i0;in;i)来做微秒延时。编译器优化等级一变循环消耗的时间就会变代码在-O0能跑通开-O2就挂了这种坑我踩过不止一次。3.2 起始信号与数据读取的完整驱动整套驱动代码按功能拆成几个函数发送起始信号、读取一个字节、读取完整40位数据。先看发送起始信号void DHT11_Start(void) { GPIO_SetBits(GPIOA, GPIO_Pin_0); // 空闲高电平 delay_us(100); // 稍微稳定一下 GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 拉低总线 delay_us(20000); // 低电平持续20ms满足≥18ms要求 GPIO_SetBits(GPIOA, GPIO_Pin_0); // 释放总线 delay_us(30); // 释放后主机需要等30us }然后是读取一个字节。读取的要点是先等待DHT11拉低总线50us的低电平然后等待它拉高再用延时测量高电平持续的时间。我习惯用两个循环等待电平变化再用一个延时判断高低uint8_t DHT11_ReadByte(void) { uint8_t data 0; for (int i 0; i 8; i) { while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) RESET); delay_us(40); if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) SET) { data (data 1) | 0x01; } else { data (data 1) | 0x00; } while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) SET); } return data; }这段代码的思路是先等待DHT11把总线拉低表示一位数据的开始然后等待它拉高再延时40us。如果延时后总线仍然为高说明高电平时间较长这一位是“1”如果延时后总线已经变为低说明高电平时间短这一位是“0”。这个判断逻辑是DHT11驱动最核心的“技巧”。时序图中的26-28us和70us差异在代码里就是用一个固定延时来判别的你必须保证这个延时精确落在两者之间。GPIO模式的切换放在读取前GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin GPIO_Pin_0; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOA, GPIO_InitStruct);我通常把GPIO模式切换封装成一个函数在发送完起始信号、等待应答之前调用。3.3 完整读取流程与数据解析下面是一次完整读取的封装函数。先初始化引脚为输出发送起始信号切换输入等待应答然后读取40位数据uint8_t DHT11_ReadData(uint8_t *humidity_int, uint8_t *humidity_dec, uint8_t *temp_int, uint8_t *temp_dec) { uint8_t buf[5] {0}; DHT11_GPIO_Init(); // 配置为输出 DHT11_Start(); // 发送起始信号 GPIO_Mode_Input(); // 切换为输入模式 // 等待应答信号 uint16_t timeout 10000; while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) SET) { if (--timeout 0) return 1; // 超时传感器无响应 } while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) RESET); while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) SET); // 读取40位数据 for (int j 0; j 5; j) { buf[j] DHT11_ReadByte(); } // 恢复输出模式 DHT11_GPIO_Init(); // 校验 if ((buf[0] buf[1] buf[2] buf[3]) 0xFF buf[4]) { *humidity_int buf[0]; *humidity_dec buf[1]; *temp_int buf[2]; *temp_dec buf[3]; return 0; } return 2; // 校验失败 }需要注意的是读取完成后必须把引脚模式切回输出并拉高一是为了让总线恢复空闲状态二是避免GPIO长期处于浮空输入导致功耗异常。我在调试时发现如果读取结束后不把引脚切回输出下次读取时起始信号可能发不出来。3.4 数据显示串口与OLED双路输出驱动写好了数据也读回来了接下来就是显示。最常见的两种方式串口打印和OLED屏幕显示。串口打印最简单用STM32的USART把温湿度值格式化输出就行printf(Humidity: %d.%d%% Temperature: %d.%dC\r\n, humidity_int, humidity_dec, temp_int, temp_dec);不过用printf之前记得配置好串口并且重定向fputc函数到USART发送接口。OLED则用I2C接口的0.96寸屏居多SSD1306驱动芯片把温湿度值格式化到显存里的字符串区域然后刷新显示。实测下来这两种方式对DHT11驱动本身没有影响。但是要注意OLED的I2C通信和串口打印都依赖中断和定时器如果这些中断干扰了DHT11的读取时序最终还是会导致校验失败。4. 常见问题与排查技巧实录4.1 数据一直是0或者0xFF这个是我被问得最多的问题。如果你读回来的温度和湿度都是0或者都是255大概率不是DHT11坏了而是初始化或读取流程没走通。排查步骤我建议按这个顺序来第一步用万用表测一下DHT11的VCC和GND确认供电没问题。第二步检查DATA引脚和MCU引脚之间的连接是否可靠杜邦线接触不良是常态。第三步确认上拉电阻有没有接有的模块自带上拉有的没有。第四步用逻辑分析仪抓一下引脚波形看看起始信号、应答信号有没有发出来。如果波形显示主机发送了起始信号但DHT11没有应答总线一直为高说明DHT11可能没有正常上电或者已损坏。如果应答正常但数据都是0说明数据位判定逻辑有问题重点检查延时时间。4.2 温湿度偶发跳变、校验失败这种问题比完全读不到更隐蔽。温湿度数值偶尔跳一下或者校验和偶发失败通常有四个原因一是刷新频率太高。DHT11的数据更新频率实际上是1Hz左右也就是每秒最多读取一次。如果你在主循环里连续读取很容易读到传感器正在刷新中的数据导致校验失败。建议在两次读取之间至少加1秒延时。二是供电不稳。DHT11虽然工作电压范围宽但如果供电纹波很大尤其是用开发板的3.3V LDO供电还同时带多个外设时传感器状态可能不稳定。可以在VCC和GND之间加一个100nF的陶瓷电容实测能明显改善稳定性。三是总线受到干扰。如果DHT11和MCU之间的连线太长比如超过20cm信号反射和寄生电容会导致高低电平持续时间变形。把线剪短或者用双绞线问题通常能解决。四是中断干扰。如果读取DHT11的过程中系统响应了中断比如串口中断、定时器中断导致延时函数被打断时序就会崩掉。最简单的规避方式是在读取DHT11期间关闭中断或者使用临界区保护。我实际调试中遇到过一个情况读取过程中一个定时器中断频繁触发导致DHT11返回的数据偶尔多一个字节。最后在读取函数前后加了__disable_irq()和__enable_irq()问题立刻消失。4.3 延时函数卡死问题热搜词里有“stm32延时函数delay卡死”这个词这个问题我太熟悉了。现象是程序运行到delay_us()后就死循环用调试器看发现卡在while等待标志位那里。最常见的原因是SysTick的中断优先级配置和SysTick_Handler中断服务函数冲突。如果系统里用了HAL_Delay()或者别的基于SysTick的延时它们和自定义的delay_us()会互相干扰。我的建议是delay_us()不要依赖SysTick中断完全用轮询标志位的方式实现。上面代码里用的是SysTick_CTRL_COUNTFLAG_Msk这个标志位在计数器从1计数到0时被硬件置1读取后自动清零不依赖中断就不会卡死。另一种情况是主频配置不对。代码里用count us * 72去加载计数值如果芯片实际主频不是72MHz延时精度就会出问题极端情况下会导致信号持续时间超出DHT11的识别范围表现为数据总是错位。4.4 平台与型号兼容性问题STM32家族非常庞大F1系列是Cortex-M3内核72MHz主频F4系列是Cortex-M4内核168MHz主频还有一些国产兼容芯片如APM32、GD32也声称可以“直接跑STM32程序”。我实际测试过同一份DHT11驱动在STM32F103和APM32F103上确实能直接跑通因为它们的GPIO寄存器布局和时钟树逻辑基本一致。但如果你想移植到F4系列就必须重新计算延时参数和GPIO配置。F4的GPIO模式和F1不同需要配置为GPIO_MODE_OUTPUT_PP和GPIO_MODE_INPUT同时注意AFIO的设置差异。还有一个小坑F4系列如果开启了JTAG引脚复用SWD调试口和PA0这类普通GPIO没有冲突但如果你把DHT11接在PA13、PA14这些调试口上调试器会干扰数据读取表现为一进调试模式就读不到数据单独跑反而正常。5. 从DHT11到更复杂的项目扩展5.1 数据上传从本地显示到远程监控如果你已经让DHT11在本地正常显示数据了下一步可以做数据上云。最简单的方案是配合ESP8266模块通过串口或者SPI把温湿度数据发送出去ESP8266再通过MQTT协议发布到云平台。我做过一个宿舍环境监测的小项目STM32读取DHT11后每5秒通过串口发一帧数据给ESP8266ESP8266用AT指令集连接到MQTT服务器手机端订阅主题就能实时看到温度和湿度。整个链路里DHT11的读取是基础但如果DHT11驱动不稳定上层的所有数据都是白搭。5.2 与RTOS结合多任务环境下的DHT11读取如果你用FreeRTOS这类实时操作系统DHT11的读取要注意任务优先级和临界区。DHT11的时序是微秒级的如果在读取过程中被更高优先级的任务抢占数据就会出错。我建议把DHT11的读取放在一个独立任务里并且把这个任务的优先级设得足够高读取期间用临界区保护。还有一种更稳妥的做法用定时器捕获方式读取DHT11把电平变化的时间戳记录下来再由任务去解析这些时间戳。这样即使任务调度导致中间有延迟只要时间戳是准确的数据就不会错。这个方法稍微复杂一些但稳定性比裸机延时高一个量级。5.3 升级替代什么时候不适合用DHT11虽然DHT11很适合入门但它也有明显的局限。湿度精度在±5%RH温度精度在±2℃这个精度在智能家居环境监测里够用但在实验室环境、工业控制场景里就不够了。如果你需要更高精度可以看看AHT20和SHT30这两款传感器。AHT20也是单总线或者I2C接口精度比DHT11高很多价格还不贵SHT30是I2C接口内部有校准。它们和DHT11的驱动思路完全不同但如果你已经把DHT11的时序搞明白了切换过去的学习成本会低很多。我个人在实际使用中的体会是DHT11更适合作为学习“单总线协议”的敲门砖真要放到产品里我会优先考虑AHT20这类稳定性和一致性更好的传感器。但对于刚入门STM32的同学先用DHT11把时序、校验、中断这些基础概念练扎实绝对是一件性价比极高的事。最后再分享一个小技巧如果你在调试DHT11时总是怀疑自己的代码有问题不妨先用手头的逻辑分析仪抓一次波形把起始信号、应答信号、每一位数据的波形对照时序图逐段比对。大多数时候问题一眼就能看出来。调试单片机就是这样先把物理层的信号搞清楚了再回头看代码思路会清晰很多。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →