ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F1系列全面解析:从内核选型到实战调试要点

STM32F1系列全面解析:从内核选型到实战调试要点

很多做嵌入式的朋友,入门接触的第一颗芯片就是STM32F1系列,甚至不少人工作三五年之后,项目里最常用的还是这颗芯片。这不是情怀,而是这颗基于ARM Cortex-M3内核的经典MCU,确实把性能、外设丰富度、成本和使用门槛平衡得很好。这篇文章我想从选型、内核、外设、工程搭建、实战调试到踩坑排查,把STM32F1系列的关键信息一次说透,给刚入门的同学一条清晰的路径,也让已经用过F1的工程师能对照着补上一些容易忽略的细节。

熟悉STM32F1的人都知道,它最大的价值不是某个参数特别突出,而是整个生态极其成熟。从芯片本身到库函数、开发工具、参考手册、例程、论坛讨论,你能想到的问题几乎都有人踩过并留下了解决方案。这意味着你在网上搜一个问题,十有八九能直接找到针对F1的答案,对于学习和开发来说,这种“确定性”非常宝贵。

我最早接触F1的时候,用的是标准外设库,后来过渡到HAL库,再后来自己也手写过寄存器版本的程序。折腾了一圈之后,我对F1的理解反而更清晰了:它是一颗“内核简单、外设丰富、文档完善”的芯片。你不需要像玩高端处理器那样去考虑缓存一致性、MMU、复杂中断嵌套,只需要把时钟配好、外设初始化对、中断服务函数写得严谨,系统就能稳定跑起来。这篇文章的内容,就是围绕这几点展开的。

1. 先看清楚F1在ST产品线里的定位

1.1 型号命名规则:看标号就能猜到芯片能力

STM32F1系列最常见的几个子系列是F101、F102、F103、F105、F107,其中出镜率最高的是F103。命名规则其实很有规律,以“STM32F103C8T6”为例,拆开来理解:

  • STM32是品牌前缀;
  • F代表通用型Flash系列,与之对应的是L系列(低功耗)、H系列(高性能)等;
  • 103是具体产品线,代表“增强型”,主频最高72MHz、Flash最大512KB、RAM最大64KB,外设也最全;
  • C代表引脚数,C对应48脚,R对应64脚,V对应100脚,Z对应144脚;
  • 8代表Flash容量,6对应32KB,8对应64KB,B对应128KB,C对应256KB,D对应384KB,E对应512KB;
  • T代表封装形式,T是LQFP封装;
  • 6代表温度范围,6是-40到85℃工业级,7是-40到105℃。

有了这套规则,你看到“STM32F103RCT6”就能立刻反应出来:64脚、256KB Flash、LQFP封装、工业级温度范围。这个能力在选型时非常实用,因为F1系列型号很多,数据手册里不可能每个型号都翻一遍,看懂命名规则能帮你快速缩小范围。

实际选型时,还要区分F101、F103、F105/107这几个子系列。F101是基本型,主频36MHz,没有USB、没有CAN;F103是增强型,主频72MHz,USB、CAN、ADC、定时器基本都齐了;F105/107则带有USB Host/OTG和以太网MAC,适合做需要网络或USB主机通信的场景。如果你只是做电机控制、传感器采集、简单人机交互,F103的性价比和资料丰富度是最高的。

1.2 F1、F0、F4、G4之间的取舍:为什么F1还值得选

经常有人问:现在F0那么便宜、F4性能那么强,为什么还要用F1?我的看法是,F1的最大优势不在于性能,而在于“生态成熟度”和“资源的确定性”。

拿F0来说,它基于Cortex-M0内核,主频48MHz,价格确实低,但外设和库的支持相比F1要弱一些,很多资料和例程都是基于F103写的,移植到F0需要调整的地方不少。F4基于Cortex-M4F内核,带FPU和DSP指令,主频168MHz,性能确实强很多,但功耗和成本也上去了。如果你只是用一颗MCU做逻辑控制、通信转发、数据采集,F1的72MHz主频完全够用,没必要为了性能多花钱、多花精力。

