当前位置: 首页 > news >正文

基于天空星STM32F407的DHT11单总线温湿度传感器驱动移植与数据解析实战

基于天空星STM32F407的DHT11单总线温湿度传感器驱动移植与数据解析实战

最近在做一个环境监测的小项目,需要用到温湿度传感器,DHT11因为价格便宜、接口简单,成了我的首选。但网上找的代码直接用在天空星STM32F407开发板上,总是读不出数据,或者数据乱跳。折腾了半天,终于把时序和数据解析都调通了。今天我就把整个移植过程、代码细节,还有我踩过的坑,手把手分享给大家,让你也能在自己的天空星开发板上快速驱动DHT11。

1. 认识一下我们的主角:DHT11

在写代码之前,咱们先花几分钟了解一下DHT11到底是个啥,以及它怎么跟我们“说话”。这能帮你更好地理解后面的时序操作。

DHT11是一个数字温湿度复合传感器,它最大的特点就是单总线通信。什么叫单总线?简单说,就是发送命令、接收数据,全都只用一根线(除了电源和地)。这根线既是命令线也是数据线,省引脚,但时序要求比较严格。

它的基本参数如下,咱们心里有个数:

参数规格
工作电压3.3V - 5.5V (天空星的3.3V刚好)
测量范围湿度20%-90%RH,温度0-50℃
测量精度湿度±5%RH,温度±2℃
分辨率8位(湿度小数部分固定为0)
通信协议单总线
引脚3个 (VCC, DATA, GND)

注意:DHT11的湿度小数部分输出恒为0,所以湿度分辨率是1%RH。温度的小数部分是有数据的,分辨率是0.1℃。

它和单片机通信的整个过程,可以想象成一次“握手对话”:

  1. 单片机(主机)说:“喂,DHT11,在吗?”-> 主机发送开始信号(拉低总线至少18ms)。
  2. DHT11(从机)回答:“在的,请讲。”-> DHT11响应,先拉低80us,再拉高80us。
  3. DHT11开始“汇报数据”-> 连续输出40位数据(0或1)。
  4. 汇报结束-> DHT11拉低总线54us后释放,主机需要把总线拉高。

接下来,我们就按照这个“对话”流程,在天空星开发板上把它实现出来。

2. 搭建工程与硬件连接

2.1 硬件连线

首先,把DHT11模块和天空星开发板连起来,非常简单,就三根线:

DHT11模块引脚连接到天空星STM32F407
VCC (引脚1)3.3V 电源引脚
DATA (引脚2)我们选择PB0(你也可以换成其他GPIO)
GND (引脚3)GND 引脚

提示:DHT11模块上通常已经集成了一个上拉电阻(一般是4.7K或10K),连接到DATA线。这保证了总线在空闲时能保持高电平。如果你的模块没有,需要在DATA线和3.3V之间外接一个4.7K的上拉电阻。

2.2 创建工程文件

在你的工程目录下(比如User文件夹),新建两个文件:

  • dht11.c– 驱动函数的实现
  • dht11.h– 引脚定义和函数声明

3. 编写驱动代码:一步步实现“对话”

现在进入核心部分,咱们在dht11.cdht11.h里把代码填上。我会把每一段代码是干什么的、为什么这么写,都讲清楚。

3.1 头文件定义 (dht11.h)

头文件主要是做宏定义和声明函数,方便我们修改引脚和调用函数。

#ifndef _BSP_DHT11_H_ #define _BSP_DHT11_H_ #include "stm32f4xx.h" /************** 引脚配置区 - 根据你的实际接线修改 ****************/ #define RCU_DHT11 RCC_AHB1Periph_GPIOB // 时钟,对应GPIOB #define PORT_DHT11 GPIOB // 端口,GPIOB #define GPIO_DHT11 GPIO_Pin_0 // 具体引脚,PB0 // 设置DHT11数据引脚输出高或低电平的快捷操作 #define DATA_GPIO_OUT(x) GPIO_WriteBit(PORT_DHT11, GPIO_DHT11, x ? Bit_SET : Bit_RESET) // 读取DHT11数据引脚电平状态的快捷操作 #define DATA_GPIO_IN GPIO_ReadInputDataBit(PORT_DHT11, GPIO_DHT11) // 声明全局变量,用于存储读取到的温湿度值 extern float temperature; extern float humidity; // 函数声明 void DHT11_GPIO_Init(void); // 初始化GPIO引脚 unsigned int DHT11_Read_Data(void); // 核心:读取一次完整的40位数据 float Get_temperature(void); // 获取处理后的温度值 float Get_humidity(void); // 获取处理后的湿度值 #endif

