STM32F042F6P6DHT11温湿度检测实战从硬件搭建到数据采集全流程解析最近在整理工作室的物料翻出来几片STM32F042F6P6的最小系统板和几个DHT11温湿度传感器。这让我想起几年前带学生做嵌入式入门项目时这个组合几乎是“标配”——成本低廉、资源够用、时序典型非常适合用来理解单片机如何与外部传感器“对话”。很多初学者在拿到硬件后往往卡在从原理图到稳定代码的最后一公里。这篇文章我就以这个经典组合为例带你走一遍完整的开发流程从硬件连线、时序分析到代码逐行调试最后通过串口把数据“可视化”出来。目标不是复现一个能用的程序而是让你彻底明白每一步背后的“为什么”下次遇到I2C、SPI等其他总线协议时也能举一反三。1. 硬件选型与最小系统搭建对于嵌入式开发新手来说第一步往往不是写代码而是搞清楚你手里的“积木”都是什么以及如何把它们正确地拼在一起。STM32F042F6P6是一款基于ARM Cortex-M0内核的微控制器它属于STM32F0系列主打高性价比和低功耗。这里的“最小系统”指的是能让这颗MCU跑起来的最基本电路通常包括电源、复位、时钟和程序下载接口。1.1 核心器件与连接要点我手头的这块最小系统板已经集成了3.3V稳压电路、复位按键、8MHz晶振以及一个用户LED连接在PB1引脚。你需要额外准备的是DHT11温湿度传感器模块市面上常见的是三引脚VCC, GND, DATA或四引脚多一个NC空脚的模块化产品自带板载上拉电阻对新手更友好。USB转TTL串口模块比如CH340G、CP2102等用于MCU与电脑通信打印调试信息。ST-LINK V2仿真器/下载器用于给STM32烧录程序和在线调试。杜邦线若干。连接时共地GND是首要原则。所有模块的GND引脚必须连接在一起形成一个共同的参考电位。具体连接关系如下表所示设备/接口STM32F042F6P6 引脚DHT11模块USB转TTL模块ST-LINK V2电源 (3.3V)3.3V (VDD)VCC(通常由USB自供电不接MCU电源)3.3V (给MCU供电)地 (GND)GNDGNDGNDGND数据线PA7 (可配置为GPIO)DATA--串口PA2 (USART2_TX)-RX-串口PA3 (USART2_RX)-TX-调试接口SWDIO (PA13)--SWDIO调试接口SWCLK (PA14)--SWCLK注意上表假设你使用USART2进行串口通信这是STM32F042F6P6最常用的串口之一。DHT11的数据线理论上可以连接任何具有中断和输入输出功能的GPIO引脚这里我们选择PA7。连接好硬件后先别急着写传感器代码。一个良好的习惯是先验证最小系统本身是否工作正常。最直观的方法就是点灯。1.2 验证最小系统从点灯开始点灯程序是嵌入式界的“Hello World”。它不仅能测试硬件还能帮你建立开发环境如Keil MDK、STM32CubeIDE等和下载流程。针对PB1上的LED我们需要理解其驱动方式。查看原理图如果LED阳极接3.3V阴极接PB1那么这就是一个“灌电流”接法当PB1输出低电平0V时电流从3.3V经LED流向PB1LED点亮输出高电平3.3V时两端无压差LED熄灭。下面是一个简单的LED测试程序核心使用标准外设库Standard Peripheral Library// led.h #ifndef __LED_H #define __LED_H #include stm32f0xx.h #define LED_PIN GPIO_Pin_1 #define LED_PORT GPIOB #define LED_RCC RCC_AHBPeriph_GPIOB #define LED_ON() GPIO_ResetBits(LED_PORT, LED_PIN) // 输出低电平灯亮 #define LED_OFF() GPIO_SetBits(LED_PORT, LED_PIN) // 输出高电平灯灭 void LED_Init(void); #endif// led.c #include led.h void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 1. 使能GPIOB时钟 RCC_AHBPeriphClockCmd(LED_RCC, ENABLE); // 2. 配置PB1为推挽输出模式 GPIO_InitStructure.GPIO_Pin LED_PIN; 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_NOPULL; // 不上拉也不下拉 GPIO_Init(LED_PORT, GPIO_InitStructure); // 3. 初始状态灯灭 LED_OFF(); }在主函数中调用LED_Init()然后在一个循环里让LED闪烁。如果LED能按照预期闪烁恭喜你最小系统、开发环境和下载流程都已就绪。这是所有后续工作的基石。2. 理解单总线协议DHT11的“语言”DHT11使用的是单总线1-Wire协议。顾名思义只用一根数据线DATA完成双向通信。这根线需要接一个上拉电阻通常4.7K~10KΩ模块上一般已集成。通信过程由主机MCU发起双方必须严格遵守既定的时序。2.1 通信时序深度拆解一次完整的DHT11数据读取分为三个主要阶段主机启动信号、从机响应信号和数据传输。理解每个阶段的时间要求至关重要因为DHT11对时序非常敏感。主机启动信号MCU将DATA引脚设置为输出模式并输出一个至少18ms的低电平。这个低电平告诉DHT11“我要开始读取数据了”。随后MCU拉高DATA线20-40us并立即将引脚切换为输入模式准备接收DHT11的回应。从机响应信号DHT11检测到主机拉高后会先拉低DATA线80us作为应答信号。接着DHT11会拉高DATA线80us表示即将开始发送数据。数据传输之后DHT11开始发送40位5字节数据。每一位数据都以一个50us的低电平开始。紧接着是一个高电平其持续时间决定了数据是0还是126-28us的高电平表示位‘0’。70us的高电平表示位‘1’。40位数据的格式为8位湿度整数 8位湿度小数 8位温度整数 8位温度小数 8位校验和。对于DHT11小数部分通常为0。提示校验和是前四个字节湿度整数、湿度小数、温度整数、温度小数相加后的低8位。如果(湿度整数湿度小数温度整数温度小数)的低8位等于接收到的校验和字节则数据有效。2.2 时序实现的代码策略在代码层面实现上述时序有两种主流方法延时等待法和外部中断法。对于初学者延时等待法更直观。延时等待法的核心挑战在于如何精准测量高电平的持续时间以区分0和1。我们不能用delay_ms这样粗粒度的函数。通常的做法是等待50us的起始低电平结束DATA线变高。延时一个略大于26-28us但小于70us的时间例如40us。再次读取DATA引脚电平如果此时为低电平说明高电平持续时间短该位是‘0’。如果此时仍为高电平说明高电平持续时间长该位是‘1’。等待该位的高电平周期结束DATA线变低准备读取下一位。这里的关键是一个高精度的微秒级延时函数delay_us()。在STM32中我们可以利用系统滴答定时器SysTick来实现。// systick.c - 基于SysTick的精确延时 #include stm32f0xx.h static uint8_t fac_us 0; // us延时倍乘数 static uint16_t fac_ms 0; // ms延时倍乘数 // 初始化延时函数SYSCLK单位为MHz void SysTick_Init(uint8_t SYSCLK) { // SysTick时钟源设置为HCLK/8 SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); fac_us SYSCLK / 8; // 系统时钟48MHz时fac_us6 fac_ms (uint16_t)fac_us * 1000; } // 延时nusnus的最大值约为0xFFFFFF*8/SYSCLK (对于24位LOAD寄存器) void delay_us(uint32_t nus) { uint32_t temp; SysTick-LOAD nus * fac_us; // 设置重装载值 SysTick-VAL 0x00; // 清空计数器 SysTick-CTRL | SysTick_CTRL_ENABLE_Msk; // 启动倒数 do { temp SysTick-CTRL; } while ((temp 0x01) !(temp (1 16))); // 等待时间到达CTRL的COUNTFLAG位被置1 SysTick-CTRL ~SysTick_CTRL_ENABLE_Msk; // 关闭计数器 SysTick-VAL 0X00; // 清空计数器可选 }有了精确的delay_us()我们就能在代码中“画出”DHT11要求的时序图了。3. 驱动开发从GPIO操作到数据解析理解了协议我们就可以动手编写驱动代码了。驱动层代码主要完成两件事一是精确控制GPIO引脚模拟通信时序二是将接收到的40位原始数据解析成有意义的温湿度值。3.1 GPIO的灵活配置DHT11的DATA引脚需要在输出模式主机发送启动信号和输入模式主机接收数据之间动态切换。我们需要封装两个函数。// dht11_gpio.c #include dht11_gpio.h #include stm32f0xx_gpio.h #include stm32f0xx_rcc.h // 将DATA引脚配置为推挽输出 void DHT11_GPIO_Output(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_AHBPeriphClockCmd(DHT11_RCC, ENABLE); GPIO_InitStruct.GPIO_Pin DHT11_PIN; GPIO_InitStruct.GPIO_Mode GPIO_Mode_OUT; GPIO_InitStruct.GPIO_OType GPIO_OType_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStruct.GPIO_PuPd GPIO_PuPd_NOPULL; GPIO_Init(DHT11_PORT, GPIO_InitStruct); } // 将DATA引脚配置为上拉输入非常重要 void DHT11_GPIO_Input(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_AHBPeriphClockCmd(DHT11_RCC, ENABLE); GPIO_InitStruct.GPIO_Pin DHT11_PIN; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IN; GPIO_InitStruct.GPIO_PuPd GPIO_PuPd_UP; // 启用内部上拉电阻 GPIO_Init(DHT11_PORT, GPIO_InitStruct); } // 读取DATA引脚电平 uint8_t DHT11_Read_Pin(void) { return GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN); }注意DHT11_GPIO_Input()中我们配置了上拉GPIO_PuPd_UP。这是因为在单总线协议中总线在空闲状态需要被上拉至高电平。如果配置为浮空输入GPIO_PuPd_NOPULL总线电平可能不稳定导致读取数据失败。3.2 核心数据读取函数实现结合时序分析和GPIO操作我们可以编写最核心的DHT11_Read_Data函数。这个函数将完成一次完整的通信和数据获取。// dht11.c #include dht11.h #include delay.h // 包含之前实现的delay_us函数 // 读取DHT11的40位数据 uint8_t DHT11_Read_Data(DHT11_Data *data) { uint8_t buf[5] {0}; uint8_t i, j; uint8_t byte 0; // ---------- 1. 主机发送启动信号 ---------- DHT11_GPIO_Output(); // 设置为输出模式 DHT11_DATA_OUT(0); // 拉低至少18ms delay_us(18000); // 延时18ms DHT11_DATA_OUT(1); // 拉高20-40us delay_us(30); // ---------- 2. 切换为输入等待从机响应 ---------- DHT11_GPIO_Input(); // 设置为上拉输入模式 // 等待DHT11拉低响应超时检测 i 0; while(DHT11_Read_Pin() 1) { delay_us(1); if(i 100) return DHT11_ERROR_NO_RESPONSE; // 超时无响应 } // 等待80us低电平结束 i 0; while(DHT11_Read_Pin() 0) { delay_us(1); if(i 100) return DHT11_ERROR_RESPONSE_TIMEOUT; } // 等待80us高电平结束 i 0; while(DHT11_Read_Pin() 1) { delay_us(1); if(i 100) return DHT11_ERROR_RESPONSE_TIMEOUT; } // ---------- 3. 开始接收40位数据 ---------- for(j0; j5; j) { // 循环5次读取5个字节 byte 0; for(i0; i8; i) { // 每个字节8位 // 等待50us低电平起始位结束 while(DHT11_Read_Pin() 0); // 简单等待可加超时 delay_us(40); // 延时40us后采样 byte 1; // 左移一位为新的数据位腾出位置 if(DHT11_Read_Pin() 1) { byte | 1; // 如果40us后仍是高电平则为1 } // 等待当前位的高电平周期结束 while(DHT11_Read_Pin() 1); // 简单等待可加超时 } buf[j] byte; // 存储一个字节 } // ---------- 4. 通信结束总线拉高 ---------- DHT11_GPIO_Output(); DHT11_DATA_OUT(1); // ---------- 5. 校验数据 ---------- if(buf[4] (uint8_t)(buf[0] buf[1] buf[2] buf[3])) { >// usart.c - 串口初始化及printf重定向 #include usart.h #include stdio.h // 初始化USART2 波特率9600 void USART2_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; USART_InitTypeDef USART_InitStruct; NVIC_InitTypeDef NVIC_InitStruct; // 1. 使能时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // 2. 配置PA2(TX)为复用推挽输出PA3(RX)为浮空输入 GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_1); // USART2_TX GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_1); // USART2_RX GPIO_InitStruct.GPIO_Pin GPIO_Pin_2; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF; GPIO_InitStruct.GPIO_OType GPIO_OType_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStruct.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_InitStruct.GPIO_Pin GPIO_Pin_3; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF; GPIO_InitStruct.GPIO_PuPd GPIO_PuPd_NOPULL; // RX浮空输入 GPIO_Init(GPIOA, GPIO_InitStruct); // 3. 配置USART2参数 USART_InitStruct.USART_BaudRate 9600; USART_InitStruct.USART_WordLength USART_WordLength_8b; USART_InitStruct.USART_StopBits USART_StopBits_1; USART_InitStruct.USART_Parity USART_Parity_No; USART_InitStruct.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStruct.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART2, USART_InitStruct); // 4. 使能USART2 USART_Cmd(USART2, ENABLE); } // 重定向printf到USART2 int fputc(int ch, FILE *f) { USART_SendData(USART2, (uint8_t)ch); while(USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); return ch; }在工程设置中需要勾选“Use MicroLIB”以支持标准的C库函数这样printf才能正常工作。4.2 主程序逻辑与数据呈现最后我们将所有模块整合到主函数中形成一个完整的应用。// main.c #include stm32f0xx.h #include delay.h #include led.h #include usart.h #include dht11.h #include stdio.h DHT11_Data dht11_data; int main(void) { uint8_t ret; // 系统初始化 SysTick_Init(48); // 系统时钟48MHz LED_Init(); USART2_Init(); DHT11_Init(); // DHT11初始化主要是GPIO初始化为高电平 printf(System Init OK.\r\n); printf(STM32F042 DHT11 Demo Start...\r\n\r\n); while(1) { ret DHT11_Read_Data(dht11_data); if(ret DHT11_OK) { printf(Temperature: %d.%d C\r\n, dht11_data.temp_int, dht11_data.temp_deci); printf(Humidity: %d.%d %%RH\r\n, dht11_data.humi_int, dht11_data.humi_deci); printf(Checksum: OK\r\n); } else if(ret DHT11_ERROR_NO_RESPONSE) { printf(Error: DHT11 No Response!\r\n); } else if(ret DHT11_ERROR_CHECKSUM) { printf(Error: Checksum Error!\r\n); } else { printf(Error: Unknown Error Code %d\r\n, ret); } printf(-------------------\r\n); LED_ON(); // 读取数据时LED亮作为指示 delay_ms(2000); // DHT11两次读取间隔需大于1秒 LED_OFF(); delay_ms(300); } }编译、下载程序到开发板。打开电脑上的串口调试助手如Putty、SecureCRT或单片机IDE自带的终端设置好对应的COM口和波特率9600。如果一切顺利你将每隔大约2秒看到一次温湿度数据的输出。常见问题与排查无任何输出检查串口线连接TX/RX是否接反、电源、地线以及代码中串口初始化是否正确。输出乱码确认波特率设置是否与代码中一致均为9600。一直显示“No Response”或校验错误检查DHT11的VCC和GND是否接对电压是否在3.3V-5V。重点检查DATA线的上拉电阻模块通常自带如果自己接传感器务必接上4.7K上拉电阻到VCC。检查delay_us函数的精度。可以尝试稍微调整delay_us(40)这个采样等待时间比如35us或45us因为不同批次的DHT11或不同的MCU主频可能导致时序略有偏差。确保在读取间隙delay_ms(2000)足够长DHT11需要至少1秒的休息时间。当串口调试助手上稳定地跳出温湿度数值时这个从硬件到软件、从信号到数据的完整闭环就真正跑通了。这个过程里最宝贵的不是最后那几行代码而是排查时序不对时示波器上的波形分析是校验失败时对数据字节的逐位比对。这些经验会让你在面对更复杂的SPI屏、I2C传感器时心里更有底。