ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32Cube_FW_F1_V1.8.0固件包拆解与最小HAL工程搭建避坑

STM32Cube_FW_F1_V1.8.0固件包拆解与最小HAL工程搭建避坑

简介:面向STM32F1系列微控制器的固件开发包,适合嵌入式初学者、竞赛选手与需要快速搭建原型的工程师,用于解决外设驱动、中间件集成和工程初始化配置繁琐的问题。压缩包约95.73MB,文件总数与类型明细暂未提供;按描述内容涵盖HAL与LL驱动、USB OTG、FatFS、FreeRTOS等中间件、应用示例及HAL/LL文档,并支持Keil MDK、IAR与GCC ARM等开发环境。已有1096人学习,说明其在STM32F1开发群体中具有一定参考价值。读者可据此熟悉ADC、CAN、DMA、GPIO、I2C、SPI、TIM、USART等外设的API用法,结合示例理解从基础功能到复杂应用的工程组织,并借助CubeMX生成的初始化代码减少底层配置时间,把精力放在应用逻辑与方案验证上。

1. 一个 200MB 的压缩包,为什么老项目还在用它

上周有人拿着一个编译不过的工程来找我,stm32f1xx_hal_conf.h找不到、_estack未定义、串口打出来全是乱码。最后发现他只是把 STM32Cube_FW_F1_V1.8.0 解压后,直接拿Projects里某个评估板工程改芯片型号,驱动层和启动文件压根没对齐。这个包本身没问题,问题在于它是个“全家桶”,不是“即插即用工程”。

STM32Cube_FW_F1_V1.8.0 是 STM32F1 全系的固件集合:CMSIS 内核与设备文件、STM32F1xx_HAL_Driver 驱动层、各评估板与 Nucleo 的完整例程,外加 USB Device/Host、FatFs、FreeRTOS 这些中间件。它解决的是“从零把 F1 跑起来”这件事——启动文件、时钟树、外设初始化、链接脚本,全都有参考实现。适合两类人:一类是维护十年前 F103 老项目、需要一份稳定基线的人;另一类是刚接手 F1 板子,想把 HAL 库摸清楚而不是靠 CubeMX 点几下就完事的人。

2. 拆开压缩包:Drivers、Projects、Middlewares 三层的依赖关系

解压之后目录看着乱,其实职责边界很清楚。搞不清这三层谁依赖谁,后面一定会把“例程能跑”和“我的板子能跑”混为一谈。

2.1 顶层目录各自负责什么

先看一张职责表,后面所有操作都围绕它展开:

目录里面是什么什么时候需要动它
Drivers/CMSIS内核头文件、设备头文件、system_stm32f1xx.c、各工具链的启动文件模板换芯片型号、换编译器时
Drivers/STM32F1xx_HAL_Driverstm32f1xx_hal_xxx.c/h与stm32f1xx_hal_conf_template.h裁剪外设、改HSE_VALUE、改断言开关
MiddlewaresUSB Device/Host 库、FatFs、FreeRTOS工程确实用到时才加入
Projects按“板子名/外设名/工具链”组织的完整工程抄时钟配置、抄外设初始化顺序
Utilities板级 BSP,如 LCD、EEPROM 驱动一般不用,评估板专用

关键点在于:Drivers是“可复用的”,Projects是“可参考但不可直接复用的”。很多人翻车的起点,就是把Projects当成模板整包拷贝,结果里面那套评估板的晶振频率、引脚定义、链接脚本全带过来了,跟自己手上的板子对不上。

2.2 HAL 驱动层与 CMSIS 的调用链

HAL 不是独立存在的,它压在两件事上面:编译器宏选中的设备头文件,以及启动文件里定义的中断向量表。链条是这样的:

/* stm32f1xx.h 里靠编译宏决定包含哪个具体型号的头文件 */ #if defined(STM32F103xB) #include "stm32f103xb.h" #elif defined(STM32F103xE) #include "stm32f103xe.h" #else #error "Please select first the target STM32F1xx device used in your application" #endif