3.2 引脚初始化与模式切换

DHT11通信过程中,主机需要频繁切换引脚的输入输出模式。我们先写好初始化函数和两个模式切换函数。

dht11.c文件中:

#include "dht11.h" #include "board.h" // 包含你的延时函数头文件,如delay_ms, delay_us #include "stdio.h" // 如果需要打印调试信息 float temperature = 0; // 全局变量,存储温度 float humidity = 0; // 全局变量,存储湿度 /** * @brief DHT11 GPIO初始化 * @note 上电后只调用一次,将引脚初始化为输出模式并拉高。 */ void DHT11_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 1. 使能GPIOB的时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); // 2. 配置PB0为推挽输出,上拉,高速模式 GPIO_InitStructure.GPIO_Pin = GPIO_DHT11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; // 输出模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 速度可以快一些 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 内部上拉(与外部上拉双重保障) GPIO_Init(PORT_DHT11, &GPIO_InitStructure); // 3. 初始状态拉高总线,使其处于空闲状态 DATA_GPIO_OUT(1); } /** * @brief 将数据引脚设置为输出模式 * @note 用于主机发送开始信号和结束信号时。 */ void DHT11_GPIO_Mode_OUT(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_DHT11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(PORT_DHT11, &GPIO_InitStructure); } /** * @brief 将数据引脚设置为输入模式 * @note 用于主机接收DHT11的响应信号和数据时。 */ void DHT11_GPIO_Mode_IN(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_DHT11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; // 输入模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 保持上拉 GPIO_Init(PORT_DHT11, &GPIO_InitStructure); }

3.3 核心通信函数:DHT11_Read_Data

这是整个驱动的灵魂,它完整地实现了前面说的“四次握手”。我加了详细的注释,你跟着时序图看就明白了。