还有一个很现实的因素:很多企业量产项目里已经在用F1,硬件方案、测试用例、供应链都很成熟。作为工程师,接手这类项目时最怕的不是芯片性能不够,而是换一颗芯片后所有东西都要重新验证。所以F1在量产项目里仍然有非常大的保有量。学习阶段用F1还有一个额外好处:网上教程、例程、答疑帖数量是所有STM32系列里最多的,遇到问题基本都能搜到答案,入门成本非常低。

2. 内核与外设:从根本上决定了你的代码怎么写

2.1 Cortex-M3内核的几个关键特性

STM32F1的内核是ARM Cortex-M3,主频最高72MHz。这颗内核虽然不复杂,但有几个特性是理解F1编程模型的关键。

第一个是中断控制器NVIC。Cortex-M3支持最多240条中断,但F1具体实现了多少要看芯片型号。F103系列实现了60个可屏蔽中断通道,支持16级可编程优先级(4位优先级位,实际使用的是高4位,所以是0~15级)。这里有个容易踩坑的地方:Cortex-M3的优先级数字越小,中断优先级越高。也就是说,你配置NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0,这个中断的抢占优先级是最高的,不是最低的。很多新手把0当成最低优先级,结果中断一直不按预期触发,排查半天才发现是优先级理解反了。

第二个是位带操作。Cortex-M3支持位带区,在F1上,SRAM的0x20000000~0x200FFFFF区域和片上外设的0x40000000~0x400FFFFF区域各有一段对应的位带别名区。通过操作别名区地址,你可以实现对单个比特的原子读改写。这个功能在F1上实现GPIO翻转非常方便,经典的点灯代码就是通过位带操作实现的。不过实际开发中,直接用库函数或者寄存器操作也已经很快,位带更多是理解层面的价值。

第三个是低中断延迟。Cortex-M3引入了尾链和晚到中断机制,中断响应延迟只有12个时钟周期,中断切换时能省掉一部分现场保存和恢复的开销。在F1@72MHz下,这个延迟是纳秒级的,对于大多数控制类应用来说完全够用。写中断服务函数时,你只需要保证ISR尽量短、不要在ISR里做耗时操作,系统调度就不会出问题。

2.2 时钟树:F1最容易绕晕但又必须搞懂的地方

时钟系统是F1的一大门槛,很多程序“跑不起来”或者“串口乱码”,根源都是时钟没配好。F1的时钟源有四种:HSI(内部高速RC,8MHz)、HSE(外部高速晶振,通常接8MHz)、LSI(内部低速RC,40kHz)、LSE(外部低速晶振,通常接32.768kHz)。

系统时钟SYSCLK的常见配置路径是:HSE晶振 -> PLL倍频 -> SYSCLK -> AHB总线 -> APB1/APB2外设总线。F103的最高配置是HSE 8MHz,PLL倍频9倍,得到72MHz SYSCLK。这里要注意APB1和APB2的最高频率不同:APB2最高72MHz,APB1最高36MHz。如果某个外设挂在APB1上,你给它提供超过36MHz的时钟,外设就可能工作异常。

时钟树里有几个容易出错的细节:

  • APB1上的定时器时钟会自动倍频。如果APB1分频系数是2(即APB1=36MHz),那么APB1上的定时器时钟是72MHz,而不是36MHz。很多人在计算定时器分频时忘了这个倍频,导致定时时间不对。
  • ADC时钟最高14MHz。ADC挂靠在APB2上,APB2是72MHz,所以ADC的分频至少是6分频(72/6=12MHz)。配置不好会导致ADC采样值跳动明显。
  • 使用HSE时,如果晶振不起振,系统会自动切换到HSI,但此时主频变成8MHz。这时程序里所有依赖72MHz的延时、通信波特率都会出错。

