免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

STM32 HAL库驱动DHT11温湿度模块:单总线时序与稳定读取实践

STM32 HAL库驱动DHT11温湿度模块:单总线时序与稳定读取实践 简介面向嵌入式及物联网初学者的DHT11温湿度模块驱动代码适用于Arduino、STM32等常见微控制器平台解决环境温湿度采集与集成难题。压缩包共3个文件包含C源文件与头文件以及一个内嵌压缩包整体仅2KB代码精简清晰便于阅读和移植。已有311人学习下载。驱动代码覆盖传感器初始化、命令发送、40位数据读取、校验和验证及错误处理等完整流程并附带典型时序说明与移植要点可帮助开发者快速理解单总线通信机制直接嵌入LCD显示、数据存储或远程上报等实际项目。这份资源对刚接触DHT11或需要轻量级驱动参考的开发者是实用且高效的参考资料。1. dht11温湿度模块的坑时序比“温湿度”本身更难第一次用STM32F1读dht11温湿度模块你大概率会卡在一个诡异的地方万用表量电压正常串口也能打印“数据”但温湿度总在0和65535之间跳来跳去。问题出在单总线时序上——dht11只用一根DATA线完成双向通信0和1靠高电平持续时间来区分HAL_Delay这种毫秒级延时根本没法满足微秒级的采样窗口。这篇文章会用HAL库写一个最小可用的dht11温湿度模块驱动把单总线协议、上拉电阻、校验和重试一次讲清楚。适合正在调STM32F1裸机或HAL工程的嵌入式开发者也适合刚学会看原理图、想搞懂时序的人。2. dht11温湿度模块的单总线协议与40位数据格式2.1 DHT11的bit怎么用电平宽度表示DHT11的数据线在空闲时被上拉电阻保持为高电平。开始通信时主机先把总线拉低至少18ms然后释放这个低电平脉冲让传感器从休眠状态醒来。接着DHT11会主动拉低总线约80us再释放并拉高约80us表示“我准备好了”。这就是响应信号。响应之后传感器连续输出40bit数据。每个bit都从一段约50us的低电平开始然后总线被释放。关键在高电平持续多长时间如果高电平持续26us到28us代表逻辑0。如果高电平持续70us代表逻辑1。所以软件读取bit的标准做法是先等低电平结束然后延时约40us再采样总线电平。如果读到高就是1如果读到低就是0。因为逻辑0的高电平已经结束总线回到低而逻辑1的高电平还在持续。这个采样点必须落在30us到50us之间选40us左右最安全。伪代码可以写成这样while (DATA_PIN 0); // 等低电平结束 delay_us(40); // 延时到bit电平的中后段 bit DATA_PIN; // 高1低0这段逻辑是DHT系列通用的换到DHT21、DHT22时只需要调采样延时协议骨架不变。2.2 从40位数据里拆出温湿度与校验值40位数据的排列顺序是固定的高字节在前。字节位置内容例子第1字节湿度整数部分0x45即69%RH第2字节湿度小数部分0x00第3字节温度整数部分0x01即1℃第4字节温度小数部分0x00第5字节校验和0x46实际温湿度为湿度 第1字节 第2字节 * 0.1温度 第3字节 第4字节 * 0.1。上面的例子就是湿度69.0%温度1.0℃。校验和是前四个字节相加后取低8位如果结果不等于第5字节这次读取就应该扔掉。有一个很多资料没提的细节当温度低于0℃时第3字节最高位会被置1。比如收到0x81 0x00代表-1.0℃不是129℃。所以在做温度判读时要先检查第3字节的bit7决定是否需要做有符号数转换。3. dht11温湿度模块接线与STM32F1的GPIO设计3.1 原理图与上拉电阻从嘉立创画图到实物板DHT11有两种常见形态四脚直插封装和四线模块。直插封装引脚定义为VCC、DATA、GND、NC空脚模块板通常只要接VCC、GND、DATA三条线。DATA线必须接上拉电阻阻值范围4.7k到10k。上拉电阻把空闲总线拉到高电平同时配合开漏输出实现双向通信。如果省掉上拉你会发现读到的数据全是0因为总线没法回到高。模块版本一般已经在板上焊好了上拉电阻和0.1uF去耦电容但自己画原理图时这两样必须补上。用嘉立创画图时我一般会在DATA引脚旁边放一个4.7k电阻到VCC再在电源引脚旁放0.1uF电容到GND。封装上注意四脚DHT11的NC脚不要悬空到其他网络画PCB时让DATA走线尽量短避免靠近电源或PWM信号线。常见连接方式DHT11引脚STM32F1引脚外部电路VCC3.3V或5V0.1uF电容到GNDDATA任意GPIO如PB84.7k上拉到VCCGNDGND无NC不接悬空注意DHT11供电范围是3.3V到5.5V但DATA脚电平会跟随VCC。如果STM32F1是3.3V供电建议DHT11也接3.3V避免5V电平直接灌进芯片引脚。3.2 用HAL库把引脚配置成开漏输出GPIO配置有个选择用推挽输出加方向切换还是用开漏输出加外部上拉。我更推荐后者。开漏输出模式下写1等于释放总线由上拉电阻拉高写0等于强拉低。读取时直接用HAL_GPIO_ReadPin读引脚电平即可不需要在输入和输出模式之间来回切。STM32F1的GPIO输出寄存器不影响读取到的IDR寄存器所以开漏模式天然适合单总线。初始化代码__HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_8; // 数据线接PB8 GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; // 开漏输出 GPIO_InitStruct.Pull GPIO_NOPULL; // 外部已有上拉 GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; // 高速模式减少边沿变形 HAL_GPIO_Init(GPIOB, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET); // 初始置高这段代码的关键是GPIO_MODE_OUTPUT_OD。如果误用推挽输出又忘记切换方向主机拉高时会和DHT11的低电平输出打架轻则读错数据重则损坏引脚。GPIO_NOPULL也不是随手写的开漏模式下打开内部上拉会让外部上拉计算变得不稳定最好只保留一个。4. 用HAL库写dht11温湿度驱动最小可用的读取实现4.1 微秒延时与位读取函数HAL_Delay只能做到毫秒级而DHT11要求微秒级延时所以要先准备一个delay_us。这里用Cortex-M3的DWT计数器不需要额外占用定时器static void delay_us(uint32_t us) { DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; while (DWT-CYCCNT us * 72); // 72MHz主频1us约72周期 }位读取函数跟随协议逻辑先等低电平开始再等低电平结束延时40us采样总线。static uint8_t DHT11_ReadBit(void) { uint16_t timeout 500; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_8) GPIO_PIN_SET timeout--); timeout 500; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_8) GPIO_PIN_RESET timeout--); delay_us(40); return (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_8) GPIO_PIN_SET) ? 1 : 0; }完整读取函数负责发起启动信号、等待响应、收集40位数据并做校验#define DHT11_PIN GPIO_PIN_8 #define DHT11_PORT GPIOB int DHT11_Read(float *humi, float *temp) { uint8_t data[5] {0}; uint16_t timeout; HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); delay_us(20); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); delay_us(20000); // 启动信号至少18ms HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); // 释放总线 delay_us(30); if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET) { timeout 200; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET timeout--); timeout 200; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET timeout--); for (int i 0; i 40; i) { data[i / 8] 1; if (DHT11_ReadBit()) { data[i / 8] | 0x01; } } if ((uint8_t)(data[0] data[1] data[2] data[3]) data[4]) { *humi data[0] data[1] * 0.1f; *temp data[2] data[3] * 0.1f; return 0; } } return -1; }启动信号之后第一次判断GPIO_PIN_RESET是在确认DHT11把总线拉低进入响应状态。之后两个等待循环分别跳过80us低电平和80us高电平。40次循环里每8位拼成一个字节正好填充5字节数组。校验通过后转换成浮点温度湿度。这里有个容易被忽略的坑delay_us(20000)在72MHz下循环20次万次如果被中断频繁打断实际延时可能远大于20ms甚至导致后续超时判断失效。因此调用DHT11_Read时最好关掉会造成长时间阻塞的中断或者在临界区外调用并接受偶尔的读取失败。4.2 驱动里的3个必调参数DHT11驱动能跑起来容易跑稳定需要关注下面几个参数。参数推荐值调参说明主机拉低时间20ms低于18ms时传感器可能不响应超过30ms会白白增加功耗bit采样延时40us延时太短会把0读成1太长会把1读成0建议在34us到45us之间试读取间隔2sDHT11单次转换周期约1秒连续读太快会拿到上一次缓存值主机拉低时间决定了DHT11是否进入工作状态。我见过有人把HAL_Delay(1)当成18ms用结果传感器几乎不响应偶尔响应也是乱码。开个定时器在IO上抓波形是排查这类问题最快的方法没示波器就在启动信号前后各翻转一次另一个LED用肉眼判断延时是否明显过短。位采样延时是整个驱动里最敏感的参数。如果传感器离MCU很远、线上电容比较大高电平边沿会变缓40us不一定是最优点。这时可以写一个循环从30us到50us扫描采样点看哪个值下校验通过率最高。读取间隔也不能省。DHT11内部没有FIFO上一次读到的数据会一直保持直到下一次转换完成。如果应用层每秒读50次你会看到温度湿度完全不变这是传感器特性不是驱动死循环。5. 让dht11温湿度模块读数更稳的3个收尾技巧5.1 校验失败后延迟重读而不是把缓存清零读取失败时直接返回-1很容易让上层把温湿度显示成0用户一看就认为传感器坏了。常见做法是保持上一次有效值并在后台重试。重试间隔用2s避开DHT11的转换周期。int retry 0; while (retry 5 DHT11_Read(humi, temp) ! 0) { retry; HAL_Delay(2000); }如果连续5次都失败再上报错误。这样临时插拔线缆、电源抖动导致的一次失败不会影响显示。5.2 用连续读数做突变过滤DHT11正常工作时湿度变化不会每秒超过几个百分点。如果相邻两次读数湿度跳了10%以上温度跳了3℃以上大概率是受到了电机启停、继电器吸合这类电磁干扰。增加一个简单的判定if (fabs(new_humi - last_humi) 10.0f || fabs(new_temp - last_temp) 3.0f) { // 丢弃本次数据保持上次结果 return; }这个技巧特别适合用在空调控制器、大棚环境采集这些需要平滑曲线的场景能省掉后面加卡尔曼滤波的麻烦。5.3 排查前先量上拉和供电做了一堆软件优化还是偶尔读错回头量一下数据线空闲电平。DHT11数据线在没通信时应该接近VCC如果只有2V左右说明上拉电阻过大或走线太长。把4.7k上拉换成2.2k或者剪短杜邦线到20cm以内很多玄学问题直接消失。供电也尽量单独从STM32F1板子的3.3V引脚拉别和舵机共用一个电源输出。先把上拉电阻换成4.7k再回来调采样延时通常比反复改代码更管用。本文还有配套的精品资源点击获取
返回列表