刚开始接触嵌入式的人问我的第一句话,十有八九是“我应该从哪块板子开始”,我的答案这三四年一直没变过:先上STM32F1系列。并不是说它性能有多猛、功能有多新,而是它实在太适合作为“第一块认真学的单片机”了。基于Cortex-M3内核、主频最高72MHz、外设覆盖面广、价格便宜到可以放心折腾,再加上从入门教程到竞赛代码到量产项目,全网资料多到你想避都避不开。而最近被反复提起的DHT11温湿度传感器,恰好是F1系列单片机上最经典的实战组合——用它来理解时序、GPIO、超时保护这些概念,几乎是嵌入式入门最顺的一条路。这篇文章我就从F1系列本身开始讲,配合DHT11这个具体场景,把选型、建工程、写驱动、排故障这些环节完整走一遍。
1. 为什么STM32F1系列依然值得上手:先搞懂它强在哪
很多人会有个疑问:现在F4、H7都这么便宜了,G0、L4这些新系列也一大堆,为什么还要回头学一个十几年前的F1?这个问题其实挺关键,想清楚它,你后面学任何单片机都不会跑偏。
1.1 Cortex-M3带来的硬实力
STM32F1系列最核心的型号F103,内核是基于ARM Cortex-M3架构,最高工作频率72MHz。这个规格放到今天看当然不算夸张,但对一个入门者或者大多数中小型嵌入式项目来说,性能是绰绰有余的。Cortex-M3是ARM在M0/M0+之上的经典内核,支持Thumb-2指令集、单周期乘法、硬件除法指令,处理逻辑运算和数值计算时效率很高。相比老一代8051或者其他8位机,F103在处理复杂协议栈、浮点运算辅助、任务调度这些场景时,完全是两个维度的体验。
硬件上还有很多让开发变舒服的设计:比如NVIC中断控制器,每个外设都能用中断方式工作,不必一直轮询浪费CPU;比如位带操作,可以对一个bit单独做置位和清零,控制GPIO时特别直观;再比如DMA,可以把串口、SPI、ADC的数据搬运工作交给硬件,CPU只需要处理结果。这些能力放到入门阶段虽然不会全用到,但学会了它们,以后换F4、H7等更高端的系列,会发现思路是通用的,无非是外设更多、频率更高、多了一些新特性。
F1系列的供电范围通常是2.0V到3.6V,内部集成RC振荡器、PLL锁相环、多个定时器、多个USART、I2C、SPI、ADC等常用外设。也就是说,做一套带屏幕显示、按键输入、传感器采集、串口通信、PWM电机控制的小项目,一块F103芯片就能全包,省去了很多外部扩展芯片的麻烦。
1.2 什么项目适合F1:选它之前先过一遍这几点
不是说所有项目都适合用F1,每个人在立项时应先做个快速评估。我自己总结了几条判断标准:
- 项目是否需要丰富的通信接口。比如同时要用到两路串口、一路SPI给屏幕、一路I2C接传感器,F103的多个USART和硬件SPI/I2C可以直接应付。
- 是否需要复杂的浮点运算或DSP。如果要做音频滤波、大量浮点矩阵运算,F1的Cortex-M3没有硬件FPU,纯软件算会慢不少,这时候上F4或H7更合适。普通的温湿度读取、ADC采样、PID控制,F1完全没问题。
- 成本和供货稳定性。F1系列出货量巨大,市面上兼容品和国产替代型号很多,成本很低,学生党、小批量产品非常愿意选它。
- 资料和学习曲线。这一条其实很关键。对新手来说,F1的教程量、例程数量是其他系列无法比的。遇到问题一搜就有答案,这个隐性价值远比性能参数重要。
如果你只是做一个小型传感器数据采集节点、智能家居控制器、教学实验板、竞赛小车控制板,F1系列都是很稳的选择。如果项目要跑复杂的RTOS图形界面、做海量数据运算,那F1会捉襟见肘,也就不用勉强了。
1.3 F1与新一代M0/M4的取舍
这里我多说两句选型上的取舍,因为太多人卡在这一步。F1系列虽然老,但它的定位是“通用增强型”,比M0系列多出来的不只是主频,更重要的是外设数量:更多定时器、更多USART、更多GPIO、更完整的中断系统。在一些场合你可能会想,G0系列或者L4系列能不能替代F1?当然能,但它们各自的侧重点不同。G0系列主打低成本低功耗,外设裁剪得更狠;L4系列在低功耗上有优势但价格更高,适合电池供电产品。F1最典型的优势就是“均衡”:性能比M0强,外设比M0全,价格比L4便宜,加上大量现成代码,导致它始终是很多产品的“保底选择”。
很多年前我在一个项目里需要用STM32采集多路模拟量并做串口协议上报,团队里有人坚持用最新系列,有人坚持用F103C8T6。最后评估下来还是选了F103:因为项目对功耗不敏感,对成本敏感,需要三路串口和几十路普通IO,F103的库存和参考设计一抓一大把,开发周期能缩短一半。这件事给我的启发是:芯片选型不是选参数最漂亮的,而是选最合适的。
2. 型号命名、封装与选型:拿到芯片先别急着写代码
STM32F1系列内部也不是铁板一块,看型号看得懂,才能找到最适合自己那块。曾经有个学员拿了一块板子兴冲冲来问“是不是F103都能跑同一个程序”,结果板子上的丝印是STM32F103RCT6,他自己买的板子是STM32F103C8T6,程序烧进去直接卡死——当然不完全是因为Flash大小,但型号差异确实会影响选择启动文件、链接脚本和外设配置。
2.1 看懂STM32F103C8T6这一串字符
以最常见的STM32F103C8T6为例,拆开来看:
- STM32:系列品牌前缀,表示ST公司基于ARM内核的32位MCU。
- F1:其中1表示产品子系列,F1是最通用的增强型系列。
- 103:代表增强型,外设配置较为完整。同族还有101(基本型)、102(USB基本型)、105/107(带USB OTG和CAN)等。
- C:引脚数,C是48引脚,R是64引脚,V是100引脚,Z是144引脚。
- 8:Flash容量,8表示64KB,C表示256KB,E表示512KB。
- T:封装形式,T是LQFP,H是BGA,U是UFQFPN等。
- 6:温度等级,6表示-40到85°C,7表示-40到105°C。
所以“C8T6”翻译过来就是48引脚、64KB Flash、LQFP封装的工业级芯片。市面上很多经典的“最小系统板”和“Blue Pill”就是这颗芯片,便宜、能跑基本外设、引脚数量对大多数传感器和屏幕来说也够用。如果想要更大Flash和更多引脚,常见选择就是RCT6(64引脚/256KB)和ZET6(144引脚/512KB),很多开发板厂家用ZET6来做带屏幕、带网络、带各种扩展接口的综合实验板。
2.2 不同封装下的管脚分配与复用陷阱
选封装时一定要看的不仅是“够不够用”,还要看管脚冲突。F1系列不少引脚是多复用功能引脚,比如PA9/PA10默认就是USART1_TX/RX,PA2/PA3可以用来做USART2或ADC等。用STM32CubeMX或者直接查数据手册的“Alternate function mapping”表格,能看出每个引脚能复用成哪些外设,这是写代码前必须做的一件事。
更常见的坑是调试接口占用。F103的PA13、PA14、PA15、PB3、PB4这几个引脚默认是SWD和JTAG调试接口。如果你初始化时不注意,把PA15当普通GPIO用,直接配置成输出,会导致调试器连接不稳定,甚至每次烧录都要按复位键碰运气。正确做法是,如果需要用到这几个引脚,应该在初始化GPIO时钟前或初始化时关闭JTAG,只保留SWD模式,使用复用重映射功能,然后才能当普通IO使用。我见过很多人在这一步卡了整整一天,最后发现就是PA15一直在跟调试器抢控制权。
另外不同封装的电源脚、地脚和晶振脚位置也不一样,画PCB或接线时一定要对照具体封装的数据手册。有人用STM32F103C8T6做最小系统,直接把8MHz晶振接在OSC_IN/OSC_OUT上,又发现PC14/PC15也被占用——这就需要看“OSC32_IN”和“OSC32_OUT”与普通IO的重映射关系,避免设计冲突。
2.3 按项目定位选型速查
我整理了一个常用的选型参考表,基本覆盖F1系列里最常见的几个核心型号:
| 型号 | 引脚数 | Flash | RAM | 典型应用方向 |
|---|---|---|---|---|
| STM32F103C4T6 | 48 | 16KB | 6KB | 极小节点、简单传感器采集 |
| STM32F103C8T6 | 48 | 64KB | 20KB | 入门学习、小型控制器、DHT11/OLED等小项目 |
| STM32F103RBT6 | 64 | 128KB | 20KB | 需要较多IO的中型项目 |
| STM32F103RCT6 | 64 | 256KB | 48KB | 带屏幕、协议栈、稍复杂逻辑的项目 |
| STM32F103ZET6 | 144 | 512KB | 64KB | 综合实验平台、完整产品原型 |
RAM和Flash这两个指标是很容易被忽略的。现在有些人习惯什么都用HAL库,一个工程编译下来基础代码就占几十KB Flash,如果选了C4T6的16KB,工程还没写几行就要砍资源了。所以选型号之前最好先估算一下:有没有屏幕字库?有没有协议栈?是不是要跑RTOS?有没有大数组?这些决定Flash和RAM够不够用。入门阶段直接选C8T6起步,64KB Flash和20KB RAM做温湿度计、OLED显示、按键交互这类项目绰绰有余。
3. 工程搭建与最小系统:绕过F1入门三道坎
选好了芯片型号,接下来就是建开发环境、搭工程、把程序烧进去。这个阶段看起来简单,其实暗藏不少坑。有一段经典经历:我帮一个朋友调他的F103板子,他用的开发板商家给了别人的例程,烧进去LED不亮,他以为是板子坏了,最后发现是工程里包含的芯片型号选错了,启动文件用的启动文件不匹配,导致SystemInit阶段就进死循环。这种问题对新手来说非常隐蔽。
3.1 标准外设库还是HAL库:别纠结,按目的选
很多新手的第一反应是:现在STM32不是推荐用STM32CubeMX+HAL库吗?为什么还要学标准外设库?这里我给出明确看法。做产品开发和快速原型,用CubeMX+HAL确实效率高,图形化配置时钟树、引脚、外设参数,生成初始化代码,尤其对F4/H7这些外设复杂的系列非常友好。但放在F1入门阶段,尤其是为了理解GPIO、时钟、定时器这些底层机制时,标准外设库更合适。
原因很简单,F1的资料、例程、老项目绝大多数都是基于标准库写的。你网上搜“DHT11 STM32F103”,出来的代码十有八九是标准库风格,GPIO_InitTypeDef、RCC_APB2PeriphClockCmd这种API,直接看着库函数就能猜到每一步在做什么。HAL库在一些底层操作上封装更深,寄存器操作容易被藏起来,一旦遇到运行时问题,新手很难定位到底是在配置哪一步出了问题。
我个人的建议是一条“先标准库、后HAL”的路径:先用标准库把GPIO翻转、串口发送、定时器中断这几个基本功啃明白,理解了寄存器层面发生了什么,再切到CubeMX+HAL做项目开发,这时你会觉得HAL库非常像“自动生成的代码模板”,出问题也能顺着HAL库的调用关系追到是哪一步配置出的错。
3.2 最小系统:时钟、复位、电源与BOOT
F103的最小系统其实并不复杂,但每一处都有讲究。电源部分:3.3V供电,每个电源引脚就近加一个100nF去耦电容,VDD和VSS之间的电容布局直接影响高频率运行时稳定性。复位电路:NRST引脚接到10kΩ电阻到3.3V,再接一个100nF电容到GND,用来滤波,保证上电复位信号干净。启动方式:BOOT0和BOOT1引脚的电平组合决定芯片从Flash启动、SRAM启动还是系统存储器(内置Bootloader),正常运行时BOOT0要拉低,从Flash启动。如果要通过串口ISP下载程序,可以把BOOT0拉高,配合复位操作,进入系统存储器里的Bootloader。
时钟部分是最容易让人困惑的。F103内部有一个8MHz的HSI内部RC振荡器,精度一般,通常我们更愿意用外部8MHz晶振配合PLL倍频到72MHz主频。系统启动时,启动文件会调用SystemInit,完成从默认内部时钟切换到用户配置的外部晶振+PLL。如果你的外部晶振没焊好、负载电容不对或者晶振起振失败,程序就会卡在SystemInit的等待超时循环里,表现就是LED不闪、调试器能连上但跑不起来。
一个常见入门坑就是:外部晶振电路没做对,但代码里配置成了HSE作为PLL源。解决办法有两个,一是检查晶振电路,二是暂时把系统时钟配置改成用HSI,同时把PLL倍频系数改成和内部时钟匹配,保证芯片能跑起来后再排查硬件。实操中我见过有人直接用内部HSI把主频配到64MHz,也能正常工作,只不过串口波特率会受内部RC精度影响,要求不高时也不是不能用。
3.3 烧录的三种方式和各自坑点
F103可以用三种常用方式烧录:SWD调试接口、串口ISP、ST-Link的JTAG模式。SWD是现在的首选,只需要SWDIO、SWCLK、GND、3.3V四根线,配合ST-Link或者J-Link就能下载和在线调试。注意线长不要超过20cm,否则时钟信号容易受干扰,出现烧录时断连。如果遇到连接失败或烧录不稳定,可以试试在调试工具里选择“Connect under Reset”模式,在下电复位瞬间抢占调试接口,能解决不少引脚复用引起的连接问题。
串口ISP方式不需要调试器,只需要一个USB转TTL模块连接到USART1的PA9/PA10,然后BOOT0拉高,按一下复位,芯片就会进入内置Bootloader,用配套软件下载固件。这个方式很适合批量烧录或者手头没有ST-Link的场景,但要注意串口电平必须是3.3V的,不能直接拿5V的USB转串口模块去接,否则时间长了可能烧坏芯片的RX引脚。很多便宜的USB转TTL模块可以跳线切换3.3V和5V电平,实测前要确认一下。
还有一种很隐蔽的坑是,买到的所谓“最小系统板”上的复位电路故意省掉了外部复位按键,只靠上电复位。这种情况下,如果程序进入死循环或者关闭了看门狗,复位操作只能靠断电重新上电,调起程序来很痛苦。我的建议是动手搭最小系统时,复位按键和BOOT拨码开关一定要预留,后面调试时你会感谢自己当初留下了这两个设计。
4. 用DHT11来一次硬核实战:时序驱动的完整实现
接下来进入最容易让新手抓狂、也最有含金量的部分:把DHT11接到F103上,并且正确读出温湿度数据。之所以拿DHT11做例子,是因为它几乎占全了嵌入式开发里最基础也最关键的几个概念:单总线通信、微秒级延时、时序判断、超时保护。很多新手拿到这个模块,照着网上的代码抄一遍也能出数据,但一旦把传感器换一个牌子或者换一个GPIO口就读不出来了,原因就是没搞懂协议本身。
4.1 为什么DHT11最适合作为F1的第一个传感器
DHT11是一个数字温湿度传感器,单总线协议,供电范围3.3V到5.5V,测量范围是20%到90%RH湿度,0到50°C温度,精度约为±5%RH和±2°C。单看参数,DHT11并不以精度见长,比它精度高得多的还有DHT22、SHT30等,但它好就好在协议简单、成本便宜、逻辑直观,而且几乎没有现成库可用,必须自己写时序。对学习来说,这就是金子。
DHT11的“单总线”意思是数据线只有一根,既能输出数据又能接收主机控制信号。F103的GPIO通过切换输入输出模式,可以模拟这根总线的时序。整个过程不依赖I2C、SPI之类的现成外设,完全靠代码控制引脚高低电平和采样时机。这就能逼着你去理解“拉低多少微秒、释放总线、等响应信号、按时间窗口读每一位”这些底层细节。学会了DHT11,以后碰DS18B20、DHT22这类单总线器件,思路几乎可以平移过去。
4.2 单总线时序细读:起始信号、响应与40bit数据
先把这个协议的完整时序掰开揉碎。
第一步是主机发起通信。F103把数据引脚配置为输出,先拉低至少18毫秒,这个低电平时间必须足够长,让DHT11检测到总线被占用,然后释放总线(拉高),延时20到40微秒。这时DHT11会主动把总线拉低,大约80微秒,作为响应信号,随后再拉高大约80微秒,表示“我准备好了,开始传数据”。
第二步是数据帧。DHT11一次传输40个bit,顺序是:8bit湿度整数、8bit湿度小数、8bit温度整数、8bit温度小数、8bit校验和。注意,F1入门时经常有人把顺序弄混,以为先来的是温度整数,实际是先湿度后温度。校验和的计算方式是前四个字节相加,取低8位,和第五个字节相等才算数据有效。
每一位数据的传输方式都一样:先是约50微秒的低电平,表示起始位,然后是高电平,高电平持续的时间长短代表这一位是0还是1。高电平持续26到28微秒左右代表0,持续约70微秒代表1。所以在读每一位时,正确做法是等待低电平结束,然后延时大约40微秒,在这个时间点去采样引脚。如果此时引脚仍然为高,说明是高电平持续较长的1;如果已经变低,说明是0。这也是DHT11驱动代码里最核心的部分。
还有一个容易忽略的点:校验和错误不代表整个通信失败,有时候只是干扰导致某一位读错了。常见的处理方法是丢弃这一帧数据,重新发起一次读取。但也不能一直死循环重读,不然主程序会被阻塞住,最好设置一个最大重试次数。
4.3 完整驱动代码:从初始化到校验
下面给出一份可以直接在标准外设库工程里用的DHT11驱动框架。我用宏定义把端口和引脚抽出来,方便以后换引脚时只改一处。
// dht11.h #ifndef __DHT11_H #define __DHT11_H #include "stm32f10x.h" #define DHT11_GPIO_PORT GPIOB #define DHT11_GPIO_CLK RCC_APB2Periph_GPIOB #define DHT11_GPIO_PIN GPIO_Pin_8 void DHT11_GPIO_Config(void); uint8_t DHT11_Read_Data(uint8_t *humi, uint8_t *temp); #endif// dht11.c #include "dht11.h" #include "delay.h" static void DHT11_Set_Output(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStructure); } static void DHT11_Set_Input(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStructure); } static uint8_t DHT11_Read_Byte(void) { uint8_t i, data = 0; for (i = 0; i < 8; i++) { while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == Bit_RESET); delay_us(40); if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == Bit_SET) { data |= (uint8_t)(0x80 >> i); } while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == Bit_SET); } return data; } uint8_t DHT11_Read_Data(uint8_t *humi, uint8_t *temp) { uint8_t data[5] = {0}; uint8_t i; uint8_t timeout = 0; DHT11_Set_Output(); GPIO_WriteBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN, Bit_RESET); delay_ms(20); GPIO_WriteBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN, Bit_SET); delay_us(30); DHT11_Set_Input(); timeout = 0; while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == Bit_SET) { if (++timeout > 200) return 1; delay_us(1); } timeout = 0; while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == Bit_RESET) { if (++timeout > 200) return 1; delay_us(1); } timeout = 0; while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == Bit_SET) { if (++timeout > 200) return 1; delay_us(1); } for (i = 0; i < 5; i++) { data[i] = DHT11_Read_Byte(); } if ((data[0] + data[1] + data[2] + data[3]) == data[4]) { *humi = data[0]; *temp = data[2]; return 0; } return 2; }主程序里可以这样调用:
uint8_t humi = 0, temp = 0; if (DHT11_Read_Data(&humi, &temp) == 0) { printf("Humi: %d.%d%% Temp: %d.%dC\r\n", humi / 10, humi % 10, temp / 10, temp % 10); } else { printf("Read DHT11 error\r\n"); } delay_ms(500);这里有个容易踩的细节,DHT11返回的整数部分实际是“扩大十倍”的数值。比如湿度读数45表示45.0%RH,温度读数23表示23.0°C。这段代码里直接用整数和小数分离打印,就不用去纠结浮点数格式化了,省内存又直观。
4.4 延时精度与关中断的坑
很多从Arduino或者51单片机转过来的人,写延时喜欢用for循环空转。在F103上面,最直接的教训就是:用for循环做微秒级延时,在72MHz主频下极不稳定。不同的编译器优化级别、不同的代码上下文,循环执行时间能差好几倍。DHT11的时序窗口只有几十微秒,一旦延时偏了,读出来的位就全乱,表现就是数据校验失败或者读出来全是0xFF。
正确的做法是使用Systick定时器或者通用定时器产生可靠微秒延时。Systick是Cortex-M内核自带的24位递减计数器,用它做delay_us和delay_ms非常合适。给了延时函数之后,在读取DHT11数据的整个过程中还有个隐藏问题:中断。如果系统里开了串口中断、定时器中断,在读时序时突然来了一个中断服务函数,几微秒到几十微秒的响应延迟会直接破坏DHT11的位判断。所以读取DHT11前要关闭中断,读完整个40bit后再恢复中断。很多例程没有这一步,靠运气也能工作,因为在教学环境里中断少,但一旦接入真实系统,就会偶发性地出现数据异常,非常难排查。
还有一个实际经验:读DHT11不能太频繁。DHT11本身的采样周期大约1秒,而且它的响应起始信号需要一定时间准备数据。读完一次后至少间隔500毫秒到1秒再读下一次,否则传感器还在低功耗状态或者正在采样,强行拉低起始信号会导致响应超时。这个限制不是F103的问题,是传感器的物理特性。
4.5 实跑效果与波形观察
我把上面的驱动接在STM32F103C8T6最小系统板上,数据引脚用PB8,外部加了一个4.7kΩ上拉电阻到3.3V,然后用逻辑分析仪抓了波形。起始信号那段能看到一个约20毫秒的低电平,然后是30微秒左右的高电平,之后DHT11拉低约80微秒、拉高约80微秒的响应,再后面就是40个bit的低电平加不同宽度的高电平。展开看单个bit:0的高电平宽度大约26微秒,1的高电平宽度大约70微秒,和协议手册完全对得上。串口打印的温湿度数据也稳定,连续跑两个小时没有出现校验错误。
这时候有个很值得注意的现象:如果不加上拉电阻,直接靠STM32内部上拉,逻辑分析仪上看到的波形上升沿明显变缓,在40微秒那个采样点,电平有时还没稳定到高电平,导致数据读取偶尔出错。DHT11的数据线需要一个阻值大概在4.7kΩ到10kΩ之间的外部上拉电阻。如果你用的是现成的DHT11模块,大多数模块上已经自带上拉电阻,可以直接接;但如果用的是裸传感器,一定要自己加上拉。这也是很多“昨天还能读,今天读不出来”问题的根源之一。
5. 排查示例:DHT11与F1组合的常见翻车现场
这个部分我把它做成一个实战排障案例,因为我自己在带新人时发现,很多问题不是无线索的玄学,而是有明确规律的。遇到DHT11读不出来,第一反应不应该是换传感器、换板子,而是按顺序排查,大部分情况下五分钟就能定位。
5.1 三板斧定位法
第一板斧是查电平。用万用表或者示波器测量DHT11数据引脚在空闲时的电平。如果总线空闲是0V,那说明上拉电阻没接,或者传感器被拉死了。如果空闲是高电平,说明基本供电和上拉没问题。
第二板斧是查起始信号。用示波器单次触发抓主机拉低那一下。如果看不到约20毫秒低电平,说明代码没跑到输出拉低那一步,或者GPIO配置有问题,常见的就是没开启GPIOB的时钟(RCC_APB2PeriphClockCmd没调用),读写寄存器完全无效。第三板斧是查响应。如果能看到起始信号,但紧接着没有DHT11拉低约80微秒的响应,那问题多半出在传感器供电、接线或传感器本身坏了。
我用逻辑分析仪排查过一个案例:主机发完起始信号后,总线就一直高电平,DHT11完全不响应。检查后发现是传感器数据线接到了PA11,恰好默认复用功能里这个脚是USB DM,USB外设也开着,两个功能同时抢这个引脚,导致电平状态混乱。换到普通GPIO口后立刻恢复。这类问题在F1的引脚复用表上都有明确标注,排查时先查一遍引脚功能表,能省很多力气。
5.2 问题速查表
这里整理一个最常见的DHT11+F103故障速查表,基本覆盖了新手能遇到的90%情况。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 读出来的数据全是0xFF | 数据线上没有上拉电阻或上拉电阻太大 | 加4.7kΩ到10kΩ外部上拉 |
| 卡在等待DHT11响应,程序不往下走 | 传感器没供电、数据线接错、GPIO未开启时钟 | 检查供电和接线,确认RCC时钟已使能 |
| 数据校验和一直错误 | 延时函数不精准、读取时有中断干扰 | 使用Systick延时,读取期间关中断 |
| 隔一段时间就读不到数据 | 读取间隔太短、电源纹波大 | 至少间隔500ms再读,电源加去耦电容 |
| 换了一个GPIO口就不行 | 引脚被复用或上拉没接 | 查引脚复用表,确认外部上拉存在 |
| 串口打印乱码 | 系统时钟配置和串口波特率不匹配 | 确认PLL倍频值与波特率配置一致 |
5.3 几个提升稳定性的细节
如果你要让DHT11在一个需要长时间运行的设备上稳定工作,还有几个额外的建议。第一是给DHT11的供电引脚加一个100nF去耦电容,尽量靠近传感器安装,这个电容能明显降低供电纹波对时序判断的影响。第二是在读DHT11的整个函数中加一个总超时保护,防止传感器在极端情况下不响应时,主循环被阻塞死。代码里我每个while等待都加了超时计数,这个习惯在所有单总线传感器驱动里都适用。
第三个细节很多人不重视:不要直接拿DHT11的数据线去做长距离传输,超过2米就很悬。DHT11是为近距离板内通信设计的,长距离应该换成I2C总线传感器或者用RS485等差分方式。我在一个小项目里为了布线方便把DHT11放在了离主板30厘米的位置,用的普通杜邦线,结果干扰抖动导致偶发读错。后来把线缩短到10厘米以内,问题立刻消失。
这个小节其实可以延伸出很多经验,但核心原则就一句话:单总线协议的传感器,对电压、时序、干扰都非常敏感,所有问题都可以从供电、接线、时序三个维度去排查。先把这三项确认一遍,再怀疑代码和芯片。
最后再分享一个个人习惯:每个F103工程我都会在板上预留一个GPIO测试点,调试DHT11这类时序敏感器件时,把它接到逻辑分析仪上观察波形。很多时候代码逻辑觉得没问题,但波形一看,上升沿慢得离谱、脉冲宽度忽宽忽窄,问题原因直接就暴露了。用STM32F1做嵌入式开发,最忌“我觉得没问题”,永远要让数据说话。DHT11这个小小传感器看着不起眼,但它教给你的时序分析和排查思路,能用到你以后所有单总线、I2C、SPI器件的驱动开发里。