系统上电后默认使用HSI 8MHz作为系统时钟,所以你下载程序后如果没配置时钟树,外设的时钟频率和你预期完全不一致,比如串口波特率配置成115200,实际通信速率却完全不对。这就是为什么F1工程的初始化代码里,第一步基本都是配置时钟。

2.3 核心外设的定位:GPIO、USART、TIM、ADC、SPI、I2C

F1的外设矩阵已经覆盖了绝大多数常规嵌入式应用:

  • GPIO:F103最多有GPIOA~GPIOG共112个IO,每个IO都可以配置为输入、输出、复用功能或模拟输入,输出模式还分推挽和开漏。F1的GPIO翻转速度最快50MHz,对于普通信号控制绰绰有余。
  • USART:F103有最多5个USART,支持同步/异步模式、硬件流控,波特率最高4.5Mbps。USART1挂在APB2上,USART2/3挂在APB1上。
  • 定时器:F103有最多8个定时器,包括2个高级定时器TIM1/TIM8、4个通用定时器TIM2/3/4/5、2个基本定时器TIM6/TIM7。高级定时器支持互补输出和死区插入,直接驱动H桥或三相逆变器很方便。
  • ADC:F103有2个12位ADC,最多支持16个外部通道,规则组和注入组的概念让多通道采样更灵活。
  • SPI:F103有2个SPI,支持全双工、半双工,速率最高18Mbit/s。
  • I2C:F103有2个I2C,支持标准模式100kHz和快速模式400kHz。

这里想多说一句I2C。F1的I2C硬件模块因为一些设计上的问题,早期版本使用时偶尔会出现总线锁死现象。很多工程师(包括我)在项目里宁可自己用GPIO模拟I2C时序,也不愿意用硬件I2C。我个人的建议是:如果你用新的库或驱动,硬件I2C一般没问题;但如果你的项目对稳定性要求很高,或者你在排查问题时发现I2C总线卡死,最简单的方案就是切换到软件模拟I2C,时序自己掌握,反而更可控。

3. 开发环境与工程搭建:选对路,后面能省大量时间

3.1 标准外设库、HAL库还是寄存器

这个问题几乎每个用F1的人都会遇到。我的理解是:F1官方支持的标准外设库(SPL)已经停止更新,但它是F1最经典、最稳定的固件库。HAL库是ST目前的通用库,优点是风格统一,适合在多个系列之间移植,缺点是代码量大、抽象层较多,实时性和代码体积不如标准外设库,而且HAL库在F1上的支持其实不如标准外设库贴切。

如果你刚开始学F1,我推荐先跑一遍标准外设库的例程,因为它的风格接近寄存器操作,函数命名直接,你能比较容易把库函数和芯片外设的寄存器对应起来。等你理解了外设的工作原理,再迁移到HAL库就很容易了。

还有一种方案是干脆用寄存器操作。对于简单的项目,点灯、按键扫描、串口发送,寄存器代码反而更直观,整个工程就几个文件,编译下载都很快。我见过一些老工程师做量产项目依然用寄存器方式,主要原因就是可控、没有隐藏逻辑、调试方便。不过寄存器方式对阅读手册的能力要求高,不适合入门阶段硬啃。

3.2 最小系统与硬件设计的核心细节

F1的最小系统包含供电、复位、时钟、启动模式和调试接口。

供电方面,F103的工作电压是2.0V到3.6V,内核电压通过内部稳压器提供,标准设计是VDD接3.3V,每个电源引脚旁边放一个100nF去耦电容,VBAT引脚接电池或直接接3.3V。如果用到ADC,VDDA和VSSA建议额外加滤波,模拟参考电压VREF+接一个干净的3.3V。

复位电路用10kΩ上拉电阻加100nF电容接地,NRST引脚外部加一个按键到地,这是最常见的方案。也可以直接用STM32的内部上电复位电路,外部电路可以简化,但产品化设计时我还是建议保留外部RC复位。