这段代码的含义是:工程里必须定义STM32F103xB这类型号宏,否则整个 HAL 编译不过,报错就是上面那句#error。很多人看到这个报错去翻main.c,其实应该在工程预定义宏里找。

除了型号宏,还必须定义USE_HAL_DRIVER,它决定stm32f1xx.h是否把 HAL 的头文件引进来;不定义,HAL_Init()直接找不到声明。

另一件容易被忽略的事:stm32f1xx_hal_conf.h在包里叫stm32f1xx_hal_conf_template.h,是个模板。必须复制一份改名为stm32f1xx_hal_conf.h,放在工程的 include 路径里。模板里有一堆#define HAL_XXX_MODULE_ENABLED,注释掉哪个,对应外设的驱动就不参与编译。这一步没做,会出现“头文件里明明有HAL_ADC_Init的声明,链接时却说未定义”的怪现象——因为对应的.c被条件编译排除了。

2.3 从 Projects 里挑一个最接近的例程

挑例程的原则是“芯片型号优先,外设其次,板子最后”。具体做法:

  1. 先确认自己的芯片 Flash 容量档位。中容量(64/128KB)对应startup_stm32f103xb.s,大容量(256KB 以上)对应startup_stm32f103xe.s,启动文件选错,中断向量表就偏移了。
  2. 在Projects下找芯片型号一致的目录,再找外设最接近的子目录。比如要调 UART 中断接收,就找UART_IT这类例程,而不是找 LED 例程再自己加。
  3. 只抄三样东西:SystemClock_Config()的时钟参数、对应外设的MspInit引脚与时钟使能、NVIC 的优先级设置。工程文件、链接脚本、板级宏定义全部按自己的板子重写。

用一段 bash 快速看清明容量档位的启动文件都有哪些:

# 进入 CMSIS 的设备模板目录,列出所有启动文件 cd Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates ls arm/ | grep startup # 典型输出里会同时存在 xB / xE / x8 等档位,按自己芯片选 # startup_stm32f100xb.s startup_stm32f103xb.s startup_stm32f103xe.s ...

参数说明:arm/下是给 armcc 用的,gcc/下是给 arm-none-eabi-gcc 用的,iar/下是 IAR 格式。后缀.s只是汇编语法不同,内容里的向量表顺序完全一致。别把 armcc 的启动文件喂给 GCC,报错会是一堆无法识别的伪指令,而不是“文件不对”这种友好提示。

3. 手搭最小 HAL 工程:从启动文件到第一个 LED

CubeMX 生成的工程能跑,但它把很多“为什么”藏起来了。自己搭一遍最小工程,是为了把HAL_Init()到main()之间到底发生了什么搞清楚。这一步做完,后面任何 F1 工程出问题,你都知道该从哪一层往下查。

3.1 启动文件与链接脚本的对应关系

启动文件负责三件事:初始化栈顶指针、填中断向量表、在Reset_Handler里调用SystemInit再跳到main。链接脚本负责告诉链接器 Flash 和 RAM 的起始地址与长度。两者必须匹配,否则就是经典的“程序下载进去了但不跑”。

先建一个最小目录结构:

mkdir -p demo/{Core/Inc,Core/Src,Drivers/CMSIS,Drivers/HAL,Startup} # 复制设备头文件与内核头文件 cp -r STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Device/ST/STM32F1xx/Include/* demo/Drivers/CMSIS/ cp -r STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Include/* demo/Drivers/CMSIS/ # 复制启动文件(按自己的芯片档位选) cp STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc/startup_stm32f103xb.s demo/Startup/ # 复制 system 文件 cp STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/system_stm32f1xx.c demo/Core/Src/

逻辑说明:设备头文件(stm32f103xb.h等)定义了寄存器地址和外设结构体,内核头文件(core_cm3.h等)定义了 NVIC、SysTick 这些内核外设的操作。两者分属不同目录,缺一个就是几百个未定义符号。

