这两年新出的控制器一个比一个卷,新内核、大内存、低功耗,各种营销词堆在评测页上。但说句实在话,我在自己的项目里依然保留了不少 F1 的库存,甚至有些新设计继续选它。很多人不理解,STM32F1系列都这么多年了,为什么还在用?这就好比装修选材料,如果一套方案已经被验证过无数次、踩坑成本足够低,为什么还要冒险去碰不熟悉的组合?
STM32F1系列是一类基于 Cortex-M3 内核的 32 位微控制器,主频可以跑到 72MHz,片内 Flash 从几十 KB 到 512KB 不等,SRAM 通常在 20KB 到 64KB 这个量级。外设覆盖 GPIO、定时器、串口、I2C、SPI、ADC、DAC、DMA 和 USB 这些常用模块。它最大的价值不是跑分,而是用一个成熟稳定的平台把产品的硬件和软件风险压到最低,尤其适合做中小规模控制类产品、仪器仪表、驱动器和入门级物联网节点。
这篇文章不是带你逐条念手册,而是从选型、环境搭建、外设配置到故障排查,讲清楚一套能直接落地的完整思路。如果你正在考虑用 F1 做项目,或者已经入手但被各种初始化细节折腾得头疼,这篇应该能帮你省不少时间。
1. 项目概览与选型逻辑
1.1 F1系列凭什么还不过时
先解决一个最核心的问题:为什么还要选一块老芯片?
第一,生态成熟度太高了。从寄存器手册、参考例程、常见问题记录到第三方库,几乎你能踩的坑,前面都有人踩过。我前期做过一次统计,一个 F1 项目从立项到出原型,如果只做常规外设,一半以上的时间花在软件框架上,而芯片本身的阻力非常小。这意味着团队可以把精力集中在业务逻辑,而不是反复看勘误手册。
第二,成本可控。F1 系列量产多年,价格透明、供货渠道多,选型时不太容易被单一供应商牵制。很多国产替代型号也直接兼容 F1 的引脚和软件接口,切换时的改动量很小。对于消费类电子和工业模块这种对成本敏感的场景,这很重要。
第三,性能跟需求匹配。72MHz 主频、12 位 ADC、多个定时器,这种组合做电机驱动控制、传感器采集、协议转换,完全够用。真正不够用的,是那种需要跑复杂算法或者大量并发处理的场景,但真到那一步你也大概率不会看 F1。
1.2 什么项目真正适合用F1
不是所有项目都适合 F1,我会习惯按需求倒推。
如果只是点个灯、转个风扇,任何廉价 8 位控制器都行,没必要顺手牵一匹大马。但如果需要多路 AD 采集、一路 PWM 调速、两路串口和一段状态机逻辑,F1 就很舒服。工业变送器、步进电机驱动板、电量采集模块、温湿度变送器这类产品,性能要求不高不低,外设需求却又杂又具体,正是 F1 的典型应用区间。
如果项目需要乘除法频繁计算或者浮点运算,F1 的软件浮点会有些吃力。比如一个需要实时解算位姿的机器人关节,F1 做起来会很勉强。这种场景我宁可换更高端的带硬件单精度浮点的内核,也不愿意靠优化算法硬扛。
一句话总结:F1 适合的,是那些“功能明确、外设丰富、成本敏感、不追求极限性能”的项目。
1.3 F1的“体格”认知
用之前,脑子里得有这张地图:Cortex-M3 内核负责算数、跳转、中断响应,外面是一堆通过总线矩阵挂着的外设模块。
F1 是典型的 Flash 与 SRAM 分离架构,代码在 Flash 里执行,运行时数据放 SRAM。中断控制器支持从 0 到 15 共 16 个优先级分组,优先级抢占和子优先级可以按需求分配。外设通过 APB1 和 APB2 两条总线挂载,APB1 最高 36MHz,APB2 最高 72MHz。定时器、串口、I2C 等设备分别挂在不同的总线上,初始化之前最好先看一眼自己的设备挂在哪条总线,否则容易犯时钟源没打开的低级错误。
搞懂这套结构之后,再去看芯片手册里的时钟树,你会觉得那棵树不是迷宫,而是一张有规则的地图。
2. 关键参数与硬件设计
2.1 容量型号与资源取舍
F1 系列下面还分不同容量等级,选型时需要先确认自己的代码量预估和外设需求。不要只看价格,资源不足后期换料更折腾。
| 容量等级 | Flash 范围 | SRAM 范围 | 常见外设概况 | 适合场景 |
|---|---|---|---|---|
| 小容量 | 16KB~32KB | 4KB~8KB | GPIO、定时器、串口、SPI、I2C、ADC 基础版 | 简单控制、传感器变送、小家电 |
| 中容量 | 64KB~128KB | 16KB~20KB | 外设数量增加,单 ADC、多定时器 | 仪表、驱动控制、协议转换 |
| 大容量 | 256KB~512KB | 48KB~64KB | USB、DAC、多 ADC、丰富通信接口 | 复杂控制、数据采集、需要 USB 的场景 |
常规产品我比较推荐中容量起步。64KB Flash 看起来不大,但如果你用的是标准外设库,纯业务代码一般能控制在一二十 KB 以内,剩下的留给协议栈和调试信息。如果你的产品需要 USB 协议栈、文件系统或者字库,直接上大容量,省得后期为空间发愁。
2.2 时钟系统的硬约束
F1 内部默认有一个约 8MHz 的内部 RC 振荡器,精度一般,适合上电快速启动。外部晶体通常是 8MHz,通过 PLL 倍频到最高 72MHz。这里有个硬规矩:APB1 总线最高 36MHz,APB2 总线最高 72MHz。如果配置分频器时把 APB1 弄到 72MHz,下面的定时器、串口都会跟着出问题,典型表现是串口波特率怎么调都不对。
所以我在配置时钟时,习惯按这个顺序走:
- 选择时钟源,一般用外部晶体 HSE。
- 配置 PLL 倍频系数到目标主频。
- 配置 AHB 预分频、APB1 预分频、APB2 预分频。
- 等待 PLL 锁定,切换系统时钟到 PLL 输出。
- 打开需要用到的外设时钟。
这套逻辑在文档里都有,但实际操作时很容易漏掉等 PLL 锁定的动作。如果切换时钟太早,系统会跑在一个不稳定频率上,偶尔正常偶尔乱套,这种问题最难查。
2.3 供电、引脚和电路细节
硬件设计上先盯三件事:电源水质量、复位、BOOT 引脚。
电源部分,每颗电源引脚旁边都应该放 100nF 去耦电容,这点老生常谈但经常有人犯。VDDA 引脚建议再串一个磁珠或者小电阻,再接一个 1uF 电容到地,这能明显提高 ADC 采样的稳定性。我遇到过 ADC 采样跳字严重的情况,最后查出是 VDDA 纹波太大。
复位电路不要搞得太复杂,一个 0.1uF 电容配一个 10k 上拉电阻就够了。但注意复位引脚对噪声敏感,如果 PCB 上复位走线太长,或者旁边跑大电流开关信号,会导致芯片频繁复位。解决办法是缩短走线,必要时加一个小电容滤波。
BOOT0 和 BOOT1 引脚不能悬空。正常运行时 BOOT0 必须拉低,否则上电会进入 ISP 下载模式,表现为程序不跑。这个坑非常低端,但我在协助排查时确实遇到过好几次,都是因为 BOOT0 引脚漏电阻或者跳线帽接触不良。
3. 开发环境与工程组织
3.1 建立可复现的开发环境
开发 F1,没有人会全程靠寄存器硬写,效率太低了。我推荐的做法是:一个支持 ARM 编译器的集成开发环境,一个支持 SWD 协议的调试器,再加一个串口调试助手。
F1 支持 JTAG 和 SWD 两种调试接口,实际调试我只用 SWD,占用引脚少,接线简单,SWDIO、SWCLK、GND 三条线就够。调试器选型没什么特别的,认准支持 Cortex-M 内核的就行,下载速度默认即可,不折腾。
这里有个容易忽略的细节:SWD 接口的下载线不要太长,超过 20cm 就可能在高速下载时不稳定。如果板子已经做出来且走线长,可以把下载速度调到最低档,稳定第一。
3.2 标准外设库与HAL库的取舍
F1 开发有两种主流软件框架:标准外设库和老一点但依然常见的 HAL 库。标准库代码更贴近寄存器,结构透明,编译体积小,但我个人觉得它更适合项目中期以后稳定使用,因为一切都在明面上,调试起来不神秘。HAL 库封装程度更高,便于跨系列移植,但中间层厚,出了问题要往下翻好几层。
多数中小型项目,我建议直接选标准外设库,再用一套自己封装的外设驱动模块。这样既保住开发效率,又不会在底层封装上迷失太久。如果你的团队后续要移植到更新系列,或者产品线复杂,HAL 库会更合适,但代价是一开始学习成本更高。
不用太纠结哪个更“好”,关键是选定之后不要反复横跳。我见过不少项目,开发到中途从标准库换到 HAL 库,结果驱动全要重写,时间全浪费了。
3.3 一个规范工程的结构
F1 工程源码我并不建议搞成大一统的单文件输出。规范工程应该至少分成这样几块:
- 启动文件,负责初始化堆栈、中断向量表和调用 main。
- 内核和设备头文件,包含寄存器和外设定义。
- 系统初始化代码,尤其把时钟配置集中到这里。
- 外设驱动,每个外设一个源文件,提供对外接口。
- 业务应用层,只调用驱动接口,不直接操作寄存器。
这样分层的理由很简单:外设驱动可以复用,业务逻辑在更换板卡时不需要大改。如果调试时发现某个外设行为不对,也能快速定位是初始化问题还是业务调用问题,不用从一堆代码里大海捞针。
4. 核心里设实操与参数拆解
4.1 时钟初始化:先从内部时钟起步
很多人拿到板子第一步就去配外部晶振,结果代码一跑就死。我调试时钟时习惯先让系统用内部 8MHz RC 震荡跑起来,确保基本调试链路没问题,再切外部晶振。这样可以分清楚问题是出在晶振硬件还是软件配置。
以下是一个常见的 HSE + PLL 倍频到 72MHz 的初始化片段:
void SystemClock_Init(void) { RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); if (RCC_WaitForHSEStartUp() == SUCCESS) { RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); RCC_HCLKConfig(RCC_SYSCLK_Div1); /* AHB = 72MHz */ RCC_PCLK1Config(RCC_HCLK_Div2); /* APB1 = 36MHz */ RCC_PCLK2Config(RCC_HCLK_Div1); /* APB2 = 72MHz */ RCC_PLLCmd(ENABLE); } }注意这里有一个细节:先配置 PLL 参数并切换系统时钟,最后才使能 PLL。如果顺序反过来,系统时钟可能短暂停留在未锁定的 PLL 输出上,导致偶发死机。
倍频系数 9 是怎么来的?外部晶体 8MHz,乘 9 等于 72MHz。如果外部晶体换了,这个系数也要跟着变。比如用 12MHz 晶体,倍频系数应该是 6,而不是照抄网上的 9。很多串口乱码问题就出在这里:时钟本身不对,波特率怎么算都是错的。
4.2 GPIO的8种模式和上下拉选择
GPIO 是 F1 里使用频率最高的外设,但模式选择这一点很多人并不细想。GPIO 有 8 种模式,归纳起来是四类:输入、输出、复用输入、复用输出。
普通输入用于读取按键、拨码开关和外部电平状态,一般选择浮空输入或上拉输入。按键电路通常一端接地,另一端接 GPIO,这时就要用内部上拉,按键按下时读到低电平,松开时读到高电平。注意有些按键电路是高电平有效,那就要用下拉输入。
输出模式里推挽输出和开漏输出最常用。推挽输出能主动输出高电平和低电平,适合直接驱动 LED、继电器控制信号和普通的逻辑电平输出。开漏输出只能主动拉低,高电平要靠外部上拉电阻,这种模式适合多个设备挂在同一条信号线上,比如 I2C 的总线结构。
GPIO 速度设置也是一个点。速度越快,边沿越陡峭,EMI 风险越高;速度太慢,波特率高的通信时序又可能变形。对于普通 LED 和控制信号,低速档即可。SPI 和串口这类通信引脚,建议中速或高速档,前提是走线不要太长。
4.3 定时器PWM输出与输入捕获
F1 的定时器分高级定时器、通用定时器和基本定时器。通用定时器做 PWM 输出是最常见的用法,比如控制电机转速、调节灯光亮度。
配置 PWM 最核心的两个参数是周期和占空比。周期由预分频系数和自动重载值决定:PWM频率 = 定时器时钟 / ((预分频+1)*(自动重载值+1))。比如定时器时钟想跑 72MHz,预分频设为 71,则定时器计数频率是 1MHz,自动重载值设为 999,则 PWM 频率就是 1kHz,占空比调节步进刚好是 0.1%。
这里容易犯的两个错:第一,APB1 总线上的定时器时钟不是 36MHz 而是翻倍后的 72MHz,因为在配置 APB1 分频不为 1 的情况下,定时器时钟会自动倍频。我遇到的不少 PWM 频率计算偏差就是漏了这层倍频。第二,预分频和自动重载值是“数值+1”参与计算,因为从 0 开始计,这一点刚上手的人最容易忽略。
输入捕获常用于测量外部脉冲频率或脉冲宽度。配置要点是把捕获通道对应的 GPIO 设置为复用输入,然后在捕获中断里读取比较寄存器值。两次捕获的差值乘以定时器计数周期,就是两个脉冲之间的时间。如果脉冲太快,要注意用 DMA 搬运捕获值,不然中断频率高了,CPU 根本来不及处理。
4.4 串口通信与中断优先级分组
串口是 F1 和外部世界打交道最常用的接口。初始化串口时有几个固定参数:波特率、数据位、停止位、校验位。波特率的本质是一个分频值,硬件时钟 72MHz 分频后产生对应的位时钟。所以前面反复强调时钟要准确,就是因为波特率完全依赖时钟基准。
串口中断优先级配置也需要花点心思。F1 的中断优先级分组决定有多少位给抢占优先级,多少位给子优先级。如果你同时用串口接收和定时器中断,建议把严格实时相关的中断设为较高抢占优先级,把普通信息处理放在低优先级。这样不会因为一条串口数据把整个控制周期打乱。
多字节接收时,最简单的方案是每收一个字节进一次中断,把数据放进环形缓冲区,主循环再处理。这样可以保证不丢字节,也能把接收和处理解耦。如果直接用数组接收不定长数据,还要自己实现帧解析,后面调试会麻烦很多。
4.5 ADC多通道采样与DMA搬运
F1 的 ADC 是 12 位精度,支持多个通道。连续扫描多个模拟量时,如果每次都在主循环里调用一次转换再等待结果,CPU 会一直被占住。更合理的做法是配置 ADC 为扫描模式,启动 DMA,让转换结果自动搬到内存数组里,CPU 只需要在 DMA 传输完成中断里处理这几个采样值。
注意 ADC 的参考电压。通常 ADC 参考电压是 VDDA,如果 VDDA 有波动,采样值也会波动。追求精度时,外部基准源比内部参考更可靠。另外,ADC 输入阻抗不可能是无穷大,信号源内阻过高会拉低采样精度,高阻信号源前建议加一级运放跟随器。
转换时间也不要一味追求快。ADC 的采样时间太短,内部采样电容充不饱,结果会偏小。对一般传感器,我习惯把采样周期适当拉长,宁慢勿错。
5. 常见问题与排查速查
5.1 下载失败和识别不到芯片
SWD 下载失败是最常见的问题,而且往往不是芯片坏了。先检查四件事:GND 是否连接,SWDIO 和 SWCLK 是否接反,目标板是否上电,调试器引脚是否有虚焊。我遇到过一块板子怎么都连不上,最后量了一下发现 SWCLK 走线被覆铜铜皮刮到,产生微弱短路。
如果接线没问题,尝试把下载速度降到最低。高速模式下,SWD 信号边沿很容易受分布电容影响,特别是杜邦线连接时,降速基本都能解决。还可以在软件里设置复位连接:连接目标板前先拉低复位引脚,等调试器建立连接后再释放复位。
另一个方案是用 ISP 模式擦除芯片。把 BOOT0 拉高,BOOT1 拉低,复位后再上电,芯片进入 ISP 引导区,这时可以用串口工具连上并擦除 Flash。擦掉异常程序后,恢复到正常模式,再使用 SWD 下载即可。这个办法能救回大部分“假死”芯片。
5.2 程序跑飞、串口乱码、定时不准
这三个现象的背后经常是同一个原因:时钟配置错误。
处理思路是先确认实际主频。你可以点一个翻转频率已知的 GPIO,用示波器或逻辑分析仪测量实际翻转频率。如果 GPIO 翻转频率比预期高一倍或者出现不定值,基本可以断定 PLL 或预分频配错了。
串口乱码要分两段排查:一段是物理链路有没有虚焊、电平转换芯片方向对不对;另一段是波特率是否匹配。如果 9600 波特率能通但 115200 乱码,很大概率是时钟偏了,而不是接线问题。折中方案是先降低波特率试通,确认链路没问题再说。
定时器时间不准时,检查定时器时钟是否真的如你所想。APB1 预分频不为 1 时,定时器时钟是 APB1 的两倍,这个特性容易被人忘掉。我习惯把定时器初始化里的时钟来源写在注释里,避免半年后自己都忘了当时怎么算的。
| 故障现象 | 优先检查项 | 次要检查项 |
|---|---|---|
| SWD 连不上 | 接线、GND、供电 | 下载速度、复位脚干扰 |
| 上电后不跑 | BOOT0 是否拉低 | 复位电路、晶振起振 |
| 串口乱码 | 波特率与时钟一致性 | 电平转换、地线共地 |
| ADC 跳字 | VDDA 纹波 | 信号源内阻、采样时间 |
| 定时器时间偏大/偏小 | 预分频计算 | APB1 倍频未考虑 |
| 程序偶发死机 | 看门狗和中断优先级 | 电源质量、未初始化外设 |
5.3 硬件设计上的坑
再补几个硬件方面容易踩的坑。
外部晶振的两个负载电容不是随便选的,要按晶振手册里的 CL 值计算。电容过大会导致起振慢,过小会导致频率偏出,串口都会受影响。如果起振困难,还可以在晶振旁并联一个 1M 电阻,帮助放大电路快速稳定起振。
GPIO 直接驱动继电器这类感性负载很容易出事。虽然 F1 引脚能输出几十毫安,但继电器线圈的反电动势会打坏引脚。合理做法是 GPIO 外接三极管或 MOS 管开关,线圈两端并联续流二极管。这个二极管极性反了会炸,一定要留意。
调试接口的复位引脚不要和板上其他复位电路共用一条线,除非你确定不会互相干扰。SWD 调试时,调试器通常会控制复位引脚,如果板子上还有一颗外部看门狗芯片也在驱动复位,两边会打架,表现为下载不稳定、程序跑飞。
6. 最后再分享几点个人体会
真正用熟 F1,不是把那几百页手册背下来,而是建立起一套排查问题的反射弧。遇到现象,先分清是硬件还是软件;如果是软件,再分是配置还是逻辑。很多时候问题并不复杂,只是因为缺少系统化的排查顺序,才显得玄学。
我自己的习惯做法是:每做一个新初步接触的板子,先写一套最小系统测试,把时钟、GPIO、串口、定时器都点亮一遍,确认底板健康再往上加功能。这样后续出问题,就能快速判断是不是某颗外设芯片的问题,而不是怀疑初始化代码。
还有一点,工程里每一处关键配置都尽量加注释,标明来源、计算过程和注意事项。这不是给谁看的表面功夫,而是给几个月后大概率会健忘的自己留线索。写清楚之后,接手项目的人也会少走很多弯路。
STM32F1系列能在这个圈子活这么多年,靠的不是参数惊艳,而是稳定和透明。把这块芯片用扎实了,很多看似高深的外设原理,在你手里也会慢慢变成熟视无睹的基础操作。希望这篇文章能帮你少踩几个坑,把更多时间用在真正有价值的地方。