时钟电路是F1系统稳定性的重点。8MHz晶振的两个引脚分别接约10pF到20pF的负载电容,电容具体值参考晶振手册。布局时晶振要尽量靠近芯片的OSC_IN和OSC_OUT引脚,走线短而直,不要过孔,周围不要走高频信号。很多批量生产的板子出现“部分能跑部分不能跑”的问题,多半就是晶振布局或负载电容选得不合适。

启动模式由BOOT0和BOOT1引脚决定。BOOT0=0,BOOT1任意,从主Flash启动,这是正常模式。BOOT0=1,BOOT1=0,从系统存储器启动,也就是进入ISP下载模式。BOOT0=1,BOOT1=1,从SRAM启动,调试时偶尔用。量产设计时,BOOT0一般直接接10kΩ下拉电阻到地,避免用户误操作进入ISP模式。

调试接口用SWD即可,四根线:SWDIO、SWCLK、GND、VCC,比JTAG少两根线,也省引脚。当然ST-Link的JTAG口也能用,但SWD在F1上更常用,因为它的速度快、占用IO少,而且下载调试都很稳定。

3.3 工程初始化流程:从复位向量到main

我建议你把“启动文件 -> 系统初始化 -> 外设初始化 -> 主逻辑”这条链理解透。

启动文件(startup_stm32f10x_hd.s)是汇编写的,作用是定义中断向量表、分配堆栈、调用SystemInit函数初始化时钟、然后跳转到main。如果你用的是标准外设库,SystemInit函数在system_stm32f10x.c里,它会默认配置时钟到72MHz(通过宏定义选择)。如果你用的是HAL库,类似的工作在HAL_Init和SystemClock_Config里完成。

实际工程中,有几个容易忽视的坑:

  • 中断向量表的位置。如果你把程序放在Flash起始地址0x08000000,默认没问题。但如果你做Bootloader,App程序放在0x08008000之类的位置,必须在App初始化时重新设置向量表偏移。F1没有专门的VTOR寄存器,需要借助系统存储器和中断向量重映射。F105/107可以在选项字节里设置,但F103传统的做法是通过修改内存映射来实现。简单说,带Bootloader的F103方案要特别小心中断向量重映射,否则App里的中断一个都不会触发。

  • 堆栈大小的设置。启动文件里Stack_Size和Heap_Size默认值通常是0x400(1KB)和0x200(512B)。如果你用了printf、浮点格式化输出或者较大的局部数组,Stack_Size要加大,例如改成0x1000(4KB)。否则程序跑着跑着突然进入HardFault,很可能就是栈溢出了。

  • 优化等级对时序的影响。用Keil或者GCC编译F1工程时,当你把优化等级从-O0调到-O2,有些依赖空循环延时的代码时序会变化。如果你的项目里有软件延时或者用延时模拟时序,注意在改动优化等级后重新测试。

3.4 常用开发工具链的搭配建议

编译环境我用过Keil MDK、IAR、GCC(STM32CubeIDE和命令行工具链),简单说下感受。

Keil MDK在F1开发里用户量最大,工程模板和破解方法到处都有,调试界面直观,但代码编辑器相对老旧。Keil的编译速度不慢,工程配置也比较简单,尤其是你用标准外设库的时候,Keil的体验是很好的。需要注意的是,Keil 5之后需要安装对应的器件支持包(DFP),否则创建工程时找不到STM32F103型号。

STM32CubeIDE是ST官方推出的免费IDE,基于Eclipse,自带GCC工具链和STM32CubeMX图形化配置功能。如果你用HAL库,我个人建议直接用STM32CubeIDE,因为整个流程是通畅的:CubeMX配置引脚和时钟生成工程,CubeIDE编译调试,代码迁移也方便。唯一缺点是IDE启动和编译相对慢一些,介意的同学可以继续用Keil。

