
1. 为什么STM32F1到今天还值得花时间学如果你最近在电子论坛或者开源硬件社区里逛大概率会看到一个现象一边是各种高性能新芯片层出不穷另一边是大量工程师、学生、爱好者依然在拿STM32F1系列做项目。尤其是搭配DHT11温湿度传感器做环境监测这个组合几乎成了很多人入门嵌入式开发的第一套实战方案。我自己也是从这个组合开始真正理解单片机到底怎么用的所以这篇内容就围绕STM32F1系列和DHT11这个经典搭配把从选型、原理、接线、代码到踩坑排查的完整链路讲透。STM32F1系列是意法半导体基于ARM Cortex-M3内核推出的32位微控制器家族最经典的型号包括STM32F103C8T6、STM32F103RCT6、STM32F103ZET6等。它最大的特点是外设资源丰富、资料生态极其庞大、价格亲民而且开发工具链成熟。你几乎可以在任何一家电子元器件平台上买到它的最小系统板十几块钱就能起步。对于想从8位单片机进阶到32位开发的人来说F1系列是一个非常好的跳板——它不会像某些新系列那样让你在底层配置上花太多时间但又足够让你理解时钟树、中断优先级、DMA、外设寄存器这些核心概念。而DHT11温湿度传感器则是另一个极端便宜、简单、单总线通信、只有四个引脚。它精度一般温度±2℃、湿度±5%RH采样周期不低于1秒但对于学习单总线时序、GPIO方向切换、微秒级延时这些嵌入式基本功来说它是最好的教具之一。把STM32F1和DHT11放在一起你实际上是在练三件事第一理解32位MCU的GPIO如何灵活配置第二掌握单总线协议的时序控制第三学会用定时器或者系统滴答来做精确延时。这套组合适合谁如果你是电子类专业的学生正在做课程设计或者毕业设计如果你是转行做嵌入式开发的初学者想找一个能跑通完整流程的项目如果你是有经验的工程师想快速验证一个环境监测方案的原型——STM32F1加DHT11都能满足。它不追求高性能但能让你把基础打扎实。接下来我会从整体设计思路开始一步步拆到代码和调试细节尽量把每个“为什么这么做”讲清楚。2. 整体方案设计与核心思路拆解2.1 为什么选STM32F1而不是其他系列选型这件事很多人一上来就看主频、看Flash大小、看外设数量但实际做项目的时候真正决定体验的是生态和容错空间。STM32F1系列的主频通常是72MHzFlash从64KB到512KB不等SRAM从20KB到64KB。对于DHT11这种单总线传感器来说这个性能绰绰有余甚至可以说是大炮打蚊子。但为什么不用更便宜的8位机因为STM32F1的GPIO配置更灵活你可以通过寄存器或者标准外设库精确控制引脚模式而且在微秒级延时上72MHz的主频配合SysTick或者定时器能做出比8位机更稳定的时序。另一个关键原因是调试工具。STM32F1支持SWD调试你只需要两根线就能单步跟踪、看变量、设断点。相比之下很多8位机还在用串口打印调试效率差很多。再加上STM32CubeMX这类图形化配置工具初始化代码几乎可以一键生成省去了大量查手册配寄存器的时间。当然如果你用的是标准外设库或者直接操作寄存器也能更深入理解底层但CubeMXHAL库的组合对新手更友好。还有一点容易被忽略STM32F1的供电范围是2.0V到3.6V通常用3.3V。而DHT11的工作电压是3.3V到5.5V两者可以直接对接不需要额外的电平转换。如果你用5V的单片机DHT11的数据引脚输出是5V电平虽然很多STM32的IO口标称5V容忍但长期看还是3.3V对3.3V更稳妥。所以从电气兼容性上这套组合也是顺理成章的。2.2 DHT11的单总线协议到底是怎么回事DHT11用的是一种叫单总线的通信方式顾名思义数据和命令都走一根线。这根线在空闲时被上拉电阻拉高主机和从机通过拉低和释放总线来传递0和1。具体来说一次完整的通信流程是这样的主机先拉低总线至少18毫秒然后释放接着从机响应拉低80微秒再释放80微秒之后从机连续发出40位数据每一位都以50微秒的低电平开始然后高电平的持续时间决定是0还是1——26到28微秒表示070微秒左右表示1。这个协议看起来简单但对时序要求很严。如果你用STM32F1的HAL库延时函数比如HAL_Delay它的最小单位是毫秒根本不够用。所以必须用微秒级延时通常有两种做法一种是直接用SysTick定时器做忙等待另一种是用一个通用定时器配置成1微秒计数。我个人的习惯是用SysTick因为它在整个工程里都能用而且不占用额外外设。但要注意SysTick的中断优先级和系统时钟配置有关如果你在中断里调用延时可能会出问题。所以DHT11的读取最好放在主循环或者低优先级任务里避免在中断上下文中操作。还有一个细节DHT11的数据引脚是开漏输出还是推挽实际上DHT11的数据线是双向的主机需要先输出低电平然后切换成输入模式去读从机的响应。在STM32F1上你可以把GPIO配置成开漏输出加上拉电阻这样释放总线时靠外部上拉电阻拉高不需要频繁切换模式。但更常见的做法是输出时用推挽读的时候切成浮空输入或者上拉输入。两种方式都能用但开漏加外部上拉在总线冲突时更安全。我一般会在数据线和VCC之间接一个4.7kΩ到10kΩ的上拉电阻实测4.7kΩ在短线缆下波形最干净。2.3 系统框架与数据流向整个项目的框架其实很清晰STM32F1作为主机通过一个GPIO引脚与DHT11通信读取40位数据然后解析出湿度的整数部分、湿度的 decimal 部分、温度的整数部分、温度的小数部分最后一位是校验和。校验规则是前四个字节相加取低8位如果等于第五个字节说明数据有效。解析完成后你可以把数据通过串口打印到电脑或者显示在OLED屏幕上也可以上传到上位机做记录。数据流向是DHT11内部有一个电容式湿度传感元件和一个NTC测温元件它们输出的模拟信号经过内部ADC转换成数字量再由DHT11自己的小MCU处理成40位数据。STM32F1只负责发起通信和读取结果。这里要注意DHT11的采样周期不能低于1秒如果你读得太快它会返回上一次的数据或者直接不响应。所以主循环里最好加一个1秒以上的延时或者用定时器来触发读取。如果你打算做低功耗项目DHT11并不是一个省电的选择因为它每次测量都需要主机拉低总线至少18毫秒这段时间MCU不能休眠。但如果你只是做桌面环境监测功耗不是问题。另外DHT11的测量范围是温度0到50℃湿度20%到90%RH如果你需要更宽的范围或者更高精度可以考虑DHT22或者SHT30但那是另一个话题了。3. 核心细节解析与实操要点3.1 硬件连接与上拉电阻的选择先看接线。DHT11通常有三个引脚或者四个引脚三个引脚的是裸传感器四个引脚的是模块。模块上一般已经集成了上拉电阻和滤波电容接线更简单。以三引脚裸传感器为例VCC接3.3VGND接地DATA接STM32F1的某个GPIO比如PA0。如果你用的是四引脚模块其中一个引脚是NC不用接。上拉电阻的选择很关键。DHT11的数据线在空闲时必须保持高电平所以需要一个上拉电阻。阻值太小功耗大而且从机拉低时电流大阻值太大上升沿变缓高速通信时可能误判。DHT11的通信速率不高一位数据最长70微秒所以10kΩ通常没问题。但如果你用的线缆比较长比如超过20厘米建议用4.7kΩ这样上升沿更快。我实测过在面包板上用10kΩ读取成功率大概95%换成4.7kΩ后基本100%。另外电源引脚旁边最好加一个100nF的去耦电容减少电源噪声对传感器的影响。还有一个容易忽略的点STM32F1的GPIO在复位后默认是浮空输入如果你直接接DHT11在初始化之前数据线是浮空的可能导致DHT11误判。所以建议在GPIO初始化之后先输出高电平一段时间让总线稳定。如果你用的是开漏输出模式外部上拉电阻会自然把总线拉高但开漏模式下输出高电平实际上是释放总线所以写1就是高阻态。这一点在代码里要特别注意。3.2 微秒级延时的实现与精度校准前面提到DHT11的时序需要微秒级延时。在STM32F1上最常用的方法是利用SysTick定时器。SysTick是一个24位的递减计数器可以配置成1毫秒中断或者更短。但如果你用它做微秒延时通常的做法是先配置SysTick的时钟源为HCLK或者HCLK/8然后写一个忙等待函数读取当前的计数值循环直到经过指定的微秒数。具体来说假设系统时钟是72MHzSysTick的时钟源设为HCLK/8也就是9MHz那么每个计数周期是1/9微秒约0.111微秒。你要延时10微秒就需要计数90次。但SysTick是递减的所以你要先读取当前值然后计算目标值注意处理重装载的情况。这种方法的精度取决于中断是否关闭。如果在延时期间有中断发生计数可能会被干扰。所以更稳妥的做法是在微秒延时函数里暂时关闭全局中断延时结束后再打开。但关闭中断时间不能太长否则会影响其他外设。DHT11的最长延时是18毫秒如果你关闭中断18毫秒可能会丢失串口数据或者导致其他定时器不准。所以通常只在读取40位数据的那几毫秒里关闭中断发起起始信号的那18毫秒可以用普通延时。另一种方法是用一个通用定时器比如TIM2配置成1微秒计数然后轮询计数器。这种方法的优点是不依赖SysTick也不怕中断干扰但占用一个定时器资源。如果你项目里定时器够用我推荐这种方式。配置步骤是使能TIM2时钟设置预分频器为72-1假设72MHz自动重装载值为65535然后启动计数。延时函数就是读取当前计数值加上需要的微秒数然后等待计数器到达目标值。注意要处理计数器溢出的情况。不管用哪种方法都建议用示波器或者逻辑分析仪校准一下。我遇到过因为编译器优化导致延时不准的情况比如在延时循环里用了volatile变量但优化等级设成-O2后循环被优化掉了。所以延时函数里的变量一定要加volatile或者用汇编插入NOP指令。实测下来用SysTick加volatile变量在72MHz下延时误差可以控制在1微秒以内足够DHT11用了。3.3 GPIO模式切换的时机与陷阱DHT11的通信过程中GPIO需要在输出和输入之间切换。具体流程是主机先输出低电平至少18毫秒然后切换成输入模式等待从机的响应。从机响应后会连续发出40位数据主机一直保持输入模式读取。读取完成后主机再切换回输出模式输出高电平让总线回到空闲状态。这里有几个陷阱。第一切换模式的时候如果从输出直接切到输入而外部上拉电阻还没把总线拉高可能会读到低电平。所以切换之前最好先输出高电平等一小段时间再切换成输入。第二STM32F1的GPIO模式配置需要操作CRL和CRH寄存器如果你用HAL库调用HAL_GPIO_Init函数会重新配置整个端口可能影响同一端口的其他引脚。所以建议把DHT11的数据引脚单独放在一个端口上或者用寄存器操作只修改那一个引脚的配置位。第三输入模式最好用上拉输入这样即使外部上拉电阻没焊内部上拉也能保证空闲时是高电平。但内部上拉电阻比较大大概40kΩ左右上升沿会比较慢所以外部上拉电阻还是必要的。还有一个细节在读取40位数据时每一位的开始都是50微秒的低电平然后是高电平。你需要先等待低电平结束然后测量高电平的持续时间。如果高电平持续26到28微秒就是0如果持续70微秒左右就是1。但实际测量时由于中断和代码执行时间可能会有几微秒的误差。所以判断阈值通常设在40微秒左右高电平时间大于40微秒算1小于算0。这个阈值不是固定的你可以根据实测调整。我一般会在读取函数里加一个超时机制如果等待高电平的时间超过100微秒就认为通信失败直接返回错误。4. 实操过程与核心环节实现4.1 工程搭建与CubeMX配置我习惯用STM32CubeMX来生成初始化代码因为这样可以少查手册而且时钟树配置直观。具体步骤是打开CubeMX选择STM32F103C8T6然后配置系统时钟。在RCC里把HSE设为Crystal/Ceramic Resonator然后在Clock Configuration里把HCLK设为72MHz。接着配置一个GPIO引脚比如PA0先设为GPIO_Output初始电平为高。然后再配置一个串口比如USART1波特率115200用于打印数据。最后在Project Manager里选择MDK-ARM或者STM32CubeIDE生成代码。生成代码后你需要手动添加DHT11的驱动文件。我一般会建一个dht11.c和dht11.h把延时函数、GPIO模式切换函数、读取函数都放在里面。延时函数可以用SysTick但CubeMX生成的代码里已经初始化了SysTick为1毫秒中断所以你不能直接改SysTick的配置否则会影响HAL_Delay。这时候有两个选择一是用HAL库的HAL_GetTick和忙等待实现微秒延时但精度不够二是用另一个定时器比如TIM3专门做微秒延时。我推荐第二种因为不影响系统滴答。配置TIM3的步骤是在CubeMX里使能TIM3时钟源选内部时钟预分频器设为72-1计数器周期设为65535然后生成代码。在代码里启动TIM3HAL_TIM_Base_Start(htim3)。然后写一个延时函数void delay_us(uint16_t us) { uint16_t start __HAL_TIM_GET_COUNTER(htim3); while ((__HAL_TIM_GET_COUNTER(htim3) - start) us); }注意这里要用无符号16位减法这样即使计数器溢出也能正确计算。但如果你延时超过65535微秒这个函数就不适用了。DHT11的最长延时是18毫秒也就是18000微秒在范围内。4.2 DHT11读取函数的完整实现读取函数是整个项目的核心。我把它分成几个步骤发起起始信号、等待响应、读取40位数据、校验、解析。先看发起起始信号void DHT11_Start(void) { DHT11_SetOutput(); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); HAL_Delay(20); // 至少18ms HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); delay_us(30); DHT11_SetInput(); }这里用HAL_Delay(20)是因为18毫秒用微秒延时也行但HAL_Delay更简单。注意拉低之后要先输出高电平再切换成输入这样总线能快速回到高电平。切换成输入后从机会在80微秒内拉低总线然后释放。所以接下来要等待从机的响应uint8_t DHT11_WaitResponse(void) { uint32_t timeout 0; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { if (timeout 10000) return 0; } timeout 0; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET) { if (timeout 10000) return 0; } timeout 0; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { if (timeout 10000) return 0; } return 1; }这段代码依次等待从机拉低、释放、再拉低。如果超时说明传感器没响应。超时阈值10000是根据循环执行时间估算的实际调试时可以调整。读取40位数据的函数uint8_t DHT11_ReadByte(void) { uint8_t byte 0; for (int i 0; i 8; i) { while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET); delay_us(40); if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { byte | (1 (7 - i)); while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET); } } return byte; }这里先等待50微秒的低电平结束然后延时40微秒再读引脚。如果还是高电平说明这一位是1否则是0。读完1之后要等待高电平结束准备下一位。这个逻辑简单但有效实测在72MHz下很稳定。读取完40位后进行校验uint8_t DHT11_ReadData(uint8_t *temp, uint8_t *humi) { uint8_t buf[5]; DHT11_Start(); if (!DHT11_WaitResponse()) return 0; for (int i 0; i 5; i) { buf[i] DHT11_ReadByte(); } if (buf[4] (buf[0] buf[1] buf[2] buf[3])) { *humi buf[0]; *temp buf[2]; return 1; } return 0; }注意DHT11的湿度小数部分和温度小数部分通常都是0所以只取整数部分就够了。如果你需要小数可以取buf[1]和buf[3]但DHT11的分辨率是1小数部分没有实际意义。4.3 主循环与串口输出主循环里每隔2秒读取一次然后通过串口打印int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); MX_TIM3_Init(); HAL_TIM_Base_Start(htim3); uint8_t temp, humi; char msg[64]; while (1) { if (DHT11_ReadData(temp, humi)) { sprintf(msg, Temp: %d C, Humi: %d %%\r\n, temp, humi); } else { sprintf(msg, DHT11 read error\r\n); } HAL_UART_Transmit(huart1, (uint8_t*)msg, strlen(msg), 1000); HAL_Delay(2000); } }这里用HAL_Delay(2000)保证采样周期大于1秒。如果你用RTOS可以把读取任务放在一个独立的线程里用osDelay。串口输出方便你在电脑上用串口助手查看数据。如果你没有USB转串口模块也可以用ST-Link的虚拟串口但需要额外配置。5. 常见问题与排查技巧实录5.1 读取失败的原因分析与排查表DHT11读取失败是新手最常遇到的问题。我整理了一个排查表按概率从高到低排列现象可能原因排查方法解决方法一直返回错误接线错误检查VCC、GND、DATA是否接对重新接线确保VCC是3.3V偶尔成功上拉电阻太大用示波器看数据线波形换成4.7kΩ上拉电阻完全无响应延时不准用逻辑分析仪抓时序校准微秒延时关闭中断数据校验失败电源噪声测量VCC纹波加100nF和10uF电容读取间隔太短采样周期不足检查主循环延时确保间隔大于1秒温度湿度不变传感器损坏换一个DHT11测试更换传感器我遇到过最诡异的一次是读取函数在调试模式下正常但全速运行就失败。后来发现是编译器优化把延时循环优化掉了。解决方法是在延时函数里加volatile变量或者把优化等级降到-O0。但-O0会影响整体性能所以更好的做法是用定时器做延时不依赖循环。5.2 时序调试的实战经验如果你有逻辑分析仪调试DHT11会轻松很多。把探头接在数据线上触发方式设为下降沿然后看波形。正常的起始信号是主机拉低18毫秒然后释放从机拉低80微秒释放80微秒然后开始40位数据。每一位的低电平都是50微秒高电平26到28微秒是070微秒是1。如果你看到高电平时间不对比如都是50微秒那可能是延时函数不准。如果看到从机根本没响应那可能是主机拉低时间不够或者上拉电阻没接。没有逻辑分析仪怎么办可以用串口打印调试信息。比如在等待响应的每个循环里打印一个字符看卡在哪一步。但串口打印本身会影响时序所以只能用来判断大致位置不能用来精确测量。另一个办法是用示波器的单次触发功能虽然不如逻辑分析仪方便但也能看个大概。还有一个经验DHT11对电源很敏感。如果你用USB供电电脑的USB口噪声比较大可能导致读取失败。我试过用电池供电成功率明显提高。所以如果你一直调不通可以试试换一个干净的电源或者在VCC和GND之间并一个100uF的电解电容。5.3 从DHT11进阶到其他传感器的思路DHT11玩熟了之后你可能会觉得它精度不够、响应慢。这时候可以看看DHT22它的协议和DHT11类似但精度更高测量范围更宽价格也贵不了多少。DHT22的数据格式是40位但湿度和温度各占16位有小数部分。读取代码只需要稍微改一下解析部分。如果你想要更专业的方案可以看SHT30或者SHT31它们用I2C接口精度更高而且有CRC校验。从单总线转到I2C你需要重新配置STM32F1的I2C外设但CubeMX可以帮你生成大部分代码。I2C的调试比单总线简单因为有时钟线同步不容易出现时序问题。再往上你可以把数据上传到云端或者本地服务器。STM32F1加上ESP8266或者ESP32做WiFi透传就能把温湿度数据发到MQTT服务器。这时候DHT11的精度可能不够看但作为学习链路从传感器到MCU到通信模块到上位机整个流程跑通一遍收获会很大。我个人在实际操作中的体会是DHT11最大的价值不是它的精度而是它逼着你去理解时序、去调延时、去看波形。这些东西在以后做更复杂的项目时都会用到。比如你以后用SPI或者I2C虽然硬件外设帮你处理了大部分时序但遇到通信失败时你还是得回到波形上去找原因。所以花时间把DHT11调稳绝对不亏。最后再分享一个小技巧如果你在读取DHT11的时候经常失败可以在读取函数外面包一层重试机制。比如连续读三次只要有一次成功就返回成功。这样能显著提高稳定性尤其是在电源不太干净的环境里。但重试之间要加至少1秒的间隔否则DHT11来不及完成下一次测量。这个技巧我在好几个项目里都用过实测有效。