
聊到STM32F1系列很多做嵌入式的朋友第一反应就是这芯片不是早就说停产了吗怎么还在用先别急着下结论。F1系列是ARM Cortex-M3横空出世那几年最能打的一代产品2017年前后官方确实宣布除部分型号外不再接新订单但市场上的存量芯片、量产方案、教学资料、开发板到今天依然满天飞。如果你正准备学单片机、给产品做选型或者手里正维护着一套稳定运行的老F1代码这篇文章应该能帮你省点时间。我会把F1系列当成一个老朋友来聊它的家底、脾气、常用外设怎么伺候以及那些年大家踩过的坑。我会用实际开发中的视角把时钟树、GPIO、串口、定时器、ADC、DMA这些核心内容逐个拆开讲每个关键点都会告诉你为什么这么做。这个系列之所以能成为无数人的入门芯片就因为它把“能用”和“好用”平衡得恰到好处。内核是ARM Cortex-M3最高主频72MHz片上Flash、RAM、丰富的外设资源一应俱全最关键的是资料极多、生态成熟。不管你是学生、转行入门的菜鸟还是被项目追着跑的工程师在这篇文章里都能找到可以直接抄作业的东西。1. 先认清F1系列的家底与定位1.1 产品家族图谱F103凭什么成为街机STM32F1系列官方叫“主流型”产品但内部其实分了好几档。F101是入门型号主频36MHz资源少适合极简控制场合F102在F101基础上加了USB设备控制器F103是全家桶里最畅销的型号主频72MHzFlash容量从16KB一路做到512KBRAM最高64KB通用外设基本齐全F105和F107则属于互联型产品线F105带USB OTGF107在F105基础上增加以太网MAC控制器适合做联网类设备。真正让F103封神的是它在性价比和资源规模之间找到了一个几乎完美的平衡点。以最经典的STM32F103C8T6为例64KB Flash、20KB RAM、48个引脚价格在正常行情下只要几块钱但片上集成了USART、SPI、I2C、ADC、定时器、DMA、USB、CAN等外设。这个配置放在今天依然能应付一大批产品需求更别说在它刚发布的那个年代同价位几乎找不到对手。国内无数开发板、课程、毕业设计、量产项目都围绕它展开后面积累的代码和教程量让新手几乎不可能遇到“搜不到答案”的问题。1.2 内核与外设的总线布局72MHz从哪来F1系列的灵魂是ARM Cortex-M3内核单周期乘法指令、硬件除法、嵌套向量中断控制器NVIC、可选的MPU内存保护单元这些特性在2007年那会儿非常能打。芯片内部的时钟架构非常有意思外部8MHz晶振HSE经过锁相环PLL倍频最高可以到72MHz这个频率就是系统主频SYSCLK。主频并不能直接驱动所有外设芯片内部把时钟分成了三级总线AHB连接内核、Flash和DMAAPB1是低速外设总线最高36MHzAPB2是高速外设总线最高72MHz。你可以这样理解AHB像城市主干道所有车都能跑APB2像高速公路AD转换器、USART1、高级定时器等“高要求乘客”走这条APB1像限速更严的次干道普通定时器、USART2/3、I2C、SPI、看门狗这些“普通乘客”走这条。这个总线布局直接决定了外设的时钟频率。例如USART1挂在APB2上如果APB2是72MHz波特率时钟就是72MHzUSART2/3挂在APB1上波特率时钟顶多36MHz。再比如通用定时器TIM2/3/4虽然挂在APB1上但F1有个特殊设计只要APB1预分频不等于1定时器时钟就自动加倍从36MHz翻回72MHz。很多新手在这里栽跟头以为APB1只有36MHz就照此计算定时器频率结果PWM输出快了一倍半天找不到原因。1.3 F1为什么到现在还值得聊除了情怀F1至今仍有很强的现实价值。第一是存量巨大很多工厂里的老设备、老产品线还在跑F1维护升级需要有人懂。第二是硬件简单外设寄存器比F4/H7少得多中断和DMA通道也没有那么多复杂映射作为学习Cortex-M3的入门芯片信息量刚刚好不至于被一堆高级特性砸晕。第三是价格稳定下来之后性价比依然能打在不需要浮点运算、不需要超高速USB、不需要大容量内存的场景F1完全够用。但我也不建议把它神化。2021年前后那波芯片荒F1的价格一度涨到离谱的程度很多项目被迫换到F0/G0/F4甚至国产替代。所以如果你做一个全新产品我真建议先看看ST当前主推的G0、G4或者F4它们在某些维度确实比F1强一个段位。F1更像是“经典老车”性能不算惊艳但稳定可靠、修车便宜、懂的人多。2. 时钟与启动第一次点亮系统的两座大山2.1 时钟树配置为什么默认都是HSE加PLL倍频任何一个F1工程启动后第一件事是配置时钟。标准外设库的SystemInit函数默认逻辑是优先尝试外部8MHz晶振HSE等待就绪后通过PLL倍频到72MHz然后切换系统时钟到PLL输出。这个过程可以浓缩成一句话HSE起振、等稳定、PLL倍频、切换主时钟。配置代码大致是这样void SystemClock_Config(void) { RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); // 打开外部晶振 if (RCC_WaitForHSEStartUp() SUCCESS) // 等待HSE稳定 { RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); // 8MHz * 9 72MHz RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); // 系统时钟切到PLL RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB SYSCLK RCC_PCLK1Config(RCC_HCLK_Div2); // APB1 36MHz RCC_PCLK2Config(RCC_HCLK_Div1); // APB2 72MHz RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) RESET) {} } }这里最关键的是HSE_VALUE这个宏。标准库的stm32f10x.h里默认写的是8000000如果你的PCB上焊接的是12MHz晶振但宏没有改PLL倍频9倍会得到108MHz芯片直接超频轻则串口乱码重则程序跑飞。反之如果宏写了12MHz但实际晶振是8MHz系统主频就只剩48MHz定时器时间全部偏慢。这个错误我见过太多次排查起来通常要花掉一下午。为什么默认用HSE加PLL而不是直接用内部HSI因为HSI是芯片内部RC振荡器出厂精度虽然能用但温漂比较大。串口通信要求波特率误差在百分之二以内内部RC在温度变化后很难保证这个精度。而外部晶振的精度取决于晶体本身通常能做到几十个ppm级别通信和定时都更可靠。如果你的产品对成本极敏感、对时序精度要求不高也可以选HSI省掉两颗晶振负载电容但别指望它跑高精度波特率。2.2 BOOT引脚与启动模式BOOT0拉低是王道F1有BOOT0和BOOT1两个引脚它们的电平组合决定了芯片从哪里启动。这个机制的背后是芯片内部固化的启动逻辑上电后根据两个引脚的状态把PC指针和栈指针定位到不同的存储区。启动模式表非常直观BOOT1BOOT0启动区域使用场景任意0主Flash正常运行的应用程序01系统存储器出厂Bootloader串口ISP下载11SRAM调试、临时运行程序量产板上最常见的做法是把BOOT0用10K电阻下拉到地BOOT1也下拉到地也就是默认从主Flash启动。当你需要刷固件时如果板子上没有引出BOOT0跳线就只能用SWD接口下载。如果你想用串口ISP方式给芯片烧程序得先把BOOT0拉高、复位芯片、用串口工具连接USART1烧完后拉低BOOT0、再复位一次。我还见过一个典型的坑有人为了省事BOOT0悬空结果芯片时而正常启动、时而跑到Bootloader里就是因为这个引脚没有确定电平。数字逻辑里悬空引脚就是“不定态”千万不要在关键引脚上留悬空。2.3 最小系统硬件设计实战F1芯片跑起来对硬件的要求其实很低一个最小系统只需要五个部分电源、复位、时钟、BOOT引脚、下载调试接口。电源部分每个VDD引脚旁边就近放一颗100nF陶瓷电容电源入口再加一颗4.7uF到10uF的电解电容这是保证芯片稳定工作的底线。复位电路NRST引脚接一颗10K电阻上拉到3.3V再接一颗100nF电容到地按下复位按钮时电容放电拉低引脚。时钟部分8MHz主晶振并联1M电阻两个引脚各接一颗20pF负载电容到地。32.768kHz的RTC晶振只有在需要低功耗实时时钟时才需要如果产品用网络校时或者对时间精度要求不高可以省掉。下载调试接口我强烈建议引出SWD接口只需要SWDIO、SWCLK、VCC、GND四根线用一个几块钱的下载器就能烧录和调试比J-TAG的二十多根线省事太多。我做过的最简单的验证板一块F103C8T6一个8MHz晶振两个20pF电容一个复位按钮一个SWD四针座然后就直接点亮了。没有复杂的参考设计也没用外部EEPROM整个板子跑串口、ADC、PWM全部正常。这个方案实测下来非常稳后来小批量生产也基本沿用这个框架。3. GPIO与常用外设最容易出成绩也最容易翻车的地方3.1 GPIO模式与速度选择的门道F1的GPIO没有F4那种独立的MODER/OTYPER寄存器而是沿用经典的CRL/CRH寄存器设计。每个引脚占用4位配置其中MODE位控制速度和输出模式CNF位控制具体功能。这个结构看起来古老但理解它反而能让你把引脚模式彻底搞清楚。配置时要回答三个问题方向是输入还是输出输入是浮空、上拉还是下拉输出是推挽还是开漏我用一个口诀记忆普通输出选推挽外部上拉或共享总线选开漏输入要看外部有没有上拉电阻。比如点亮LED推挽输出可以直接驱动但如果是I2C的SDA/SCL、或者多个设备共用的中断线就必须开漏输出并外部上拉否则两个设备一个输出高一个输出低就会直接短路打架。F1的GPIO输出速度分成2MHz、10MHz、50MHz三档。这个速度参数不是越高越好它本质是控制输出驱动器的翻转速率。对于LED、按键这类低频信号2MHz就够SPI时钟、PWM波通常选10MHz只有对边沿要求特别快的信号才需要50MHz。速度选择过高会增加EMI辐射噪声过低会让波形边沿变缓可能出现通信不稳定的问题。我见过有人把所有引脚都配成50MHz结果产品做EMC测试时噪声超标改成匹配的速度后问题消失。还有一个F1特有的坑USART1的TX/RX是PA9/PA10I2C1的SDA/SCL是PB6/PB7这些引脚本身支持5V容忍也就是可以接5V电平设备而不损坏芯片。但模拟输入的ADC引脚不支持硬件手册里明确标注了哪些是FT引脚。做板子之前一定要翻开数据手册的引脚定义表格蓝色标记的FT才是5V容忍千万不要想当然。3.2 USART串口从配置到乱码排查串口是F1开发中最常用的外设也是排查起来最玄学的外设之一。配置的核心就三块波特率、GPIO复用、中断或DMA。以USART1为例波特率时钟来自APB2的72MHz你需要把TX配置为复用推挽输出RX配置为浮空输入或上拉输入然后设置波特率、数据位8、停止位1、无校验。标准外设库的写法非常清晰GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; // TX GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽 GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; // RX GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE);串口乱码的问题十有八九出在时钟源不匹配。标准库默认HSE_VALUE是8MHz如果你用12MHz晶振但没改宏虽然在HSE启动阶段能起振但SystemInit里PLL倍频计算就会乱套实际主频不是72MHz。USART波特率计算依赖这个主频主频错了一切都错。我踩过一次坑板子晶振贴错成12MHzHSE_VALUE还是8MHz串口输出全部是乱码用示波器打晶振引脚才发现频率不对。另一个容易忽略的是波特率误差。USART的分频思路是PCLK除以(16乘以目标波特率)得到一个分频值然后把整数和小数分别写入USART_BRR寄存器。由于寄存器只能是16位精度有些波特率天生就会产生微小误差。比如APB1上的USART2/3PCLK是36MHz目标波特率115200理论分频值是19.53寄存器取整后误差约0.1%在可接受范围。但如果目标波特率是250000这种非典型值误差就可能超过2%通信就不可靠了。遇到特殊波特率时先用计算器算一下误差超过1.5%就要考虑改主频或者换一个接近的波特率。3.3 定时器与PWMF1最实用的模拟量输出手段定时器是F1系列里最能体现“性价比”的外设。TIM1是高级定时器支持互补输出、死区插入和刹车功能适合驱动H桥和电机控制TIM2/3/4是通用定时器支持PWM、输入捕获、编码器接口TIM6/7是基本定时器只能做定时没有输入输出引脚。很多人一上来就用TIM1做普通PWM结果输出引脚没有波形原因就是高级定时器在输出比较使能后还必须开启主输出MOE位否则比较通道的输出级是关闭的。这个细节在标准库对应一句代码TIM_CtrlPWMOutputs(TIM1, ENABLE);忘了这句话的人十有八九要对着示波器怀疑人生五分钟。PWM频率的计算公式也不复杂PWM频率等于定时器时钟除以(预分频PSC1)再除以(自动重载ARR1)。已知72MHz定时器时钟想要20kHz PWM只需要让(PSC1)*(ARR1)3600。比如PSC0、ARR3599输出就是20kHz分辨率3600级的占空比。如果电机需要20kHz又希望占空比分辨率到0.1%可以适当加大PSC来降低频率或者换更高主频的芯片。这是个数学取舍没有银弹。还要注意定时器的时钟问题。TIM2/3/4挂在APB1上APB1预分频默认是2此时APB1实际是36MHz但F1硬件会让定时器时钟自动乘以2重新变成72MHz。所以只要APB1分频不是1TIMxCLK就是72MHz。这个设计经常被人忽视很多人把PSC算错一倍输出频率直接减半校准的时候一脸懵。3.4 ADC采集12位精度背后的参考与采样F1内置的是12位逐次逼近型ADC最多3个ADC模块共用18个通道包括16个外部引脚通道和2个内部通道。内部通道一个是温度传感器一个是内部参考电压VREFINT。很多项目为了省事直接用VDD做参考电压这在大规模量产时是有风险的因为电源芯片本身有噪声和误差。如果你的产品要做比较精确的电压测量正确做法是把ADC参考电压接到一颗独立的高精度基准源比如3.3V的基准芯片然后校准零点。ADC时钟频率不能超过14MHz这个限制来自逐次逼近型ADC的工作原理比较器的建立时间需要足够长。APB2是72MHz所以ADC分频只能选6分频、8分频也就是12MHz或9MHz。很多人直接选4分频得到18MHzADC寄存器倒是能写进去但转换结果精度会明显下降尤其在高温环境下更明显。我做过对比测试同一块板子18MHz和12MHz时钟下测同一电压结果差了十几个LSB换成12MHz后稳定很多。采样周期设置同样要小心。ADC转换时间 采样周期 12.5个ADC周期。如果你外接的信号源内阻很高比如一个几十K欧的分压电阻网络就需要用长采样周期来让采样电容充到稳定电压否则测量值会偏低而且读数跳动。常见配置是采样时间设为55.5周期或239.5周期牺牲一点速度换稳定。ADC引脚对电源噪声非常敏感板子上ADC参考引脚和模拟电源引脚附近一定要加滤波电容且滤波电容要靠近引脚放置走线尽量短粗。这是PCB层面的功夫电路图完全看不出来但直接影响实测效果。3.5 硬件I2C到底是不是真的难用网上流传F1的硬件I2C有bug这个说法有一定事实基础但不完全准确。F1的硬件I2C是早期设计总线状态机的确不如后来的F4/G4系列好用对错误恢复和仲裁的处理不够灵活一旦总线上出现意外时序比如设备中途掉电、SDA被拉低太久主机容易卡死在某个状态需要彻底复位外设才能恢复。我做过几个项目对比很真实一个量产温度采集模块用的硬件I2C偶发出现总线卡死导致设备无响应后来加了超时检测和软件复位才稳定另一个项目直接全部用软件模拟I2C就是GPIO开漏加翻转逻辑自己控制几年跑下来零问题。所以我的建议是分场景如果I2C总线上只有一两个自己知根知底的设备硬件I2C能用但必须做超时处理和总线恢复逻辑如果总线设备复杂、由第三方提供、行为不确定软件模拟I2C反而更可控。软件模拟的速度通常不超过400kHz对于大多数传感器足够了。软件模拟I2C的核心就是两个GPIO引脚开漏输出并外部上拉起始信号、停止信号、数据位高低电平全部用延时翻转实现。写起来比硬件I2C多几十行代码但每一行都在你的掌控之中出问题非常容易定位。对新手来说我反而推荐先用软件模拟I2C跑通传感器再回头研究硬件I2C至少心里有个对照。4. DMA、Flash与中断把程序写活的几个关键4.1 DMA搬数据为什么能省下CPUDMA直接存储器访问在F1上是好东西它让外设和内存之间的数据搬运不经过CPUCPU干自己的活搬完了DMA再通知你。最经典的场景是ADC连续采样ADC顺序扫描多个通道数据不断写入内存数组整个过程CPU全程休息。另一个经典场景是串口接收不定长数据用DMA把USART接收到的字节自动存到缓冲区配合空闲中断判断一帧数据的结束这是很多工业通信协议的标配做法。F1有两个DMA控制器DMA1有7个通道DMA2有5个通道后者只在F105/107上存在。DMA通道和外设有固定的映射关系比如USART1_TX对应DMA1通道4USART1_RX对应DMA1通道5ADC1对应DMA1通道1。配置DMA时参数不要搞混外设地址固定、内存地址递增、传输方向、数据宽度、循环模式。ADC连续采样建议开启循环模式传输完一轮自动重新开始不需要软件重新触发。我用过最长的一个DMA配置是同时开了五路串口1发送、串口1接收、ADC采样、定时器捕获、SPI发送。配置完成后整个系统几乎零CPU中断负担。DMA传输完成中断和半传输中断都很有用半传输中断可以在缓冲前半部分填满时就开始处理配合双缓冲技巧实现“边传边算”对音频采集类应用尤其合适。4.2 Flash读写掉电保存参数的正确姿势F1片上Flash既能跑程序也能像EEPROM一样用来保存配置参数。这里有一个非常重要的前提F1的Flash不是EEPROM它不能按字节随便改写。写操作只能把1写成0要把0变回1必须整页擦除。F1中容量Flash页大小是1KB大容量到512KB以上的页大小是2KB。也就是说你想改一个字节至少要擦除整整一页。掉电保存参数的常规做法是在Flash末尾预留一到几页专门存配置。每次保存时先读出一页数据到RAM修改后擦除该页再把新数据写回去。如果产品经常保存参数比如每小时保存一次Flash官方标称擦写寿命大约一万次照这个频率大概一年就到寿命。解决思路有两个一是把保存次数降下来比如离上次保存不到一定时间就忽略新参数二是做磨损均衡轮流使用多个页每次都写最新的页让四页乃至八页轮流承担擦除寿命直接乘以页数。写Flash期间还有一个硬性限制芯片正在写Flash时不能同时从Flash取指令执行否则总线出错。标准库的FLASH_ProgramWord会等待BSY位清空看起来封装好了但有经验的工程师会在这个期间把全局中断关闭。因为中断服务程序也在Flash里写Flash过程中一旦来中断CPU要去Flash取中断向量和中断函数代码这就撞在枪口上了。正确姿势是__disable_irq(); FLASH_Unlock(); FLASH_ErasePage(FLASH_BASE 0x3F800); FLASH_ProgramWord(FLASH_BASE 0x3F800, newValue); FLASH_Lock(); __enable_irq();如果中断不可避免更稳妥的解法是把Flash操作函数复制到SRAM里执行启动文件里预留RAM空间这个复杂度一般产品不需要。4.3 中断优先级分组与NVIC别把所有中断都设成一样F1的NVIC支持多个中断源优先级分抢占优先级和子优先级两套维度。抢占优先级高的中断可以打断正在处理的低优先级中断实现真正的嵌套抢占优先级相同的中断之间不能互相打断只能排队。子优先级则只用于同抢占优先级内部谁先来先服务。优先级分组值AHB到APB再到定时器倍频这些概念贯穿了整个F1开发。后来换到F4/H7虽然时钟树复杂了很多但底层逻辑完全一样。如果不用HAL库换到新平台时你会发现除了寄存器和库函数名字变了外设的工作方式高度相似。这就是我为什么一直支持“把基础原理学扎实再上工具”的原因。6. 最终代码怎么组织才不给自己埋雷6.1 用状态机处理串口命令串口是F1项目最常见的交互入口我强烈建议不要在串口中断里做业务逻辑只做收数据、存数据、置标志。主循环里再用状态机去解析命令。比如一个简单的温控设备串口收到“SETTEMP:25\r\n”中断只负责把字节逐字收入缓冲区等收到换行符后置一个标志。主循环看到标志才解析校验通过后去更新目标温度。这样做的好处是即使上位机一条指令发了一半就断电也不会把设备搞成奇怪状态因为缓冲区超时未完成会被清空。状态机的写法看起来比顺序结构复杂但实际上更稳。我见过不少人直接在串口中断里写解析代码调试的时候没问题一旦现场电磁干扰导致字节错位解析逻辑就会卡在奇怪状态必须整机复位。状态机加超时复位能在最大程度上避免这种问题。6.2 驱动层与业务层分离F1工程变大的第一道坎就是驱动代码混乱。我的习惯是每个外设封装成独立的模块只对外暴露几个函数接口。GPIO初始化、寄存器操作全部留在驱动模块内部业务代码只调用类似Sensor_ReadTemperature这样的接口。这样做的收益在维护阶段非常明显接手的人不需要关心底层寄存器细节改驱动不影响业务逻辑换芯片平台时驱动层重写、业务层基本可复用。对于F1这种资源不算富裕的单片机模块化会稍微增加一些函数调用开销但换来的是代码可读性和可维护性。我见过一个上千行全写在main.c里的老项目调试时想加个功能都心惊胆战因为改一行可能会影响完全不相干的部分。模块化的成本是初期多写一点头文件和接口定义收益是后面省十倍的时间。6.3 版本管理与配置备份F1项目大多是单片机裸机或配合RTOS运行代码量通常在几千到几万行之间看似不需要版本管理但我建议从一开始就用代码仓库管起来。哪怕只有一个人开发也要在每次关键改动前提交一次。这不是形式主义而是当你改坏了一个功能想回退到昨天那个“能跑”的版本时的救命稻草。配置参数同样要有版本号。掉电保存的内存结构里第一个字段放版本号第二位放结构体长度。每次升级固件时启动逻辑检查这两个字段如果版本不符就恢复默认参数。否则你很可能遇到这种情况老固件保存的参数结构体比你新固件的短新固件一读越界轻则参数错乱重则直接死在某个奇怪的地址。这个设计在F1这类没有文件系统的单片机上尤其重要我用这个方式在产品迭代中避免了至少三次大的数据兼容问题。7. 写在最后说了这么多其实核心想表达的是F1这个系列就像一位技术扎实的老工程师它不追新、不炫技但每一分功能都落到实处。我接触F1大概是从标准外设库时代开始的那会儿仿真器还算奢侈品很多人靠一个串口打印走天下后来慢慢有了便宜的下载器、JTAG/SWD调试、逻辑分析仪开发环境友好多了。但现在回头看恰恰是那个资源不算富裕的环境逼着我把时钟树、寄存器、外设时序这些基础概念啃得滚瓜烂熟。如果你正在用F1做产品我的建议是把驱动层和业务层写干净引脚分配在工程初期就规划好少用反人类的隐式配置。如果你刚入门不用纠结买哪块开发板F103C8T6的经典板子足够你折腾大半年把串口、定时器、ADC、DMA逐个练一遍比看一百篇教程都管用。最后再分享一个小技巧工程里保存一个外设配置速查表格把每个引脚的复用功能、时钟来源、中断通道写在注释里半年后你回头维护代码时会感谢自己。F1最大的价值是它帮你建立的那套“看时序、查寄存器、算时钟”的思维方式这东西换到任何MCU上都用得上。