1. 为什么STM32F1系列至今仍是入门首选
做嵌入式开发这么多年,我见过太多芯片来来去去,有的刚上手就被市场淘汰,有的更新换代之后旧型号几乎没人再用。但有个系列,从我入行到现在,始终活跃在各类项目、教学板、产品方案里,它就是STM32F1系列。哪怕现在市场上已经有了性能更强的F4、H7,也有一众国产替代芯片,F1依然是很多人接触32位单片机时的第一块敲门砖。
说直白一点,STM32F1依然是目前学习ARM Cortex-M3架构最友好的平台,也是很多量产产品做成本控制时的可靠选择。它不像8位机那样资源吃紧,也不像高性能芯片那样配置复杂,它的外设数量、库函数完善程度、社区资料密度,都处在一个特别适合进阶的位置。我刚带过的不少新人,从51单片机跳到STM32F1,基本两到三周就能上手做小项目,这个学习曲线在32位单片机里是相当友好的。
我最早接触F1系列是在做一个温度采集网关的项目,当时需要多路ADC采样、定时器做PWM控制、串口上报数据,还要有足够的GPIO去接按键和指示灯。对比了一圈,最后选了STM32F103RCT6。那颗芯片的配置在今天看来非常普通,但在当时的项目场景里,几乎找不出更合适的方案。从那之后,F1系列就变成我设计中小型嵌入式系统的默认选项之一。
这篇文章不是去抄数据手册,而是基于我实际开发过程中的经验,把STM32F1系列从选型、工程搭建、核心外设配置到项目排坑的完整链路梳理一遍。无论你是刚开始学32位单片机,还是做方案选型时需要评估F1是否够用,这篇文章应该都能给你一些参考。
2. 架构与系列矩阵:先搞懂你手上拿的是哪颗芯片
2.1 Cortex-M3内核与F1的资源底座
STM32F1全系列基于ARM Cortex-M3内核,主频最高72MHz。现在听着不高,但它的真实价值在于:Cortex-M3内核具备硬件除法指令、位带操作、嵌套向量中断控制器(NVIC)、可屏蔽中断和严格意义上的实时响应能力。这些特性让F1可以胜任大多数工业控制、仪器仪表、物联网节点的逻辑处理任务。
再说存储资源。F1系列最典型的两档配置,一个是64KB Flash加20KB SRAM的C8级别,一个是256KB Flash加48KB SRAM的RC级别,往上还有512KB Flash的ZG级别,对应引脚数也从48脚到144脚不等。选型时有个常见的误区,就是只盯着Flash大小看,忽略SRAM。实际上在跑FreeRTOS或者较多任务状态机时,SRAM往往先不够用。我做过一个数据采集项目,程序逻辑不算复杂,但需要缓存多路传感器的历史数据,就是SRAM卡了脖子,最后不得不换成高一级的芯片。
F1系列的电源域做得也比较清晰,2V到3.6V供电范围,内部集成RC振荡器、PLL锁相环、多种低功耗模式。低功耗模式里最有实用价值的是停止模式和待机模式。停止模式下SRAM和寄存器内容能保持,我做过的一个电池供电设备就是靠它在等待唤醒期间把电流压到微安级别。选型时如果产品对功耗敏感,F1的低功耗能力是够用的。
2.2 一张表理清F1的常见型号
很多新手面对一长串型号就头疼,什么C8T6、RBT6、RCT6、ZET6,看着都差不多。实际拆解一下就很简单。型号命名规则里,中间数字代表Flash容量,后面的字母和数字代表引脚数和封装,前面的一两个字母代表某种特性。
| 型号示例 | Flash容量 | SRAM容量 | 引脚数 | 典型封装 | 适用场景 |
|---|---|---|---|---|---|
| STM32F103C8T6 | 64KB | 20KB | 48 | LQFP48 | 小节点、简单控制器 |
| STM32F103RBT6 | 128KB | 20KB | 64 | LQFP64 | 小型仪表、简单电机控制 |
| STM32F103RCT6 | 256KB | 48KB | 64 | LQFP64 | 数据采集、中等逻辑量的项目 |
| STM32F103ZET6 | 512KB | 64KB | 144 | LQFP144 | 复杂外设扩展、早期教学板 |
如果做产品原型验证,我建议直接选RCT6,Flash余量充足,引脚数也够用,不至于焊到一半发现引脚不够转接。如果只是写写学习Demo,C8T6性价比很高,市面上也有大量配套最小系统板,开发效率很高。
2.3 选型时容易忽略的隐藏点
真正决定你能不能把F1用好的,不是型号本身,而是配套的时钟管理。STM32F1的时钟来自身外的高速晶振或者内部RC振荡器,经过PLL倍频到系统主频。但要注意,F1系列的Flash预读取缓冲区和等待周期设置必须与主频匹配。如果你把主频设置到72MHz而不配置Flash等待周期,程序会随机跑飞,表现就是一会儿正常一会儿崩溃,排查起来极其折磨人。
选型时还要注意的一个点是调试接口。F1系列默认支持SWD和JTAG,SWD只需要两根线,占用PA13和PA14。很多新手在画板时把PA13、PA14当成普通GPIO用了,结果程序烧了一次之后第二次就无法连接调试器。这个坑我见到过太多次,如果你确实需要把这两个引脚当GPIO用,需要在代码中明确禁用调试端口,并且谨慎评估后续调试需求。
3. 开发路线怎么选:标准库、HAL库还是寄存器
3.1 三条路线的基本盘
STM32F1的开发方式跟其他芯片不太一样,它经历了三套代码体系的迭代。最早是ST官方提供的标准外设库,后来推出了HAL抽象层库,再往后又有了LL库。再加上可以直接操作寄存器,就构成了三条并行路线。
标准外设库的代表写法是GPIO_Init()加GPIO_InitStructure结构体,这种风格非常贴近早期的C语言习惯,每一句都能猜到它最终配置了哪个寄存器。HAL库则是HAL_GPIO_Init()加参数结构体,配置流程更长,但抽象程度更高。LL库是介于两者之间的轻量方案,代码量接近寄存器操作,但保留了可读性。
我给新人的建议分两种情况。如果你以后想去大平台做消费类产品或者复杂项目,直接学HAL库是对的,因为它的驱动分层方式跟后续的STM32MP系列、无线系列更接近,也更容易配合CubeMX生成的工程。如果你的项目对代码体积和执行效率有极致要求,或者你在做裸机小系统,标准外设库的代码风格更直接,调试时反推寄存器更轻松。寄存器操作作为理解手段是必须掌握的,但不要整个工程全部手写寄存器,维护成本和出错概率都太高。
3.2 CubeMX到底要不要用
STM32CubeMX是官方图形化配置工具,可以直接勾选外设、设定时钟树、生成初始化代码。很多老工程师抵触它,觉得生成的代码太重、太绕。但以我这两年带项目的经验看,CubeMX在F1上的意义是把你从重复的初始化劳动里解放出来,把时间花在业务逻辑上。
我现在的做法是:用CubeMX生成初始化骨架,但业务代码全部自己写在外围。这样既避免了手写初始化时粗心漏配,也保证了核心控制逻辑的自主性。CubeMX生成的代码里有几个关键区域,一般是/* USER CODE BEGIN 3 */这种用户代码保护区,下次重新生成代码时这些区域内容不会被覆盖。你会用这两个标记,就掌握了CubeMX工程不被冲掉的命门。
用CubeMX时有个头疼的问题,就是它生成的工程首次编译经常报一个警告,说固件包版本和工具链版本不匹配。我的处理方式很简单,不要迷信最新版固件包,选择跟你的开发环境版本差距不大的那个包。F1系列的HAL库更新节奏不算快,稳定比新功能更重要。
3.3 最小工程模板的搭建思路
不管用哪条路线,工程骨架里必须有这么几个文件:启动文件、系统时钟初始化、中断向量表、主循环。启动文件是芯片上电后最先执行的汇编代码,它完成堆栈初始化、中断向量表拷贝、进入main()之前的准备。很多人移植工程时报HardFault,大概率是启动文件跟芯片型号不匹配。
时钟初始化这块,我强烈建议初期就固定成外部8MHz晶振加PLL倍频到72MHz的配置,不要依赖内部RC。内部RC的精度在常温下做调试够了,但产品在温度变化大的环境下,通信时序就可能出偏差。当然,一些对成本极度敏感的产品可以只用内部RC,但通信速率需要降档设计。
工程模板里还要处理的是中断服务函数的命名。STM32的标准启动文件把中断向量表里每个入口都命名好了,比如USART1_IRQHandler,你的中断服务函数必须叫这个名字才能被启动文件正确绑定。很多人自己写中断函数叫UART1_Handler,结果编过跑起来,中断触发就直接进HardFault。这个问题在标准库和HAL库时代都存在,本质是中断向量表机制没理解透。
4. 核心外设实操拆解:从GPIO到定时器
4.1 GPIO的八种模式,并不是每种都要记
很多新手一看到GPIO有八种模式就头大。其实不需要死记,按使用场景分就三类。输入类场景,包括浮空输入、上拉输入、下拉输入、模拟输入,你只需要根据外部电路决定是否启用内部上下拉。输出类场景,开漏输出和推挽输出,推挽输出能输出高和低两种电平,开漏输出只能主动拉低,要得到高电平得靠外部上拉。第三类是复用功能,比如串口的TX引脚,必须配置成复用推挽输出,UART才能正常工作。
我踩过一个项目级的大坑,是在一个I2C应用中,把SCL和SDA引脚配置成了推挽输出。I2C协议本身要求开漏输出,推挽输出在电平握手阶段会发生总线冲突,导致部分从设备偶发性无响应。排查了两天才发现是GPIO模式配置问题。后来所有I2C接口我一律用开漏加外部上拉,再没出过这类故障。
GPIO操作还有一个高频需求是位操作。STM32F1支持位带操作,可以把某个位映射到另外一段地址空间,通过读写映射地址来操作单独一个引脚,实现类似PAout(1)=1的写法。位带操作对比先读后写的GPIO_WriteBit,少了读改写竞争的风险,这在多线程或多中断场景里非常重要。不过位带操作地址计算容易犯错,我建议初学者先用库函数,等理解了BSRR寄存器的用法再来玩位带。
4.2 串口是整个调试体系的地基
做嵌入式开发,串口调试是基本功中的基本功。F1系列有多个USART外设,USART1挂在APB2总线上,最高频率跟系统主频一致,能跑到更高速率,USART2和USART3挂在APB1上,时钟频率只有36MHz。
配置串口的关键参数有四个:波特率、数据位、停止位、校验位。波特率出错是新手最常碰到的,现象是收发乱码。排查时先检查CubeMX里的时钟树,看看USART挂载的总线时钟是多少。比如USART1挂在APB2,如果你把APB2配成36MHz,那USART1的最高波特率理论上限就会减半,虽然一般9600和115200不会受影响,但如果你做高速非标准波特率,就可能超出能力范围。
串口调试还有个实用技巧,就是启用接收中断并使用环形缓冲区。中断里只负责把收到的字节扔进环形队列,主循环里再解析队列里的数据。这样避免了在中断里做耗时解析导致丢帧,也让主循环和中断之间的数据交互变得无锁化。F1的SRAM不大,环形缓冲区建议开256字节,一般调试协议足够用了。
4.3 定时器是F1的万金油模块
定时器是STM32F1系列里功能最丰富的外设群组。从基础功能上讲,Tout = (Prescaler + 1) × (Period + 1) / TimerClockFreq,这条公式是整个定时器配置的核心。比如系统72MHz时钟下,要产生1kHz的PWM波形,预分频器PSC设置为71,那么定时器时钟就是1MHz,自动重装载寄存器ARR设置为999,输出频率就是1k。注意PSC和ARR都要减1,因为寄存器是从0开始计数的,这个细节我见过各种漏掉之后频率翻倍的。
定时器的PWM模式是电机控制和调光项目的关键。F1的高级定时器TIM1和TIM8支持互补输出和死区插入,非常适合H桥驱动场景。普通定时器TIM2到TIM5则胜任基础的PWM输出。实际项目里,我要提醒的是PWM频率的选择。做电机驱动时,PWM频率太低会听见明显的电流噪声,太高则MOS管开关损耗剧增。我常用的经验值是10kHz到20kHz之间,这个区间能兼顾噪声和控制精度。
编码器接口模式是定时器的另一个隐藏技能。TIM4和TIM3支持正交编码器接口模式,可以直接接增量式编码器。我之前做一个转台角度反馈项目,用TIM4的编码器模式读取编码器AB相脉冲,配置一次之后,转动方向、角度增量都会自动累计到计数寄存器里,主循环只需要读取并换算就行,极大减轻了CPU负担。这个模式不常用,但一旦需要,替代方案的成本和复杂度都会明显增加。
5. 中断与DMA:保证实时性的两把钥匙
5.1 NVIC优先级分组是个必须先统一的设计决策
STM32F1的NVIC支持两级优先级:抢占优先级和子优先级。具体怎么分配,由NVIC_PriorityGroupConfig()这个函数决定,它可以分成4种组合方式。这个问题最坑的地方在于,整个工程必须统一分组,你在一个模块里用了组2,另一个模块里用了组3,那么中断响应的相对关系就完全无法预测了。
实际项目里,我的优先级分配原则非常固定:需要快速响应的硬实时中断(比如定时器采样中断)分配高抢占优先级,不需要抢占同级子优先级的策略来区分;依赖数据完整性的非实时中断(比如串口接收DMA传输完成中断)放在低抢占优先级。这样做的好处是,中断嵌套层级不会太深,不容易出现栈溢出。
中断服务函数里最忌讳的是做耗时操作。一个最简单的规则:中断服务函数不要调用HAL_Delay()或者任何阻塞等待的函数,不要把浮点运算或大数组拷贝放在中断里。我接手过一个崩溃的项目,问题就是有人在串口中断里同步往Flash写数据,一次写操作要几十毫秒,中断被长时间卡住,更高优先级的中断全部被堵死,最后看门狗复位。这种Bug排查难度极高,最佳策略是从设计上避免。
5.2 DMA是让串口和ADC解放CPU的正道
使用串口时,若收发大量数据,CPU每收到一个字节都会触发一次中断,这在115200波特率下大约每87微秒一次。频繁进中断不仅占用CPU,还可能因为处理不及时丢数据。DMA可以自动把外设数据搬运到内存,搬运完成后才触发一次完成中断。
F1的DMA控制器有两条总线,DMA1负责常规外设,DMA2与以太网等高速外设关联。配置DMA时有三个关键参数:方向、外设地址、内存地址、数据宽度和传输长度。一个容易遗漏的细节是,DMA配置完成之后会自动禁用通道,下次启动DMA传输前要重新使能。如果用HAL库,HAL_UART_Receive_DMA()函数每次调用时内部会处理使能,但如果你手写寄存器开发,这个细节就会变成反复出现的Bug。
ADC采集搭配DMA循环模式,是我最推荐的采样结构。F1的ADC完成一次转换后产生事件,由DMA把结果搬到数组的一个槽位里,DMA到末尾后自动回卷,这就在不打断主循环的情况下形成了一个连续采样缓冲区。主循环需要数据时,只需要取数组的最近值。这个结构非常适合电机电流采样、电压监测等周期性任务。
6. 实际项目中积累的排坑经验
6.1 启动失败:Flash容量选型与烧录配置
遇到程序能编译但下载后运行不正常,先查一个点:烧录算法跟芯片的Flash容量是否匹配。现在很多IDE在你选择芯片型号之后会自动匹配烧录算法,但如果你用的是调试器厂商自带的一套配置,很容易出偏差。现象就是下载显示成功,但芯片不跑,或者跑一小段就挂。
这类问题的排查方法是,先用调试器读芯片的ID和Flash大小,跟选型手册对照。再确认工程里宏定义是STM32F10X_MD还是STM32F10X_HD,这个宏决定了标准库去操作哪些寄存器。如果你用的芯片是256KB的RCT6,但工程配置成了中等密度STM32F10X_MD,Flash操作相关的功能就会出问题,症状经常非常隐蔽,可能只是程序跑飞。
另外一个容易忽视的启动坑是启动文件选择。不同容量的芯片要选对应密度的启动文件,startup_stm32f10x_md.s对应中等密度,hd.s对应高密度。选错最典型的现象是中断不响应,因为中断向量表位置都偏移了。我之前帮一个朋友看项目,他说他写的串口中断死活不触发,我远程看了工程文件,启动文件用的是ld密度,芯片是C8T6中等密度,换掉启动文件后立马正常。
6.2 硬件复位为何反复随机:时钟树与Flash等待周期
随机复位是所有嵌入式项目里最磨人的Bug。我遇到过一次,代码简单到只点灯,但隔几十秒就自动复位。先怀疑电源,用示波器看3.3V并没有明显跌落。再查引脚,没有任何干扰源。最后把注意力放到RCC配置上,发现时钟初始化时PLL倍频到了72MHz,但没有配置Flash的等待周期寄存器。
STM32F1的Flash读取速度有限,当主频超过一定阈值时,必须插入等待周期。72MHz主频下需要两个等待周期,如果设置成0,Flash读取时序不满足要求,CPU取指偶尔失败,表现就是随机性复位。这种问题和芯片本身没关系,纯粹是初始化代码不完整。CubeMX生成的工程会自动处理,但手写代码或者从老项目拷贝的初始化代码经常漏掉,解决方式是在系统时钟配置函数里调用FLASH_SetLatency(FLASH_Latency_2)。
这类问题给我的启发是:凡是和时序相关的问题,都要先检查初始化是否符合数据手册要求,不要一上来怀疑芯片坏了。芯片真的坏的概率,远低于配置代码不完整的概率。
6.3 中断疑似丢失:优先级和标志位清除顺序
串口接收中断偶发性丢数据,是调试嵌入式时的又一高频难题。排查思路分两层。第一层,检查是否使能了接收中断并且清除了接收标志位。F1系列的串口状态寄存器里,RXNE位表示接收数据寄存器非空,进中断后必须清这个位,通常是读数据寄存器就会自动清除。如果你用库函数,USART_ReceiveData()本身就完成了这个动作,但如果你自己操作寄存器,就很容易漏掉清除操作,导致中断不断重入或者被永久卡住。
第二层,需要关注中断优先级分组和嵌套。如果多个中断同时抢占响应,低优先级中断可能长时间得不到响应,给人的感觉像是丢了数据。我的处理方法是,对于必须保证不丢的数据流(比如接收上位机指令),尽量用DMA加空闲中断的组合,DMA负责连续接收,空闲中断负责判断一帧数据结束,这样CPU不参与逐字节搬运,大幅降低丢数据的可能性。
这种问题本质上是对NVIC机制的敬畏不够。当你同时使用五六个中断时,一定要画一张中断优先级表,把所有中断的抢占优先级、子优先级列清楚,否则你就是在给自己埋雷。
6.4 烧录器能连上但程序写不进:读保护与时序解锁
还有一个实际项目中很难扛的坑:下载器能识别芯片,但Flash编程失败。这种情况有几个常见原因。第一是芯片开了读保护,这常见于你烧过量产固件,量产工具开了RDP级别保护。解决办法是使用调试器做全片擦除,正确的擦除命令会先把读保护级别降到无保护,但注意全片擦除会丢失芯片内所有数据,包括出厂校准值。
第二是目标板供电不足。调试器通过SWD接口给目标板供电,有些板子功耗大一点,调试器的供电能力扛不住,导致编程过程中电压跌落,Flash写入失败。我的做法是调试阶段单独给目标板上稳定的3.3V电源,调试器的供电只当参考电平,不要指望它带大负载。这个方法帮我在很多省成本的低功耗板上避了雷。
6.5 常见问题速查表
| 现象 | 可能原因 | 排查顺序 |
|---|---|---|
| 程序下载后不运行 | 启动文件密度不匹配 | 1. 核对芯片型号 2. 检查启动文件 3. 检查宏定义 |
| 随时随机复位 | Flash等待周期未配置 | 1. 检查时钟宏 2. 检查FLASH配置 3. 示波器抓复位波形 |
| 串口乱码 | 波特率误差或时钟配置错误 | 1. 确认波特率 2. 查USART挂载总线时钟 3. 用逻辑分析仪看波形 |
| 中断不触发 | 中断服务函数命名不对 | 1. 核对IRQHandler名字 2. 检查NVIC使能 |
| ADC读数跳变严重 | 采样时间太短或参考电压不稳 | 1. 增大采样时间 2. 检查参考电压去耦 |
| 烧录器连接失败 | SWD引脚被复用或板子供电不足 | 1. 按住复位再连接 2. 检查SWD接线 3. 单独供电 |
7. 从F1起步的后续演进思路
很多人在F1上练完手之后,问我要不要直接跳到F4或者H7。我的看法是,F1到F4之间的跨越,远没有51到F1的跨越难度大。如果你能把F1的时钟树、NVIC优先级、DMA工作方式、定时器配置这些核心概念真正理解透,换到F4时,你多适应的是浮点运算单元、更复杂的中断映射、更快的总线矩阵,底层思维模型并没有改变。
F1系列在今天依然是成熟产品设计中的常客,尤其在成本敏感、功能固定的工业控制器场景里。它经过十几年的量产验证,资料详实,可用的参考设计和代码库非常庞大,这是一种无形资产。在做方案选型时,如果芯片资源够用,为了追求新架构去增加供应商风险和技术风险,并非理性的选择。
我个人在这些年的实践中,最深的体感是:芯片型号只是载体,真正有价值的是你理解CPU怎么启动、时钟怎么分发、外设怎么协同、中断怎么流转。F1系列恰好是一个你能把所有机制都摸透的平台,它没有那么多黑盒行为,IO口就是映射到那几组寄存器,时钟树就那么几条主线,出问题你能用数据手册加调试器一步步追到底。这种掌握底层规则的机会,在更复杂的芯片上反而不容易得到。
所以我给所有建议都很直接:想入门32位单片机,直接啃F1;在做低成本产品方案,别嫌它老,它长期稳定供货本身就是一种优势;研究外设协作,用F1的裸机工程多写几个整合Demo,把串口、定时器、ADC、DMA、中断全串起来,吃透这套玩法,后面无论做Linux驱动还是高端MCU开发,都会走得更顺。最后说个小技巧,调试F1时,把串口波特率、定时器频率这些参数全部写成宏定义放在头文件里,改动只用改一处。这个习惯看起来简单,但到项目后期维护时,能帮你省下大量的定位时间。