
简介面向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 里挑一个最接近的例程挑例程的原则是“芯片型号优先外设其次板子最后”。具体做法先确认自己的芯片 Flash 容量档位。中容量64/128KB对应startup_stm32f103xb.s大容量256KB 以上对应startup_stm32f103xe.s启动文件选错中断向量表就偏移了。在Projects下找芯片型号一致的目录再找外设最接近的子目录。比如要调 UART 中断接收就找UART_IT这类例程而不是找 LED 例程再自己加。只抄三样东西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 工程从启动文件到第一个 LEDCubeMX 生成的工程能跑但它把很多“为什么”藏起来了。自己搭一遍最小工程是为了把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 -mcpucortex-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 头文件-mcpucortex-m3 -mthumb是 F1 的内核与指令集漏了-mthumb会编译出 ARM 指令链接时和 Thumb 的启动文件对不上报“互相调用的目标状态不兼容”。-ffunction-sections -fdata-sections配合链接时的--gc-sections能把没调用到的函数整段扔掉。F1 的 Flash 只有 64KB 到 512KBHAL 全量链接进去很容易超标这两个选项是后面第 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 的 PCLK2115200 波特率16 倍过采样下# USARTDIV PCLK / (16 * BaudRate) 72e6 / (16 * 115200) 39.0625 # 整数部分 39 - BRR 高 12 位小数部分 0.0625 * 16 1 - BRR 低 4 位 # BRR (39 4) | 1 0x271HAL 会自己算这个值不用手填但知道算法能帮你判断问题出在哪。排查清单按顺序走读SystemCoreClock变量看是不是 72000000。不是说明时钟没配对先回 4.1。看HAL_UART_Init()里的BaudRate字段。CubeMX 生成的代码有时会因为时钟树改动没重新生成字段还停在旧值。确认 GPIO 的复用模式。F1 的 USART1 在 PA9/PA10 上必须设为GPIO_MODE_AF_PP并且使能__HAL_RCC_AFIO_CLK_ENABLE()。少了 AFIO 时钟引脚复用不生效现象是能发不能收或者完全没有波形。最后才怀疑线材和电平。提示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/128KBxE对应 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 是 Flashdatabss 是 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 又占 RAMbss只占 RAM。裁剪的目标是让text逼近但不超过 Flash 容量同时给databss留出至少 20% 的栈空间余量否则运行期一深调用就溢栈。.map文件里的排序结果会直接告诉你哪个模块最值得删。我见过一个项目最大的一个.o不是业务代码而是printf的浮点格式化占了将近 8KB。把%f换成定点打印一下就回来了。从那以后我每次裁剪完都强制走一遍size加 map 排序不看到具体数字不签字。希望帮到你。本文还有配套的精品资源点击获取