ARTICLE DETAIL

资讯详情

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

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

STM32驱动DHT11温湿度传感器:单总线协议与HAL库实战解析 很多人第一次接触STM32做的第一个传感器项目就是DHT11温湿度检测。便宜、够用、代码量适中还能顺便把GPIO操作、时序协议、串口调试这些基本功都过一遍确实是入门阶段性价比很高的一块板子。但这个模块有一个特点它用的是单总线协议时序要求非常严格网上教程虽然多能一次跑通的却不多。我前后在几个项目里都踩过DHT11的坑从最初的裸机读数据到后来换成HAL库重写驱动再到把它接进小型的物联网节点里做周期上报慢慢把这块小传感器的脾气摸透了。这篇文章就把我在STM32上驱动DHT11的完整思路整理出来内容包括DHT11底层工作原理、硬件电路怎么接最稳、HAL库和标准库两种方式下的驱动怎么写、以及实际调试时最容易遇到的几个问题。无论你是刚把开发环境搭好、准备跑第一个外设的新手还是想把手头项目里温湿度采集部分做得更稳的开发者这篇内容都可以直接照着操作。1. DHT11到底是怎么工作的先搞懂单总线协议DHT11看起来只是个四脚小器件但它的通信方式跟I2C、SPI这类标准总线都不一样。它用一根数据线完成双向通信协议是定制化的单总线时序主机和设备之间靠严格的电平时间长度来区分“0”和“1”。理解了这套时序后面写驱动、排查问题都会轻松很多。1.1 为什么DHT11能这么便宜DHT11内部集成了一个电阻式湿度传感元件和一个NTC测温元件再加上一个8位单片机做信号处理和单总线编码封装在一起后对外只有三个引脚VCC、GND、DATA。它便宜的原因在于精度本身不高湿度精度±5%RH温度精度±2℃测量范围也有限。但对于大多数消费级、低成本场景比如室内环境监测、智能鱼缸、机柜温湿度告警这类需求这个精度完全够用了。这里要注意DHT11和DHT22是两代产品。DHT22的精度更高湿度±2%RH温度±0.5℃分辨率也更好但价格贵好几倍。选型时如果项目对精度要求不高DHT11就够如果要做数据记录或者需要小数点后一位的分辨率直接上DHT22更省事。二者的驱动时序基本兼容只是数据位定义略有差异所以先学会DHT11再切到DHT22的成本很低。1.2 单总线通信的完整握手过程DHT11的数据引脚是开漏输出外部需要接一个上拉电阻通常4.7kΩ到10kΩ空闲时数据线保持高电平。一次完整的读取过程分三个阶段第一阶段主机发起起始信号。主机先把数据线拉低持续至少18ms我一般拉到20ms以上确保DHT11能识别然后释放总线。这个低电平时间不能太短DHT11内部单片机需要一定时间唤醒时间太长也没关系无非是多等一会儿。第二阶段DHT11响应。主机释放总线后DHT11会把数据线拉低80us再拉高80us作为应答信号。如果总线上没有设备响应或者接线有问题这80us的低电平不会出现读到的数据就是全1或者全0。第三阶段数据输出。响应完成后DHT11开始连续输出40bit数据顺序是湿度整数部分、湿度小数部分、温度整数部分、温度小数部分、校验和每个字节8位高位在前。40bit发送完毕DHT11释放总线回到空闲状态。每一位数据的表示方式很有意思每一位都以50us的低电平开始表示“开始位”然后根据高电平的持续时间区分“0”和“1”。高电平持续26到28us表示“0”持续70us表示“1”。所以读取数据的关键就是测量每bit高电平的宽度。校验规则也很简单前四个字节相加低8位等于第五个字节就说明数据有效。比如湿度整数0x34、湿度小数0x05、温度整数0x18、温度小数0x01那校验和应该是0x34 0x05 0x18 0x01 0x52。校验不对的时候建议直接丢弃这次数据不要拿错误数据去做控制逻辑。2. 硬件连接与电路设计别小看这颗上拉电阻DHT11的硬件连接看起来简单到只需要三根杜邦线但在实际项目中电路设计的好坏直接影响读取成功率。尤其当数据线比较长、或者模块供电电压不稳的时候问题会非常明显。2.1 引脚连接方式DHT11模块通常有三种形态裸芯片四脚直插、小板模块带焊盘和上拉电阻、以及封装好的防水探头。最常见的开发板配套模块板上已经集成了上拉电阻和去耦电容直接用杜邦线连STM32即可。引脚对应关系如下DHT11引脚STM32引脚说明VCC3.3V或5VDHT11供电范围3.3V~5.5VDATA任意GPIO本文以PA0为例需要配置为开漏输出或推挽输出外部上拉GNDGND共地必须接好NC悬空空脚不需要连接如果是裸芯片必须在DATA引脚和VCC之间接一个4.7kΩ上拉电阻。有些同学直接用推挽输出模式驱动不加上拉电阻短距离测试也能工作但这是不稳定的。因为DHT11的数据引脚是开漏输出如果没有外部上拉设备拉高电平的能力基本没有只能靠STM32侧的推挽输出来维持逻辑高电平这会带来两个问题一是通信时电平翻转的沿不够陡峭时序容错变差二是如果引脚配置切换不及时总线状态可能不确定。稳妥的做法是MCU侧GPIO用开漏输出模式配合外部上拉电阻或者用推挽输出模式但必须保证外部电路有上拉。2.2 供电与滤波细节DHT11对供电纹波不算敏感但如果你用STM32开发板上的3.3V供电且板上还有其他大电流外设比如ESP8266、蜂鸣器、舵机DHT11的读数可能会偶发异常。这是因为Wi-Fi模块或电机启动瞬间会把电源电压拉低导致DHT11内部逻辑混乱输出无效数据。我实测下来给DHT11单独供电或者从电源模块引一路干净的3.3V/5V读取成功率会有明显改善。另外在VCC和GND之间并联一个0.1uF去耦电容放置在靠近DHT11供电引脚的位置能有效抑制高频噪声。这个电容在很多模块上已经焊好了但你如果自己画PCB一定要记得加。2.3 数据线长度与线序问题DHT11的单总线协议对时序要求虽然严格但对于20cm以内的杜邦线问题不大。如果数据线超过50cm寄生电容会拉慢电平转换速度导致MCU采样到的脉冲宽度发生偏差可能出现一部分bit读成0、一部分bit读成1最终校验失败。另外我之前遇到过一种诡异现象模块正常、代码正常但插上某个特定杜邦线后数据就一直超时。排查了半天发现是那根杜邦线内部接触不良时通时断。所以排查DHT11问题时第一步永远先换线、换供电、换模块排除硬件接触问题再去查代码。3. 软件设计与驱动实现HAL库和标准库都能跑DHT11的驱动不依赖任何标准外设库本质上就是“GPIO输出控制微秒级延时GPIO输入捕获”。HAL库或标准库的差异只体现在GPIO初始化和电平读写函数的调用上时序部分完全一样。这部分我给出HAL库版本的实现并对关键代码做逐段说明。标准库版本只需要把GPIO操作函数替换成GPIO_SetBits、GPIO_ResetBits、GPIO_ReadInputDataBit即可。3.1 微秒级延时的正确姿势HAL_Delay()只能做到毫秒级而DHT11时序里面大量用到几十微秒的延时所以必须自己实现us级延时。常见方案有两种使用SysTick定时器或者使用DWTData Watchpoint and Trace模块的周期计数器。我更推荐DWT方案因为它不需要占用SysTick中断也不会影响HAL库原本的时基。核心代码如下static void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } static void DWT_Delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) ticks); }这段代码的原理是使能DWT的周期计数器让它在每个内核时钟周期自动加1然后通过计算两个时刻之间的差值来延时。SystemCoreClock是内核时钟频率例如STM32F103在72MHz主频下SystemCoreClock / 1000000 72也就是延时1us需要72个周期。需要注意如果主频改了延时时间也要跟着变所以用SystemCoreClock动态计算而不是写死一个数字。为什么强调不要用普通的for循环做延时因为编译器优化等级不同同样的循环次数在不同优化级别下延时差异非常大。代码在-O0下调试正常一开-O2优化时序就乱了这种问题排查起来特别头疼。3.2 复位与应答检测初始化DHT11时先让MCU拉低数据线保持20ms然后拉高并释放总线。这里有个容易踩的坑GPIO方向切换要及时。在HAL库中如果用推挽输出模式释放总线就是把引脚置高但设备应答时会把总线拉低如果此时引脚还是输出模式就会出现总线争用问题。所以读取数据前要把GPIO切换到输入模式。#define DHT11_GPIO_PORT GPIOA #define DHT11_GPIO_PIN GPIO_PIN_0 #define DHT11_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE() void DHT11_Start(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; 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); // 释放总线后保持30us GPIO_InitStruct.Pin DHT11_GPIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); }为什么释放总线后要延时30us因为DHT11接收到起始信号后需要一小段时间准备应答。这个时间不能太长否则会错过应答信号的前沿。30us是一个比较安全的值既给设备留了响应时间又不会错过80us低电平的开始。3.3 数据位读取的核心函数读取每一位的关键是判断高电平持续时间。标准做法是先等待低电平结束也就是50us低电平的尾部然后开始计时高电平宽度。uint8_t DHT11_ReadByte(void) { uint8_t data 0; for (int i 0; i 8; i) { // 等待50us低电平结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_RESET); DWT_Delay_us(40); // 延时40us后采样 if (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { data | (0x80 i); } // 等待高电平结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET); } return data; }这段代码的思路是每个bit开始时数据线会先出现50us的低电平此时不断循环等待直到变高。变高之后延时40us然后采样引脚电平。如果采样到高电平说明高电平持续时间超过了40us那就是“1”如果采样到低电平说明“0”的短高电平26~28us已经结束那就是“0”。判断逻辑其实很粗暴但足够可靠。为什么要采样两次而不是直接计时因为用DWT延时40us后采样比频繁调用HAL_GPIO_ReadPin去轮询更稳定也更容易理解。如果MCU主频较低或代码有其他中断干扰轮询方式的误差会变大。3.4 数据校验与完整读取流程40bit数据读取完成后需要做校验。完整的读取函数如下uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] {0}; // 重新初始化GPIO为输出模式 GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_GPIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); // 发送起始信号 DHT11_Start(); // 等待应答信号先低后高 uint32_t timeout 10000; while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_SET) { if (--timeout 0) return 1; // 超时无设备应答 } while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) GPIO_PIN_RESET) { if (--timeout 0) return 1; } // 读取40bit数据 for (int i 0; i 5; i) { data[i] DHT11_ReadByte(); } // 校验 uint8_t checksum data[0] data[1] data[2] data[3]; if ((checksum 0xFF) ! data[4]) { return 2; // 校验错误 } *humidity data[0]; // 湿度整数部分 *temperature data[2]; // 温度整数部分 return 0; }这里返回的湿度和温度都是整数。DHT11的小数部分在很多应用中可以忽略因为它的精度本身只有±5%RH和±2℃小数位基本没有参考价值。但如果你用DHT22建议把小数位也解析出来因为DHT22的分辨率是0.1。主循环里调用的频率要注意DHT11的数据更新周期是1秒到2秒所以读取间隔不能小于1秒。如果连续快速读取第二次读取大概率失败。我一般在主循环里用一个1秒的软件定时标志保证两次读取之间至少间隔1秒。4. 常见问题与排查技巧实录DHT11的坑大多数集中在读取失败、数据异常、以及编译环境相关这三类问题上。下面把我在实际项目中遇到过的典型问题和排查思路整理出来。4.1 读取一直超时总线没有响应现象不管怎么调延时DHT11_ReadData的返回值永远是1超时无应答。排查顺序如下第一测硬件。用万用表量DHT11的VCC和GND之间是否有稳定的3.3V或5V电压。再测DATA引脚空闲时的电平正常应该是高电平。如果是0V说明上拉电阻没接或者模块损坏。第二查接线。DATA引脚是否真的连接到了正确的GPIO杜邦线是否插紧我遇到过一次GPIO初始化用的是PA0但杜邦线插到了PA1查了两小时才发现。第三确认GPIO配置。开漏输出模式下必须先设置输出寄存器为高再切换成输入模式否则释放总线后引脚是低电平。有些代码库在GPIO_Init之后没有把ODR置位导致总线一直被拉低DHT11永远收不到合法的起始信号。第四确认延时是否准确。如果DWT_Delay_Init没有调用或者SystemCoreClock的值不对20ms延时会严重偏移。建议在调试时加一个引脚翻转示波器看波形确认起始信号的拉低时间确实大于18ms。4.2 数据能读到但校验一直失败现象DHT11_ReadData返回2读出来的五字节数据变化毫无规律或者某一位总是读错。这个问题通常出在位读取的延时精度上。DHT11的“0”高电平时间是26~28us“1”高电平时间是70us读bit时40us的采样点是关键。如果延时不准确偏长超过10us以上就会导致“1”被误判成“0”因为采样点落到了高电平结束之后。另外一个容易被忽略的原因中断干扰。如果项目里开了定时器中断、串口中断而且中断处理函数耗时较长可能导致读bit过程中的40us延时被拉长进而采样错乱。解决方法是在读取DHT11数据这段时间里暂时屏蔽优先级不高的中断临界区保护或者把DHT11读取放在一个中断频率较低的任务里。还有一种情况GPIO的翻转速度配置太低。如果用HAL库初始化GPIO为输入模式时设置了GPIO_SPEED_FREQ_LOW会影响输入采样电路的响应速度导致边沿检测发生偏移。建议GPIO速度至少设置成GPIO_SPEED_FREQ_HIGH。4.3 读取失败烧录时报错“error: no stm32 target found”这个错误虽然和DHT11本身无关但在调试DHT11项目时特别常见因为很多人会手滑把DHT11的DATA线接到SWDIO引脚PA13或者SWCLK引脚PA14上。接上去之后调试器无法和STM32建立连接于是在烧录时报错error: no stm32 target found! if your product embeds debug authentication, please check whatever debug authentication is enabled遇到这个问题先把DHT11的所有接线拔掉再尝试连接ST-Link/J-Link。如果拔线后能正常连接说明是引脚冲突如果仍然连不上检查调试器接线、目标板供电、以及BOOT0引脚电平。STM32F103等芯片如果BOOT0被拉高会进入系统存储器启动模式此时内核不执行用户程序SWD仍然可用但有些情况下调试连接也会异常。另外如果程序中初始化GPIO时不小心把SWDIO引脚PA13、PA14复用成了普通GPIO输出并且把电平拉低就会导致调试器无法连接。这种情况下只能按住复位键的同时点击烧录或者用串口ISP方式擦除芯片。4.4 电脑无法识别串口设备管理器出现感叹号DHT11项目里经常要加串口打印数据新手使用国产STM32开发板时可能会遇到USB转串口芯片驱动问题。如果设备管理器里出现感叹号设备名类似“STM32 Virtual COM Port”有两种情况一种是板载的USB转串口芯片比如CH340、CP2102驱动没有安装下载对应厂商的驱动安装即可。另一种是STM32芯片本身支持USB虚拟串口比如STM32F103C8T6、F407但你烧录的程序里集成了USB库此时电脑会枚举出一个“STM32 Virtual COM Port”如果驱动有问题或者固件异常就会显示感叹号。这个和DHT11没有直接关系但会影响你观察串口打印的温湿度数据。排查思路先确认板子上有没有独立的USB转串口芯片如果有检查它的驱动如果没有说明需要用STM32的USB接口实现串口通讯这时需要检查USB库的版本和中断配置。常见的问题是USB库版本和HAL库版本不匹配或者USB时钟配置错误。4.5 开机第一次读取正常之后一直失败这也是DHT11很经典的“脾气”模块上电后需要1秒左右的稳定时间如果上电后立刻读取大概率超时。另外DHT11内部的湿敏电容需要一定时间完成充放电稳定频繁读取会导致它内部状态没恢复。解决办法很简单初始化完成之后延时2秒再开始第一次读取之后每次读取间隔至少1秒。如果项目里用RTOS可以单独创建一个DHT11任务以2秒周期挂起唤醒读取效果比较稳定。这里补充一个细节即使读取失败也不要连续快速重试否则容易打乱DHT11内部的状态机。失败时建议直接等到下一个周期再试。5. 应用场景与进阶扩展DHT11能玩出的花样DHT11虽然只是个基础传感器但把它和STM32的不同外设组合起来能做出不少有意思的东西。这里分享几个我已经落地过的场景给正在纠结“接下来做什么”的读者一些参考。5.1 智能鱼缸环境监测我之前帮朋友做过一个STM32鱼缸控制器DHT11负责测量环境温湿度DS18B20测水温再加上一个水泵继电器和LED补光灯。DHT11的数据用OLED屏实时显示温湿度超过设定阈值时触发蜂鸣器报警。这个项目里最值得注意的一点是鱼缸周围湿度常年偏高DHT11探头不能直接放在鱼缸盖正上方否则水汽会直接附着在传感器表面导致湿度读数长期处于饱和状态。建议把传感器放在离水面有一定距离、通风的位置。另外如果鱼缸用了加热棒环境温度会受加热棒影响测温数据要和水温参数配合来判断不能只凭DHT11一个数据做决策。5.2 宿舍/家居智能控制配合ESP8266或机智云模块STM32把DHT11采集的温湿度数据通过串口发送给Wi-Fi模块再上传到云平台实现手机远程查看。这个场景也是热词里“stm32 8266 宿舍控制灯开发 实战”比较典型的延伸。实际开发中有个坑STM32的串口发送是阻塞式的如果DHT11读取数据和串口发送同时进行串口中断可能会打断DHT11的时序。建议在架构上把两个任务错开先读完DHT11把结果缓存到全局变量再进行串口发送避免在DHT11读取过程中占用过多的CPU时间。如果必须并发可以在DHT11读取时屏蔽串口中断或者把DHT11读取的优先级提到最高。5.3 与K210等AI芯片配合做数据感知最近不少人在做K210与STM32通讯的项目比如用K210做图像识别用STM32做传感器采集和控制。这种异构架构里DHT11通常挂在STM32上STM32把温湿度数据通过串口或SPI/I2C发给K210K210再结合视觉信息做综合决策。这种场景下要注意通讯协议的设计不能简单地把温湿度作为裸数据发送建议定义一个简单的帧格式比如帧头设备ID湿度温度校验这样K210解析起来更稳。还有一个细节如果STM32和K210的供电不是同一个电源串口通讯两端必须共地否则可能出现数据乱码或者通讯不稳定。5.4 数据处理的小技巧滤波与迟滞DHT11读数本身有一定波动尤其湿度值相邻两次读取可能跳变几个百分点。如果直接拿这个值去控制继电器或者触发电机设备会频繁启停。我的做法是在应用层加一个简单的滑动滤波保留最近5次湿度值每次取平均作为当前湿度。如果要做阈值控制再加一点迟滞。比如设定湿度低于40%开加湿器高于45%才关加湿器这样能避免设备在临界点反复切换。温度数据相对来说稳定一些但如果你发现温度读数在1℃范围内来回跳也可以用同样的平均值滤波处理。需要注意的是DHT11的温度测量响应速度比较慢不要期望它能快速捕捉温度突变。6. 踩坑总结与个人体会DHT11这个模块代码量不大难度也主要在时序控制上但实际调试中暴露出来的问题往往来自硬件细节、环境干扰、以及工程配置而不是协议本身。我把这几年涉及DHT11的项目经验浓缩成几条建议大家可以直接参考。第一一定要给DHT11留足“静默时间”。上电稳定1秒、两次读取间隔至少1秒这条如果做不到后续所有问题都会被放大。第二GPIO方向转换要果断。输出模式释放总线后要立即切换到输入模式中间不要插入额外的延时。有的同学在切换模式前还用HAL_Delay延时几毫秒这会导致总线悬空状态持续时间过长增加误码概率。第三数据校验必须写。即使你的项目只是demo也建议养成读取数据后做校验的习惯。校验失败就抛掉这一帧等下一周期再读。这个习惯能让你在后续接DHT22、甚至接其他单总线传感器时省下大量排查时间。第四示波器或逻辑分析仪是真的神器。调试DHT11时用示波器抓一次起始信号和应答信号的波形比盲调代码高效十倍。先看主机信号是否正常再看设备应答是否出现最后看每个bit的高低电平宽度是否符合协议问题出在哪一层一目了然。DHT11不是精度最高的传感器但它的单总线通信机制是一个非常好的学习样本。搞懂了它再去读DHT22、DS18B20这类同样基于单总线的传感器你会发现套路都差不多都是主机拉低触发、设备应答、然后按bit输出数据。把DHT11的驱动写稳了后面很多外设驱动都能触类旁通。另外顺便说一句如果你用的是APM32这类国产替代芯片DHT11驱动代码基本可以直接复用因为它们的内核和GPIO寄存器结构兼容性很好。但切芯片前最好对照一下数据手册确认GPIO开漏模式和延时函数所需的内核时钟设置别默认万事大吉。
返回列表