查看工程代码建议用source insight,或者直接VS Code加插件,这类编辑器看代码的体验比Keil好不少。如果你用标准外设库做开发,代码量大了之后,用VS Code看代码、Keil编译下载,是我个人比较推荐的效率组合。

4. 一个能直接上手的实战:按键、LED、串口与定时器

4.1 硬件连接与功能需求

我们用最经典的配置来做一个完整的入门例程:STM32F103C8T6核心板,板载8MHz晶振,LED接PC13(因为很多最小系统板把LED接到了PC13),一个按键接PA0并外部下拉到GND(按下时PA0变成高电平),串口1通过USB转TTL模块连接电脑,PA9是USART1_TX,PA10是USART1_RX。

功能需求很简单:系统启动后LED以1Hz频率闪烁;按一下按键,串口发送一条按键事件消息;串口每收到一条“led on”指令,LED常亮;收到“led off”指令,LED熄灭;收到“toggle”指令,LED切换状态;任意未知指令,回发提示信息。

这个例程虽然小,但覆盖了GPIO、外部中断、USART中断、定时器中断、时钟配置、串口协议处理几个F1开发里最基础也最重要的知识点。做完这个,F1的常用模块基本就过了一遍。

4.2 关键代码与参数计算

先看时钟初始化。启动文件调用SystemInit之后,我们用宏定义让SystemInit把系统时钟配置到72MHz。标准外设库中通过stm32f10x.h里的宏#define SYSCLK_FREQ_72MHz 72000000来实现,配合system_stm32f10x.c完成整个时钟链路配置。如果你不用固件库而选择寄存器方式,时钟初始化的代码是这样的:

void SystemClock_Config_72MHz(void) { // 使能HSE RCC->CR |= RCC_CR_HSEON; while (!(RCC->CR & RCC_CR_HSERDY)); // 使能PLL,配置为HSE 8MHz * 9 = 72MHz RCC->CFGR |= RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9; // 配置AHB不分频,APB1二分频,APB2不分频 RCC->CFGR |= RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE1_DIV2 | RCC_CFGR_PPRE2_DIV1; // 使能PLL并等待锁定 RCC->CR |= RCC_CR_PLLON; while (!(RCC->CR & RCC_CR_PLLRDY)); // 切换系统时钟到PLL RCC->CFGR |= RCC_CFGR_SW_PLL; while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL); }

这段寄存器代码比库函数精简得多,逻辑也非常清晰:开HSE、等待稳定、配置PLL倍增因子、配置总线分频、启动PLL、等待锁定、切换系统时钟。理解了这段代码,再回过头看库函数SystemInit里的一堆RCC->CR和RCC->CFGR操作,就明白是在干什么了。

定时器部分,我们配置TIM2为更新中断,定时周期1秒。这里要把定时器时钟先算清楚。F103的TIM2挂在APB1上,APB1分频为2,所以APB1为36MHz,但APB1上的定时器时钟自动加倍到72MHz。因此TIM2的输入时钟是72MHz。

定时器配置为向上计数,设定预分频器PSC为7200-1,计数周期ARR为10000-1,这样定时器时钟是72MHz。f_TIM = 72MHz / 7200 = 10kHz,也就是说计数器每0.1毫秒加1。f_Update = 10kHz / 10000 = 1Hz,即每秒触发一次更新中断。如果你想改成500ms,ARR设为5000-1即可。

库函数写法如下:

void TIM2_Init(void) { TIM_TimeBaseInitTypeDef TIM_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InitStructure.TIM_Prescaler = 7200 - 1; TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_InitStructure.TIM_Period = 10000 - 1; TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &TIM_InitStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); }

定时器中断回调函数里翻转LED状态即可。

串口部分,配置USART1为115200波特率、8位数据、1位停止位、无校验,并启用接收中断。计算波特率时,USART1挂载在APB2上,APB2为72MHz,直接将72MHz代入USART_BRR计算。想要115200波特率,则USARTDIV = 72000000 / (16 * 115200) = 39.0625,取整后写入USART_BRR值对应39.0625附近。库函数只用调用USART_Init即可完成,但理解这个计算过程能帮你在遇到乱码时快速判断是时钟问题还是波特率问题。