参数说明:system_stm32f1xx.c里的SystemInit()在进入main()之前执行,默认把时钟切到 HSI 8MHz 并设置向量表位置。它不会帮你配 PLL,所以main()里必须自己再调一次时钟配置,否则你以为是 72MHz,实际跑在 8MHz 上,串口波特率整体偏 9 倍。

3.2 最小 main.c 骨架与 MspInit 回调

下面这段是可以直接抄的骨架,只做一件事:PC13 翻转。

#include "stm32f1xx_hal.h" static void SystemClock_Config(void); static void Error_Handler(void); int main(void) { /* HAL_Init 内部会做三件事:设 Flash 预取与等待周期、设 NVIC 优先级分组、启动 SysTick */ HAL_Init(); /* 必须在 HAL_Init 之后调用,因为它依赖 HAL 内部的时基 */ SystemClock_Config(); /* 使能 GPIOC 时钟,F1 的外设时钟默认全关,不开就是写寄存器也没反应 */ __HAL_RCC_GPIOC_CLK_ENABLE(); GPIO_InitTypeDef gpio = {0}; gpio.Pin = GPIO_PIN_13; gpio.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */ gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOC, &gpio); while (1) { HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); HAL_Delay(500); /* 依赖 SysTick 中断,中断被关掉这里会死等 */ } } static void SystemClock_Config(void) { RCC_OscInitTypeDef osc = {0}; RCC_ClkInitTypeDef clk = {0}; osc.OscillatorType = RCC_OSCILLATORTYPE_HSE; osc.HSEState = RCC_HSE_ON; osc.HSEPredivValue = RCC_HSE_PREDIV_DIV1; /* F1 特有的 PLL 输入分频 */ osc.PLL.PLLState = RCC_PLL_ON; osc.PLL.PLLSource = RCC_PLLSOURCE_HSE; osc.PLL.PLLMUL = RCC_PLL_MUL9; /* 8MHz * 9 = 72MHz */ if (HAL_RCC_OscConfig(&osc) != HAL_OK) { Error_Handler(); } clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; clk.AHBCLKDivider = RCC_SYSCLK_DIV1; clk.APB1CLKDivider = RCC_HCLK_DIV2; /* APB1 上限 36MHz,不能 DIV1 */ clk.APB2CLKDivider = RCC_HCLK_DIV1; /* APB2 可到 72MHz */ /* 48MHz 以上必须插等待周期,72MHz 对应 LATENCY_2,填错会随机死机 */ if (HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_2) != HAL_OK) { Error_Handler(); } } static void Error_Handler(void) { __disable_irq(); while (1) { } }

逻辑说明:HAL_Init()里调用了HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4),也就是 4 位全部给抢占优先级,16 级抢占、0 级子优先级。如果你后面要上 FreeRTOS,这个分组必须保持,因为 FreeRTOS 的临界区依赖“所有优先级位都是抢占位”这个前提。

参数说明:HSEPredivValue这个字段别的系列没有,只有 F1 有。它的作用是给 PLL 的输入做 1 分或 2 分。板子上是 8MHz 晶振就写DIV1,是 16MHz 以上又不想换晶振,可以DIV2再乘。这个参数填错,表现是时钟能起来但频率不对,示波器量 MCO 引脚才能发现。

HAL_RCC_ClockConfig()的第二个参数是 Flash 等待周期。72MHz 必须FLASH_LATENCY_2,写成LATENCY_0的现象是:简单程序能跑,一进中断或者一开 DMA 就 HardFault,因为取指速度跟不上。

3.3 编译宏与优化选项

编译命令行需要三个关键宏,缺一个都编不过或者行为异常:

arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb \ -DSTM32F103xB -DUSE_HAL_DRIVER \ -ICore/Inc -IDrivers/CMSIS -IDrivers/HAL/Inc \ -O2 -ffunction-sections -fdata-sections \ -c Core/Src/main.c -o build/main.o

