STM32内核精讲 | 第十章:复位序列与启动文件
💡 本文是《STM32内核精讲》栏目的第十篇。前九篇我们学习了寄存器模型、异常处理、AAPCS 等。从本篇开始,我们将聚焦于代码的“源头”——从上电到
main()之间发生了什么。理解复位序列和启动文件,是读懂启动代码、实现 Bootloader、排查 HardFault 的根本前提。
📌 一、引言:从“上电”到“main()”之间发生了什么?
很多嵌入式初学者以为,把程序烧写到 Flash 里,上电后就会自动执行 main()。但实际上,main() 只是故事的结局,而不是开始。
在上电到 main() 之间,经历了一系列硬件自动执行和启动代码配合完成的关键步骤:
- 硬件从向量表自动取出初始 MSP 和复位向量;
- 启动代码设置堆栈、初始化中断向量表;
- 可选的
SystemInit()配置系统时钟; - C 运行时初始化(
__main)负责将已初始化数据从 Flash 复制到 RAM,并将未初始化数据清零; - 最后才跳转到
main(),开始执行用户的 C 程序。
这一整套流程,通常被封装在芯片厂商提供的 startup_xxx.s 启动文件中。但理解其内部机制,对调试 HardFault、优化代码尺寸、实现 Bootloader 乃至手写裸机程序都至关重要。
📌 二、复位序列:硬件自动完成的两件大事
当 MCU 上电或外部复位引脚被拉低后,Cortex‑M 内核会自动执行一个非常简洁但关键的序列。在离开复位状态后,Cortex‑M 做的第一件事就是读取以下两个 32 位整数的值:
- 从地址 0x0000_0000 取出 MSP(主堆栈指针) 的初始值
- 从地址 0x0000_0004 取出 PC 的初始值(复位向量)
然后将这两个值分别写入 MSP 和 PC 寄存器。这整个过程是硬件自动完成的,不需要任何软件指令干预。
Cortex‑M 与传统 ARM 的一个重要区别在于:传统 ARM 架构总是从 0 地址执行第一条指令,而 Cortex‑M 的 0 地址处提供的是 MSP 的初始值,而非指令。
由于 Cortex-M 使用向下生长的满栈,MSP 的初始值必须是堆栈内存的末地址加 1。举个例子:
- 如果堆栈区域占据
0x20007C00 ~ 0x20007FFF,则 MSP 初始值应为0x20008000(即堆栈起始地址 + 堆栈大小)
这种预先初始化堆栈的机制确保了:即使复位后的第一条指令还没来得及执行就发生了 NMI 或其他 Fault,中断服务程序也已经有了可用的堆栈。
📌 三、向量表的结构与填充
3.1 向量表的结构
向量表(Vector Table)是一个以 WORD(32位)为单元的数组,存储在 MCU 内存的起始位置(默认 0x0000_0000),每个条目存放对应异常或中断服务程序(ISR)的入口地址。
在 Cortex-M3/M4 架构中,向量表的内容依次为:
| 偏移地址 | 内容 | 说明 |
|---|---|---|
| 0x000 | __initial_sp | 初始主堆栈指针 |
| 0x004 | Reset_Handler | 复位处理程序地址 |
| 0x008 | NMI_Handler | 不可屏蔽中断处理程序 |
| 0x00C | HardFault_Handler | 硬故障处理程序 |
| 0x010 | MemManage_Handler | 存储器管理故障(仅 M3/M4/M7 支持) |
| 0x014 | BusFault_Handler | 总线故障 |
| 0x018 | UsageFault_Handler | 用法故障 |
| … | … | … |
| 0x03C | PendSV_Handler | PendSV 处理程序 |
| 0x040 | SysTick_Handler | SysTick 定时器中断 |
| 0x044+ | IRQ0_Handler | 外部中断 |
在向量表中放置的是 32 位地址而非跳转指令。由于 Cortex-M 在 Thumb 状态下执行,向量表中的每个地址都必须将最低位(LSB)置 1(即地址为奇数),表示 Thumb 模式。
启动文件 startup.s 通过 AREA 伪指令定义 RESET 段,将其放置在 Flash 的 0 地址处。MDK 生成的分散加载文件将 RESET 段设置在 Flash 的起始地址,从而确保了向量表在正确的位置。
3.2 向量表重定位(VTOR)
向量表默认位于 Flash 起始地址(0x00000000),但在 Bootloader 跳转、OTA 升级或需要动态修改中断处理函数时,可能需要将向量表重定位到 RAM 或其他地址。
Cortex‑M3/M4/M7/M33 等内核通过 VTOR(Vector Table Offset Register)寄存器支持向量表重定位。VTOR 位于系统控制块(SCB)中,地址为 0xE000ED08。在绝大多数 STM32 型号中,VTOR 复位值为 0,即向量表从地址 0x00000000 开始。
VTOR 对齐要求
- 架构下限:Cortex‑M 架构规定 VTOR 的
bit[6:0]为保留位(必须为 0),因此最低要求 128 字节对齐。 - 芯片实现:以 STM32 为例,其外部中断数量约 84 个,加上 16 个系统异常,总异常数向上取整到 128 个。128 × 4 = 512 字节,因此实际要求 512 字节对齐。不同系列可能要求不同,请以对应参考手册为准。
重定位的核心代码(增加了更严谨的对齐检查):
// 设置 VTOR 寄存器
#define SCB_VTOR (*((volatile uint32_t *)0xE000ED08))
void relocate_vector_table(uint32_t new_address) {
// 实际要求:对齐到 512 字节(最低 128 字节,STM32 通常要求 512)
if (new_address & 0x1FF) { // 检查低 9 位是否为 0(512 对齐)
return; // 对齐错误,不执行
}
SCB_VTOR = new_address;
__DSB();
__ISB(); // 确保后续指令使用新向量表
}
在 Bootloader 跳转到应用程序前,通常需要从应用程序的起始地址读取初始 MSP 和复位向量,设置 MSP 后跳转到复位向量执行。
📌 四、SystemInit() 与时钟配置(仅内核视角)
在启动代码中,Reset_Handler 通常会调用 SystemInit() 函数,然后才进入 C 运行时初始化。SystemInit() 通常由芯片厂商提供(位于 system_stm32xxx.c),用于配置系统时钟、PLL 等。
SystemInit() 的主要职责(依据 CMSIS 规范)包括:
- 选择系统时钟源(HSI/HSE/PLL)
- 配置 PLL 倍频和分频因子
- 设置 AHB/APB 总线预分频器
- 更新
SystemCoreClock全局变量,供SysTick等外设使用。
部分 STM32 系列还在
SystemInit()中配置 Flash 等待周期(Flash Wait State),这属于芯片厂商的实现细节,并非 CMSIS 强制要求。但它是保证高主频下 Flash 访问稳定的必要步骤。
与专栏定位的衔接:
需要强调的是,SystemInit() 的时钟配置是内核能够以正确频率运行的前提——SysTick 定时器的精确延时、指令执行速度、总线访问时序,都建立在正确的时钟配置之上。如果时钟配置错误,后续所有依赖时间基准的内核功能(如 SysTick 的中断周期)都会出现偏差。本专栏不深入具体寄存器配置细节(属于芯片厂商外设范畴),但读者应当知道:在启动代码中,系统时钟的配置优先级高于 C 运行时初始化,因为时钟正确后,后续的 Flash 复制(从 Flash 到 RAM)才能以正确的时序完成。
📌 五、__main 与 C 运行时初始化
链接器会将 C 库中的 __main 入口函数链接到程序中,负责建立 C 运行环境(__main 不是用户写的 main 函数)。以 ARM Compiler(MDK)为例,其初始化序列包含三个功能块:
- __main → __scatterload:负责建立运行时的存储器映射。
- __scatterload:将已初始化数据(RW 段)从加载地址(Flash)复制到执行地址(RAM),并将未初始化数据(ZI 段)清零。
- __rt_entry:设置应用程序的栈和堆,初始化 C 库,然后跳转到用户
main()。
当用户程序执行完毕后,__rt_entry 负责关闭 C 库并将控制权交还给调试器(在裸机嵌入式应用中,main() 通常不会返回)。
5.1 __scatterload 的作用
映像文件在 Flash 中的布局为:RO → RW → ZI。其中 RO(代码和只读数据)在支持 XIP(就地执行)的系统中(如 STM32)在 Flash 中原地执行;RW(已初始化的全局变量)必须从 Flash 复制到 RAM;而 ZI(未初始化的全局变量)则只需在 RAM 中保留空间并清零,没有数据从 Flash 复制。
__scatterload 执行的核心工作包括:
- 将 RW 段(初始值存储在 Flash 中)复制到 RAM 中的目标地址;
- 将 ZI 段所在的 RAM 区域清零;
- 最后跳转到
__rt_entry。
若跳过 RW 段复制和 ZI 段清零,全局变量的值将不可预测,这是程序运行时出现“未初始化变量不归零”或“初始值与预期不符”的根源。
5.2 __rt_entry 的作用
__rt_entry 负责:
- 设置应用程序的栈和堆边界;
- 初始化标准 C 库函数及其静态数据;
- 在 C++ 程序中调用全局对象的构造函数;
- 最后跳转到
main()函数。
使用微库(–specs=nano.specs)时,初始化过程有所简化,但基本逻辑不变。
5.3 为什么需要 __main?
main() 函数的存在本身会强制链接器链接 __main 和 __rt_entry 中的初始化代码。如果没有 main()(例如直接用汇编编写的裸机程序),这些初始化步骤就不会被链接,C 库中的某些功能(如 printf、malloc)将无法正常工作。
对于手工编写的裸机程序(如 10.5 节的最小化启动),往往绕过 __main,在汇编中直接跳转到 main,但这要求用户自行完成 RW/ZI 初始化,且不依赖 C 库的标准输入输出功能。
📌 六、链接脚本与内存布局概述
启动代码和 C 运行时初始化的行为,依赖于链接脚本(Linker Script,.ld 文件)定义的内存布局。以下是链接脚本如何与本章前文内容衔接的关键点:
- 链接脚本的 MEMORY 区域(
FLASH和RAM)定义了 Flash 和 RAM 的地址范围,直接影响 §4 中SystemInit()的时钟配置可行性; - 向量表 通过
SECTIONS的.isr_vector段放置到 Flash 起始地址,与 §3 的向量表位置要求直接对应; - RW 段和 ZI 段 在链接脚本中分别被分配加载地址(
AT > FLASH)和执行地址(> RAM),§5 中__scatterload正是依据这些链接器符号完成 RW 段的复制和 ZI 段的清零; - 链接脚本定义的 _estack 符号直接对应 §2 中 MSP 初始值的来源。
链接脚本与启动代码的协作关系可以概括为:链接脚本告诉链接器代码和数据放在哪里(编译时),启动代码在运行时根据这些信息建立正确的 C 运行环境。这是嵌入式裸机系统的基本“分工协作”模式。
以下是一份简化版的链接脚本关键片段(以 STM32 为例,其他芯片地址可能不同):
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2M /* STM32 Flash 起始地址 */
RAM (rw) : ORIGIN = 0x20000000, LENGTH = 128K /* STM32 RAM 起始地址 */
}
SECTIONS
{
/* 向量表段,必须位于 Flash 起始地址 */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
} > FLASH
/* 注:.isr_vector 作为第一个段配合 MEMORY 中 Flash 起始地址,可保证向量表位于 Flash 起始处。实际工程中也可通过 AT 表达式显式指定。 */
/* 代码段 */
.text : { *(.text) } > FLASH
/* 只读数据段 */
.rodata : { *(.rodata) } > FLASH
/* 已初始化数据段——加载地址在 Flash,运行地址在 RAM */
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data)
. = ALIGN(4);
_edata = .;
} > RAM AT > FLASH
_sidata = LOADADDR(.data);
/* 未初始化数据段 */
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss)
. = ALIGN(4);
_ebss = .;
} > RAM
/* 堆和栈区域 */
._user_heap_stack :
{
. = ALIGN(8);
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} > RAM
}
注意:以上链接脚本语法适用于 GNU Linker (LD),与 ARM Compiler 的分散加载文件(
.sct)语法不同。不同开发环境请选择对应的链接脚本。
理解链接脚本的结构和含义,是进一步深入理解 §5 C 运行时初始化、以及自行编写最小化启动代码的必由之路。
📌 七、最小化启动:纯汇编实现
在某些极简场景中,我们希望绕过庞大的启动文件和 C 运行时库,仅用纯汇编实现一个最简单的“裸机”程序。这种思路常用于:
- 理解启动过程的最小化演示;
- 开发 Bootloader 的核心框架;
- 极度受限的资源环境(代码尺寸只有几百字节)。
7.1 最小启动代码(纯汇编)
以下是一份可直接运行的 Cortex‑M4 最小启动汇编代码(ARM Compiler / Keil 语法)
; minimal_startup.s
; 适用于 Cortex-M4 (STM32F4 等)
; 语法:ARM Compiler (Keil MDK)
AREA RESET, CODE, READONLY
THUMB
; 导出向量表符号,供链接器使用
EXPORT __Vectors
EXPORT Reset_Handler
; 定义向量表
__Vectors
DCD __initial_sp ; 0x00: 初始 MSP
DCD Reset_Handler ; 0x04: 复位向量
; 复位处理程序
Reset_Handler
; 设置堆栈指针(若硬件已加载,本行可省略,但显式设置更安全)
LDR SP, =__initial_sp
; 可选:设置全局指针(某些 ABI 需要)
; LDR R0, =_sdata
; LDR R1, =_edata
; ... 复制 RW 段(如果需要)
; 跳转到主程序(C 函数或汇编循环)
BL main
B . ; 死循环(防止跑飞)
END
同时需要提供一个 main.c(最简单的 C 程序,但不依赖完整 C 运行时):
// main.c - 不依赖标准库的最小 C 程序
void main(void) {
while (1) {
// 用户代码(例如 GPIO 翻转,不涉及外设配置)
}
}
7.2 绕过 C 运行时初始化
上面的代码中,Reset_Handler 直接调用 main,跳过了标准的 C 运行时初始化过程。这意味着:
- 全局变量不会自动初始化:初值非零的全局变量将保持未初始状态(未从 Flash 复制)。
- ZI 段(BSS,未初始化的全局变量和静态局部变量)不会自动清零。
- 标准 C 库(如
printf、malloc)无法正常工作。
因此,这种最小启动方式仅适用于:
- 完全用汇编或纯 C 且不依赖全局变量和静态局部变量的程序;
- 用户自己实现 RW 段复制和 ZI 段清零的场景;
- 用于教学演示,帮助理解启动代码本质。
如果需要支持全局变量,需要在 Reset_Handler 中手工添加 RW 段复制和 ZI 段清零的代码,并通过链接脚本获得 _sdata、_edata、_sbss、_ebss 等符号。有兴趣的读者可以参考编译器和链接器手册中的详细说明。
📌 八、总结
8.1 本篇核心要点
- 复位序列:硬件自动从
0x00000000取初始 MSP,从0x00000004取复位向量,然后跳转执行。 - 向量表:位于 Flash 起始地址,第一项是初始 MSP,第二项是 Reset_Handler,后面依次为各异常和中断的入口地址。通过 VTOR 可实现向量表重定位(架构下限 128 字节对齐,STM32 通常要求 512 字节对齐)。
- SystemInit():在进入 C 运行时前配置系统时钟和 PLL,为内核运行提供正确的时间基准,直接关系 SysTick 等定时器功能的准确性。Flash 等待周期等属于厂商实现细节。
- C 运行时初始化:
__main→__scatterload负责 RW 段复制和 ZI 段清零,__rt_entry设置堆栈并初始化 C 库,最后跳转到main()。 - 链接脚本:定义了 Flash 和 RAM 的地址范围、各内存段(
.isr_vector、.text、.data、.bss等)的映射关系,以及堆栈边界。启动代码和 C 运行时的行为依赖链接脚本生成的关键符号。不同工具链语法有差异。 - 最小化启动:纯汇编实现的最简启动代码,可跳过 C 运行时初始化,适用于资源受限或教学场景。注意绕过初始化会导致全局变量和静态局部变量未初始化。
8.2 下篇预告:《低功耗模式 —— 内核的睡眠哲学》
从下篇开始,我们将探索内核的低功耗机制:睡眠、深度睡眠与待机模式的区别,WFI/WFE 指令的使用,以及 RTOS 空闲任务中如何优雅进入低功耗(Tickless 模式本质)。
💬 读者问题专栏 · 问题征集
你在阅读启动代码或调试系统启动问题时,是否遇到过:
- 程序跳转到
main()前卡死在某个地方(通常与 RW/ZI 段初始化或 VTOR 配置相关)? - 全局变量初值不符合预期,怀疑 RW 段复制环节有问题?
- 手工编写裸机程序时,不知道如何正确设置向量表和堆栈?
- Bootloader 跳转到应用程序后,中断无法响应(通常与向量表重定位缺失有关)?
欢迎留言,我会在 《Cortex‑M 有问必答》 中专题解答。
📢 关于作者与更多内容
我是 BackCatK Chen,长期关注嵌入式底层、国产半导体与 AI 算力芯片。
如果你对芯片架构、行业趋势感兴趣,欢迎关注我的公众号,获取更多宏观技术观察。
文章标签:Cortex-M 复位序列 启动文件 向量表 VTOR SystemInit C运行时 链接脚本
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐




所有评论(0)