尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32 HAL库驱动DHT11温湿度传感器:单总线时序与GPIO模拟详解

STM32 HAL库驱动DHT11温湿度传感器:单总线时序与GPIO模拟详解 简介本资源是一套基于STM32F103ZET6微控制器与DHT11传感器的嵌入式温湿度检测完整工程面向嵌入式初学者及单片机课程实践者解决环境参数采集、协议解析与串口通信等典型物联网感知层开发问题。压缩包共141个文件含33个头文件.h定义外设驱动与协议结构、32个源文件.c实现GPIO时序控制、DHT11单总线通信、UART数据发送及系统初始化逻辑另有.o、.d、.axf、.hex等编译中间与输出文件以及Keil MDK工程配置.uvprojx、.uvoptx、链接脚本.sct和批处理工具keilkilll.bat整体大小为3.85MB。已有7354人学习下载资源结构规范包含标准STM32固件库如stm32f10x_rcc.c、stm32f10x_adc.c等与完整可编译工程无需额外配置即可烧录运行特别适合掌握Cortex-M3底层驱动、传感器协议时序分析及嵌入式调试全流程的实战训练。 STM32F103ZET6驱动DHT11这个项目我最早是在实验室帮学弟调代码时接触到的。当时他板子上的DHT11死活读不出数据串口打印全是0我过去一看时序代码里延时全是瞎写的GPIO模式也没切换能读出来才怪。后来我自己认真啃了一遍DHT11的手册和单总线协议把整个流程理顺之后发现这其实是一个非常典型的“简单外设不简单”的项目——它不涉及复杂的外设寄存器配置也不像I2C、SPI那样有标准协议栈可用核心全靠GPIO模拟时序。但恰恰是这种“朴素”的通信方式把单片机最基础的GPIO操作、延时精度、上拉电阻设计、数据校验这些基本功全都串起来了。如果你能把DHT11彻底调通再看DS18B20、单总线温湿度传感器、甚至一些自定义单总线协议基本都能轻松上手。这篇文章我就从项目整体思路、DHT11单总线协议拆解、HAL库代码实现到常见问题排查完整过一遍。适合刚学完STM32但还不太清楚怎么把传感器接上来、怎么写驱动的新手也适合那些用标准库写过一遍、想转到HAL库重新梳理逻辑的开发者。1. 项目整体设计与开发思路1.1 为什么选STM32F103ZET6 DHT11这个组合先说结论这套组合是入门嵌入式传感采集的“黄金搭档”但不是性能最强的搭档。STM32F103ZET6属于大容量增强型产品Cortex-M3内核主频最高72MHzFlash有512KBRAM有64KB片上外设极其丰富。你拿它去点个DHT11说句实话是大炮打蚊子。也正因为如此这个项目最适合拿来练手——你不需要考虑资源紧张的问题所有精力都可以集中在逻辑和时序本身。DHT11呢是个数字温湿度传感器单总线通信能测20%-90%RH湿度±5%RH误差和0-50℃温度±2℃误差。精度确实一般一个DHT11只要几块钱但胜在接口简单、驱动量小、响应速度也能接受。对于室内环境监测、大棚温湿度记录、学习型项目完全够用。从学习价值上看DHT11有两个关键点非常值得深挖一是单总线协议对时序要求极高微秒级的延时都不能乱二是它只有一根数据线既要支持主机拉低总线发起读操作又要支持传感器拉低总线发送响应GPIO方向必须反复切换。你在驱动它的时候实际上是在实践三件事微秒级延时的实现、GPIO输入输出的敏捷切换、数据校验的处理。1.2 开发方式选型标准库还是HAL库这个项目用标准库写网上能搜到一大堆参考代码多数是操作寄存器或者直接用GPIO_WriteBit、GPIO_ReadInputDataBit配合Delay_us延时函数去实现的。对于想深入理解寄存器底层的人来说标准库版本值得读一遍代码量小逻辑一目了然。但我推荐你用HAL库理由很实际现在ST官方主推HAL库CubeMX可以完成时钟树、GPIO、串口的可视化配置工程初始化代码全部自动生成省去了重复造轮子的时间。DHT11驱动本身逻辑不复杂HAL库里GPIO的读写API也足够满足需求不会因为封装造成性能瓶颈。唯一需要注意的是HAL库的函数调用有一定开销尤其是GPIO_WritePin和GPIO_ReadPin在循环里高频调用时时序余量可能会被压缩。但只要延时函数够准驱动代码写得干净完全没问题。我个人的观点是如果你以后要用STM32做项目HAL库是绕不开的如果你想把单片机的底层吃透标准库或者寄存器版本是必修课。这个项目完全可以作为从标准库过渡到HAL库的桥梁两边各写一遍收获非常大。1.3 硬件接线方案与原理图要点DHT11是3引脚封装也有4脚的贴片版本但第四脚NC悬空引脚定义分别是VCC、DATA、GND。接线非常简单VCC接3.3V或5V都可以DHT11供电范围是3.3V-5.5V但建议统一用3.3V避免电平转换问题DATA接单片机的任意一个GPIO本文以PB12为例GND接GND核心的硬件细节是DATA线上必须接一个上拉电阻阻值4.7kΩ到10kΩ之间都可以推荐4.7kΩ。因为DHT11的数据线和主机之间是开漏关系总线空闲时必须保持高电平。你在自己做板子或者用面包板搭电路时这个上拉电阻经常会被漏掉结果就是通信失败——这是DHT11项目最常见的硬件原因之一。如果你用嘉立创EDA画原理图建议在DHT11的DATA引脚和3.3V之间画一个4.7kΩ电阻同时要在VCC和GND之间加一个100nF的去耦电容靠近DHT11放置能有效降低电源噪声对传感器读数的影响。还有一点值得注意DHT11的数据线尽量短不要从电机、继电器旁边走线否则电磁干扰会直接把时序打乱。2. 读懂DHT11单总线协议项目实施前必啃的硬骨头2.1 DHT11的“单总线”到底怎么理解DHT11的数据通信只有一根线叫单总线1-Wire跟DS18B20是同一种通信思想但两者的时序定义完全不同不能混用。单总线的核心特点是半双工、异步、低速。同一时刻总线上只能有一个设备在驱动电平其他设备必须处于高阻状态。DHT11作为从机平时完全沉默只有主机发起“起始信号”之后它才会开始响应并发送数据。这就像你和一个人打电话正常情况下对方不吭声等你先喊一声“喂”他才开始说话。而且这通电话只有一根线只能一个人说另一个人听。DHT11就是典型的这种通信模式。从电平角度看总线空闲是高电平。主机发起通信时先把总线拉低一段时间起始信号然后释放总线并等待DHT11响应。DHT11检测到起始信号后会先把总线拉低一段时间响应信号再拉高一段时间然后开始逐位发送40bit数据。整个过程完全靠电平跳变的时间长度来区分含义。2.2 时序拆解起始信号、响应信号、数据位DHT11的完整通信过程可以拆成五个阶段每一个阶段的延时都必须控制在手册规定的范围内下面按顺序讲第一步主机拉低总线持续至少18ms。这个低电平就是“起始信号”目的是让DHT11从待机状态醒来准备发送数据。很多新手在这里延时写太短比如只拉低几毫秒就释放DHT11根本没有反应过来后面肯定读不到数据。第二步主机释放总线然后延时20-40us。这一步是给DHT11留出“感知”起始信号的时间。注意这个延时不能太长一旦超过40usDHT11可能认为主机还在发信号就不会进入响应流程了。第三步DHT11拉低总线80us再拉高80us。这两个动作合起来是DHT11的响应信号告诉主机“我准备好了接下来要发数据了”。如果是读不到数据或者数据为全0第一步排查这里——示波器或者逻辑分析仪挂上去看有没有这个80us的低电平和80us的高电平。第四步DHT11连续发送40bit数据每一位的格式都是先拉低50us再拉高。引脚上表现为一个50us的低电平脉冲紧接着一个高电平脉冲。这个高电平持续26-28us代表“0”持续70us代表“1”。判断数据位是0还是1就是测量这个高电平的宽度。第五步40bit数据发送完成之后DHT11主动释放总线总线被上拉电阻拉回高电平整个通信过程结束。这里有一个非常重要的细节DHT11每次通信时间是固定的从上电到完成一次采样大约需要4ms。而且DHT11内部采样频率较低两次读取间隔最好大于1秒最少也不要低于500ms。如果你在循环里连续快速读取DHT11会来不及刷新内部数据返回的都是上一次的缓存值。2.3 40bit数据格式与校验算法DHT11一次通信发送40bit数据按顺序依次是湿度整数部分8bit湿度小数部分8bit温度整数部分8bit温度小数部分8bit校验和8bit湿度小数部分和温度小数部分在DHT11实际输出中始终为0因为DHT11的分辨率就是1%RH和1℃小数位没有实际意义。但DHT22也叫AM2302会把小数位填充真实数据因为它的分辨率是0.1%RH和0.1℃。所以如果你以后换传感器数据解析代码基本可以复用只是小数位的含义变了。校验和的算法非常朴素前四个字节相加取低8位和第五个字节比较相等则校验通过。实际代码里我一般这样写uint8_t check_sum dht11_data[0] dht11_data[1] dht11_data[2] dht11_data[3]; if (check_sum dht11_data[4]) { // 数据有效 }校验的好处是不用担心线路干扰导致的错误数据配合“连续多次读取结果一致再使用”的策略基本能过滤掉99%的异常读数。2.4 时序里最容易踩的隐藏坑DHT11的时序坑非常多我按实际踩过的顺序列几个最典型的第一个坑微秒级延时不准。很多开发板的delay_us是用while循环减变量实现的但不同编译优化级别下循环时间会变。我在Keil MDK默认-O0优化下写的延时到了-O2优化之后直接加速了2-3倍时序全乱。解决办法是用定时器做微秒延时或者用DWT数据观察点触发单元的CYCCNT寄存器这种延时跟编译器优化无关只跟主频有关稳定可靠。第二个坑GPIO方向切换时机不对。主机释放总线时必须把GPIO切回输入模式或者复用开漏模式如果还保持推挽输出高电平等于一直拉着总线DHT11根本没法控制总线电平通信必然失败。第三个坑读取数据时序采样点太靠后。判断0还是1核心是测量高电平持续时间。我建议在上升沿之后第40us的时候读一次引脚电平——这个时间点0和1的差距最明显稳定性最好。如果你等到第60us再读1没问题但0可能已经被拉了读到的还是0问题不大但如果你在上升沿后20us去读1和0都处于高电平阶段读出来全是1。3. HAL库驱动DHT11的完整实现3.1 用CubeMX完成基础初始化配置打开STM32CubeMX新建工程芯片型号选择STM32F103ZET6。配置项主要有三个RCC时钟选择HSE外部晶振在Clock Configuration页面把系统时钟设为72MHz。DHT11的时序对时钟频率并不是严格敏感但72MHz下微秒延时最容易计算。GPIO引脚把PB12设置为GPIO_Output初始电平设为High。这里有个细节DHT11的数据线通信过程中需要主机切换输入输出CubeMX里默认配置成输出模式即可代码中通过函数动态切换。USART串口如果你想调试时把温湿度数据打印到电脑上配置一个串口。我通常用USART1波特率115200参数8-N-1。CubeMX会自动生成串口初始化代码后面直接用HAL_UART_Transmit发送数据就行。工程生成方式Toolchain选择MDK-ARM生成完成后用Keil打开。3.2 微秒延时方案选型HAL库自带HAL_Delay函数单位是毫秒精度无法满足DHT11微秒级的时序需求。你需要一个us级的延时函数。最简单的方案是用定时器比如用TIM4做1us为单位的计时static void delay_us_tim(uint32_t us) { __HAL_TIM_SET_COUNTER(htim4, 0); HAL_TIM_Base_Start(htim4); while (__HAL_TIM_GET_COUNTER(htim4) us); HAL_TIM_Base_Stop(htim4); }TIM4的预分频系数设为72-1自动重载值设为0xFFFF这样计数器每计数一次正好是1us。这个函数的优点是原理简单、精度稳定缺点是调用HAL_TIM_Base_Start和Stop的开销比较大每次调用有几百纳秒的额外延迟在时序临界点的位置要注意补偿。如果不想用定时器另一个推荐方案是根据内核时钟周期做延时也就是DWT延时。Cortex-M3内核有一个CYCCNT寄存器每来一个内核时钟周期就加一72MHz下1us就是72个周期利用它可以实现非常精确的微秒延时static void delay_us_dwt(uint32_t us) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; while (DWT-CYCCNT us * 72); }这种方式的优点是代码量极小、精度极高、不占用定时器资源我强烈推荐你在工程里用这个方案。其实Cyclone系列调试器调试裸机程序时也常用CYCCNT做代码耗时统计是一个非常好用的内核特性。3.3 DHT11驱动代码逐段解析先定义GPIO操作的宏方便后面的读写#define DHT11_GPIO_PORT GPIOB #define DHT11_GPIO_PIN GPIO_PIN_12 #define DHT11_OUT_HIGH() HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET) #define DHT11_OUT_LOW() HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_RESET) #define DHT11_IN_READ() HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN)然后是GPIO模式切换函数输入模式和推挽输出模式之间的切换用HAL库的标准写法void DHT11_Pin_Mode_Output(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_GPIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStruct); } void DHT11_Pin_Mode_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); }接下来是读取一个字节的函数。DHT11的位读取是按顺序进行的先等低电平开始再等低电平结束也就是高电平开始然后延时40us采样电平。我把核心逻辑注释写在代码里方便对照uint8_t DHT11_Read_Byte(void) { uint8_t data 0; for (int i 0; i 8; i) { // 等待低电平开始 while (DHT11_IN_READ() GPIO_PIN_RESET); // 低电平结束进入高电平此时开始计时 delay_us_dwt(40); // 延时40us后采样高电平持续超过40us则为1 if (DHT11_IN_READ() GPIO_PIN_SET) { data (data 1) | 0x01; } else { data (data 1) | 0x00; } // 等待当前位的高电平结束 while (DHT11_IN_READ() GPIO_PIN_SET); } return data; }这个函数有个潜在问题如果DHT11没有正常输出while循环会一直等如果总线上是低电平或者高电平卡死程序会死锁。在实际工程里建议给while加一个超时退出机制。一个简单有效的做法是设置一个计数器若循环次数超过某个值则直接返回0并在上层识别为读取失败。我给一个带超时判断的读位版本uint8_t DHT11_Read_Byte_Timeout(uint32_t timeout) { uint8_t data 0; uint32_t cnt 0; for (int i 0; i 8; i) { cnt 0; while (DHT11_IN_READ() GPIO_PIN_RESET) { if (cnt timeout) return 0; } delay_us_dwt(40); if (DHT11_IN_READ() GPIO_PIN_SET) { data (data 1) | 0x01; } else { data (data 1) | 0x00; } cnt 0; while (DHT11_IN_READ() GPIO_PIN_SET) { if (cnt timeout) return 0; } } return data; }3.4 主逻辑与数据解析读取函数封装成一次完整的DHT11读取过程uint8_t DHT11_Read_Data(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] {0}; // 主机拉低起始信号至少18ms DHT11_Pin_Mode_Output(); DHT11_OUT_LOW(); HAL_Delay(20); // 释放总线切换输入模式拉高由外部上拉电阻完成 DHT11_OUT_HIGH(); delay_us_dwt(30); DHT11_Pin_Mode_Input(); // 等待DHT11响应先拉低80us再拉高80us // 这里需要等待低电平出现然后等待低电平结束再等待高电平结束 uint32_t cnt 0; while (DHT11_IN_READ() GPIO_PIN_SET) { if (cnt 10000) return 1; } cnt 0; while (DHT11_IN_READ() GPIO_PIN_RESET) { if (cnt 10000) return 1; } cnt 0; while (DHT11_IN_READ() GPIO_PIN_SET) { if (cnt 10000) return 1; } // 读取40bit数据 for (int i 0; i 5; i) { data[i] DHT11_Read_Byte_Timeout(10000); } // 恢复总线为空闲高电平切换为输出高 DHT11_Pin_Mode_Output(); DHT11_OUT_HIGH(); // 校验和验证 if ((data[0] data[1] data[2] data[3]) data[4]) { *humidity data[0]; *temperature data[2]; return 0; } return 2; }主函数里调用就非常简单int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); MX_TIM4_Init(); uint8_t hum, temp; uint8_t res; while (1) { res DHT11_Read_Data(hum, temp); if (res 0) { char msg[64]; sprintf(msg, Temp: %d.%d C, Hum: %d.%d %%RH\r\n, temp 8, temp % 256, hum 8, hum % 256); HAL_UART_Transmit(huart1, (uint8_t*)msg, strlen(msg), 1000); } else { char msg[32]; sprintf(msg, DHT11 read failed: %d\r\n, res); HAL_UART_Transmit(huart1, (uint8_t*)msg, strlen(msg), 1000); } HAL_Delay(2000); } }等等上面这段sprintf的写法有误导性我实际使用时是按照整数和小数分开打印的也就是说湿度的高字节是整数部分、低字节是小数部分。DHT11的小数部分为0所以简单的做法是sprintf(msg, Temp: %d C, Hum: %d %%RH\r\n, temp, hum);如果只想看整数直接用data[0]和data[2]就行。4. 实测结果与常见问题排查实录4.1 我实测的一组数据调通之后我把DHT11放在桌面上室温大约26℃左右用串口助手连续采集了15分钟每隔1秒读一次。输出的数据基本稳定在温度26-27℃湿度52%-56%之间波动。有一个现象值得注意湿度读数在小范围内持续跳动比如52、53、54、53这样跳这是正常的。DHT11在空气中的响应本身就存在一定波动并不是传感器的故障。反过来如果湿度读数长时间纹丝不动反而要检查一下传感器是不是坏了或者环境是不是太干燥了。另外如果用手捏住DHT11几秒后温度和湿度读数都会明显上升说明传感器响应正常只是热响应速度确实不快DHT11大概需要2-3秒稳定到新环境值。还有一个实用经验是读取间隔最好设在2秒以上。DHT11手册上建议读取间隔不小于1秒但实际测试中1秒间隔偶尔会出现校验失败2秒之后基本稳如老狗。原因可能是DHT11内部采样和刷新需要时间如果读取过于频繁上一次的数据还没更新或者内部状态机还没就绪就会导致误码。4.2 常见问题速查表我在调试和帮人排查的过程中整理了下面几个高频问题现象主要原因解决办法读到的全是0起始信号时间太短DHT11未响应或引脚未正确切换输入模式检查起始低电平是否达到18ms以上释放总线后改用输入模式读取数据有时正确有时乱码微秒延时不准采样点偏移换用定时器延时或DWT延时检查主频是否为72MHz校验和一直失败电源纹波大、电池供电电量不足、数据线过长或靠近干扰源加100nF去耦电容缩短数据线长度改用稳压电源供电串口打印乱码波特率不匹配、系统时钟配置错误检查串口配置和CubeMX时钟树设置读取函数卡死while循环没有超时保护总线某处拉死了给每个等待循环加上超时退出机制温度正常但湿度偏高传感器靠近发热元件或人体调整传感器安装位置避免热源影响偶尔读到255或0xFF线路接触不良或DHT11损坏重新插拔传感器用万用表测量引脚电压4.3 排查DHT11不工作的标准流程如果你遇到DHT11完全不出数不要急着换传感器或者重写代码按照这个流程排查一遍基本能定位问题第一步确认供电。用万用表量DHT11的VCC和GND之间是否有稳定电压至少有3.3V。如果电压低于3VDHT11的时序会偏移通信大概率失败。第二步确认上拉。DATA引脚和VCC之间必须有上拉电阻如果面包板上只插了三条杜邦线而没有电阻那DATA线悬空的时候电压是不稳定的根本不能通信。第三步确认GPIO模式切换。用逻辑分析仪或者示波器看总线波形主机发起起始信号后总线上是否出现DHT11的响应波形80us低80us高。如果响应波形正常说明硬件和起始时序都没问题如果看不到响应问题在起始信号或传感器本身。第四步检查延时时间。软件仿真或者示波器实测主机输出的起始低电平时间是否在18ms左右。有的代码里HAL_Delay在中断环境下会被扰乱导致延时明显变短这时需要改在无中断环境下调用或者换用基于定时器的延时。第五步换一个DHT11。DHT11是很便宜的传感器但质量参差不齐个别模块不稳定。我遇到过几次所有时序都对就是读不出数据换一个DHT11就好了。说一个重要的经验DHT11的读数据过程对中断非常敏感。如果主循环里开了很高频率的定时器中断刚好在读取每一位的采样点附近触发采样到的电平宽度就会变导致数据错乱。我的做法是在启动一次DHT11读取之前关掉那些非关键中断读完再恢复。如果你的系统里必须保留中断可以考虑把DHT11的驱动放到一个独立的、定时触发的任务里并且在这个任务执行期间关闭被抢占的中断。5. 扩展方向与实测体会5.1 从这个项目能扩展出去的方向DHT11调通之后我觉得有两个方向特别值得继续做一个方向是换DHT22AM2302。DHT22的通信时序和DHT11几乎一模一样只是数据格式里小数位有意义测量范围更大-40到80℃0到100%RH精度也更高。你只需要微调解析代码其他完全复用。换上去之后整个系统的数据质量会有肉眼可见的提升对于小型气象站、室内空气质量监测这类场景更合适。另一个方向是接OLED屏幕本地显示。ST7789或者SSD1306的OLED屏都是常见的I2C或者SPI接口配合STM32F103ZET6绰绰有余。整个系统做成一个独立的小型温湿度计比单纯串口打印要直观得多而且正好能练一下GUI代码的编写和页面切换逻辑。还有一些更进阶的思路比如把温湿度数据通过ESP8266模块上传到服务器或者在本机用FreeRTOS创建一个采集任务把DHT11的读取放到独立任务中用信号量控制采集间隔。这些对操作系统的任务调度、资源管理也会有更深的理解。5.2 个人实际调试过程中的一点体会写到最后说点实在的。我第一次调DHT11时用的是一段网上抄来的标准库代码抄过来之后发现一运行就卡死为了查问题把时序图对着手册一帧一帧地比到最后才发现是GPIO开漏输出和上拉电阻没配合好。后来我把整段逻辑拆开边分析边重写才真正把DHT11这一套搞明白。这个项目给我的最大感受是DHT11让我重新理解了“简单外设”这四个字。它的硬件设计很朴素跟SPI、I2C这类工业标准总线完全没法比但就是要求你把每一微秒都安排得明明白白把GPIO的方向切换、上拉电阻的作用、微秒延时的实现方式、数据校验的思路全部打通。如果你能把这个传感器彻底调稳再去碰那些协议复杂的外设会轻松很多。另外调试时一定要善用串口打印和逻辑分析仪。不要只靠串口打印“读不出来”这样粗糙的结论逻辑分析仪能直接看到总线上每一段波形的宽度一眼就能看出是起始信号太短、响应信号没有出现、还是数据位采样时间不对。没有逻辑分析仪的话可以用示波器代替效果类似。现在每次跑这个Demo看着串口助手一行行温度湿度数据刷出来还是会觉得挺有意思的。下一次我再调DHT22的时候至少不用从零开始了。本文还有配套的精品资源点击获取
返回列表