/** * @brief 读取DHT11的温湿度数据 * @retval 0: 读取或校验失败 * @retval 其他值: 读取成功的40位原始数据(校验字节已剥离) */ unsigned int DHT11_Read_Data(void) { int i; long long val = 0; // 用于拼接40位数据,用64位变量存储 int timeout = 0; float small_point = 0; unsigned char verify_num = 0; // ========== 第一步:主机发送开始信号 ========== DATA_GPIO_OUT(0); // 主机拉低总线 delay_ms(19); // 保持低电平至少18ms,这里给19ms留点余量 DATA_GPIO_OUT(1); // 主机释放总线(拉高) delay_us(20); // 拉高后等待20us,等待DHT11响应 // ========== 第二步:等待DHT11的响应信号 ========== DHT11_GPIO_Mode_IN(); // 主机切换为输入模式,准备“听”DHT11说话 // DHT11会先拉低总线80us左右作为应答 timeout = 5000; // 设置一个超时计数器,防止死等 while((!DATA_GPIO_IN) && (timeout > 0)) timeout--; if(timeout <= 0) return 0; // 超时,说明DHT11没响应 // 然后DHT11会拉高80us左右,准备发送数据 timeout = 5000; while(DATA_GPIO_IN && (timeout > 0)) timeout--; if(timeout <= 0) return 0; // 超时,响应信号异常 // ========== 第三步:接收40位数据 ========== // 关键点:如何区分数据0和数据1? // 两者都是先发54us低电平,然后: // - 数据0: 接着发 26-28us 高电平 // - 数据1: 接着发 70us 高电平 // 我们通过检测高电平的持续时间来区分。 #define CHECK_TIME 28 // 这是一个经验值,介于26-28us之间,用于判断 for(i = 0; i < 40; i++) // 循环接收40个bit { timeout = 5000; // 等待每个bit起始的54us低电平过去 while((!DATA_GPIO_IN) && (timeout > 0)) timeout--; if(timeout <= 0) return 0; // 低电平持续时间异常 // 关键判断:延时CHECK_TIME后,检测引脚电平 delay_us(CHECK_TIME); // 延时约28us // 此时,如果是数据0,高电平时间短,已经变回低电平 // 如果是数据1,高电平时间长,仍然保持高电平 if(DATA_GPIO_IN) // 还是高电平,说明是数据1 { val = (val << 1) + 1; // 左移一位,末尾加1 } else // 已经是低电平,说明是数据0 { val = val << 1; // 左移一位,末尾加0(默认就是0) } // 等待这个bit剩余的高电平时间过去,准备接收下一个bit timeout = 5000; while(DATA_GPIO_IN && (timeout > 0)) timeout--; // 这里不一定要判断超时,因为如果是数据0,本身已为低电平,循环直接跳过 } // ========== 第四步:结束通信,释放总线 ========== DHT11_GPIO_Mode_OUT(); // 主机切回输出模式 DATA_GPIO_OUT(1); // 拉高总线,回到空闲状态 // ========== 第五步:数据校验与解析 ========== // 数据格式:8bit湿度整数 + 8bit湿度小数 + 8bit温度整数 + 8bit温度小数 + 8bit校验和 // 校验和 = 前四个字节(32位)相加,取低8位 // 计算前32位数据的和(忽略进位,只取低8位效果) verify_num = (val >> 32) + ((val >> 24) & 0xFF) + ((val >> 16) & 0xFF) + ((val >> 8) & 0xFF); // 与接收到的第5个字节(校验和)比较 verify_num = verify_num - (val & 0xFF); if(verify_num != 0) // 校验和不相等 { // 校验失败,返回0 return 0; } else // 校验成功 { // 解析湿度数据 (前16位) humidity = (val >> 32) & 0xFF; // 湿度整数部分 small_point = ((val >> 24) & 0xFF) * 0.1; // 湿度小数部分(通常为0) humidity = humidity + small_point; // 解析温度数据 (中间16位) temperature = ((val >> 16) & 0xFF); // 温度整数部分 small_point = ((val >> 8) & 0xFF) * 0.1; // 温度小数部分 temperature = temperature + small_point; // 可以返回原始数据(去掉校验和的部分),方便其他处理 return (unsigned int)(val >> 8); } }

3.4 数据获取函数

这两个函数很简单,就是返回全局变量里存好的值。注意:必须先成功调用DHT11_Read_Data()后,这里返回的值才是最新的。

/** * @brief 获取最近一次读取的温度值 * @retval 温度值(单位:℃) */ float Get_temperature(void) { return temperature; } /** * @brief 获取最近一次读取的湿度值 * @retval 湿度值(单位:%RH) */ float Get_humidity(void) { return humidity; }

4. 在主函数中调用与测试

驱动写好了,最后一步就是在主函数里调用它,并打印出结果看看。这里假设你已经初始化好了串口(比如USART1)用于打印。

#include "board.h" #include "bsp_uart.h" // 你的串口初始化头文件 #include <stdio.h> #include "dht11.h" int main(void) { // 1. 系统初始化 board_init(); // 2. 串口初始化,用于打印数据 uart1_init(115200U); // 3. DHT11引脚初始化 DHT11_GPIO_Init(); delay_ms(1000); // 上电后给DHT11一点稳定时间 printf("DHT11 Demo Start\r\n"); while(1) { // 4. 读取数据 if(DHT11_Read_Data() != 0) // 如果读取成功 { // 5. 获取并打印温湿度 printf("Temperature = %.1f C\r\n", Get_temperature()); printf("Humidity = %.1f %%RH\r\n", Get_humidity()); } else { printf("DHT11 Read Error!\r\n"); } // 6. 每隔1秒读取一次 delay_ms(1000); } }

5. 调试心得与常见问题

按照上面的步骤做完,理论上串口就能打印出温湿度了。如果没数据或者数据不对,别急,我把我调试时遇到的几个坑总结一下:

  1. 时序是重中之重:DHT11对时序非常敏感。delay_us(20)delay_ms(19)CHECK_TIME这些延时值很关键。如果用的是SysTick或者定时器实现的微秒延时,一定要校准准确。可以用逻辑分析仪或者示波器抓一下波形,看看拉低拉高的时间对不对。

  2. CHECK_TIME这个值需要微调:代码里我设的是28。这个值是用来区分数据0和1的阈值。如果发现数据全是0或者全是1,可以尝试把这个值调小一点(比如25)或调大一点(比如30),找到最稳定的值。

  3. 上拉电阻必须要有:确保DATA线上有上拉电阻(模块自带或外接)。没有上拉电阻,总线无法在空闲时保持高电平,通信肯定会失败。

  4. 供电要稳定:DHT11虽然功耗低,但供电不稳也会导致读取失败。确保3.3V电源干净,如果走线长,可以在VCC和GND之间加一个0.1uF的滤波电容。

  5. 两次读取间隔:DHT11完成一次数据采集后,需要一点时间(手册建议至少1秒)才能进行下一次读取。所以主循环里的delay_ms(1000)不要删掉或改得太短。

  6. 初始化后先等待:系统上电后,DHT11_GPIO_Init()拉高了总线,但最好像示例那样等待1秒以上,让DHT11完全准备好。

代码移植过去后,如果串口打印出正常的温湿度值,恭喜你,DHT11驱动就成功跑起来了!你可以把这个驱动用到你的温室监控、智能家居或者数据记录项目里了。

http://www.jsqmd.com/news/477892/

相关文章:

  • 二叉树(精选答案)
  • 利用InternLM2-Chat-1.8B构建学术论文润色与语法检查工具
  • 【训练营】第1篇:基于ESP32-C3/ESP32的智能调光器硬件设计详解(兼容安信可模块)
  • 无锁编程与原子操作
  • 2026年建筑装饰项目必看:铝单板厂家选型指南与核心适配场景实测 - 品牌推荐
  • Qwen3-VL-8B聊天系统新手入门:快速搭建你的第一个AI助手
  • 单颗器件实现 550V 击穿电压和 0.8A 电流,并实现 200V/1A 开关操作
  • 2026年品牌营销策划公司选型指南:基于企业增长需求的三维适配地图 - 品牌推荐
  • NVIDIA Profile Inspector显卡性能优化实战指南:从参数调校到游戏体验升级的完整解决方案
  • 百度网盘直链解析终极方案:baidu-wangpan-parse颠覆式满速下载技术全解析
  • 文脉定序系统SolidWorks设计文档管理应用:零件库智能检索
  • use_jadx_open_it
  • 【PHP AI代码可信度白皮书】:基于17万行LLM生成代码的实测数据,揭示3类不可绕过的人工复核节点
  • PID控制算法实战:从原理到代码实现
  • StructBERT文本相似度模型在AI编程助手场景的应用:代码片段检索与推荐
  • 2026年钢筋网片厂家实力推荐:专业生产建筑钢筋网片、焊接钢筋网片、冷轧带肋钢筋网片,源头工厂直供,品质与口碑双重保障 - 品牌企业推荐师(官方)
  • Qwen2.5-32B-Instruct Python爬虫进阶:Scrapy框架集成
  • 【网络工程实战】三层交换机VLAN间路由配置与故障排查
  • web
  • 百度飞桨(PaddlePaddle)安装全攻略:从环境检查到成功验证
  • Phi-3-Mini-128K惊艳表现:处理含57个函数定义的Python项目并生成模块间调用图
  • STM32F103C8T6移植FreeRTOS内存优化实战指南
  • Phi-3-Mini-128K作品集:用产品PRD文档生成测试用例+边界值分析+自动化脚本框架
  • 写作小白救星 AI论文工具 千笔AI VS PaperRed,MBA专属高效利器!
  • Allegro17.4异形焊盘实战:从DXF导入到Padstack的完整流程
  • 【25考研】南开计算机复试:C/C++编程能力测试深度解析与实战指南
  • SIMetrix 8.30 电路仿真软件中利用.PARAM命令动态配置元器件参数值的技巧
  • 建筑领域三维点云数据处理的关键技术与实践应用
  • 泰山派开发板PCIE-WiFi上网实战:RTL8852BE模块驱动配置与网络连接(含完整镜像)
  • C#实现基于硬件信息的软件授权加密系统实战