参数说明:-DSTM32F103xB决定设备头文件选哪份,型号写错会直接命中#error;-DUSE_HAL_DRIVER决定是否引入 HAL 头文件;-mcpu=cortex-m3 -mthumb是 F1 的内核与指令集,漏了-mthumb会编译出 ARM 指令,链接时和 Thumb 的启动文件对不上,报“互相调用的目标状态不兼容”。

-ffunction-sections -fdata-sections配合链接时的--gc-sections,能把没调用到的函数整段扔掉。F1 的 Flash 只有 64KB 到 512KB,HAL 全量链接进去很容易超标,这两个选项是后面第 6 章裁剪的基础。

4. 时钟树与中断优先级:F1 上最容易翻车的两块

F1 的出问题现场,八成集中在时钟和中断这两块。它们共同的特点是:配错了不一定立刻崩,可能跑几分钟才 HardFault,或者只在某个外设工作时才出错,查起来极费时间。

4.1 时钟配置里的三个硬约束

F1 的时钟树有三条不能破的线,破了就是随机故障而不是编译错误:

约束数值破了之后的典型现象
SYSCLK 上限72MHz芯片发烫、长时间运行死机,短期内看不出
APB1 上限36MHz定时器、USART2/3、I2C 全部异常,串口乱码
ADC 时钟上限14MHzADC 采样值乱跳,校准通不过

ADC 时钟这一条最隐蔽,因为它不在HAL_RCC_ClockConfig()里配,而是挂在 APB2 上再分频。72MHz 的 PCLK2 下,ADC 预分频必须选 6 分频,得到 12MHz。这一点在HAL_RCCEx_PeriphCLKConfig()或ADC的初始化里设置,很多从别的系列迁过来的人会漏掉。

/* ADC 时钟独立于系统时钟配置,必须在 ADC 初始化前设置 */ RCC_PeriphCLKInitTypeDef pclk = {0}; pclk.PeriphClockSelection = RCC_PERIPHCLK_ADC; pclk.AdcClockSelection = RCC_ADCPCLK2_DIV6; /* 72MHz / 6 = 12MHz,上限 14MHz */ if (HAL_RCCEx_PeriphCLKConfig(&pclk) != HAL_OK) { Error_Handler(); }

逻辑说明:RCC_PERIPHCLK_ADC这个标志告诉 HAL 只改 ADC 那一路分频,不影响其他外设。参数RCC_ADCPCLK2_DIV6是唯一在 72MHz 系统下合法的选择之一,DIV4会得到 18MHz,超过上限。

还有一个更前置的问题:HSE 起振失败。HAL_RCC_OscConfig()返回HAL_ERROR就是没起来。常见原因是晶振负载电容不匹配,或者HSE_VALUE宏写错。后者不会导致起振失败,但会导致所有基于时钟计算的外设(串口、定时器、延时)全部按错误频率工作。改HSE_VALUE的位置在stm32f1xx_hal_conf.h,改完必须全工程重编,只重编main.c不生效。

4.2 NVIC 优先级分组与 SysTick 的优先级陷阱

HAL_Init()定了 4 位抢占优先级,之后所有HAL_NVIC_SetPriority()的第二个参数范围是 0 到 15,数字越小优先级越高。这里有个 F1 特有的坑:HAL 把 SysTick 的优先级设成了最低(15)。

/* 中断优先级设置的标准写法 */ HAL_NVIC_SetPriority(USART1_IRQn, 5, 0); /* 抢占 5,子优先级 0 */ HAL_NVIC_EnableIRQ(USART1_IRQn);

参数说明:第一个参数是中断号,第二个是抢占优先级,第三个在 GROUP_4 分组下恒为 0。抢占优先级决定能不能打断正在执行的中断服务函数。

SysTick 优先级最低这件事带来的直接后果是:在任何优先级高于 15 的中断服务函数里调用HAL_Delay(),SysTick 无法抢占,计数器永远不增加,程序就死在那句HAL_Delay上。现象是“一进中断就再也出不来”,用调试器看会停在HAL_Delay的while循环里。这就是最典型的翻车场景之一,规避办法只有一个:中断服务函数里不用HAL_Delay,要做延时就用计数器或者干脆置个标志回主循环处理。

