☰
STM32F407 启动文件:从上电复位到 main()
2026/9/26 7:26:50 网站建设 项目流程

平时编写 STM32 程序,通常从main()开始。但芯片上电后,需要先设置栈指针、找到程序入口、配置系统时钟,并准备好 C 程序的运行环境,才能执行main()。

本文以STM32F407、Keil MDK 和标准外设库工程为例,整理启动文件中的主要知识点。

一、从上电到 main() 的完整过程

在选择从内部 Flash 启动的情况下,基本过程如下:

芯片上电或复位 ↓ CPU 通过启动地址映射读取向量表 ↓ 将向量表第一项加载到主栈指针 MSP ↓ 根据向量表第二项进入 Reset_Handler ↓ 调用 SystemInit(),配置系统时钟等 ↓ 进入 C 运行库 __main,准备 C 运行环境 ↓ 调用用户编写的 main()

其中,读取初始栈顶和复位入口,是 CPU 复位硬件自动完成的。随后,CPU 才开始执行Reset_Handler中的指令。

二、CPU 为什么能找到启动代码?

启动文件经过汇编、链接后,会和其他代码一起写入 Flash。CPU 运行时使用的是机器指令和数据,源文件名只用于开发阶段。

CPU 能找到启动代码,是因为硬件与链接布局遵循同一个约定:启动向量表的前两个 32 位数据,分别保存初始栈顶和复位入口。

假设程序从 Flash 起始地址0x08000000开始存放:

Flash 地址保存的内容
0x08000000初始栈顶地址__initial_sp
0x08000004复位入口Reset_Handler的地址
后续地址其他异常和中断处理函数的地址

每项占 4 字节,因此第二项的地址比第一项多 4。

STM32F407 从 Flash 启动时,会把 Flash 映射到从0x00000000开始的启动地址空间。因此,CPU 复位时读取0x00000000和0x00000004,就能读到 Flash 中的这两项。

地址映射不会把 Flash 内容复制一遍,只是让同一块存储器可以通过另一个地址访问。

一个具体例子

假设编译后的内容如下,地址仅用于说明:

存放位置内容含义
0x080000000x20001000初始栈顶在 RAM 的0x20001000
0x080000040x08000201复位入口,最低位 1 表示 Thumb 状态
0x08000200启动代码的机器指令Reset_Handler的实际指令位置

复位后,CPU 自动完成:

读取第一项 → MSP = 0x20001000 读取第二项 → 根据 0x08000201 设置执行状态 从 0x08000200 开始执行指令

Cortex-M4 使用 Thumb 指令集,因此入口地址值的最低位需要为 1。

三、启动文件中的栈配置

启动文件中常见以下代码:

Stack_Size EQU 0x00000400 AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp

这段代码描述了一块大小为 1 KB 的栈区域。

写法含义
EQU定义常量,作用类似 C 的#define
Stack_Size栈大小,0x400 = 1024字节
AREA STACK定义名为STACK的段
NOINIT该区域不要求启动时初始化内容
READWRITE该区域可读可写
ALIGN=3按2³ = 8字节对齐
SPACE Stack_Size在该段中保留指定字节数
Stack_Mem栈区域起点的标号
__initial_sp栈区域末尾的标号,用作初始栈顶

这些是汇编器和链接器使用的定义。链接器会根据工程配置,为这块区域安排 RAM 地址。

栈在 RAM 中是什么样的?

假设这 1 KB 栈从0x20000000开始:

高地址 0x20000400 ← __initial_sp,初始 MSP 0x200003FF ← 栈区域最后一个字节 ... 0x20000000 ← Stack_Mem,栈区域第一个字节 低地址

实际保留的范围是:

0x20000000 ~ 0x200003FF

初始栈指针指向这块区域上边界的下一个地址0x20000400。

STM32F407 使用向低地址增长的栈:入栈时,栈指针减小;出栈时,栈指针增大。

栈有什么用?

栈主要用于保存:

  • 函数调用时需要保留的寄存器和返回信息。
  • 需要放入内存的局部变量。
  • 部分函数参数。
  • 中断发生时的 CPU 现场。

局部变量和函数参数是否使用栈,还取决于编译器。有些值可以直接保存在寄存器中。

例如,一次普通中断进入时,CPU 的基本异常栈帧包含 8 个寄存器值,共 32 字节:

R0、R1、R2、R3、R12、LR、PC、xPSR

