【STM32】基于CubeMX与HAL库的DHT11温湿度传感器驱动开发实战

📅 发布时间:2026/7/7 20:34:55 👁️ 浏览次数:
【STM32】基于CubeMX与HAL库的DHT11温湿度传感器驱动开发实战
1. 从零开始认识你的硬件伙伴DHT11与开发环境搭建大家好我是老李一个在嵌入式领域摸爬滚打了十多年的工程师。今天咱们不聊那些高大上的理论就实实在在地聊聊怎么用STM32配合CubeMX和HAL库把那个小小的DHT11温湿度传感器给“驯服”让它乖乖地把数据吐出来。很多新手朋友一听到“驱动开发”、“时序逻辑”就头大觉得门槛太高。别怕我当初也是这么过来的踩过不少坑。今天我就用最“白话”的方式带你走一遍完整的流程保证你跟着做一遍就能点亮技能树。首先咱们得搞清楚手头的“家伙事儿”。DHT11一个非常经典的数字温湿度复合传感器。它便宜、够用对于大多数室内环境监测项目来说精度完全足够。它通过一根数据线单总线协议和你的单片机通信这就意味着你只需要占用一个GPIO口就能同时读取温度和湿度非常节省资源。我手头这个是四针的版本VCC GND DATA NC其中NC是空脚不用接。记住它的供电电压是3.3V到5.5V咱们的STM32开发板通常提供3.3V直接接上就行别接5V小心烧了。工欲善其事必先利其器。我们的软件三件套是STM32CubeMX、Keil MDK-ARM或者你喜欢的IDE比如IAR、STM32CubeIDE以及串口调试助手。STM32CubeMX是ST官方出的图形化配置工具简直是新手和老手的福音它能帮你自动生成芯片初始化代码配置时钟、外设引脚大大减少了我们手动写寄存器配置的时间也避免了低级错误。HAL库硬件抽象层是ST主推的库函数它的代码可读性比早期的标准库要好移植起来也更方便虽然执行效率上可能有一点点牺牲但对于我们这种应用来说完全不是问题。第一步打开CubeMX创建一个新工程。在芯片选择器里根据你的开发板型号找到对应的STM32芯片比如我用的是一块常见的F103C8T6核心板。点击“Start Project”。接下来就是最关键的引脚配置环节了。2. CubeMX图形化配置像搭积木一样配置你的单片机进入工程后你会看到一个芯片的引脚图界面。我们的目标很明确第一给DHT11的数据脚分配一个GPIO第二为了精准实现DHT11苛刻的微秒级延时我们需要配置一个基本定时器第三为了方便查看数据我们通常还会配置一个串口把数据打印到电脑上。### 2.1 配置GPIO引脚在芯片图上找到你想用的GPIO比如我习惯用PA5。点击这个引脚在弹出的功能列表里选择“GPIO_Output”。首先设置为输出是因为通信起始阶段需要单片机主动拉低电平。这里有个小细节在右侧的“GPIO”配置栏里我们可以设置这个引脚的模式。初始状态我们设为“Output Push Pull”推挽输出默认输出电平设为高电平。别小看这个初始高电平DHT11在空闲时要求总线由上拉电阻拉高我们初始化成高电平就模拟了这个状态。### 2.2 配置一个基本定时器TIMDHT11的通信时序对时间要求非常严格比如主机拉低总线必须至少18毫秒而响应信号的高电平持续时间是20-40微秒。用HAL_Delay()函数只能实现毫秒延时对于几十微秒的延时精度不够。所以我们需要一个硬件定时器来帮忙。在左侧的“Pinout Configuration”选项卡里找到“Timers”。以STM32F103C8T6为例它有好几个定时器。我们选一个基本定时器比如TIM1。点击它在“Mode”里将“Clock Source”选择为“Internal Clock”内部时钟。然后切换到“Configuration”选项卡下的“Parameter Settings”。这里我们需要设置定时器的计数频率。假设我们的系统主频是72MHz这是F103的常见频率。我们希望定时器计数一次是1微秒。那么预分频系数Prescaler可以设置为72-1。因为72MHz / 72 1MHz也就是说计数器每1微秒加1。然后设置计数周期Counter Period为最大值65535对于16位定时器。这样我们的定时器就能实现最长65535微秒的精确延时了。配置好后别忘了在“NVIC Settings”中勾选定时器的全局中断不过我们的延时函数采用查询方式不一定要开中断先勾上也没关系。### 2.3 配置串口用于数据打印为了能看到读取到的温湿度值配置一个串口非常必要。找到“Connectivity”里的USART1。选择模式为“Asynchronous”异步通信。波特率可以设为115200。引脚会自动分配为PA9TX和PA10RX。这样我们就能通过USB转TTL模块在电脑的串口助手上看到数据了。所有外设配置完毕点击右上角的“Project Manager”选项卡。给工程起个名字选择好工程存储路径。关键的一步来了“Toolchain / IDE”一定要选择你使用的IDE比如“MDK-ARM V5”。在“Code Generator”那里我强烈建议勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”这会把每个外设的代码单独生成文件结构更清晰。最后点击“GENERATE CODE”CubeMX就会自动生成一个完整的Keil工程了。3. 深入时序理解DHT11的“单总线语言”代码生成好了先别急着写驱动。咱们得花点时间搞懂DHT11到底是怎么“说话”的。这就像两个人电报通信必须遵守相同的密码本。DHT11的密码本就是它的单总线通信协议。整个通信过程分为三步主机发起信号、传感器响应、传感器发送40位数据。我用一个更生活的比喻假设单片机是老师DHT11是学生。老师想提问读数据。第一步老师大喊一声“起立”拉低总线至少18ms然后坐下等待拉高20-40us。学生听到后首先要回答“到”拉低总线80us作为响应然后表示“我准备好了”拉高80us。之后学生开始用摩斯电码0和1的电平组合回答40位数据这40位数据包含了湿度整数、湿度小数、温度整数、温度小数和校验和。那么0和1的摩斯电码怎么区分呢这由高电平的持续时间决定。每一位数据都以一个50us的低电平开始随后是一个高电平。如果这个高电平持续约26-28us就表示这是“0”如果持续约70us就表示这是“1”。我们的单片机就需要在这个高电平期间延时大约30-40微秒后去采样引脚电平。如果采样到高电平说明之前的高电平持续时间长是“1”如果采样到低电平说明高电平持续时间短已经结束了是“0”。听起来有点绕没关系你只需要记住核心我们的代码任务就是严格按照这个时间线去控制引脚输出并在精确的时刻去读取引脚输入。任何时间上的偏差都可能导致通信失败。这也是为什么我们必须用定时器来实现微秒延时而不是用空循环——空循环的时间受编译器优化和主频影响太大极不稳定。4. 手把手编写驱动从微秒延时函数到完整数据读取现在我们打开生成的Keil工程开始编写DHT11的驱动代码。我建议在“Src”和“Inc”文件夹下分别新建dht11.c和dht11.h文件这样代码结构清晰也方便以后移植到其他项目。### 4.1 构建精准的微秒延时函数首先我们需要一个可靠的微秒延时函数。CubeMX已经帮我们初始化好了TIM1我们可以利用它来计时。// dht11.c #include dht11.h #include tim.h // 包含定时器的头文件 void Delay_us(uint16_t us) { uint16_t start_tick, target_tick; // 获取当前计数器的值 start_tick __HAL_TIM_GET_COUNTER(htim1); // 计算目标计数值考虑到计数器是向上计数到ARR65535然后溢出 target_tick start_tick us; // 如果目标值超过了定时器的自动重载值需要处理溢出 if(target_tick start_tick) // 发生了溢出 { // 等待计数器溢出 while(__HAL_TIM_GET_COUNTER(htim1) start_tick); // 重新设定起点和终点从0开始算 start_tick 0; target_tick us; } // 等待计数器达到目标值 while(__HAL_TIM_GET_COUNTER(htim1) target_tick); }这个函数比简单粗暴的循环更精准。它先获取定时器当前的计数值然后加上需要延时的微秒数得到目标值最后原地等待直到定时器计数达到这个目标值。这里处理了计数器溢出的情况更加健壮。记得在dht11.h里声明这个函数。### 4.2 动态切换GPIO输入输出模式DHT11的单总线协议要求同一个引脚在通信过程中有时是输出单片机控制有时是输入读取传感器数据。HAL库提供了非常方便的函数来动态切换。void DHT11_SetPin_Output(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; // 推挽输出 GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; // 高速模式响应快 HAL_GPIO_Init(DHT11_PORT, GPIO_InitStruct); } void DHT11_SetPin_Input(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; // 浮空输入 GPIO_InitStruct.Pull GPIO_NOPULL; // DHT11模块通常自带上拉电阻这里可以不启用 HAL_GPIO_Init(DHT11_PORT, GPIO_InitStruct); }这里我用宏定义DHT11_PIN和DHT11_PORT来指代具体的引脚和端口比如#define DHT11_PIN GPIO_PIN_5#define DHT11_PORT GPIOA。这样以后换引脚只需要改宏定义非常方便。### 4.3 核心读取一位和一字-节数据这是驱动最核心的部分直接对应我们前面分析的时序逻辑。uint8_t DHT11_Read_Byte(void) { uint8_t i, data_byte 0; for(i0; i8; i) { data_byte 1; // 左移一位为下一位数据腾出位置 // 等待50us低电平起始信号结束 while(HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET); // 延时40us这个时间是区分数据0和1的关键 Delay_us(40); // 判断高电平持续时间 if(HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { data_byte | 1; // 读到的是1 // 等待高电平结束如果是1高电平还会持续一段时间 while(HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET); } // 如果40us后读到的是低电平说明是0data_byte不用或1保持0 } return data_byte; }这个函数循环8次读取一个字节。每次循环先等待数据位开始的低电平过去然后延时40微秒再去采样。如果采样到高电平说明传感器发送的是‘1’就将data_byte的对应位置1并等待这个高电平结束如果采样到低电平说明是‘0’就什么也不做。循环开始时的data_byte 1操作确保了先读到的位是字节的高位。### 4.4 整合完整的读取流程与校验最后我们把所有步骤串起来并加上数据校验。uint8_t DHT11_Read_Data(uint8_t *temp, uint8_t *humi) { uint8_t buf[5] {0}; uint8_t retry 0; uint8_t i; // 1. 主机发起开始信号 DHT11_SetPin_Output(); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); HAL_Delay(18); // 拉低至少18ms HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); Delay_us(20); // 拉高20-40us // 2. 切换为输入模式等待传感器响应 DHT11_SetPin_Input(); // 等待传感器拉低响应80us while(HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET retry 100) { Delay_us(1); retry; } if(retry 100) return 0; // 响应超时失败 retry 0; // 等待传感器拉高80us while(HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET retry 100) { Delay_us(1); retry; } if(retry 100) return 0; // 响应异常失败 // 3. 开始接收40位数据 for(i0; i5; i) { buf[i] DHT11_Read_Byte(); } // 4. 校验数据 if(buf[4] (buf[0] buf[1] buf[2] buf[3])) { *humi buf[0]; // 湿度整数部分 *temp buf[2]; // 温度整数部分 // 注意DHT11小数部分通常为0精度要求高可用DHT22 return 1; // 读取成功 } return 0; // 校验失败 }这个函数是给上层调用的接口。它完成了从启动信号到接收校验的全过程并将有效的温度和湿度整数部分通过指针参数返回。校验和是前四个字节相加与第五个字节比较这是确保数据在传输过程中没有出错的重要环节。5. 实战调试与常见问题排坑指南代码写完了编译下载到板子里。打开串口助手设置好波特率你可能会看到一堆乱码或者什么都没有。别慌调试是嵌入式开发的常态。我结合自己踩过的坑给你梳理几个最常见的“翻车点”。### 5.1 时序问题最棘手的“玄学”故障时序不准是DHT11驱动失败的头号原因。表现就是根本读不到数据或者读到的数据全是0或255。检查延时函数确保你的微秒延时函数是准确的。可以用这个函数翻转一个引脚然后用示波器测量高低电平的持续时间看是不是你设定的微秒数。如果没有示波器可以尝试稍微调整Delay_us(40)中的这个40值比如改成35或45有时候传感器个体有差异。检查起始信号确保主机拉低的时间足够长HAL_Delay(18)拉高的时间足够短Delay_us(20)。拉高后必须尽快切换到输入模式。响应等待超时代码里的retry循环就是超时判断。如果一直卡在等待响应的循环里说明传感器根本没反应。检查硬件连接VCC和GND是否接反数据线是否接触不良传感器是不是坏了还有一个容易被忽略的点数据引脚必须接一个4.7K到10K的上拉电阻到VCC。很多模块已经集成了这个电阻但如果你用的是单独的传感器元件务必自己加上### 5.2 数据校验失败读到了但不对有时候能进入数据读取阶段但最后校验和不通过函数返回0。电气干扰单总线对干扰比较敏感。如果连接线过长或者旁边有电机等大电流设备干扰可能导致数据位中某个0被读成1或者相反。尽量缩短连接线并远离干扰源。电源问题DHT11在启动和转换数据时需要一定的电流。如果电源线太细或电源带载能力不足可能导致电压瞬间被拉低传感器工作异常。确保供电稳定可以在VCC和GND之间并联一个100uF的电解电容进行滤波。读取间隔太短DHT11完成一次数据采集后需要一段时间手册上说是至少1秒才能进行下一次读取。如果你在循环里不停地调用读取函数间隔小于1秒它可能还没准备好就会返回错误数据。在主循环里每次读取后至少加一个HAL_Delay(2000)等两秒再读下一次。### 5.3 优化与进阶思考当你成功读取到稳定的数据后可以考虑做一些优化。增加重试机制在主函数调用DHT11_Read_Data时如果返回失败可以尝试重试几次比如3次只要有一次成功就采用。浮点数计算虽然DHT11小数位通常是0但代码结构上我们已经拿到了buf[1]和buf[3]。如果你想显示带一位小数的值可以这样计算float real_humi buf[0] buf[1]*0.1;。移植到其他平台我们这个驱动代码的核心是时序逻辑和HAL库的GPIO操作。如果你想移植到其他品牌的单片机或者使用标准库只需要替换掉HAL_GPIO_WritePin、HAL_GPIO_ReadPin、HAL_Delay和定时器延时函数这些硬件相关的部分通信逻辑是完全一样的。最后把读取到的温湿度通过串口打印出来吧。在main.c的循环里你可以这样写uint8_t temperature, humidity; if(DHT11_Read_Data(temperature, humidity)) { printf(Temperature: %d C, Humidity: %d %%\r\n, temperature, humidity); } else { printf(DHT11 Read Failed!\r\n); } HAL_Delay(2000); // 至少等待2秒再读下一次记得在main.c里包含dht11.h并实现printf的重定向到串口这需要重写_write或fputc函数网上教程很多。当你看到串口助手稳定地打印出房间的温湿度时这份成就感就是驱动我们不断探索的最大动力。嵌入式开发就是这样从点亮一个LED到驱动一个传感器每一步解决的都是实实在在的问题乐趣也就在其中。