中断服务函数还有一处必须对齐:函数名要和启动文件里的向量表完全一致。启动文件里写的是USART1_IRQHandler,你在stm32f1xx_it.c里写成USART1_IRQHandlerr或者忘了在函数体里调用HAL_UART_IRQHandler(&huart1),表现都是“中断进不去”或者“进了一次再也没进第二次”——后者是因为状态标志没被 HAL 清掉。

4.3 串口波特率对不上的排查路径

串口乱码是最高频的问题,排查顺序应该是从时钟往下走,而不是一上来就怀疑 USB 转串口线。

先算一遍理论值。72MHz 的 PCLK2,115200 波特率,16 倍过采样下:

# USARTDIV = PCLK / (16 * BaudRate) = 72e6 / (16 * 115200) = 39.0625 # 整数部分 39 -> BRR 高 12 位;小数部分 0.0625 * 16 = 1 -> BRR 低 4 位 # BRR = (39 << 4) | 1 = 0x271

HAL 会自己算这个值,不用手填,但知道算法能帮你判断问题出在哪。排查清单按顺序走:

  1. 读SystemCoreClock变量,看是不是 72000000。不是,说明时钟没配对,先回 4.1。
  2. 看HAL_UART_Init()里的BaudRate字段。CubeMX 生成的代码有时会因为时钟树改动没重新生成,字段还停在旧值。
  3. 确认 GPIO 的复用模式。F1 的 USART1 在 PA9/PA10 上,必须设为GPIO_MODE_AF_PP,并且使能__HAL_RCC_AFIO_CLK_ENABLE()。少了 AFIO 时钟,引脚复用不生效,现象是能发不能收或者完全没有波形。
  4. 最后才怀疑线材和电平。

提示:F1 的 PA13/PA14/PA15/PB3/PB4 默认被 JTAG 占用。要把这些脚当普通 GPIO 或复用功能用,必须调用__HAL_AFIO_REMAP_SWJ_NOJTAG()关掉 JTAG,只保留 SWD。这个坑的迷惑性在于编译一切正常,就是引脚没输出。

5. 避坑记录:五条来自调试现场的翻车记录

下面五条都是反复出现、且新手很难靠读代码看出来的问题。每条按“现象—原因—解决”记录,遇到时可以对号入座。

5.1 现象:编译通过,下载后不运行,调试器停不下来

原因:启动文件选错档位。中容量芯片用了startup_stm32f103xe.s,向量表长度和实际 Flash 不匹配,Reset_Handler的地址对不上,或者栈顶指针被初始化到不存在的 RAM 区域。

解决:核对芯片型号与启动文件后缀。xB对应 64/128KB,xE对应 256KB 以上,x8对应 32KB。改完启动文件必须 Clean 重编,只做增量编译时链接器可能还引用着旧的.o。

5.2 现象:程序跑一段时间后进入HardFault_Handler,且和中断频率正相关

原因:中断优先级配置越界。HAL_NVIC_SetPriority()的抢占优先级参数填了大于 15 的值,或者分组被改成了NVIC_PRIORITYGROUP_0,此时只有 1 位抢占优先级,填 5 会被截断,实际生效的值和预期完全不同。

解决:检查HAL_NVIC_SetPriorityGrouping只被调用一次,且在第 4.2 节的建议范围内。把每个中断的抢占优先级列成表贴在代码旁边,值超出 0 到 15 一律改成合法值。

5.3 现象:串口能发不能收,或者接收第一个字节后就再也不进中断

原因:中断服务函数里没有调用HAL_UART_IRQHandler(&huart1)。HAL 的接收完成、错误标志清除、状态机推进全在这个函数里做,你只在里面写自己的业务逻辑,HAL 的内部状态就永远停在HAL_UART_STATE_BUSY_RX,后续的数据直接被丢弃。