串口中断服务函数中,读取USART_ReceiveData(USART1)拿到接收字节,放入一个环形缓冲区,主循环中再从缓冲区取数据并解析指令。

4.3 下载调试与实测现象

程序编译后,用ST-Link通过SWD接口下载,打开串口调试助手,波特率设为115200。上电后应该能看到LED以1Hz频率闪烁,串口助手在按下按键时打印出按键消息。发送“led on”后LED停止闪烁变为常亮,发送“led off”后LED熄灭,发送“toggle”后LED状态反转,发送任意未知指令则回发提示。

实测过程中有几个现象值得关注。如果用USB转TTL模块连接,注意共地,PC的USB地和板子的GND要接一起,否则串口通信会出现随机乱码或完全无数据。此外,如果板子上的USB转串口芯片使用的是5V供电,而MCU是3.3V,注意串口电平是否兼容,很多板载方案已经做了电平转换,但外接模块时仍要确认。

我建议把这个例程跑通之后,再手动改一改代码,比如改预分频值、改波特率、改中断优先级,观察现象变化。这种动手试错带来的理解,比反复读手册要深刻得多。

5. 常见问题与排查技巧实录

5.1 程序下载失败与SWD接口异常

这是一个非常典型的入门问题:板子第一次下载正常,第二次下载提示“No target connected”或者“Connection error”。多数时候,问题出在代码里把SWD引脚重映射成了普通GPIO,比如把PA13/PA14(即SWDIO/SWCLK)配置成GPIO输出,调试接口就被关闭了。

解决办法很简单:按住板子上的复位键不放,在IDE里点击下载,等进入下载状态后松开复位键。这样调试器可以在芯片复位后的短暂窗口内连接上。如果这种方法不成功,把BOOT0拉高,使芯片上电后从系统存储器启动,此时SWD接口仍然可用,下载成功后把BOOT0拉回低电平并重新上电,程序恢复正常执行。

排查时先检查四根SWD线是否接对,再检查目标板供电是否正常,最后怀疑芯片是否死锁。我遇到过一个情况是目标板的NRST引脚被误接长线引入干扰,下载偶尔失败,处理方法是换短连接线或加一个RC滤波。

5.2 串口乱码与数据不对

串口乱码的第一排查方向是时钟频率。如果你程序里配置的HSE是8MHz但板子上实际焊接的是12MHz晶振,或者反过来,那么串口波特率会完全不对。最直接的办法是用示波器测MCO引脚(PA8)输出的时钟信号,或者改程序配置,确保SystemInit里的HSE值和实际晶振一致。

第二个排查方向是电平不匹配。3.3V的MCU和5V的USB转串口芯片通信时,如果没有电平转换,TX侧可能过压,长期使用会损坏引脚,短时间则表现为通信不稳定、偶发乱码。

第三个方向是波特率误差。如果系统时钟本身有偏差,比如使用了内部HSI而没有校准,8MHz的HSI本身精度有限,会导致115200波特率下产生累计误差,通信距离或速率稍高就出错。工程上如果对通信稳定性要求高,优先使用HSE外部晶振。

5.3 中断不触发或者触发频繁

中断不触发时,我会按这个顺序排查:先确认中断使能是否打开,NVIC配置是否执行,GPIO时钟和外设时钟是否开启,然后再看标志位是否及时清除。F1很多外设中断标志位需要软件主动清除,比如USART的接收完成标志位,如果不处理,会导致中断反复进入或后续数据无法接收。

如果你用的是标准外设库,有一个常见错误是在中断服务函数里没有调用对应的库函数清除标志位。比如USART中断里只调用了USART_ReceiveData读取数据后未调用USART_ClearITPendingBit,就会出现中断服务函数反复执行的现象。正确操作是在读取数据寄存器后清除标志位。如果用的是HAL库,HAL库在某些外设中断回调里已经做了清标志位动作,不用重复清,否则反而可能出问题。

