本笔记从零讲清一个主要问题:标准库工程怎么搭(以 F429 实操为主线);
- 适用范围:F1 / F2 / F4 / L1系列(官方提供标准库的老系列)
- 不适用:F7 / H7系列——官方未发布 SPL,工程结构直接采用 HAL/LL 库方案
一、主线总览:两个阶段
阶段一:寄存器工程(最小骨架)──先让"裸芯片"能编译 │ 往骨架上加三样东西 ▼ 阶段二:固件库工程(= 寄存器工程 + 外设函数库)二、工程文件的五大角色
角色 | F1(江协 F103 工程) | F4(F429 工程) | 作用 |
① 启动文件 (.s 汇编,唯一的汇编文件) |
|
| 上电第一段代码:建堆栈、中断向量表、跳到 main。必须选对:
|
② CMSIS 内核文件 (ARM 写的,任何公司的 Cortex-M 芯片通用) |
+ |
+ + + | 定义 NVIC/SysTick 等内核寄存器访问。可以理解为F4是F1的 F4 没有 core_cm4.c:老版 CMSIS(1.3 时代)把 SysTick_Config 等放 .c 里;新版(3/4 起)全改成 static inline 放 .h 里,不再需要 .c |
③ 设备层文件 (ST 写的) |
、 |
、 | 定义该芯片所有外设寄存器地址、中断号;
|
④ 外设库驱动 |
、 |
、 | GPIO_Init()、USART_SendData() 这些库函数本体。 |
⑤ 用户/配置文件 |
、 、 |
、 、 | 用户代码 +中断服务函数 + (库裁剪总开关,决定包含哪些外设头文件) |
阶段一:寄存器工程
1.下载标准外设固件库
首先根据芯片型号从[STM社区](https://www.stmcu.org.cn/)下载标准外设固件库
2.创建文件夹(此处以F429IGTx为例)
在工作根目录下创建文件夹(这个文件的分类根据个人喜好来)
- lib_start
- user
在固件库压缩包解压得到的STM32F4xx_DSP_StdPeriph_Lib_V1.9.0文件夹中拷贝启动文件、核心文件和设备文件到文件夹中
这里简单介绍一下,Libraries\CMSIS文件夹下是CMSIS设备文件、启动文件和核心文件的源路径,需要的文件都可以从这个目录下找到。而如果需要构建固件库工程,则需要从Libraries\STM32F4xx_StdPeriph_Driver中拷贝外设驱动文件,并且从Project\STM32F4xx_StdPeriph_Templates模板工程中拷贝配置文件(stmxxx_conf.h)和中断文件(stmxxx_it.c/.h)。
...\STM32F4xx_DSP_StdPeriph_Lib_V1.9.0\Libraries\CMSIS\Device\ST\STM32F4xx\Source\Templates\arm找到对应的启动文件startup_stm32f429_439xx.s放到lib_start中
...\STM32F4xx_DSP_StdPeriph_Lib_V1.9.0\Libraries\CMSIS\Device\ST\STM32F4xx\Source\Templates找到设备文件system_stm32f4xx.c放到lib_start中...\STM32F4xx_DSP_StdPeriph_Lib_V1.9.0\Libraries\CMSIS\Device\ST\STM32F4xx\Include找到设备文件system_stm32f4xx.h、stm32f4xx.h放到lib_start中...\STM32F4xx_DSP_StdPeriph_Lib_V1.9.0\Libraries\CMSIS\Include找到核心文件,放到lib_start中
其中arm_math.h、arm_common_tables.h、arm_const_structs.h【CMSIS-DSP 数字信号处理库的头文件,只有做 FFT/滤波等算法才需要】
core_sc000.h、core_sc300.h【SecurityCore(SC000/SC300)芯片用的,与 F429 无关】
3.打开Keil新建工程
选择刚刚整理好文件的工作目录
(1)新建分组
新建两个分组lib_start和user(我的个人习惯是分组和实际路径一致)
将lib_start文件夹下的所有文件加入到lib_start分组中,点击ok
(2)添加路径
将新建的文件夹的路径都添加进来
(3)编译测试
在user分组下,新建main.c文件(注意添加的时候路径要选择到user文件夹下)
然后右击鼠标,点击insert加入头文件。之后写一个main函数的框架,最后空一行,再编译,可见编译通过0 error(s)
补充说明
1. 问题一
如果出现编译没有通过,并且出现“unknown register name 'vfpcc' in asm”这样的问题,则与编译器有关。
./libraries/startup\core_cmFunc.h(629): error: unknown register name 'vfpcc' in asm
629 | __ASM volatile ("VMSR fpscr, %0" : : "r" (fpscr) : "vfpcc");
原因:libraries/startup 里这套 CMSIS 头文件未支持AC6(armclang V6.24)——armclang 因为基于 Clang,会同时定义 __GNUC__,于是该头文件走进了 GCC 分支,而这个分支里的内联汇编把 "vfpcc" 当 clobber 名使用,armclang 不认识这个寄存器名,直接报错。
由于每个 .c 都包含了 core_cm4.h → core_cmFunc.h,所以几乎每个编译单元都报这同一个错(这就是为什么报错数量那么多)。
修复方法:
1.(推荐,最简单)打开 libraries\startup\core_cmFunc.h,定位到第 629 行,把:
__ASM volatile ("VMSR fpscr, %0" : : "r" (fpscr) : "vfpcc");改成:
__ASM volatile ("VMSR fpscr, %0" : : "r" (fpscr) : "memory");- 升级 CMSIS 头文件:用新版 CMSIS的 core_cm4.h / core_cmFunc.h / core_cmInstr.h 替换 libraries/startup 里的旧版本(新版对 AC6 有专门的 __ARMCC_VERSION 分支)。工程上更正规,但改动较大。
- 换 AC5 编译器:Options → Target → ARM Compiler 选 “Use default compiler version 5”。
阶段二:固件库工程
1.新建文件夹与拷贝
在寄存器工程的基础上,再添加上配置文件和外设驱动文件即可搭建好一个固件库工程。
在工作根目录下创建一个新的文件夹lib_driver
- 从
\STM32F4xx_DSP_StdPeriph_Lib_V1.9.0\Libraries\STM32F4xx_StdPeriph_Driver将inc和src两个文件夹内的文件全部拷贝到lib_driver下 - 从
\STM32F4xx_DSP_StdPeriph_Lib_V1.9.0\Project\STM32F4xx_StdPeriph_Templates将stm32f4xx_it.cstm32f4xx_it.h、stm32f4xx_conf.h拷贝到user目录下/工作根目录(个人习惯)
- 从
\STM32F4xx_DSP_StdPeriph_Lib_V1.9.0\Project\STM32F4xx_StdPeriph_Templates将main.c和main.h拷贝到user目录下,毕竟Templates本身就是模板工程目录。
如果不拷贝则需要做适当的裁剪
2.文件过滤
此时,还需要对lib_driver中的文件做一个过滤,因为其中部分文件并不是F429的外设驱动文件(429没有这些外设),而是其他F4xx系列的如F407的。
- 必须删的:必须删:
stm32f4xx_fsmc.c、stm32f4xx_fsmc.h - 可以删:fmpi2c / dfsdm / dsi / spdifrx / qspi【429没有这些外设】
这些文件的#ifdef保护完好,F429 宏下内容被整体跳过,不报错、不占 Flash(链接器只把被调用的函数拉进固件),删除只是编译快几秒、日志干净,所以不是必须删,但是建议删去。
3.新建分组与添加路径、宏定义
在keil中添加lib_driver分组,并将剩余的文件加到相应分组
分组
路径
宏定义
添加宏定义:USE_STDPERIPH_DRIVER或者STM32F429_439xx,USE_STDPERIPH_DRIVER(用英文逗号分隔,不要加分号、不要换行)
其中STM32F429_439xx可以省略但是建议添加上。可以省略的原因是,当你在 Device 选项卡选了具体芯片(如 STM32F103C8),Keil 会自动在编译命令行里注入对应的型号宏,而DFP 自动加的宏不会显示在 Define 框里。
说明:添加宏定义在stm32f4xx.h中有代码印证【原因】
编译,通过。
如果不加宏定义就会出现如下错误
如果选择不拷贝模板工程的main.c/.h
1.问题
user/stm32f4xx_it.c(137): error: call to undeclared function 'TimingDelay_Decrement'; ISO C99 and later do not support implicit function declarations 137 | TimingDelay_Decrement();含义
在 stm32f4xx_it.c(中断服务函数文件)第 137 行调用了函数 TimingDelay_Decrement(),但编译器在这个文件里找不到该函数的声明。
- 在 C99 及之后的标准中,调用未声明的函数属于错误(旧 C89 只是警告,会隐式声明为 int func())。
- AC6 默认遵循较新的 C 标准,所以直接报 error,导致编译中断。
原因分析
TimingDelay_Decrement()通常出现在 ST 官方例程的main.c中,配合 SysTick 中断使用:
报错说明工程里可能存在以下情况之一:
- 该函数只写在了
main.c里,没有在main.h或其他头文件中声明,stm32f4xx_it.c包含的头文件里看不到它; - 原例程中
main.c有这个函数,但当前main.c被改写/精简,函数体删掉了,却忘了同步修改stm32f4xx_it.c; - 函数声明所在头文件没有加进
stm32f4xx_it.c的#include列表。
解决方法(任选其一)
方案 | 做法 |
① 补声明(推荐) | 在 |
② 直接声明 | 在 |
③ 修改实现位置 | 把 |
④ 删除调用 | 如果不再需要基于 SysTick 的延时,把 |
⚠️ 注意:如果选择删除,还要确认main.c中的Delay()/SysTick 配置代码是否需要同步调整,否则程序运行时延时功能会失效。
2.main.c/.h模板与线索
官方的模板文件中,在main.c文件中定义了TimingDelay_Decrement,并在main.h中声明。
该函数在stm32f4xx_it.c文件中被调用。
自己建 main.c 的正确做法【参考AI】
完全从零手写 main.c + main.h(理解原理后再用)
main.h
#ifndef __MAIN_H #define __MAIN_H #include "stm32f4xx.h" /* 芯片寄存器/外设定义,几乎所有文件都要 */ /* 需要被其他文件调用的函数,都在这里声明 */ void TimingDelay_Decrement(void); void Delay(uint32_t nTime); #endifmain.c
#include "main.h" static __IO uint32_t TimingDelay; /* SysTick 递减计数 */ void Delay(uint32_t nTime) { TimingDelay = nTime; while (TimingDelay != 0); /* 忙等 */ } void TimingDelay_Decrement(void) /* 由 SysTick 中断每 1ms 调一次 */ { if (TimingDelay != 0) TimingDelay--; } int main(void) { /* 1. 配置 SysTick = 1ms(模板 system_stm32f4xx.c 已把时钟配到 168MHz) */ if (SysTick_Config(SystemCoreClock / 1000)) while (1); /* 配置失败则死循环 */ while (1) { /* 你的代码写在这里 */ } }同时检查stm32f4xx_it.c:
#include "stm32f4xx_it.h" #include "main.h" /* ← 确保有这一行,否则看不到声明 */关键三步缺一不可:
- 定义在 main.c;
- 声明在 main.h;
- 调用方(stm32f4xx_it.c)必须
#include "main.h"。
另外新建文件后还要确认:文件已添加进 Keil 工程分组(右键 Group → Add Existing Files),且 main.h 所在目录在Options for Target → C/C++ → Include Paths里(否则#include "main.h"找不到文件)。
不用中断延时,最简版(连 main.h 都不要)
绕开 SysTick,把stm32f4xx_it.c里的TimingDelay_Decrement()调用注释掉(或换成吧相关程序移到其他文件中)
main.c
比如点个灯跑通流程
#include "stm32f4xx.h" int main(void) { RCC->AHB1ENR |= RCC_AHB1ENR_GPIOEEN; /* 使能 GPIOE 时钟 */ GPIOE->MODER |= (1 << (13*2)); /* PE13 输出 */ while (1) { GPIOE->ODR ^= (1 << 13); for (volatile uint32_t i = 0; i < 2000000; i++); /* 粗略延时 */ } }这种单文件、无跨文件调用的情况下,main.h 完全可以不要。
STM32 标准库(SPL)工程配置补充说明
适用范围:F1 / F2 / F4 / L1系列(官方提供标准库的老系列)
不适用:F7 / H7系列——官方未发布 SPL,工程结构直接采用 HAL/LL 库方案
要点速查表
| 配置项 | 正确做法 | 常见误区 |
|---|---|---|
| 头文件入 Groups | 不需要 | 误以为不加入就编译不到 |
| 多套头文件并存 | 自备路径排到 Include Paths最前 | 混用 DFP 版本导致宏冲突 |
| 模板适用系列 | F1 / F2 / F4 / L1(有 SPL) | F7 / H7 无 SPL,不能用此结构 |
| 子目录搜索 | 每个子目录逐条添加 | 以为-I会递归下钻 |
一、头文件是否需要加入 Groups?
不需要,加不加都不影响编译。
结论:core_cm4.h、stm32f4xx_gpio.h等头文件无需添加进工程分组,只需保证Include Paths 配置正确,编译器能找到它们即可。
| 项目 | 说明 |
|---|---|
| Keil 实际编译的文件 | 仅.c/.s/.cpp |
.h文件的角色 | 只在预处理阶段被#include展开,不参与独立编译 |
| 是否要拖进 Groups | 不需要。留着无害,但纯属“摆设” |
二、头文件版本冲突隐患(重要)
问题描述
工程中可能同时存在两套设备头文件:
..\libraries\startup\stm32f4xx.h—— 自备版本(2022/2/11)- KeilDFP(Device Family Pack)自带版本
两套版本若混用,可能出现宏定义不一致、莫名警告等问题。
解决方案
在Options → C/C++ → Include Paths中,把
..\libraries\startup调整到列表最前面(选中后点击↑箭头上移)。
原理:编译器按 Include Paths 的排列顺序依次搜索头文件,排在前面的路径优先命中。将自己文件夹置顶,即可确保优先使用自备的整套 CMSIS / 设备头文件,避免版本错配。
三、Include Paths 是否支持递归搜索子目录?
不支持。
- Keil 的头文件搜索路径不递归:
-I参数只在指定的那一层文件夹内查找,不会自动下钻子目录。 - 这一点与GCC 的
-I行为一致——同样是“指定哪层,就只搜哪层”。
结论:不能只写一个lib就指望它自动找到lib/a、lib/b。每个包含头文件的子目录,都必须逐条显式写入 Include Paths。
# 错误示范(递归不生效): ..\lib # 正确写法(逐一列出): ..\lib ..\lib\a ..\lib\b参考文章
正点原子STM32F429阿波罗板,固件库工程搭建_正点原子阿波罗-CSDN博客
STM32工程创建(STM32F407)_stsw-stm32065-CSDN博客
STM32系列官方标准固件库的完整下载流程_stm32固件库下载-CSDN博客
STM32F429入门(八):初识固件库_stm32f429固件库-CSDN博客
STM32F429ZI Discovery板级支持包(BSP)深度解析-CSDN博客
No.1 STM32F429IGT6开发板简介 (STM32F429/F767/H743)_stm32f429igt6中文说明-CSDN博客