假设没有额外对齐填充和浮点上下文:

中断前:MSP = 0x20000400 保存 32 字节: MSP = 0x20000400 - 0x20 = 0x200003E0 中断返回,恢复现场后: MSP = 0x20000400

中断处理函数自身还可能继续使用栈,因此一次中断的总栈开销可能超过 32 字节。

栈空间不够会怎样?

如果函数调用层次较深、局部数组较大,或者中断嵌套较多,栈就可能超过预留范围,覆盖其他 RAM 数据,造成异常结果或 HardFault。

例如:

void test(void) { volatile unsigned char buffer[2048]; buffer[0] = 1; buffer[2047] = 2; }

如果编译器为这个局部数组分配栈空间,它本身就需要 2 KB,已经超过示例中的 1 KB 栈。

HardFault 有很多原因,栈溢出只是其中一种。

四、中断向量表是什么?

中断向量表可以理解为一张“入口地址表”。CPU 根据异常或中断编号,从表中找到对应的处理函数。

启动文件中的部分定义如下:

__Vectors DCD __initial_sp DCD Reset_Handler DCD NMI_Handler DCD HardFault_Handler

DCD用于定义 32 位数据。这里保存的是地址值。

需要特别注意:

  • 第一项保存的是初始栈顶地址。
  • 从第二项开始,保存异常或中断处理函数的入口地址。
  • 这张表通常在编译、链接时就已生成,并随程序烧录到 Flash。

例如,向量表中有一项:

DCD EXTI0_IRQHandler

配置并使能相应的外部中断后,CPU 响应 EXTI0 中断时,就会通过该项找到处理函数。

void EXTI0_IRQHandler(void) { /* 判断并清除中断标志,然后处理事件 */ }

向量表只提供入口地址。要让中断正常发生,还需要配置外设、中断线路和 NVIC。

五、Reset_Handler 做了什么?

当前工程的复位处理代码核心如下:

Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT SystemInit IMPORT __main LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP

执行顺序是:

Reset_Handler ↓ SystemInit() ↓ __main

几个关键写法的含义:

写法含义
IMPORT声明一个在其他地方定义的符号
EXPORT允许其他模块引用该符号
LDR R0, =SystemInit把SystemInit的地址加载到 R0
BLX R0调用 R0 指向的函数,并保存返回地址
BX R0跳转到 R0 指向的入口
[WEAK]弱定义,可以被同名的强定义覆盖

这里的SystemInit()返回后,会继续执行跳转到__main的代码。

六、SystemInit() 怎样配置系统时钟?

STM32F407 复位后,先使用内部HSI 16 MHz时钟运行,因此 CPU 可以执行启动代码和SystemInit()。

在当前标准外设库工程中,SystemInit()会调用SetSysClock(),主要完成:

  1. 使能外部高速振荡器 HSE。
  2. 等待外部晶振起振并稳定。
  3. 配置 PLL 的分频和倍频参数。
  4. 使能 PLL,并等待锁定。
  5. 配置总线分频、Flash 等待周期等。
  6. 将系统时钟切换到 PLL 输出。

25 MHz 晶振如何得到 168 MHz?

当前板子的外部晶振为25 MHz,工程配置为:

#define PLL_M 25 #define PLL_N 336 #define PLL_P 2

对应计算:

25 MHz ÷ 25 = 1 MHz 1 MHz × 336 = 336 MHz 336 MHz ÷ 2 = 168 MHz

即:

系统时钟 = 晶振频率 ÷ PLL_M × PLL_N ÷ PLL_P = 25 MHz ÷ 25 × 336 ÷ 2 = 168 MHz

随后分配给不同总线:

时钟当前配置
SYSCLK168 MHz
AHB / HCLK168 MHz
APB1 / PCLK142 MHz
APB2 / PCLK284 MHz

这里列的是总线时钟。部分外设还会使用单独的分频或倍频规则,例如定时器时钟不能始终直接等同于 APB 时钟。

25 MHz 是软件设置出来的吗?

板上晶振的标称频率由硬件决定。软件使能振荡电路,并配置后续 PLL 和分频器。

例如:

#define HSE_VALUE 25000000

这个宏告诉软件外部时钟频率是多少。修改它不会改变晶振本身的振荡频率。

上述 168 MHz 配置对应当前标准外设库工程。其他工程可能在不同位置配置时钟,例如某些 HAL 工程会在main()中调用SystemClock_Config()。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询