解决:USART1_IRQHandler()第一行调HAL_UART_IRQHandler(&huart1),业务逻辑放在HAL_UART_RxCpltCallback()回调里。这样状态机和你的代码彻底解耦。

5.4 现象:加了看门狗之后,调试时程序总在启动后不久复位

原因:启动了 IWDG(独立看门狗),但调试时断点会让程序暂停,喂狗停止,看门狗超时触发复位。IWDG 一旦启动就无法关闭,只能复位芯片。

解决:调试阶段用DBGMCU冻结看门狗,或者干脆先不启动 IWDG,只在正式版里开。如果必须调试带看门狗的代码,把喂狗周期设置得比断点停留时间长,或者在喂狗前打条件断点。

5.5 现象:从旧版本固件包升级到 V1.8.0 后,原本正常的外设初始化返回HAL_ERROR

原因:HAL 驱动在不同子版本间有参数行为差异,比如某些HAL_XXX_Init()增加了对时钟是否使能的检查,顺序不对就直接返回错误;或者stm32f1xx_hal_conf.h里的默认HSE_VALUE与实际晶振不一致。

解决:不要整个Drivers目录覆盖升级。先把新版本的stm32f1xx_hal_conf_template.h与工程里现有的stm32f1xx_hal_conf.h做一次 diff,逐项确认宏定义;再用diff -r对比新旧 HAL 驱动目录,看自己用到的外设.c有没有改动。升级完必须跑一遍全外设回归,不能只测主流程。

6. 把固件包用薄:HAL 裁剪与体积验证

F1 的 Flash 从 64KB 起,HAL 全量链接进去,一个空工程就敢吃掉三四十 KB。资源紧的项目必须裁,而且裁完要有办法验证裁对了。我一般按这个顺序做:

第一步,在stm32f1xx_hal_conf.h里关模块开关。只保留实际用到的外设,其余HAL_XXX_MODULE_ENABLED全部注释掉。注意这只影响头文件的包含,对应的.c还在工程里,只是被条件编译成了空文件。

第二步,从工程的源文件列表里移除对应的stm32f1xx_hal_xxx.c。这一步才是真正减少编译单元。表里是常见模块与它的依赖关系,删的时候按行来,别删了hal_uart.c还留着hal_dma.c却发现编不过:

模块文件依赖的其它 HAL 文件说明
stm32f1xx_hal_uart.chal_dma.c(用 DMA 时)不用 DMA 可不加
stm32f1xx_hal_adc.c无但要保证 ADC 时钟已配
stm32f1xx_hal_tim.c无基本定时器与高级定时器共用此文件
stm32f1xx_hal_rcc.c无任何工程都不能删

第三步,开启链接期裁剪:-Os优化、-ffunction-sections -fdata-sections编译、-Wl,--gc-sections链接。这一步能把没有被任何路径引用的函数整段丢掉,效果通常比手删源文件还明显。

做完之后必须验证,不能靠“感觉小了”。用size看总量,再翻.map文件看谁在吃空间:

# 看最终三个段的占用:text 是 Flash,data+bss 是 RAM arm-none-eabi-size build/demo.elf # 在 map 文件里按 text 段大小排序,找出占用最大的目标文件 grep -E "^\s+\.text" build/demo.map | sort -k2 -n -r | head -20

参数说明:size输出的text段包含代码和只读数据,是烧进 Flash 的部分;data是已初始化的全局变量,既占 Flash 又占 RAM;bss只占 RAM。裁剪的目标是让text逼近但不超过 Flash 容量,同时给data+bss留出至少 20% 的栈空间余量,否则运行期一深调用就溢栈。

.map文件里的排序结果会直接告诉你哪个模块最值得删。我见过一个项目,最大的一个.o不是业务代码,而是printf的浮点格式化,占了将近 8KB。把%f换成定点打印,一下就回来了。从那以后我每次裁剪完都强制走一遍size加 map 排序,不看到具体数字不签字。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表