ARTICLE DETAIL

资讯详情

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

STM32驱动DHT11温湿度传感器:单总线协议详解与标准库HAL库实战

STM32驱动DHT11温湿度传感器:单总线协议详解与标准库HAL库实战 做STM32模块教程这个系列已经有一段时间了从按键、LED、数码管一路写到串口和LCD这次轮到DHT11温湿度传感器。这颗小东西在智能家居小项目、环境监测、课程设计和毕业设计里出现频率极高很多初学者的第一个“非数字量传感器”就是它。DHT11最大的特点是便宜、够用、代码量小但它的数据读取方式——单总线协议——听起来简单实际操作却有不少讲究。时序没控制好读回来的数据不是全0就是全255甚至会让程序直接卡死在while循环里。这篇教程我会从硬件接线、通信协议、标准库和HAL库的双版本代码、常见问题排查几个方面完整讲一遍。无论是刚接触STM32的新手还是准备做温湿度采集小项目的同学按着这篇文章的思路走一遍都能把DHT11跑起来也能真正理解“单总线协议”到底是怎么回事。1. 项目概述与整体设计思路1.1 DHT11这颗传感器到底特别在哪先明确一个概念DHT11不是什么高速高精度传感器它的定位就是“够用且便宜”。测量范围方面湿度在20%~90%RH温度在0~50℃湿度精度正负5%RH温度精度正负2℃。说实话这个精度做不了严格计量但做环境监测、温湿度显示、智能家居联动这类场景完全够用。它的核心参数有几个值得记一下参数数值供电电压3.3V ~ 5.5V接口类型单总线1-Wire温度测量范围0 ~ 50℃湿度测量范围20% ~ 90%RH温度精度±2℃湿度精度±5%RH采样周期≥1秒数据输出40位5字节很多同学会拿DHT11和DHT22、SHT30对比。DHT22精度更高、范围更广SHT30走I2C接口速度更快但它们的价格和复杂度也更高。对于入门来说DHT11的优势在于只有一根数据线时序逻辑清晰代码量小非常适合理解传感器通信的本质。等你把DHT11跑明白了再去用其他传感器会轻松很多。1.2 为什么DHT11只用一根IO就能收发数据这就要说到单总线协议了。所谓单总线就是主机和从机之间只用一根数据线完成双向通信。你可能会问一根线怎么既发送又接收答案是靠时间片区分。打个比方两个人用一根对讲机线通话约定好“我说的时候你听你说的时候我听”谁也不能同时说。DHT11和STM32之间的通信就是这样主机先拉低总线发一个“我要开始读数据了”的信号然后释放总线DHT11收到后接管总线发出响应信号再按照特定时序把40位数据一位一位地“说”出来。整个过程里通信的方向是由时序来决定的所以时序的准确性就是DHT11驱动的命门。这种协议的好处是省引脚、省PCB空间坏处是时序敏感对延时函数的精度要求比较高。这也是为什么很多人的DHT11代码在别人板子上跑得好好的到自己手里就各种乱码。理解了这一点后面写代码时你就知道重点应该放在哪了。2. 硬件准备与接线实战2.1 传感器模块的硬件构成市面上的DHT11有两种形态一种是裸传感器四个引脚VCC、DATA、NC、GND需要自己做上拉电路另一种是模块化小板子上面集成了上拉电阻和滤波电容三个引脚VCC、DATA、GND直接连开发板就行这也是我最推荐新手使用的版本。模块上那两个贴片元件不是白加的。DATA线上有一个4.7k~10k的上拉电阻保证总线空闲时为高电平电源引脚附近有一个104滤波电容给传感器提供一个相对干净的供电环境。如果你买的是裸传感器这两样东西一定要自己接上否则数据线上电平漂移读取结果会非常随机。很多同学DIY时图省事不加上拉电阻结果读到的数据乱跳就是这个原因。接线方面我用STM32F103为例数据线接PB0供电和地分别接3.3V和GNDDHT11引脚STM32引脚VCC3.3V或5VDATAPB0GNDGND2.2 电平匹配与供电注意事项DHT11的供电范围是3.3V到5.5V所以直接用3.3V供电没问题不要觉得传感器就一定得5V。如果你用的是5V供电要注意MCU引脚是否能容忍5V电平。STM32的普通IO最高容忍5V但为了稳妥建议在DATA线上串联一个1k~4.7k的限流电阻或者干脆用3.3V供电省心。接线时还有一个很容易被忽略的细节杜邦线不要太长。DHT11的时序是微秒级的数据线上如果接了过长的杜邦线分布电容会改变边沿的上升/下降时间导致MCU采样到错误的电平。我个人经验是数据线尽量控制在20厘米以内如果项目需要远距离放置传感器可以考虑用屏蔽线或者换用支持长线传输的数字传感器。模块刚上电时也不要立刻读数据。DHT11内部有一个RC振荡电路上电后需要稳定一段时间才能正常工作。实际操作中建议上电后至少延时1秒再发起第一次读取否则大概率会读到超时或者乱码。2.3 新手最容易忽略的硬件坑我见过不少同学在这个环节踩坑最典型的有三个第一把DATA线和VCC短接了。模块上DATA引脚和VCC如果直接短路上电后传感器很容易损坏。插杜邦线的时候一定要看清丝印。第二接反了GND和VCC。这个失误会导致传感器发烫及时断电的话还有救长时间上电基本就报废了。第三供电不稳定。如果DHT11和舵机、电机共用一个电源电机启动瞬间会把电源电压拉低DHT11直接复位或者输出乱码。解决方法是给传感器单独用一组LDO供电或者在电源端加一个大容量电解电容。3. 通信协议与数据解析核心3.1 一次完整的通信过程DHT11的通信过程可以拆成四个阶段主机起始信号、传感器响应信号、数据位传输、结束释放总线。我先把整个时序画出来后面再逐段解释。主机: _ __________________ | | 拉高20~40us | | |_|________________| 释放总线(高电平) | 传感器: ______ | |_______ 响应低80us 响应高80us具体过程是这样的主机先把数据线拉低保持至少18ms然后再拉高保持20~40us之后释放总线。DHT11检测到这个起始信号后会先输出一个80us的低电平作为响应信号紧接着再输出一个80us的高电平通知主机“我准备好发数据了”。响应信号结束后DHT11就开始逐位输出40位数据。这里有个细节主机的低电平时间不能太短。DHT11检测起始信号时有一定容差但如果你只拉低了几毫秒就放开传感器可能来不及响应直接导致超时。我习惯把低电平时间做成20ms既满足要求又留出余量。3.2 数据0和1的判断标准DHT11传输每一位数据时都会先拉低50us然后拉高。关键区别在拉高的时间长度如果高电平持续26~28us这一位是0如果高电平持续70us左右这一位是1。用程序去判断时标准做法是先等待数据线上的50us低电平结束然后延时40us左右再次读取引脚电平。如果此时引脚为高说明高电平还在持续这一位是1如果引脚已经变低说明高电平很早就结束了这一位是0。数据位低电平时间高电平时间判断方法40us后采样050us26~28us已变回低电平150us70us仍为高电平这个40us的判断时机不算特别精确但胜在稳定。因为0的高电平最长约28us40us后早就过了1的高电平最短也有约65us40us后依然处于高电平。中间有足够的区分窗口即使系统时钟有点偏差也不容易误判。3.3 40位数据如何拼接和校验DHT11一次性输出40位数据也就是5个字节。格式固定为湿度整数、湿度小数、温度整数、温度小数、校验和。校验和的算法很简单前4个字节相加取低8位如果和校验字节相等说明这帧数据有效。这个设计非常朴素但能挡住大多数传输错误。我举个实际例子假设读到的5个字节分别是0x28 0x00 0x1C 0x00 0x44。湿度整数0x28即40%湿度小数0x00温度整数0x1C即28℃温度小数0x00校验和0x44 0x28 0x00 0x1C 0x00 0x44校验通过说明这帧数据是可靠的。如果校验失败不要去猜哪个字节有问题直接丢弃这帧数据等下一次采样。DHT11的采样周期最低是1秒等1秒再读一次就好。4. 核心代码实现与逐行解读4.1 第一步搞定精确微秒延时DHT11的时序以微秒为单位而STM32标准库和HAL库自带的延时函数只提供毫秒级延时。所以第一件事就是自己封装一个微秒级延时函数。最稳妥的方案是用DWTData Watchpoint and Trace模块的CYCCNT计数器。DWT是Cortex-M3/M4内核自带的调试组件用它做延时的好处是不占用SysTick、精度高、几乎不依赖库函数。初始化代码如下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 DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) ticks); }使用前调用一次DWT_Delay_Init()之后就能随意调DWT_Delay_Us()了。注意这里SystemCoreClock必须和实际系统时钟一致如果你的板子跑了72MHz这个值就是72000000那么每微秒的Tick数就是72。如果系统时钟配置有问题延时精度也会跟着出问题后面排查时我会再提这一点。4.2 标准库版本完整核心代码先说初始化。DHT11的DATA引脚配置成推挽输出即可因为我们后面读取时直接读IDR寄存器不需要切换模式。如果模块没有上拉电阻建议把GPIO配置成开漏输出靠外部上拉提供高电平。这里我按最常见的情况写void DHT11_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_0); }接下来是读取一个数据位的函数。这个函数做了超时保护防止传感器没接好时程序卡死在while循环里这个点非常重要后面我会展开说uint8_t DHT11_ReadBit(void) { uint8_t ret 0; uint32_t timeout 0; while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) RESET) { if (timeout 100) return 0xFF; DWT_Delay_Us(1); } DWT_Delay_Us(40); if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) SET) { ret 1; timeout 0; while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) SET) { if (timeout 100) break; DWT_Delay_Us(1); } } return ret; }这段代码的逻辑是先等当前数据位的50us低电平结束然后延时40us读引脚状态如果还是高电平说明这一位是1再等待这1位的高电平结束如果已经是低电平说明这一位是0直接返回。返回0xFF表示超时调用方要能识别这个异常值。然后是完整的读取函数。这里包含起始信号、等待响应、读取40位、校验四个步骤uint8_t DHT11_ReadData(uint8_t *hum, uint8_t *temp) { uint8_t buf[5] {0}; uint8_t i; uint32_t timeout 0; GPIO_SetBits(GPIOB, GPIO_Pin_0); DWT_Delay_Us(20); GPIO_ResetBits(GPIOB, GPIO_Pin_0); DWT_Delay_Us(20000); GPIO_SetBits(GPIOB, GPIO_Pin_0); DWT_Delay_Us(40); timeout 0; while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) SET) { if (timeout 100) return 1; DWT_Delay_Us(1); } timeout 0; while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) RESET) { if (timeout 100) return 1; DWT_Delay_Us(1); } for (i 0; i 40; i) { uint8_t bit DHT11_ReadBit(); if (bit 0xFF) return 1; buf[i / 8] 1; buf[i / 8] | bit; } if ((uint8_t)(buf[0] buf[1] buf[2] buf[3]) ! buf[4]) return 2; *hum buf[0]; *temp buf[2]; return 0; }返回值建议约定一下0表示读取成功1表示通信超时2表示校验和错误。调用方根据返回值决定是更新显示还是保留上次数据这样主逻辑会干净很多。4.3 HAL库版本怎么改如果你用的是HAL库代码思路完全一样只是GPIO操作函数换了名字。主要区别就三处操作标准库HAL库写引脚GPIO_SetBits / GPIO_ResetBitsHAL_GPIO_WritePin读引脚GPIO_ReadInputDataBitHAL_GPIO_ReadPin初始化GPIO_InitTypeDefGPIO_InitTypeDef __HAL_RCC_GPIOB_CLK_ENABLE()HAL库的初始化长这样GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);读取函数的写法如下uint8_t DHT11_ReadBit_HAL(void) { uint8_t ret 0; uint32_t timeout 0; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) GPIO_PIN_RESET) { if (timeout 100) return 0xFF; DWT_Delay_Us(1); } DWT_Delay_Us(40); if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) GPIO_PIN_SET) { ret 1; timeout 0; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) GPIO_PIN_SET) { if (timeout 100) break; DWT_Delay_Us(1); } } return ret; }有同学问我HAL库能不能用HAL_Delay()来做微秒延时答案是不能。HAL_Delay()是基于SysTick的毫秒延时最小分辨率是1ms远远不够DHT11的时序要求。所以在HAL库工程里DWT延时函数依然是必需品。另外还要注意如果在FreeRTOS环境下用HAL_Delay()SysTick被系统占用必须在初始化时把HAL的时间基准切换到其他定时器否则整个工程都会出问题。这个坑我在实际项目中踩过提出来供大家参考。4.4 主循环采样逻辑避免读得太频繁DHT11两次读取间隔不能小于1秒这个限制来自传感器内部。它的测量周期本身就需要大约1秒你读得太快它根本来不及完成新一次测量返回的可能是上一次的缓存值甚至是未完成转换的中间值。所以主循环里一定要控制采样频率。我一般习惯写成这样while (1) { uint8_t hum 0, temp 0; uint8_t ret DHT11_ReadData(hum, temp); if (ret 0) { printf(hum: %d%% temp: %d.%dC\r\n, hum, temp 0x0F ? temp 0x0F : 0, temp 4); } else { printf(DHT11 read error: %d\r\n, ret); } HAL_Delay(1500); }如果你想让代码更健壮可以在主循环里做一个“连续读取失败N次才上报错误”的机制。比如连续3次失败再打印错误信息避免因为偶发的一次超时让整个逻辑乱了套。DHT11偶尔失败是正常的毕竟它是靠RC振荡器工作的传感器抗干扰能力有限。5. 常见问题排查与避坑实录5.1 读出来全是0或全是255这是DHT11新手区最经典的问题。先说结论全是0基本说明通信失败全是255多半是超时返回值没处理。如果你用的是我的代码DHT11_ReadBit()返回0xFF时buf里的数据会出现异常DHT11_ReadData()会返回错误码1。所以第一步是打印DHT11_ReadData()的返回值判断是哪一步出了问题。全是0优先检查GPIO时钟有没有打开。忘了RCC_APB2PeriphClockCmd或者__HAL_RCC_GPIOB_CLK_ENABLE()寄存器读写不会报错但引脚就是不工作。数据线有没有接对。很多人把DATA接到了相邻的引脚上程序写PB0实际插在PB1。上拉电阻是否存在。如果是裸传感器没接上拉总线空闲时电平不确定通信必然失败。全是255或者偶尔出一次正常值优先检查延时函数是否精确。确认SystemCoreClock的值是否和实际主频一致HSE外部晶振是否起振启动文件是不是选对了型号。起始信号的低电平时间是否足够。至少要18ms建议20ms。DHT11是否真的上电稳定了。刚上电立刻读传感器还没准备好。5.2 数据偶尔跳变或者明显不合理那种“大多数时候正常偶尔冒出来一个湿度9%、温度-40℃”的情况通常是外部干扰或者供电问题。经验上讲优先怀疑顺序是供电不稳 数据线过长 采样间隔太短 传感器损坏。你可以试着在VCC和GND之间并一个100uF电解电容再把数据杜邦线换成短一点的看看问题是否消失。另外排查一下数据线周围有没有高频信号比如PWM电机驱动线、继电器控制线。这些线如果和数据线平行走线很容易把干扰耦合进来。还有一个容易被忽视的点DHT11不要放在发热源附近。如果你测量的温度比室温高好几度先看看传感器旁边是不是有LDO或者MCU。温度传感器的通风条件很重要放在封闭小盒子里测出来的温度也会偏高。5.3 程序卡死在DHT11相关循环里“卡死”这个问题80%是因为while等待循环里没有超时保护。比如等待传感器响应低电平时直接写while (GPIO_ReadInputDataBit(...) RESET);一旦DHT11没接好或者已经损坏这个循环永远不会退出程序就像死机了一样。这也是为什么我在前面的代码里给每个等待循环都加了timeout计数。记住一句话任何硬件相关的等待循环都要设置超时上限。另外DHT11读取失败后程序不要立即重试。连续重试会导致传感器持续忙乱更容易失败。失败后等500ms到1s再试成功率会高很多。这个经验用一句话总结就是DHT11不能催越催越掉链子。5.4 周边环境问题快速自查表有些同学在调试DHT11时还会遇到和传感器本身关系不大但同样让人崩溃的环境问题我整理了一个速查表现象可能原因解决方向调试器连不上MCU报no target found程序跑飞、复位电路异常、接线松动按住复位再连接调试器检查SWD接线必要时擦除芯片虚拟串口号上有黄色感叹号驱动未安装或版本不对重装对应驱动换USB接口试试Keil工程找不到设备型号芯片包未安装安装对应型号的Device Family Pack串口打印乱码波特率不匹配或晶振不一致检查代码里波特率配置确认外部晶振数值这里每个问题单独展开都能写一篇长文我只提一个核心思路遇到环境问题先不要怀疑代码逻辑先把“最小系统能不能工作”确认了再回头查传感器部分。连LED点灯都点不亮的时候花一下午调DHT11时序大概率是白费功夫。我个人在实际操作中还有一个小习惯在DHT11_ReadData函数入口和出口各设一个断点用调试器看返回值。如果出口时返回0但数据不对那就是校验逻辑写错了如果函数压根没出来那就是死循环没超时。通过断点位置能快速定位问题在哪一段代码比盲猜有效得多。DHT11这颗传感器参数不亮眼精度不出众但作为第一个接触的单总线传感器它非常合格。把它的时序吃透了你以后看任何单总线器件的datasheet都能很快上手。而且它便宜坏一颗不心疼特别适合拿来练手。如果后续想扩展可以试试把DHT11的数据通过串口发给上位机做曲线或者和OLED屏配合做一个桌面温湿度计再往后就是换DHT20、SHT30这些更高级的传感器通信协议读法思路是相通的只是底层实现不同。先把这颗小传感器玩明白后面的路就好走多了。
返回列表