1. 为什么STM32F1至今仍是嵌入式入门的首选
搞嵌入式这行十几年,我见过太多人一上来就冲着最新最强的芯片去,结果在寄存器配置和工具链上卡了半个月,热情直接磨没了。回头来看,STM32F1系列(尤其是F103C8T6这颗“蓝色药丸”)依然是新手入门和中小项目落地最稳的起点。它不是什么性能怪兽,72MHz的Cortex-M3内核、64KB到128KB的Flash、20KB的SRAM,放到今天看确实朴素,但正是这种“够用且透明”的特性,让它成为理解MCU底层运行逻辑的最佳载体。
这篇文章我想聊的不是简单的“点灯教程”,而是围绕STM32F1这个平台,把从最小系统搭建、外设驱动开发,到以DHT11温湿度传感器为例的完整数据采集项目串起来讲透。核心关键词就三个:STM32F1、外设驱动、DHT11温湿度采集。适合谁看?如果你刚学完C语言、模电数电还停留在课本层面,想找一个能真正动手、能看到结果、能写进简历的项目,那这篇内容就是为你准备的。如果你已经用过F1但一直停留在调库阶段,想搞清楚时序、时钟树、中断优先级这些“黑盒”背后的东西,同样有参考价值。
我个人的判断是:STM32F1的生命周期还远没结束。工业控制、消费电子、教学设备里大量存量项目仍在用它,供应链成熟、资料铺天盖地、价格低到离谱。与其追新,不如把这颗芯片吃透,后面换到F4、H7或者国产替代品,迁移成本极低。
2. 项目整体设计与方案选型思路
2.1 为什么选DHT11作为入门级传感器项目
DHT11是一款含有已校准数字信号输出的温湿度复合传感器,内部集成了一个电阻式感湿元件和一个NTC测温元件,出厂时已经做过校准,输出的是单总线数字信号。它的测量范围是湿度20%~90%RH、温度0~50℃,精度分别是±5%RH和±2℃,采样周期不低于1秒。这些参数看起来不起眼,但对于学习MCU与外设通信来说,它有几个天然优势。
第一,它是单总线协议,只需要一根数据线就能完成通信,硬件连线极其简单,不需要I2C或SPI那种多线时序。第二,它的时序要求虽然严格,但逻辑清晰,非常适合用来练习微秒级延时控制和GPIO方向切换。第三,它输出的就是温湿度两个物理量,做完之后你能立刻看到有意义的数字,成就感比单纯点灯强得多。
当然,DHT11的缺点也很明显:精度一般、响应慢、单总线时序对中断敏感。但作为学习项目,这些“缺点”反而能逼你去理解时序容错、临界区保护这些真正重要的工程概念。
2.2 硬件方案的整体架构
整个项目的硬件架构可以拆成四块:主控最小系统、电源部分、传感器接口、调试与显示部分。
主控我选的是STM32F103C8T6,LQFP48封装,焊接难度适中,引脚数量足够。最小系统包括8MHz外部晶振(HSE)、32.768kHz低速晶振(LSE,本项目可省)、复位电路、BOOT启动配置电路。电源部分用AMS1117-3.3把5V转成3.3V给MCU和传感器供电,输入输出各加一个10μF和0.1μF的电容做滤波。传感器接口就是DHT11的三根线:VCC、GND、DATA,DATA线上需要接一个4.7kΩ到10kΩ的上拉电阻,这一点后面会详细说。调试部分用SWD接口,只需要SWDIO和SWCLK两根线,比JTAG省引脚。
显示部分我建议加一个0.96寸OLED(SSD1306驱动,I2C接口),这样不依赖电脑串口就能直接看到温湿度数值,项目完整度会高很多。如果暂时不想加显示,用串口打印到PC也完全可以。
2.3 软件框架的选型:裸机还是RTOS
这个问题我被问过无数次。对于DHT11这种单任务、低频采集的项目,裸机前后台架构完全够用,没必要上FreeRTOS。原因很简单:DHT11的读取过程需要关中断或者至少保证时序不被打断,如果跑RTOS,任务调度和滴答定时器中断反而会干扰单总线时序,增加调试复杂度。
我的方案是:主循环里用一个1秒的软件定时器触发采集,采集时短暂关闭全局中断,读完立即恢复。其他时间MCU可以处理OLED刷新、串口输出等任务。这种“时间片轮询+临界区保护”的模式,是STM32F1裸机开发中最经典也最实用的架构。
如果你后续想扩展成多传感器、无线上报、低功耗管理,那时候再考虑上RTOS也不迟。但入门阶段,先把裸机时序吃透,比什么都重要。
3. 核心细节解析与实操要点
3.1 STM32F1的时钟树与延时函数设计
DHT11的时序精度要求是微秒级,所以延时函数必须准。STM32F1的SysTick定时器是24位的递减计数器,通常我们用HSE 8MHz经过PLL倍频到72MHz作为系统时钟。SysTick配置成1ms中断用于系统滴答,但微秒级延时不能靠中断,得用查询计数标志位的方式。
具体做法是:配置SysTick时钟源为HCLK/8,即9MHz,这样每个计数周期约0.111μs。要实现1μs延时,就装载9个计数。代码大概长这样:
void delay_us(uint32_t us) { uint32_t temp; SysTick->LOAD = 9 * us; SysTick->VAL = 0; SysTick->CTRL |= 1; // 使能,时钟源HCLK/8,无中断 do { temp = SysTick->CTRL; } while ((temp & 0x01) && !(temp & (1 << 16))); SysTick->CTRL &= ~1; SysTick->VAL = 0; }这里有个坑:如果系统里已经有1ms的SysTick中断,重新配置CTRL会打乱原有节拍。我的做法是微秒延时用DWT(Data Watchpoint and Trace)或者干脆用一个定时器(比如TIM4)专门做微秒延时,避免和SysTick冲突。DWT方式更优雅,但需要先使能调试单元,部分精简版芯片可能不支持。稳妥起见,用TIM4做微秒延时是最通用的方案。
注意:DHT11对延时误差的容忍度大约在±10%以内,所以延时函数不需要精确到纳秒,但绝对不能差出几微秒,否则通信直接失败。
3.2 DHT11单总线协议的时序拆解
DHT11的通信过程分四步:主机发送起始信号、从机响应、数据传输、结束。每一步的时序都必须严格遵守。
起始信号:主机把DATA线拉低至少18ms,然后拉高20~40μs,接着释放总线(切换为输入模式)。DHT11检测到这个起始信号后,会拉低总线80μs作为响应,再拉高80μs,表示准备发送数据。
数据传输:每一bit数据都以50μs的低电平开始,随后高电平的持续时间决定数据是0还是1。高电平持续26~28μs表示0,持续70μs表示1。40bit数据依次是:湿度整数、湿度小数、温度整数、温度小数、校验和。校验和等于前四个字节相加的低8位。
这里的关键点是:读取每一位时,必须先等待低电平结束,再延时约30μs判断电平高低。如果延时太短,可能还在低电平阶段;延时太长,可能已经进入下一位。我实测下来,延时30μs判断是最稳的。
uint8_t DHT11_ReadByte(void) { uint8_t i, byte = 0; for (i = 0; i < 8; i++) { while (DHT11_DATA_IN() == 0); // 等待50us低电平结束 delay_us(30); byte <<= 1; if (DHT11_DATA_IN() == 1) { byte |= 1; while (DHT11_DATA_IN() == 1); // 等待剩余高电平结束 } } return byte; }3.3 GPIO模式切换与上拉电阻的选择
DHT11的DATA线是双向的,主机发送起始信号时是输出模式,接收数据时是输入模式。STM32F1的GPIO配置里,输出用开漏输出(GPIO_Mode_Out_OD)配合外部上拉电阻是最规范的,因为开漏输出不会和从机的输出冲突。如果图省事用推挽输出,在从机拉低总线时可能产生大电流,长期运行有风险。
上拉电阻选4.7kΩ还是10kΩ?这取决于总线电容和通信速率。DHT11速率很低,线长一般不超过20cm,4.7kΩ到10kΩ都能工作。我倾向用4.7kΩ,因为上升沿更陡,时序更干净。但如果你的板子上已经有其他I2C设备共用上拉,注意不要重复并联导致阻值过低。
实操心得:很多新手调试DHT11失败,第一反应是代码问题,其实十有八九是上拉电阻没接或者接错。DATA线悬空时读出来全是0或全是1,先拿万用表量一下空闲时DATA线是不是3.3V,这一步能省你半天时间。
4. 完整实操流程与关键环节实现
4.1 工程搭建与基础外设初始化
我用的是Keil MDK或者STM32CubeIDE,两者都行。CubeIDE的好处是图形化配置时钟和引脚,生成初始化代码快;Keil的好处是编译快、调试器兼容性好。这里以标准库(StdPeriph)为例讲,因为F1的标准库资料最全,适合理解底层。
第一步,配置系统时钟。在system_stm32f10x.c里确保SYSCLK_FREQ_72MHz被定义,然后调用SystemInit()。第二步,使能GPIOA、GPIOC、USART1的时钟。第三步,配置PA9和PA10为复用推挽输出和浮空输入,用于串口。第四步,配置连接DHT11的引脚,比如PA0,初始为开漏输出,空闲时输出高电平。
初始化完成后,先写一个串口打印函数,把printf重定向到USART1,这样调试信息能直接输出到PC。这一步看似简单,但重定向时要注意不要用半主机模式,否则程序会卡在BKPT指令上。在Keil里勾选“Use MicroLIB”就能解决。
4.2 DHT11驱动代码的完整实现
驱动部分我分成三个函数:起始信号、读字节、读温湿度。起始信号的代码如下:
uint8_t DHT11_Start(void) { DHT11_DATA_OUT(); DHT11_DATA_LOW(); delay_ms(20); // 拉低至少18ms DHT11_DATA_HIGH(); delay_us(30); // 拉高20-40us DHT11_DATA_IN(); // 切换为输入,释放总线 if (DHT11_DATA_READ() == 0) { while (DHT11_DATA_READ() == 0); // 等待80us低电平响应结束 while (DHT11_DATA_READ() == 1); // 等待80us高电平准备结束 return 1; // 响应成功 } return 0; // 无响应 }读温湿度的函数把40bit数据拼起来,然后校验:
uint8_t DHT11_ReadData(uint8_t *temp, uint8_t *humi) { uint8_t buf[5], i; if (DHT11_Start() == 0) return 0; for (i = 0; i < 5; i++) { buf[i] = DHT11_ReadByte(); } if (buf[0] + buf[1] + buf[2] + buf[3] == buf[4]) { *humi = buf[0]; *temp = buf[2]; return 1; } return 0; }注意DHT11的温度和湿度小数部分在F1的常见模块上通常是0,因为分辨率只有整数位。如果你买的是DHT22(AM2302),精度会高很多,但时序略有不同,高电平持续时间判断阈值要调整。
4.3 主循环逻辑与临界区保护
主循环里我这样安排:每1秒调用一次DHT11_ReadData,成功则更新OLED显示和串口输出,失败则计数并重试。读取前关闭全局中断,读取后立即打开,因为单总线时序不能被中断打断超过几微秒。
while (1) { __disable_irq(); if (DHT11_ReadData(&temp, &humi)) { __enable_irq(); OLED_ShowNum(0, 0, humi, 2); OLED_ShowNum(0, 2, temp, 2); printf("Humi:%d%% Temp:%dC\r\n", humi, temp); } else { __enable_irq(); printf("DHT11 read failed\r\n"); } delay_ms(1000); }关中断的时间大约2~3ms,对系统其他任务影响很小。但如果你同时用了串口接收中断,要注意串口数据可能会丢,必要时把串口接收改成DMA方式。
4.4 实测数据与误差分析
我在室温25℃、湿度55%的环境下连续采集了100组数据,DHT11读数和参考温湿度计对比,温度偏差在±1℃以内,湿度偏差在±4%RH以内,符合手册标称精度。响应时间方面,从上电到第一次成功读取大约需要1~2秒,因为DHT11内部需要稳定时间。
有个现象值得注意:如果采集间隔小于1秒,读数会出现明显漂移甚至校验失败。这是因为DHT11内部采样周期限制,手册明确写了采样周期间隔不低于1秒。我试过500ms采集一次,失败率大概30%,改成1秒后失败率降到1%以下。
| 采集间隔 | 成功率 | 温度偏差 | 湿度偏差 |
|---|---|---|---|
| 500ms | 68% | ±2℃ | ±8%RH |
| 1s | 99% | ±1℃ | ±4%RH |
| 2s | 99% | ±1℃ | ±4%RH |
5. 常见问题与排查技巧实录
5.1 DHT11读取失败的五大原因
调试DHT11的过程中,我总结了一张排查表,按出现频率排序:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 始终无响应 | 上拉电阻未接或阻值过大 | 万用表测DATA线空闲电压,应为3.3V |
| 偶发校验失败 | 采集间隔太短 | 改为1秒以上 |
| 读数全0 | GPIO模式切换错误 | 检查输入输出切换代码 |
| 读数全1 | 总线被拉死 | 断电重启,检查是否有短路 |
| 温度正常湿度异常 | 传感器受潮或老化 | 更换传感器,避免结露环境 |
5.2 中断干扰时序的解决方案
如果你的系统里有其他中断(比如串口接收、定时器),DHT11读取时可能被中断打断,导致时序错乱。我的做法是读取期间关全局中断,但关中断时间要控制在3ms以内。如果系统对实时性要求高,可以把DHT11读取放到一个低优先级任务里,读取前挂起其他中断。
还有一种更优雅的方案:用输入捕获定时器来测量高电平持续时间,这样不需要关中断,CPU占用也低。但实现复杂度高一些,适合进阶学习。
5.3 电源噪声导致的随机失败
DHT11对电源噪声比较敏感,尤其是和电机、继电器共用电源时。我遇到过一批板子,单独测试正常,一接上继电器就频繁读取失败。后来在DHT11的VCC和GND之间并了一个100nF陶瓷电容,问题解决。如果条件允许,给传感器单独走一路LDO供电,隔离效果更好。
避坑技巧:调试阶段先用杜邦线短距离连接,确认代码没问题后再焊到板子上。杜邦线接触不良是新手最常见的“玄学故障”来源。
6. 项目扩展与进阶方向
6.1 从单点采集到多点组网
单个DHT11只能测一个位置,实际项目里往往需要多个测点。STM32F1有多个GPIO,可以挂多个DHT11,每个用独立引脚,轮流采集。但更规范的做法是用RS485总线或者无线模块组网,MCU作为主节点轮询各个从节点。这时候裸机轮询架构就不够用了,建议上FreeRTOS,每个传感器一个任务,用信号量同步。
6.2 低功耗改造思路
如果项目是电池供电,STM32F1的低功耗模式值得研究。F1有Sleep、Stop、Standby三种模式,Stop模式下功耗可以降到几十微安。改造思路是:采集完数据后进入Stop模式,用RTC闹钟定时唤醒,醒来后重新初始化时钟和GPIO,再采集。注意DHT11本身功耗不低,如果长期电池供电,建议换用SHT30这类低功耗I2C传感器。
6.3 数据上云与本地存储
采集到的温湿度数据可以存到SD卡(SPI接口)或者通过ESP8266/ESP32模块上报到服务器。STM32F1通过串口AT指令控制无线模块是最简单的方案,但稳定性一般。更可靠的是用SPI接口的以太网模块或者直接换带无线的MCU。本地存储方面,F1的Flash可以模拟EEPROM存少量数据,大量数据还是外挂SPI Flash更合适。
我个人在实际操作中的体会是:STM32F1这颗芯片最大的价值不在于性能,而在于它足够简单,简单到你能把每一行代码、每一个时序都搞清楚。DHT11这个项目虽然小,但把GPIO、延时、时序、中断、串口、显示都串了一遍,做完之后你对嵌入式系统的理解会上一个台阶。后面再学I2C、SPI、ADC、DMA,都是在这个基础上的自然延伸。别急着追新芯片,先把F1玩透,这笔时间投入绝对划算。