个人学习笔记 · Cortex-M · IAR · IAP bootloader
范围:从 MCU 复位到应用程序main(),包括 IAP bootloader 向应用程序移交控制权的过程。不涉及 RTOS 调度与上下文切换。
本文重点梳理最容易混淆的机制:向量表查找、MSP 初始化、链接器定义的内存布局、C 运行时初始化、VTOR 与内存重映射的区别,以及 bootloader 的控制权移交。
1. 整体流程
Cortex-M 应用程序并不只是存放在 Flash 中的一组 C 函数。它是一个具有特定内存布局、入口地址和初始化要求的固件映像。
复位时,处理器需要回答两个问题:
- 栈应该从哪里开始?
- 程序应该从哪里开始执行?
向量表提供了这两个问题的答案。
正常启动流程
1 | MCU 复位 |
这里涉及三个不同的层次:
| 层次 | 职责 |
|---|---|
| Cortex-M 硬件 | 初始化栈并提供异常入口机制 |
| 启动代码 | 准备 MCU 并进入运行时 |
| C 运行时 | 准备 C/C++ 程序内存并调用 main() |
核心概念: main() 并不是处理器真正的入口。在它执行之前,已经完成了相当多的初始化工作。
2. 内存架构与固件布局
中等深度
2.1 地址空间
典型的 STM32 包含多个存储区域:
| 存储区域 | 典型地址 | 用途 |
|---|---|---|
| Flash | 0x08000000 |
固件与常量数据 |
| SRAM | 0x20000000 |
运行时变量、栈、堆 |
| 外设 | 0x40000000 |
GPIO、UART、定时器等 |
| 系统存储区 | 取决于具体器件 | ROM bootloader 与系统功能 |
这些都是处理器内存映射中的地址范围。处理器通过地址访问它们,不需要使用不同的指令集来分别访问 Flash 或 SRAM。
例如:
1 |
|
这段代码从该地址映射的存储区域读取一个 32 位字。
2.2 固件映像与运行时内存
固件映像通常烧录到 Flash 中,但程序运行时并不一定直接在 Flash 中使用所有数据。
例如:
1 | int counter = 10; |
典型的安排如下:
counter:带初始值的变量,初始内容保存在 Flash 中,运行时在 SRAM 中使用。status:零初始化变量,在 SRAM 中分配空间。version:常量数据,通常保存在 Flash 中。
这里引出了一个重要区别:
固件映像描述初始内容;运行时内存布局描述程序实际使用这些内容的位置。
链接器决定这些段放在哪里,以及启动代码应如何初始化它们。
3. 向量表与固件映像
深入分析
3.1 为什么需要向量表?
处理器复位时,无法直接调用 main(),因为它还不知道应用程序的栈在哪里,也不知道启动代码从哪里开始。
它需要一个标准化的位置来获取这些地址。
这个位置就是向量表。
向量表是一个由 32 位表项组成的数组。每个表项保存与处理器复位、异常或中断处理相关的地址或初始值。
前两个表项尤为重要:
| 偏移 | 表项 | 含义 |
|---|---|---|
0x00 |
初始 MSP | 主栈指针的初始值 |
0x04 |
Reset Handler | 启动代码的地址 |
0x08 |
NMI Handler | 不可屏蔽中断处理函数 |
0x0C |
HardFault Handler | 硬故障处理函数 |
0x10 |
MemManage Handler | 内存管理故障处理函数,仅适用于实现了该异常的内核 |
第一个表项不是函数地址,而是栈地址。
第二个表项才是开始执行时使用的函数地址。
3.2 解读实际的 BIN 示例
之前查看过这样一段二进制转储:
1 | Offset Bytes |
Cortex-M 以小端格式存储这些 32 位值。
小端序表示最低有效字节存放在最前面。
例如:
1 | 字节:F0 05 00 20 |
解码后的表格如下:
| 偏移 | 字节 | 32 位值 | 含义 |
|---|---|---|---|
0x00 |
F0 05 00 20 |
0x200005F0 |
初始 MSP |
0x04 |
D5 40 00 08 |
0x080040D5 |
Reset Handler |
0x08 |
61 65 00 08 |
0x08006561 |
NMI Handler |
0x0C |
65 5A 00 08 |
0x08005A65 |
HardFault Handler |
注意这些地址所属的不同区域:
0x200005F0属于 SRAM:这是初始栈指针。0x080040D5属于 Flash:这是 Reset Handler。
这正是正常 STM32 应用程序应有的形式。
3.3 为什么 Reset Handler 地址是奇数?
可以注意到:
1 | 0x080040D5 |
其最低位为 1。
这是有意设计的。
Cortex-M 处理器执行 Thumb 指令。函数指针使用 bit 0 表示 Thumb 状态。实际指令地址是对齐的,而 bit 0 携带状态标志。
因此:
1 | 保存的函数地址:0x080040D5 |
解读向量表项时,不要简单地清除 bit 0。处理器需要这个 Thumb 状态标志。
3.4 Intel HEX 示例
之前还查看过:
1 | :020000040800F2 |
这不是原始 BIN 文件,而是 Intel HEX:一种用文本表示内存记录的格式。
记录格式如下:
1 | :LL AAAA TT [DD...] CC |
| 字段 | 含义 |
|---|---|
LL |
数据字节数 |
AAAA |
16 位地址偏移 |
TT |
记录类型 |
DD |
数据字节 |
CC |
校验和 |
第一行:
1 | :020000040800F2 |
是一条扩展线性地址记录:
02:两个数据字节。0000:记录偏移。04:扩展线性地址记录。0800:地址的高 16 位。F2:校验和。
它将地址的高位部分设为 0x0800。
下一条记录的数据开头为:
1 | 40 09 00 20 D5 00 00 08 |
解码后得到:
1 | 初始 MSP = 0x20000940 |
向量表的解读方式完全相同,变化的只是固件文件的表示形式。
3.5 BIN 与 HEX 的区别
| 特性 | BIN | Intel HEX |
|---|---|---|
| 格式 | 原始二进制字节 | ASCII 文本记录 |
| 地址信息 | 文件中不显式保存地址 | 编码在记录中 |
| 地址空洞 | 通常用填充表示,也可能根据导出方式省略 | 可以通过缺失的地址范围表示 |
| 常见用途 | 在已知基地址处烧录 | 固件分发与烧录 |
BIN 文件需要从外部指定基地址。没有这个地址,仅凭文件中的字节无法判断它们应该放在 Flash 的什么位置。
HEX 文件包含地址信息,因此可以在记录中描述不同位置的数据。
请记住: BIN 文件以向量表开头是一种常见的导出方式,并不是普遍保证。向量表前也可能存在文件头、填充或自定义映像格式。
4. Cortex-M 复位与启动流程
深入分析
4.1 复位后立即发生了什么?
正常的 Cortex-M 启动过程中,处理器从启动映射所指向的向量表获取两个初始值:
- 从偏移
0x00处获取初始 MSP。 - 从偏移
0x04处获取 Reset Handler 地址。
概念上相当于:
1 | MSP = *(uint32_t *)(vector_base + 0x00); |
随后,处理器开始执行 Reset Handler。
这是硬件行为,不需要先运行 main() 或 C 运行时。
启动地址处实际可见的存储区域,取决于器件的启动配置和内存映射。
4.2 什么是 Reset_Handler?
Reset_Handler 是处理器完成复位序列后的第一个软件入口。
它通常在汇编启动代码中实现。
它的职责是为正常执行 C 代码做好系统准备。
简化后的启动流程可以表示为:
1 | Reset_Handler: |
这只是用于说明的伪代码,并不是实际启动文件的精确实现。真实启动代码可能使用不同的跳转指令和运行时入口符号。
4.3 SystemInit() 做什么?
SystemInit() 通常由 STM32 器件支持包或工程启动代码提供。
根据工程的不同,它可能会:
- 配置时钟树。
- 设置 PLL。
- 配置 FPU。
- 设置向量表偏移。
- 执行 MCU 特有的早期初始化。
这些操作并不是通用且固定的流程,应始终查看当前工程中的实际实现。
4.4 main() 之前发生了什么?
C 运行时会准备应用程序所需的内存。
例如:
1 | int counter = 10; |
在进入 main() 之前:
counter必须为10。status必须为0。
运行时初始化负责完成这些工作。
这就引出了下一个重要主题:链接器与启动代码如何协作准备 RAM。
5. MSP、链接布局与 C 运行时初始化
深入分析
5.1 为什么初始 MSP 由链接器决定?
处理器在执行正常的 C 代码之前,就需要一个有效的栈。
但处理器并不知道当前工程希望把栈放在哪里。
链接器知道工程配置中定义的内存区域与段放置规则,因此能够确定栈区域,并提供栈顶的初始地址。
初始 MSP 通常就是预留栈区域的顶部地址。
请记住,Cortex-M 的栈通常向下增长,也就是向更低的地址增长。
例如,下面是一个简化的 SRAM 布局:
1 | SRAM 高地址 |
这只是概念性布局。实际的段顺序以及堆、栈的位置取决于链接器配置。
5.2 理解 .data、.bss、栈、堆与 NOINIT
这些名称描述了不同类型的运行时内存。
| 段 / 区域 | 用途 | 启动时的行为 |
|---|---|---|
.data |
带初始值的全局 / 静态变量 | 将初始值从 Flash 复制到 RAM |
.bss |
零初始化的全局 / 静态变量 | 将对应 RAM 清零 |
| 栈 | 函数调用、局部变量、保存的寄存器 | MSP 从栈顶开始 |
| 堆 | 动态内存分配 | 由内存分配器管理 |
NOINIT |
需要保留现有内容的内存 | 普通 C 运行时不对其初始化 |
例如:
1 | int counter = 10; // .data |
.data 和 .bss 尤其重要,因为 RAM 不会自动具有 C 程序要求的初始值。
5.3 为什么需要复制 .data?
例如:
1 | int counter = 10; |
初始值 10 必须保存在固件映像的某个位置,才能在掉电后仍然保留。
因此,初始值会保存在 Flash 中。
但程序希望 counter 是一个位于 SRAM 中、可以修改的变量。
链接器会描述这两个位置:
1 | Flash:初始值 = 10 |
启动时,运行时会将初始值从 Flash 复制到 SRAM。
概念上相当于:
1 | for (src = __data_load_start, |
这只是说明性的伪代码。实际启动代码使用链接器生成的符号,也可能需要复制多个段。
5.4 为什么需要清零 .bss?
例如:
1 | int status; |
C 语言要求:具有静态存储期、但未显式指定初始值的对象,必须进行零初始化。
因此,运行时会清零相应的 RAM 区域:
1 | for (dst = __bss_start; dst < __bss_end; ) |
同样,这只是概念性代码。
如果没有这一步,status 可能保留该 SRAM 位置之前的任意值。
5.5 什么是 NOINIT?
NOINIT 段是启动代码有意不修改的一块内存区域。
当数据需要在软件重启后保留,或其他组件已经准备好该内存时,这种机制就很有用。
之前的 IAP 设计就利用这个概念来保存 SRAM 向量表:
- Bootloader 将应用程序向量表复制到 SRAM。
- 应用程序预留同一块 SRAM 区域。
- C 运行时不能清零或覆盖这块区域。
如果向量表预留区被意外划入 .bss,启动过程就可能清除已经复制好的向量表项。
如果它被划入 .data,启动过程就可能用应用程序的初始数据映像覆盖它。
因此,必须正确放置这块预留区,并将其排除在普通初始化之外。
5.6 为什么两个固件映像的初始 MSP 不同?
之前看到过两个初始 MSP 值:
1 | 映像 A:0x200005F0 |
两者之差为:
1 | 0x20000940 - 0x200005F0 = 0x350 |
关键在于:
初始 MSP 由最终链接后的内存布局决定,而不只是由配置的栈大小决定。
链接器可能会在栈区域之前放置数量不同的 SRAM 数据。差异可能来自:
- 全局 / 静态变量数量不同。
- 带初始值的段或零初始化段不同。
- 额外的库或中间件数据。
- 不同的对齐方式与段放置方式。
- 不同的链接器配置或内存区域。
- 不同的栈区域放置位置。
栈大小相同,并不保证初始 MSP 地址相同。
5.7 如何在 IAR 中验证?
不要只根据地址猜测,而应查看链接器输出。
有用的文件包括:
.icf:链接器配置文件。.map:链接器生成的内存映射文件。
重点查找:
- SRAM 内存定义。
CSTACK和HEAP的放置位置。- 可读写段。
- 零初始化段。
- 所有
NOINIT段。 - 初始栈符号,它通常与
CSTACK边界相关。
确切的符号名称取决于 IAR 运行时和工程配置。
重要的检查步骤是:
- 找到 SRAM 区域。
- 确认
.data和.bss的放置位置。 - 找到栈预留区。
- 确定其顶部地址。
- 将该地址与向量表的第一个字进行比较。
不要假设所有工程都使用相同的段顺序。
6. 向量表重定位:VTOR 与内存重映射
深入分析
这是理解之前 bootloader 设计时最重要的区别之一。
这两种机制解决的问题有关联,但作用于不同的层次:
- VTOR 改变处理器查找向量表的位置。
- 内存重映射改变某个地址处可见的物理存储区域。
6.1 什么是 VTOR?
VTOR 是 Vector Table Offset Register,即向量表偏移寄存器。
它属于 Cortex-M 的系统控制块(SCB)。
1 | SCB->VTOR = 0x08010000; |
这会告诉处理器使用位于 0x08010000 的向量表。
假设应用程序存放在:
1 | 应用程序 Flash 基地址 = 0x08010000 |
它的向量表也位于这个地址。
将 VTOR 设置为该地址后,处理器就可以在那里找到应用程序的异常和中断处理函数。
向量表本身并没有移动,变化的只是查找向量表时使用的地址。
6.2 处理器如何找到中断处理函数?
对于 Cortex-M 处理器,向量表在外部中断表项之前,包含 16 个内核异常相关表项。
对于一个外部中断:
1 | 向量索引 = 16 + IRQ 编号 |
其处理函数表项地址为:
1 | 处理函数表项地址 = VTOR + 4 × 向量索引 |
例如,某个外部中断的 IRQ 编号为 5:
1 | 向量索引 = 16 + 5 = 21 |
处理器从该位置读取处理函数地址,然后进入相应的中断处理函数。
这也解释了为什么 VTOR 设置错误时,中断一发生,处理器就可能跳转到无效地址。
6.3 位于 0x08010000 的应用程序
假设 Flash 布局如下:
1 | 0x08000000 +----------------------+ |
对于 STM32F469 这样的 Cortex-M4 器件,直接的做法是:
1 | SCB->VTOR = 0x08010000; |
这样,处理器就直接使用 Flash 中的应用程序向量表。
不需要将向量表复制到 SRAM。
该地址必须满足具体 Cortex-M 实现对 VTOR 的对齐要求。不要认为任意 Flash 地址都有效。
6.4 内存重映射做了什么?
部分 STM32 器件提供 SYSCFG 内存重映射机制。
例如,支持该功能的 STM32 可能提供:
1 | __HAL_SYSCFG_REMAPMEMORY_SRAM(); |
它会改变通过 0x00000000 地址别名访问时可见的存储区域。
概念上如下:
重映射之前:
1 | 地址 0x00000000 |
重映射之后:
1 | 地址 0x00000000 |
重点是:重映射并不会复制数据,它改变的是内存映射关系。
如果已经将应用程序向量表复制到 SRAM,重映射就可以让这张 SRAM 向量表在零地址处可见。
6.5 还原之前的 SRAM 向量表设计
之前的设计可以按以下步骤理解。
步骤 1 —— Bootloader 将应用程序向量表复制到 SRAM。
1 | Flash 中的应用程序向量表 |
步骤 2 —— 应用程序预留 SRAM 的起始区域。
应用程序为向量表预留一块区域。
之前提到过预留 48 个字:
1 | 48 × 4 = 192 字节 = 0xC0 |
概念上如下:
1 | SRAM |
应用程序的 C 运行时初始化不能覆盖这块预留区。
步骤 3 —— 应用程序将 SRAM 重映射到零地址。
1 | __HAL_SYSCFG_REMAPMEMORY_SRAM(); |
此时,处理器在零地址处查找向量表,实际访问的是 SRAM 别名。
步骤 4 —— 中断使用 SRAM 中的向量表。
向量表在物理上位于 SRAM 中,同时也通过零地址别名可见。
应用程序代码仍然可以从正常的 Flash 地址执行。
6.6 Cortex-M3 必须使用 SRAM 重映射吗?
不需要。这是对之前解释的一项重要更正。
Cortex-M3 支持 VTOR。使用 Cortex-M3 的 STM32F103 同样支持 VTOR。
SRAM 重映射是一种替代机制,可能适用于特定的启动或兼容性设计,但并不是 Cortex-M3 强制要求的做法。
| 机制 | 改变了什么? | 示例 |
|---|---|---|
| VTOR | 向量表查找基地址 | SCB->VTOR = 0x08010000 |
| SRAM 重映射 | 某个地址别名处可见的存储区域 | SRAM 在 0x00000000 处可见 |
6.7 48 个字足够容纳向量表吗?
不一定。
需要考虑:
- STM32 的具体型号。
- 实际实现的内核向量和外部中断向量数量。
- 向量表要求的对齐方式。
- SRAM 预留区的大小。
- 链接器对预留区的放置方式。
例如,包含 16 个内核表项和 43 个外部中断表项的向量表需要:
1 | 16 + 43 = 59 个字 |
192 字节的预留区无法容纳完整的这张表。
这说明了为什么必须查看具体 MCU 的参考手册和启动向量定义。不要在未核实的情况下直接照搬其他工程的预留区大小。
6.8 应该使用哪种方法?
对于 STM32F469 应用程序,优先使用直接设置 VTOR 的方法:
1 | SCB->VTOR = APP_ADDRESS; |
它更简单,也不再需要:
- 将向量表复制到 SRAM。
- 预留 SRAM 的起始区域。
- 保护预留区,避免被运行时初始化覆盖。
- 将 SRAM 重映射到零地址。
仍然值得在笔记中保留 SRAM 重映射方法,因为它有助于理解旧设计,也适用于确实需要这种机制的器件或启动架构。
7. IAP bootloader 向应用程序移交控制权
深入分析
7.1 什么是 IAP?
IAP 是 In-Application Programming,即在应用编程。
在典型的 STM32 bootloader 设计中,bootloader 可以:
- 接收固件映像。
- 擦除并写入应用程序 Flash 区域。
- 校验映像。
- 将执行权移交给应用程序。
简化的 Flash 布局如下:
1 | Flash |
Bootloader 和应用程序是两个独立的固件映像,各自拥有向量表和链接器配置。
7.2 为什么跳转到应用程序不同于复位?
MCU 发生硬件复位时,处理器会进入复位序列,并按照硬件复位行为初始化其架构状态。
但 bootloader 跳转到应用程序时,并没有发生硬件复位。
处理器会带着大量现有状态继续运行。
例如:
- 外设寄存器可能保留之前的配置。
- NVIC 中断使能状态和挂起状态可能保留。
- SysTick 可能仍在运行。
- 时钟配置可能已经被 bootloader 修改。
- 处理器可能处于不同的栈配置或中断状态。
因此,bootloader 必须在移交控制权之前做好系统准备。
7.3 应用程序需要 bootloader 做什么?
应用程序向量表包含两个重要的初始值:
1 | APP_ADDRESS + 0x00 → 初始 MSP |
例如:
1 | APP_ADDRESS = 0x08010000 |
Bootloader 必须:
- 校验应用程序映像是否可用。
- 根据需要停止或禁用中断源。
- 停止 SysTick。
- 清除或禁用相关 NVIC 中断状态。
- 设置应用程序的向量表基地址。
- 载入应用程序的初始 MSP。
- 跳转到应用程序的 Reset Handler。
随后,应用程序运行自己的启动流程。
7.4 概念性的 C 实现
下面展示了关键操作,但并不是可直接用于生产环境的代码:
1 |
|
重要的实现注意事项: 在普通 C 函数中修改 MSP,然后再调用另一个 C 函数,可能并不安全。编译器可能生成依赖原栈的栈访问或函数尾声代码。生产环境中的 bootloader 通常使用经过严格控制的汇编移交代码,或使用包含汇编序列的合适裸函数,在载入 MSP 后直接跳转,避免继续依赖旧栈。
此示例用于说明所需的数值和操作,并不是完整、安全的实现。
7.5 为什么应用程序要重新执行启动代码?
Bootloader 跳转到应用程序的 Reset Handler 后,应用程序从自己的启动入口开始执行。
1 | Bootloader |
应用程序的启动代码会初始化自己的 .data 和 .bss,准备运行时环境,并调用 main()。
因此,应用程序应构建为独立映像,拥有自己的向量表和启动代码。
但请记住:重新执行启动代码,并不意味着 MCU 已经复位。 应用程序所看到的外设和时钟,仍可能处于 bootloader 留下的状态。
7.6 应用程序校验
跳转之前,不要盲目信任向量表中的前两个字。
至少应考虑检查:
- MSP 是否位于应用程序允许使用的 SRAM 范围内。
- MSP 是否满足所需的对齐要求。
- Reset Handler 是否指向允许执行代码的区域。
- Reset Handler 是否具有预期的 Thumb 状态位。
- 映像是否通过完整性检查,例如 CRC 或密码学签名校验。
确切的校验规则取决于内存布局与安全要求。
7.7 一个实用的理解模型
应把 bootloader 的控制权移交理解为准备一个新的程序运行环境,而不是重启 MCU。
Bootloader 必须为应用程序提供其预期的条件:
| 要求 | 由谁准备? |
|---|---|
| 应用程序向量表基地址 | Bootloader |
| 应用程序初始 MSP | Bootloader |
| 有效的应用程序映像 | Bootloader |
.data 初始化 |
应用程序运行时 |
.bss 初始化 |
应用程序运行时 |
| 应用程序对外设和时钟的预期条件 | 按设计由 bootloader 和 / 或应用程序启动代码负责 |
执行 main() |
应用程序运行时 |
8. 使用 IAR 进行实际检查与调试
中等深度
本节将前面的概念与实际开发流程联系起来。
不需要记住每一个启动符号,但需要知道出问题时应该去哪里查看。
8.1 理解启动相关文件
在 IAR STM32 工程中,查找类似下面的文件:
1 | startup_stm32xxxx.s |
它们的职责各不相同:
| 文件 | 职责 |
|---|---|
startup_stm32xxxx.s |
Reset Handler、向量表和初始启动流程 |
system_stm32xxxx.c |
系统级配置,通常包含 SystemInit() |
.icf |
链接器内存区域与段放置规则 |
.map |
最终链接后的内存布局与符号信息 |
确切的文件名取决于 STM32 器件与工程。
8.2 IAR 运行时入口
可能会看到这样的符号:
1 | __iar_program_start |
它与 IAR 运行时启动过程有关。
它并不是 ARM Compiler 入口点 __main 所使用的同一个符号。
常见的 IAR 启动流程在概念上如下:
1 | Reset_Handler |
确切的顺序与实现取决于启动文件和运行时配置。
还可能遇到 __low_level_init()。它是 IAR 提供的运行时钩子,可用于早期初始化,也可能影响正常的数据初始化行为。修改之前,应查看 IAR 文档和当前工程的实际实现。
重要区别: SystemInit() 通常属于 MCU 支持 / 启动层;__iar_program_start 属于 IAR 运行时。两者职责不同。
8.3 实用的调试顺序
在以下位置设置断点:
| 断点 | 检查内容 |
|---|---|
Reset_Handler |
处理器是否进入了预期的启动代码? |
SystemInit() |
时钟和早期硬件设置是否正确? |
__iar_program_start |
控制流是否到达 IAR 运行时? |
__low_level_init() |
是否自定义了早期运行时初始化? |
main() |
启动过程是否成功完成? |
也可以检查:
SCB->VTOR- MSP
- 向量表的前两个表项
.data和.bss的地址- 应用程序 Flash 基地址
- 链接器 map 文件
8.4 一个实际排查练习
假设应用程序存放在 0x08010000,但 bootloader 跳转过去后立即失败。
按以下顺序排查:
- 读取
0x08010000处的前两个字。 - 检查 MSP 是否指向有效的 SRAM。
- 检查 Reset Handler 是否指向应用程序 Flash 区域。
- 检查 VTOR 是否设置为
0x08010000。 - 检查是否有中断通过错误的向量表触发。
- 在应用程序的
Reset_Handler处设置断点。 - 继续跟踪
SystemInit()和 IAR 运行时初始化。 - 检查是否到达
main()。
这个顺序遵循实际的启动依赖链,而不是盲目猜测原因。
9. 完整启动流程:一页速查
正常硬件启动
1 | 硬件复位 |
IAP 控制权移交
1 | Bootloader |
在 IAP 路径中,bootloader 通过软件完成最初的控制权移交。两个固件映像之间并没有发生硬件复位。
10. 快速参考
重要寄存器与函数
| 名称 | 用途 |
|---|---|
SCB->VTOR |
选择向量表基地址 |
__set_MSP(value) |
设置主栈指针 |
__get_MSP() |
读取主栈指针 |
__disable_irq() |
全局禁用可配置中断 |
__DSB() |
数据同步屏障 |
__ISB() |
指令同步屏障 |
SystemInit() |
工程 / 器件特定的早期初始化 |
Reset_Handler |
固件启动入口 |
__iar_program_start |
IAR 运行时启动入口 |
__low_level_init() |
可选的 IAR 早期初始化钩子 |
__HAL_SYSCFG_REMAPMEMORY_SRAM() |
在支持该功能的 STM32 器件上启用 SRAM 重映射 |
重要概念
| 概念 | 可以这样记忆 |
|---|---|
| 向量表 | 处理器查找初始栈和异常处理函数的位置 |
| 初始 MSP | 初始主栈的栈顶地址 |
| Reset Handler | 第一个软件启动入口 |
| VTOR | 决定处理器去哪里查找向量表 |
| 内存重映射 | 决定某个地址别名处对应哪块物理存储区域 |
.data |
需要复制到 RAM 的带初始值变量 |
.bss |
零初始化变量 |
NOINIT |
运行时初始化有意不修改的内存 |
| 链接器 | 决定内存布局与段放置方式 |
| C 运行时 | 在 main() 之前准备 C 程序环境 |
值得记住的五个区别
- 向量表与启动代码: 向量表告诉处理器从哪里开始;启动代码负责准备应用程序。
- MSP 与栈大小: MSP 是一个地址;栈大小是预留内存的容量。
- Flash 映像与 RAM 运行时: 变量初始值可能保存在 Flash 中,但运行时在 RAM 中使用。
- VTOR 与重映射: VTOR 改变向量表查找基地址;重映射改变某个地址处可见的存储区域。
- 应用程序跳转与硬件复位: 跳转到 Reset Handler 并不会复位外设或处理器状态。
11. 自测问题
试着不看前面的内容,回答以下问题。
Q1. 正常的 Cortex-M 复位后,处理器立即从哪里获取初始 MSP 和 Reset Handler 地址?
Q2. 为什么向量表的第一个表项指向 SRAM,而第二个通常指向 Flash?
Q3. 如果两个固件映像的栈和堆大小完全相同,为什么初始 MSP 仍然可能不同?
Q4. 为什么 bootloader 必须跳转到应用程序的 Reset Handler,而不能直接调用 main()?
Q5. 修改 VTOR 与将 SRAM 重映射到零地址有什么区别?
Q6. 为什么必须将 SRAM 向量表预留区排除在 .data 和 .bss 初始化之外?
Q7. 为什么经过 IAP 跳转启动的应用程序,行为可能不同于硬件复位后启动的应用程序?
12. 参考答案
Q1. 初始 MSP 和 Reset Handler 的获取位置
处理器读取启动映射所指向的向量表中的前两个 32 位表项。表项 0 提供初始 MSP,表项 1 提供 Reset Handler 地址。
Q2. 为什么分别指向 SRAM 和 Flash?
栈是运行时使用的内存结构,因此放在可写的 SRAM 中。启动代码通常放在非易失性的 Flash 中。
Q3. 为什么初始 MSP 可能不同?
链接器决定栈的实际位置。即使预留大小不变,其他 RAM 段、对齐方式和链接器配置也可能改变栈的位置。
Q4. 为什么要跳转到 Reset_Handler?
Reset Handler 负责执行启动工作,包括底层初始化和进入 C 运行时。直接调用 main() 会绕过这些必要步骤。
Q5. VTOR 与重映射有什么区别?
VTOR 改变查找向量表时使用的地址。内存重映射改变通过某个地址别名(例如 0x00000000)访问时可见的存储区域。
Q6. 为什么要保护 SRAM 向量表?
否则,运行时初始化可能覆盖 bootloader 复制的向量表项,导致异常和中断处理失效。
Q7. 为什么 IAP 启动可能不同于硬件复位?
IAP 跳转不会执行硬件复位。应用程序可能继承 bootloader 留下的外设配置、时钟设置、中断状态以及其他硬件状态。
最终理解模型
如果只记住一条链路,请记住:
1 | 向量表 |
如果只记住 IAP 的一个关键区别:
Bootloader 跳转只是移交执行权,并不会重新建立硬件复位后的状态。
理解这个区别,就能解释设计可靠 bootloader 时为什么需要采取许多预防措施。
本学习笔记结束。