还有一种情况是中断优先级配置反了,导致某些中断永远抢占不了。我之前提到Cortex-M3优先级数字越小优先级越高,如果你把高频率的外设中断配置成低优先级,把低频率的配置成高优先级,就可能发生相反的效果。从调试器里查看NVIC寄存器,可以确认实际配置的是哪个优先级。

5.4 系统跑飞与HardFault排查

程序跑飞进入HardFault是嵌入式开发里最让人头痛的问题之一,不过F1的调试支持很好,可以快速定位。发生HardFault时,先暂停调试,打开Call Stack窗口,查看当前PC指针和栈回溯。如果栈回溯失效,查看HardFault相关寄存器(SCB->HFSR、SCB->CFSR、SCB->BFAR等),它们能告诉你异常类型。

最常见的HardFault原因包括:访问空指针或野指针、数组越界、栈溢出、外设时钟未开启就访问外设寄存器、在中断服务函数里调用了一个很大的函数导致嵌套断点溢出。排查时把代码精简到最小复现,二分法定位,最有效。

如果你手头没有调试器,也可以靠状态灯辅助定位。在初始化各外设之间加LED翻转或串口打印,看程序跑到哪里不再输出,就能锁定问题区域。这个方法在老式调试环境里非常实用。

5.5 一个个人强烈的建议:验证复位与独立看门狗时要注意的点

F1内置独立看门狗(IWDG),常用于防止程序跑飞。但它在调试时有个麻烦:一旦使能,即使程序停在断点,看门狗仍然在计数,会导致芯片复位,调试体验很差。调试阶段可以先关闭IWDG,等程序稳定后再打开。如果你必须打开看门狗调试,可以在进入调试后手动执行IWDG->KR = 0xAAAA喂狗,或者通过调试器把IWDG时钟停掉。

另外提醒一点,F1的IWDG使用LSI时钟,而LSI的精度不高且随温度漂移。如果你用看门狗做超时保护,超时时间本身会有误差,这不是bug,而是芯片特性。设计时预留裕量即可。

最后分享一点个人的使用心得

我自己在项目里使用STM32F1系列已经很多年,从最早的F103C8T6小板子做实验,到后来量产设备上用到F103ZET6,我的体会是:这颗芯片并不是在所有场景下都是最优解,但在“常规控制、成本敏感、开发周期紧、技术资料少“的项目里,F1往往是最稳妥的选择。

我习惯拿到一块陌生板子之后先做三件事:一是确认供电和地线正常,二是用示波器看晶振波形是否起振,三是把最小工程下载进去点个灯。这三件事做完,板子的硬件基本就验证过了,后面写业务逻辑心里也踏实。很多时候你以为是代码问题,最后发现是板子上某个电容虚焊或者地线没接好,提前验证能省掉太多没必要的排障时间。

如果你正在学习或者即将接手F1项目,我有几个比较接地气的建议:手边常备一块F103C8T6最小系统板,成本很低;完整读一遍参考手册中“Clock”和“GPIO”两章,别偷懒;例程不要贪多,把一个工程从寄存器到库函数到HAL库各写一遍;遇到问题先查勘误表(Errata Sheet),再查论坛,最后再问别人。这些经验不一定新潮,但都是踩过坑之后沉淀下来的,照着做可以少走很多弯路。

STM32F1系列的寿命很长,虽然近些年新出的芯片在性能上有明显优势,但F1的稳定性和生态不是一朝一夕能超越的。对于嵌入式开发者来说,把F1理解透,等于打下了扎实的裸机开发和MCU应用基础,后续切换到其他内核、其他厂商的芯片,你会发现很多概念都是相通的。这就是我推荐初学者把F1作为“第一颗MCU”的根本原因。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进