本文把 Cortex-M4 从上电复位到双镜像 Bootloader 跳转 APP串成一条线。核级行为以 ARM 架构手册为准;F407 类地址、时钟、扇区、IRQ 以 ST / 兆易手册为准。文中的 APP 基址、魔数、升级路径是工程约定示例,换芯片时必须核对手册。
示例数值:APP 基址 0x08010000、魔数 0x0000A5A5、升级文件 0:/firmware/app.bin。IRQ 编号、扇区表、SRAM 容量因型号而异,不要机械套用。
0. 导读与术语速查
后面所有章节都围绕同一条故事线:上电 → 硬件读向量表 → C 库启动 → 你的 main →(可选)BL 升级 → 跳 APP。若某节读起来抽象,先回到这里查术语,再对照各章开头的「Bootloader 里会碰到」。
- MCU
- 一颗芯片 = CPU 核 + Flash + SRAM + 外设,靠总线按地址互通。
- Cortex-M4
- ARM 32 位 MCU 常用内核;只执行 Thumb-2 指令,PC 最低位必须为 1。
- Flash / SRAM
- Flash 存程序与常量(典型
0x08000000);SRAM 存变量与栈(典型0x20000000)。 - 向量表
- Flash 开头的地址表(数据,不是指令):第 0 项是栈顶,第 1 项是 Reset 入口,其后是各异常/中断 Handler 地址。
- VTOR
- 向量表偏移寄存器:告诉 CPU「当前这张表从哪个地址开始」。双镜像时 BL 与 APP 各有一张表。
- PC / SP / LR
- 程序计数器、栈指针、链接寄存器。跳转 APP 至少要换 MSP(栈)和 PC(入口)。
- MSP / PSP
- 两套栈指针。裸机 BL 通常全程用 MSP;RTOS 里线程常用 PSP,中断仍用 MSP。
- NVIC
- 嵌套向量中断控制器:使能外设 IRQ、设优先级、挂起/清除中断。
- 异常 vs 中断
- Reset、Fault、SysTick 等统称异常;口语「中断」多指外设 IRQ。
- EXC_RETURN
- 进中断时硬件写入 LR 的魔术值;
BX LR时 CPU 识别为异常返回而非普通跳转。 - Thumb 位
- 写入 PC 的地址 bit0 必须为 1;清 0 会 UsageFault。
- 小端
- 多字节整数低地址放低字节。F4 固定小端;用十六进制编辑器看 bin 时不要按大端读。
- SystemInit / __main
- 启动链:先配时钟,再拷
.data、清.bss,最后进main。 - 弱符号 Handler
- 启动文件里未实现的中断默认
B .死循环;写了同名 C 函数则覆盖。 - PRIMASK
__disable_irq()置位后屏蔽大部分 IRQ;跳转 APP 前常用。- DSB / ISB
- 内存屏障:改 VTOR 后应
__DSB(); __ISB();。 - 双镜像
- Flash 前段 Bootloader、后段 APP,各含独立向量表与代码;复位永远进 BL 表。
- 镜像信息 / 魔数
- Flash 上约定字段(magic、长度、CRC),BL 用来判断 APP 是否合法。
- 热启动跳转
- 软件改 MSP/PC 进 APP,不等同芯片复位:外设、VTOR、NVIC 不会自动恢复默认。
1. 从 C 到硅片:地址地图与 MCU 组成
本章要回答:写 C 时 CPU 实际在访问什么?Flash、RAM、外设如何排地址?
与前后章的关系:这是全文的「仓库平面图」。没有它,后面向量表基址、APP 链接地址都无从谈起。
Bootloader 里会碰到:BL 链在 0x08000000,APP 链在 0x08010000——都是地图上的不同区间。
1. 先建立心智模型
后面几百段文字都围着三件事转。先把它们当成「工厂里的三样道具」:
| 道具 | 芯片里的东西 | 关键寄存器 | 一句话 |
|---|---|---|---|
| 下一道工序单 | CPU 正在执行哪条指令 | PC(R15) | 顺序执行则加指令长度;跳转才写新地址。读 R15 得到的是当前条 +4 |
| 临时记事本 | 栈(函数调用、中断现场) | MSP / PSP(R13) | 栈向下长,复位时 SP 必须指向高地址 |
| 出事了找谁的通讯录 | 中断向量表 | SCB->VTOR | 表里存的是地址数据,不是指令 |
再加一张仓库平面图:Flash、SRAM、GPIO、USB 控制器都只是不同地址。CPU 不理解「这是程序」或「这是引脚」——它只按地址读/写 32 位。读 0x08000000 可能是指令;写某个 GPIO 置位寄存器,引脚就跳变。
图 1 MCU 不是「一个会跑 C 的黑盒」,而是核按地址访问三类资源。
CPU(这里是 Cortex-M4):取指令、做加减、改 PC、响应异常。它几乎不直接「知道」USB、Flash 擦除这些事。
MCU(以 GD32F4 / STM32F4 为例):在同一颗硅片上,把 CPU、Flash、SRAM、时钟树、GPIO、USART、USB 全速控制器、Flash 编程控制器、CRC 单元焊在一起。时钟树 ST 称 RCC,兆易称 RCU;Flash 控制器 ST 多称 FLASH,兆易称 FMC;USB 全速主机/设备 ST 常称 USB OTG FS,兆易常称 USBFS。
这类芯片的核特征(影响后面每一节):
- ARMv7-E-M,Thumb-2 指令集,没有 ARM 状态。PC 最低位必须为 1,否则 UsageFault。
- 小端(little-endian):多字节整数在存储器里「低地址放低字节」。GD32F4 / STM32F4 固定小端。详细见第 1 章大小端折叠块。
- 可选 FPU(单精度)。
SystemInit里若__FPU_USED==1,会写SCB->CPACR打开 CP10/CP11。 - 有 NVIC、SysTick、SCB,映射在
0xE0000000起的「系统区」,和 GPIO 那种片上外设不是同一段。
深入阅读:为什么这里就要提字节序
后面向量表、镜像信息块、设备魔数、硬件 CRC「按 32 位字喂入」,全部按小端解释。用十六进制编辑器看 bin 时,看到的是按地址排列的字节,不是调试器里那个uint32_t 写法。细节在下一节。
Cortex-M 可见 4 GB 地址(架构规定)。F4 级常用分段如下。容量因订货型号而异:STM32F407 数据手册写「最多 1 MB Flash、192+4 KB RAM(含 64 KB CCM)」;系统 SRAM 常见为 112 KB + 16 KB,映射在 0x20000000,CCM/TCM 在 0x10000000。GD32F407 数据手册同样常见 192 KB SRAM + 64 KB TCMSRAM。下文图中的「128 KB @ 0x20000000」只表示系统 SRAM,不是芯片全部 RAM。
| 区间 | 用途 | 复位/Bootloader 相关性 |
|---|---|---|
| 0x00000000 | 启动别名。从主 Flash 启动时,这里镜像到 0x08000000 | VTOR 复位为 0 时,读「地址 0」就是 Flash 开头的向量表 |
| 0x08000000 | 片内 Flash 物理地址 | Bootloader 常链接在这里;APP 示例放在 0x08010000(64 KB 之后) |
| 0x20000000 | SRAM | 栈、全局变量、.data/.bss |
| 0x10000000 | CCM(核紧耦合 SRAM,部分 F4 有) | CPU 快,DMA 通常到不了;许多 Bootloader 不用它 |
| 0x40000000 | APB/AHB 外设 | USART、GPIO、USB、Flash 控制器、CRC 的控制寄存器 |
| 0xE0000000 | 核内外设 | NVIC、SysTick、SCB、调试单元 |
图 2 CPU 只认地址。Bootloader 能工作,是因为复位时「地址 0」指向了含 BL 向量表的 Flash。
BOOT1,BOOT0 在复位后第 4 个 SYSCLK 上升沿锁存。BOOT0=0(BOOT1 任意)进主 Flash;10 进系统存储器(片内 bootloader / DFU);11 进片内 SRAM。BOOT0 是专用脚,BOOT1 与 GPIO 复用,采样后该 GPIO 可挪作他用。量产板通常 BOOT0 接地。GD32F4 机制同类,组合表以兆易用户手册为准。深入阅读:再深一点:位带(bit-band)、对齐、属性
Cortex-M4 对 SRAM 和外设有 bit-band 别名区:写别名区的一个字,等于对原地址的某一 bit 做原子置位/清位。GPIO 库不一定用它。自己写寄存器时:STM32 常用 BSRR 原子置位/复位;GD32 常用 BOP / BC。也可以读-改-写 ODR,但要注意中断里并发丢 bit。
对齐:LDR/STR 32 位一般要 4 字节对齐。向量表、函数入口、C 结构体里的 uint32_t 都按此理解。Cortex-M4 允许一部分非对齐访问,但向量表、栈帧必须对齐。
MPU(可选)可以把某段地址标成只读/禁止执行。多数裸机 Bootloader 不启用 MPU。HardFault/MemManage 在开 MPU 后才会变得常见。
深入阅读:大小端——向量表、魔数、CRC 为何必须按小端理解
2.1 大小端:同一个数,字节怎么排
大小端讨论的是:一个多字节整数存进连续地址时,哪一头的字节放在低地址。单字节(uint8_t)没有端序问题。
口诀:
- 小端 little-endian:低地址放低字节(Least significant byte first)。本文讨论的 F4 级 Cortex-M4 是这个。
- 大端 big-endian:低地址放高字节(Most significant byte first)。网络协议、部分 DSP/老架构常见。
以 32 位值 0x12345678 为例(最高字节是 12,最低字节是 78):
图 1b 同一逻辑值,小端与大端在连续 4 字节里的排法相反。
| 宽度 | 逻辑值 | 小端:低地址 → 高地址 | 大端 |
|---|---|---|---|
| 8 位 | 0x78 | 78 | 相同(无端序) |
| 16 位 | 0x5678 | 78 56 | 56 78 |
| 32 位 | 0x12345678 | 78 56 34 12 | 12 34 56 78 |
CPU 读一个字时做了什么
Cortex-M4 上 LDR R0, [addr](addr 4 字节对齐)按小端拼:
R0 bit[7:0] ← 内存 [addr+0]
R0 bit[15:8] ← 内存 [addr+1]
R0 bit[23:16] ← 内存 [addr+2]
R0 bit[31:24] ← 内存 [addr+3]
所以 [addr] 起若是 78 56 34 12,R0 里就是 0x12345678。
LDRH(半字)同理只用 2 字节:78 56 → 0x5678。LDRB 只搬 1 字节,与端序无关。
C 里 uint32_t x = *(uint32_t *)p; 就是一次这样的 LDR。结构体里连续的 uint32_t 字段,每个字段内部都是小端;字段之间按声明顺序排地址(还可能有对齐填充)。
SCB->AIRCR 有一个只读位 ENDIANESS(bit15):0=小端,1=大端。GD32F4 / STM32F4 以及绝大多数 Cortex-M4 固定小端,不能靠写这个位切换。建议核对手册;下文一律按小端。三种「看见这个数」的方式,不要混
| 场合 | 你看到的 | 对这类 MCU |
|---|---|---|
| C / 调试器 Watch | 0x12345678 | 已经按小端拼好的逻辑值 |
| 十六进制编辑器、U 盘 bin、Flash dump | 78 56 34 12 | 按地址从小到大列出的原始字节 |
| 串口「按字节发出」 | 先发谁后发谁 | 协议自己定,与芯片默认端序无关 |
把编辑器里的 78 56 34 12 从左到右当成 0x78563412,就是用大端眼光读小端文件,向量表、CRC、设备标志全会读错。
固件与协议里四处会踩到
1. 向量表是 32 位字数组。Flash 开头若是:
地址 编辑器里的字节 CPU 读成的 uint32_t
0x08000000 58 7B 00 20 MSP = 0x20007B58(某次链接 map 的栈顶示例)
0x08000004 49 02 00 08 PC = 0x08000249 = Reset_Handler | Thumb 位
硬件复位就是这样 LDR 两个字。若你按大端把第一组读成 0x587B0020,MSP 会指到无效地址,立刻跑飞。
2. 镜像信息块(示例:放在向量表之后预留字,如索引 98 起、偏移 0x188)全是 uint32_t。设备魔数若约定 IMAGE_MAGIC = 0x0000A5A5,在 bin 里是:
偏移 0x188+8 字节: A5 A5 00 00 → magic == 0x0000A5A5
若误读成 0xA5A50000,校验会认为「不是本机镜像」。
3. 硬件 CRC按 32 位字喂入(多项式 0x04C11DB7),不是按字节的 zlib/PNG 那种 CRC。喂入顺序是存储器里的字(已按小端组装)。同一段字节若在大端机器上按字解释,CRC 会对不上。
4. 按字或按块运算的加密:若算法按 32 位字工作(如 XXTEA),文件里的字节必须按小端拼成字。AES 的分组是 128 位(NIST FIPS 197),不是 32 位;CTR 只是工作模式。解密时按算法规定的宽度取数,不要先按大端拼。
字节序 ≠ 位序
端序管的是字节和字节谁在前。一个字节内部 bit0 仍是最低位(值为 1 的那一档),bit7 是最高位。GPIO 的 ODR bit0 对应某引脚,和「这颗芯片是小端」是两件事。不要把「小端」理解成「位也倒过来」。
寄存器里写 0x00000001 置 bit0:小端下这个字在 RAM 里第一个字节是 01,后面三个 00。这和「bit0 在最低字节」是一致的。
通信、文件、跨机器时谁说了算
| 通道 | 常见约定 |
|---|---|
| 本机 SRAM / Flash / 结构体 | 跟 CPU:小端 |
| TCP/IP、很多「网络字节序」文档 | 大端(所以要把本机 uint32_t 做 htonl 一类交换再发出) |
| UART 自定义帧 | 协议写明先发低字节还是高字节;不写就会对不上 |
| USB 描述符等多数字段 | USB 规范是小端 |
| 本机 Flash 里的 APP.bin / 升级包 | 与芯片一致:向量表和镜像信息都是小端字 |
本机内部 uint32_t a = b + c 不必每次考虑端序。只有「把这个数拆成字节发出去 / 从文件按字节拼回来 / 给另一端序的机器看」时才要动手。
拆字节(小端发出,低字节先发):
uint32_t v = 0x12345678;
uart_put(v & 0xFFu); /* 78 */
uart_put((v >> 8) & 0xFFu); /* 56 */
uart_put((v >> 16) & 0xFFu); /* 34 */
uart_put((v >> 24) & 0xFFu); /* 12 */
若协议规定大端(高字节先发),移位方向反过来。内核也有 REV(32 位字节对调)、REV16(半字内对调)指令,CMSIS 为 __REV()。
- 编辑器从左到右当大端整数读小端 bin。
- 用
(buf[0]<<24)|(buf[1]<<16)|…去拼本机 Flash 里已经按小端排好的字——那是按大端拼,会得到错误的魔数。本机应memcpy或直接*(uint32_t*)buf(注意对齐)。 - 以为「32 位单片机所以存储器按 32 位大端存放」。位数(32)和端序(小/大)是两件事。
- 把位带/bit0 和端序混在一起。
main。2. 复位与启动链:向量表与 main 之前
本章要回答:上电后、进入 main 之前,硬件与启动文件各做了什么?
与前后章的关系:承接第 1 章的地址地图:复位从 Flash 开头读两个 32 位字。
Bootloader 里会碰到:复位永远进 BL 向量表;跳 APP 则是手动再执行一次「读表填 MSP/PC」,并应改 VTOR。
2.1 复位:硅片在执行任何 C 之前
上电复位、复位脚、看门狗、或 SCB->AIRCR 的 SYSRESETREQ(NVIC_SystemReset)之后,时钟稳定,CPU 退出复位。此时还没有执行 Reset_Handler。
图 4 地址 0 和 4 是数据(两个指针),不是机器码。
用数字演一遍。假设链接结果为:
0x08000000: 20 00 00 20 // 小端 → MSP = 0x20000020(示例)
0x08000004: 49 02 00 08 // 小端 → 0x08000249 = Reset_Handler | 1
则 MSP=0x20000020,PC 从 0x08000248 按 Thumb 取指,进入 Reset_Handler。典型内容:
LDR R0, =SystemInit
BLX R0 ; 配时钟、开 FPU
LDR R0, =__main
BX R0 ; C 库:拷 .data、清 .bss,最后调用 main
到 main 时:PC 在你的 C,MSP 已因调用下降了一些,VTOR 仍是 0,向量表仍是 Flash 开头那张。
复位种类(建议核对手册)
- 上电复位 POR:SRAM 内容无定义。
- 引脚复位 / SYSRESETREQ:核和外设复位;部分实现里 SRAM 可能仍有旧数据,但 C 库会再清 bss、拷 data,不要依赖「热复位 RAM 还在」。
- 看门狗复位:类似系统复位,可在标志寄存器里区分原因。
2.2 向量表:通讯录、映像开头、启动文件
中断向量表不是程序,是一张放在 Flash 开头的地址通讯录。 表里每一格是一个 32 位数据(指针),不是机器指令。CPU 用它回答两件事:栈顶在哪、出事/中断了该把 PC 改到哪。
2.2.1 是什么、做什么
第 n 项的存放地址 = VTOR + 4×n
第 n 项的内容 = 某个函数入口 | 1 (第 0 项例外:是 MSP 初值)
CPU 不会自己猜「USB 中断该进哪个 C 函数」。它只做查表:算出向量索引 → 读 [VTOR + 4×索引] → 写入 PC。上电时还没有任何 C:硬件先读 [0] 填 MSP、读 [4] 填 PC,然后才进入 Reset_Handler。这两个字都按小端从 Flash 拼出来,见第 1 章大小端折叠块。
向量索引 = IRQn + 16
IRQn = 向量索引 - 16
例:某 F407 上 USB OTG FS IRQn = 67 → 索引 83 → 表项地址 = VTOR + 4×83 = VTOR + 0x14C
其它外设(USART、TIMER)的 IRQn 因型号而异,以启动文件向量表或手册 IRQ 章为准
| 索引 | IRQn | 名字 | 含义 |
|---|---|---|---|
| 0 | — | 初始 MSP | 复位时装进 MSP 的值(栈顶) |
| 1 | — | Reset | 硅片真正的程序入口(不是 main) |
| 2 | — | NMI | 不可屏蔽,极少用 |
| 3 | — | HardFault | 总故障入口 |
| 4 | — | MemManage | MPU / 访问违例(需使能) |
| 5 | — | BusFault | 总线错误(需使能) |
| 6 | — | UsageFault | 非法指令、PC bit0=0 等(需使能) |
| 7–10 | — | 保留 | 必须为 0 |
| 11 | — | SVCall | 系统调用,RTOS 常用 |
| 12 | — | DebugMon | 调试 |
| 13 | — | 保留 | |
| 14 | — | PendSV | RTOS 上下文切换 |
| 15 | -1 | SysTick | 系统滴答 |
| 16 起 | 0 起 | IRQ0… | 片上外设,换芯片顺序会变 |
索引 0–15 各家 Cortex-M 一样;从 16 起按这颗芯片的 IRQ 编号排。Keil 启动文件里符号常叫 __Vectors(STM32 示例 startup_stm32f407xx.s,GD32 示例 startup_gd32f407_427.s)。F407 类向量表用到 FPU 相关项时大约到索引 97。有的工程把索引 98 起(偏移 0x188)挪出来放镜像信息,向量表里对应项改为 0 或注释掉。
Keil 用 EXPORT Foo_IRQHandler [WEAK],默认落到 Default_Handler 的 B .(死循环)。C 里写同名函数则覆盖弱符号。开了 USART2 RX 中断却没写 Handler,就会卡在这里。
2.2.2 在哪?会不会拷进 RAM?
最常见做法:表只在 Flash 开头,启动时不拷进 SRAM。 Flash 已挂在 CPU 地址空间上,读 [0x08000000] 和读 SRAM 一样走总线。许多工程没有 memcpy(__Vectors, ram, …),也不把 VTOR 指到 SRAM。
| 向量表(常见 BL/APP) | 全局 .data | |
|---|---|---|
| 烧进 Flash | 是,bin 最开头 | 初值在 Flash |
| 启动时拷到 SRAM | 否 | 是(__main 拷) |
| CPU 怎么访问 | 读 Flash(VTOR=0 时经地址 0 别名) | 读 0x2000xxxx |
有人会把表拷到 RAM 再设 VTOR,为了运行时改 Handler 或少等 Flash。那是额外做法,不是复位硬件的默认行为。
Cortex-M 量产镜像里表在映像最开头,原因有两层:
- 链接器
*.o (RESET, +First)强制RESET段占据0x08000000。 - 复位时 VTOR=0,硬件只认地址 0 附近那张表。表若不在经别名映射到 0 的 Flash 开头,复位读到垃圾就会跑飞。
0x08000000 向量[0] MSP
0x08000004 向量[1] Reset_Handler
...
约 0x0800018B 索引 97 结束(98 项 × 4 = 0x188)
其后才是 Reset_Handler 等机器码
CPU 不扫描源文件里的 RESET、__Vectors 这些词,只读地址。这些名字是给汇编器/链接器排地址用的(见第 2 章折叠块「Keil AREA」)。换 GCC 会改成 .section .isr_vector,芯片行为一样,因为 Flash 开头仍是那张表。
运行时基址由 SCB->VTOR 决定(详见 5.1 VTOR)。Bootloader 运行期间常常 VTOR=0;跳 APP 后 APP(或跳转代码)应改成 APP 基址,例如 0x08010000。有的实现跳转不写 VTOR,把责任交给 APP——能工作但窗口期更危险,见第 5 章与第 6 章。
2.2.3 整份程序映像结构
一份固件不是「从第一行 C 往下执行」。示例:Bootloader 链接在 0x08000000,占用前 64 KB:
Flash 0x08000000
┌──────────────────────────────────────────┐
│ ① 向量表 RESET 段(数据) │ ← 映像开头,给硬件看
│ [0] MSP [1] Reset [2…] Handler 指针 │
├──────────────────────────────────────────┤
│ ② .text 机器码 │
│ Reset_Handler → SystemInit → __main │
│ main、驱动、各 IRQHandler 函数体 │
├──────────────────────────────────────────┤
│ ③ .rodata 常量、字符串 │
├──────────────────────────────────────────┤
│ ④ .data 的初值(给启动时拷到 RAM) │
└──────────────────────────────────────────┘
0x08010000 APP 同样结构:自己的表 + 自己的代码
执行顺序(和文件行号不是一回事):
硬件读向量表 → PC = Reset_Handler
→ SystemInit 配时钟
→ __main 拷 .data、清 .bss
→ main() 你写的逻辑
main 是口语里的入口;硅片真正的入口是向量[1]。USB 来了也不会从程序开头顺序跑,而是用 VTOR 算出 +0x14C,改 PC。栈只负责把旧 PC 压起来。
图 6 表里是指针;代码在后面;RAM 里没有这张表。
| 说法 | 对不对 |
|---|---|
| 向量表在固件 bin / Flash 镜像的开头 | 对(Cortex-M 常规链接) |
| 向量表是程序的第一段代码 | 不对,是数据 |
| CPU 上电从第一行 C 开始执行 | 不对,从向量[1] 的 Reset_Handler 开始 |
| BL 和 APP 各有一张表,各在各的镜像开头 | 对(示例 0x08000000 / 0x08010000) |
| 运行时永远用 Flash 芯片物理开头那张表 | 不对,用 VTOR 指向的那张 |
深入阅读:SRAM 内部分段(.data / .bss / 堆 / 栈,链接 map 示例)
3.1 RAM 内部结构(链接结果示例)
地址地图上的 0x20000000 是整块 SRAM。链接器再把它切成 .data、.bss、堆、栈。外设寄存器(0x40000000)和 SCB(0xE000ED00)不在这块里。CCM 0x10000000 许多工程的 scatter/ld 脚本不用。
下面数字来自某次链接的系统 SRAM 段,会随编译变化,顺序一般不变。不要把这些绝对地址抄进自己的工程。
图 2b 向量表不在这张图里。它留在 Flash 0x08000000。RAM 只有变量、堆、栈。
| 区 | 该次 map 地址 | 启动时 |
|---|---|---|
.data | 0x20000000 起约到 0x200000A8 | 从 Flash 拷贝初值 |
.bss | 随后到 0x20003B58 | 清 0 |
| HEAP | 0x20003B58,8 KB(示例) | C 库 malloc 池 |
| STACK | 0x20005B58,8 KB,顶 0x20007B58 | 复位 MSP 指向顶;PUSH 向低地址 |
栈向下、堆向上。若链接脚本把 HEAP 紧挨在 STACK 下方且中间没有保护带,栈溢出往往先撞 HEAP;这是链接布局,不是架构保证。函数代码默认仍在 Flash 取指(XIP),不会因为 RAM 有结构就把 .text 搬进来。
深入阅读:Keil AREA / GCC .isr_vector 与 scatter 脚本
7.4 AREA 是什么;启动文件怎么分段
AREA 是 Keil ARMASM 的伪指令(给汇编器、链接器看),不是 Thumb 指令,Flash 里找不到这个英文单词。作用:声明「下面这些字节属于哪一个段、属性是什么」。链接器再按 scatter 排到 Flash 或 RAM。
AREA 段名, 属性1, 属性2, ...
Keil 的 F4 启动文件(STM32 / GD32 结构相同)里常见四个 AREA:
| AREA | 属性 | 最后放到哪 |
|---|---|---|
STACK | NOINIT, READWRITE, ALIGN=3(8 字节对齐) | RAM 高处,8 KB 栈 |
HEAP | 同上 | RAM,栈下面 8 KB |
RESET | DATA, READONLY | Flash 最开头(scatter:RESET, +First) |
|.text| | CODE, READONLY | Flash,表后面的指令 |
「表从哪开始、代码从哪开始」是换了一个 AREA,不是 CPU 扫到某个标志:
AREA RESET, DATA, READONLY ; 按数据生成
DCD __initial_sp
DCD Reset_Handler ; 存的是地址,不是指令
...
AREA |.text|, CODE, READONLY ; 按 Thumb 指令生成
Reset_Handler PROC
BLX SystemInit
BX __main
DATA+DCD 输出 4 字节常数;CODE+PROC 输出指令。READONLY 进 Flash,READWRITE 进 RAM。NOINIT:不要把这段当有初值的数据写进 bin(栈/堆上电是随机值)。
启动文件按职责是五块(不是 CPU 从第 1 行跑):
- 栈、堆:只在 RAM 占坑。
__initial_sp给向量[0]。 - 向量表:
AREA RESET→ Flash 开头。 - Reset_Handler:
AREA |.text|,真正启动代码。 - 弱符号默认中断:没写的 Handler 都
B .。 __user_initial_stackheap:告诉 C 库堆栈边界。
PRESERVE8 / THUMB 是汇编器约束,同样不是给 CPU 扫描的标志。EXPORT 把符号交给链接器;IMPORT 表示符号在别的文件;[WEAK] 表示有强符号就用别人的。
C 文件看不到 AREA,编译器自己产出 .text / .data / .bss,和启动文件是同一套 section 机制。scatter 把它们钉到地址:
*.o (RESET, +First) → 0x08000000 向量表
.ANY (+RO) → 其余 Flash 代码/常量
.ANY (+RW +ZI) → RAM 的 data/bss/堆/栈
GCC / STM32CubeIDE 对照(扩充)
Keil 用 AREA RESET + scatter 的 +First。GCC 用段名,链接脚本把 .isr_vector 放到 Flash 原点,效果与 AREA 相同:CPU 仍然只认地址,不认伪指令。
/* 启动文件(示意) */
.section .isr_vector, "a", %progbits
.word _estack
.word Reset_Handler
...
/* 链接脚本(示意) */
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
.app (rx) : ORIGIN = 0x08010000, LENGTH = 448K /* 仅 APP 工程使用 */
SECTIONS {
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.text : { *(.text*) } > FLASH
.data : { ... } > RAM AT > FLASH
.bss : { ... } > RAM
}
APP 工程把 FLASH ORIGIN 改成 0x08010000,并在 Reset 之后尽快 SCB->VTOR = 0x08010000。向量表里的弱符号机制:GNU 用 .weak USART2_IRQHandler + 默认死循环,和 Keil 的 [WEAK] 同类。
跳转代码在 GCC 里同样是 CMSIS:__set_MSP、__DSB、__ISB、__disable_irq。不要用 Keil 的 __asm { ... } 语法。
__main 只把 .data 拷进 SRAM、清 .bss → main。Reset_Handler,不是 main。向量表是 Flash 开头的数据,通常不拷进 RAM。下一章讲运行中 PC/SP 如何配合中断与跳转。3. 运行中的 CPU:寄存器、栈与屏障
本章要回答:程序「正在跑」时,PC、栈、屏障各自管什么?
与前后章的关系:第 2 章讲了复位填 MSP/PC;本章讲运行过程中它们如何被函数调用和中断改写。
Bootloader 里会碰到:跳 APP 必做 __set_MSP + 调 Reset 入口;改 VTOR 后必须 DSB/ISB(见第 5 章)。
程序员模型里,你每天碰到的是 R0–R15、xPSR、以及 PRIMASK/CONTROL 等特殊寄存器。它们不在 0x08000000 的 Flash 里,而在 CPU 核内部;调试器的寄存器窗口看到的就是它们。
图 3 和「跳转 / 中断 / 栈」直接相关的是 SP、LR、PC、xPSR。
3.1 通用寄存器与调用约定(AAPCS)
- 前四个参数走 R0–R3,更多参数压栈。
- 返回值在 R0(64 位则 R0+R1)。
- R4–R11 是 callee-saved:你的函数若用它们,必须入栈再出栈。
- 硬件进中断时会自动保存 R0–R3、R12、LR、PC、xPSR。所以 ISR 可以写成普通 C 函数:编译器知道这是异常入口,会处理 R4 等。
3.2 SP:其实是两套指针 MSP 与 PSP
| 名字 | 何时用 |
|---|---|
| MSP Main Stack Pointer | 复位后默认;Handler 模式(中断里)强制用 MSP |
| PSP Process Stack Pointer | RTOS 下线程用 PSP,中断用 MSP,两边栈分开 |
CONTROL.SPSEL(bit1):0=MSP,1=PSP。复位为 0。无 RTOS 的 Bootloader 通常 全程 Thread + MSP。
栈向低地址生长:PUSH 让 SP 变小。所以向量表第 0 项必须填栈顶(高地址),不是数组的起始低地址。示例 Stack_Size = 0x2000(8 KB),__initial_sp 在该块 RAM 的最上沿。以你的启动文件 / 链接脚本为准。
SRAM 示意(地址向上变大):
0x20000000 .data / .bss / 堆(若使用)
↑
空闲
↑
SP 向下长 ← 当前 MSP
……压进去的 R0、LR、PC……
0x20002000 ← 复位时向量[0] 填的栈顶(示例,以 map 为准)
CMSIS:__get_MSP() / __set_MSP(x)。跳到 APP 时典型写法是 __set_MSP(*(uint32_t*)APP_VECTOR),相当于「把记事本换成 APP 自己的那本」,之后中断若发生,现场会压到 APP 的栈上。
栈上放的是:函数局部变量、超出 R0–R3 的参数、返回地址、被调函数保存的 R4–R11、以及硬件中断帧(R0–R3、R12、LR、PC、xPSR 共 8 字)。进入函数/中断时 SP 减小,返回时加回去,不需要 free。寿命等于「这个函数还没返回」,不能把栈上地址交给返回之后还在用的代码。
深入阅读:深入:M4 没有 MSPLIM
Cortex-M33 有栈限位寄存器,溢出可进 Fault。M4 没有。栈溢出表现为改写.bss/堆,表现为随机 HardFault,很难查。Bootloader 里大数组、文件系统长文件名、USB 缓冲都要算进这根 MSP 是否够用。
3.3 LR:普通返回 vs 异常返回
普通 BL foo:硬件把「返回后该执行的地址」写入 LR,foo 末尾 BX LR 回来。
进入中断时,硬件不把真实返回地址放进 LR,而是写入 EXC_RETURN(高 28 位为 1 的魔术值)。CPU 执行 BX LR 时发现这是魔术值,就走异常返回微码:从栈上弹出 PC、xPSR 等,而不是跳到 0xFFFFFFF9 去取指(那个地址没有代码)。
| EXC_RETURN | 返回后模式 | 用哪套栈 | FPU 上下文 |
|---|---|---|---|
| 0xFFFFFFF1 | Handler | MSP | 无 |
| 0xFFFFFFF9 | Thread | MSP | 无 |
| 0xFFFFFFFD | Thread | PSP | 无 |
| 0xFFFFFFE1 / E9 / ED | 同上三种 | 同上 | 还要恢复 FPU 帧 |
裸机从 Thread+MSP 进 USART/USB 中断时,ISR 结束 LR 多为 0xFFFFFFF9。编译器为 ISR 生成的返回就是 BX LR,你不必手写。
((void(*)(void))app_reset)() 是普通 BL 式调用:LR 被设成 Bootloader 里的返回点,不是 EXC_RETURN。若 APP 的 Reset_Handler 执行了 BX LR 返回,CPU 会回到跳转函数之后(有的实现会 for(;;) NVIC_SystemReset())。正常 APP 的 Reset 会进 C 库启动(Keil 的 __main 或 GCC 的 _start)再进 main,永不返回。3.4 xPSR:三个状态叠在一个 32 位里
- APSR:N Z C V Q,加减比较后的标志。
- IPSR:当前异常号。0 = 线程(没在 ISR 里);非 0 = 正在服务哪个异常。
- EPSR:含 Thumb 位 T。M 核上 T 必须保持 1。
进中断时硬件把整个 xPSR 压栈,返回时弹回,被打断处的标志不会乱。调试 HardFault 时,栈上的 xPSR 能告诉你「炸的时候 Thumb 位还在不在」。
3.5 开关中断:好几道闸,缺一道都进不去
| 闸 | 位置 | 常见写法(GD32 / STM32) |
|---|---|---|
| 外设 IE | 如 USART 接收非空 IE | GD32:usart_interrupt_enable;STM32 HAL:__HAL_UART_ENABLE_IT / 底层 CR1 |
| NVIC 使能 | NVIC->ISER[] @ 0xE000E100 | CMSIS NVIC_EnableIRQ(GD32 封装常叫 nvic_irq_enable) |
| PRIMASK | 特殊寄存器 | cpsid i / __disable_irq() 置 1 则屏蔽除 NMI、HardFault 外的可屏蔽异常 |
| BASEPRI | 特殊寄存器 | 屏蔽「优先级数值 ≥ 阈值」的 IRQ;裸机 Bootloader 主要用 PRIMASK |
| FAULTMASK | 特殊寄存器 | 连 HardFault 也挡,几乎不用 |
任意 IRQ 在 NVIC 里对应 ISER 的哪一位(以 F407 上 USB OTG FS / USBFS 常见 IRQn=67 为例,换芯片必须核对手册):
IRQn = 67
ISER 索引 = 67 / 32 = 2 // ISER[2]
位号 = 67 % 32 = 3 // 置 bit3
写 NVIC->ISER[2] = (1u << 3) 即可使能(CMSIS 封装好了)
CONTROL 寄存器:bit0 nPRIV(特权/非特权,裸机一般一直特权);bit1 SPSEL;bit2 FPCA(浮点上下文活动,用过 FPU 后可能置位,影响异常压栈宽度)。
PC 与 Thumb 位(跳 APP 必读):从向量表读出的入口地址 bit0 必须为 1;强转成函数指针调用即可,不要 &= ~1u 清最低位。读 R15 得到当前指令 +4 等细节见下方折叠块。
深入阅读:PC 的 +4 规则、顺序执行 vs 跳转、Thumb 位与跳 APP 示例
4.1 PC:当前条、读出来的 +4、顺序执行 vs 跳转
PC 是 R15。核内部真正在执行的,永远是起始地址为 A 的某一条指令。但「PC」这个词在三种场合含义不同,不要并成一个数:
| 你问的「PC」 | 值是什么 |
|---|---|
| 核正在执行的指令 | 起始地址 A |
汇编里读 R15(MOV r0, pc) | 固定 A+4(ISA 契约,见下) |
| 调试器窗口 | 通常显示 A(工具扣掉了流水线偏移) |
| IRQ 压栈的 PC | 返回后要执行的地址(多半是下一条) |
单片机自己跑时怎么更新 PC
不是「执行完当前条,再跳到读出来的那个 PC」。更准确:
- 从地址
A取出并执行当前指令(长度 2 或 4 字节)。 - 本条若不写 PC(普通运算、访存):下一条从
A + 本条长度取。这是顺序执行,不要说成跳转。 - 本条若写 PC(
B/BL/BX/ 写 PC):下一条从目标地址取,流水线冲掉重取。这才是跳转。
| 汇编 | 对 PC 做了什么 |
|---|---|
B label | PC ← 目标,不保存返回地址 |
BL label | LR ← 返回地址,PC ← 目标(函数调用) |
BX Rn | PC ← Rn。若 Rn 是 EXC_RETURN 魔术值,则走异常返回硬件,不是普通跳转 |
BLX Rn | 调用函数指针 |
LDR PC, [addr] | 从内存加载入口,常用于查表跳转 |
+4 不是「32 位机猜下一指令在 +4」
ARMv7-M 规定:正在执行起始地址为 A 的指令时,读 R15 得到 A+4。不管这条是 16 位还是 32 位,都是 +4。
| 当前指令 | 长度 | 真正的下一条 | 读 R15 | 读出来的是不是下一条 |
|---|---|---|---|---|
| 16 位 Thumb | 2 | A+2 | A+4 | 不是 |
| 32 位 Thumb-2 | 4 | A+4 | A+4 | 碰巧是 |
因此硬件不可能用「先把 PC 放到 A+4 当下一指令预加载,执行完再纠正」——16 位时这个「预估」从一开始就是错的。另:以 PC 为基址的 LDR 等,架构要求先把该 PC 值的 bit[1:0] 清 0 再算地址,和「读 R15 得到 A+4」可以不是同一个数。
+4 来自早期 ARM 三级流水线(取指 → 译码 → 执行):执行 A 上那条时,取指已经往前走了两拍。当年 Thumb 按 2 字节一拍,两拍就是 +4;当年 ARM 状态是 +8(Cortex-M 没有 ARM 状态)。Cortex-M 把这个变成了读 R15 的死规定,不是对「下一指令地址」的猜测。内部真正的取指指针可以更靠前,且常常一次从 Flash 取 32 位,和「读出来的 PC = A+4」不是同一个东西。
软件读不到「两条指令缝里、PC 已是下一指令」的窗口:MOV r0, pc 本身就是一条正在执行的指令,读的时候你已经在某条的执行阶段,ISA 规定只能得到该条的 A+4。IRQ 压栈 PC 才是边界上明确的返回地址。
A;下一条由「本条长度或跳转目标」决定;读 R15 永远是 A+4。三者不要并成同一次预加载。Thumb 位
Cortex-M 只执行 Thumb。写入 PC 的 32 位值中:
- bit0 = 1:合法,按 Thumb 在「该值清掉 bit0 后的地址」取指。
- bit0 = 0:试图进入 ARM 状态 → UsageFault。
因此向量表里 DCD Reset_Handler 在 bin 文件里经常是奇数(例如 0x08000249),表示「请在 0x08000248 按 Thumb 取指」。调试器有的显示偶数逻辑地址,有的显示带 T 位的奇数,以工具为准。
C 语言跳到 APP 复位函数(示例 APP 基址 0x08010000):
uint32_t entry = *(uint32_t *)0x08010004; // 已含 bit0=1
((void (*)(void))entry)(); // 不要把最低位清掉;这是写 PC,不是读 PC
错误示范:
entry &= ~1u; // 清掉 Thumb 位 → 进 APP 瞬间 UsageFault
0x08010004 处是数据(一个地址),真正的机器码在那个地址指向的地方。深入阅读:堆 / 栈 / RTOS 任务栈对比
4.6 堆、栈、RTOS 任务栈
中文「堆栈」常常只指栈。下面三个都在 SRAM 里,但谁分配、怎么长、谁释放完全不同。无 RTOS 则全程一根 MSP;启动文件仍可留一块堆给 C 库(即便业务不用 malloc)。
| 栈(stack) | 堆(heap) | RTOS 任务栈 | |
|---|---|---|---|
| 管理者 | 硬件 SP + 编译器 | malloc/free(软件) | 内核改 PSP + 每任务一块 RAM |
| 生长 | 高 → 低 | 低 → 高(常见) | 每任务各自向下长 |
| 分配 | 进函数/中断自动 | 调用 malloc | 创建任务时划(静态或从堆抠) |
| 释放 | 返回自动 | 必须 free | 任务删除或永不删 |
| 失败 | 溢出(M4 不报) | 返回 NULL | 溢出写穿隔壁任务/TCB |
| 典型裸机 BL | 一根 MSP,所有人共用(大小自定) | 可留可不留 | 无 |
全局 / static 既不在栈也不在堆,在 .data/.bss。
裸机 Bootloader
一条 MSP 贯穿全程:
main → 调 A → 调 B
↑
USB ISR 插进来,继续在同一根 MSP 上压帧
ISR 返回后 SP 回到 B 里头
没有「任务」。中断嵌套越深、局部数组越大,这 8 KB 吃得越快。若用 FatFs,长文件名可设静态缓冲(FF_USE_LFN = 1),正常路径不必 malloc。
跑 RTOS 时
每个任务要有自己的现场,否则 A 调到一半切到 B,A 的局部变量没地方放。硬件只有 MSP、PSP 两个指针;内核靠来回改 PSP,让不同任务轮流认领 PSP。
| 名字 | 谁用 |
|---|---|
| 主栈 MSP(常叫系统栈) | 只给中断/异常 |
| 任务栈 + PSP | 该任务在线程模式里的函数、局部变量 |
| 共享堆 | malloc;有的内核用它来「创建任务时再划任务栈」 |
CONTROL.SPSEL=1:线程用 PSP。一进中断强制改用 MSP。调度时把当前 PSP 存进任务控制块,再把 PSP 换成下一个任务上次的 SP,异常返回后从人家被打断的 PC 接着跑。
任务栈不是第三种硬件栈,就是普通 RAM + 一个 SP 值。两任务栈在 RAM 里相邻时,溢出就是写穿隔壁,不会在硬件上「自动变成另一个任务的栈」。
ISR 里不要 malloc。东西该放哪:整机都在的配置 → 全局;用完就扔的小缓冲 → 栈;运行时才知道大小且要跨函数活着 → 堆(检查 NULL);某任务私有且深度可算清 → 该任务栈或静态缓冲。
3.6 取指、流水线与内存屏障
Cortex-M4 是短流水线(取指-译码-执行)。你改了「之后取指该看哪」的寄存器,流水线上可能还残着旧决策。
| 指令 | 作用 |
|---|---|
DMB | Data Memory Barrier:前后访存顺序 |
DSB | Data Synchronization Barrier:等前面的访存真正完成 |
ISB | Instruction Synchronization Barrier:清空流水线,后面的指令用新上下文取指 |
写 SCB->VTOR 后的标准姿势:
SCB->VTOR = 0x08010000;
__DSB();
__ISB();
否则可能出现「VTOR 已经是新值,下一次中断仍按旧表取向量」的窗口(取决于流水线和写缓冲)。有的 Bootloader 跳转前不写 VTOR,自己运行期间不必做这组屏障;APP 里改 VTOR 时应该做。更稳妥的做法是跳转前就改到 APP 表基址,见第二部分。
4. 中断与异常:NVIC 到 Fault
本章要回答:外设事件如何变成一次 ISR?进中断时栈上有什么?
与前后章的关系:第 3 章的 SP/LR 在这里被硬件自动使用;Fault 时 stacked PC 是查错关键。
Bootloader 里会碰到:USB 升级时主循环与 ISR 抢 PC;跳 APP 前须关 SysTick、清 NVIC,否则可能进 BL 的空 Handler。
4.1 异常模型与 NVIC
异常(exception)是总称:Reset、NMI、Fault、SVC、PendSV、SysTick、外设 IRQ 都是异常。中断口语里常特指外设 IRQ。
NVIC 基址 0xE000E100,每个 IRQ 在若干 32 位寄存器里占 1 bit:
| 寄存器 | 作用 |
|---|---|
| ISER / ICER | 使能 / 除能 |
| ISPR / ICPR | 置悬挂 / 清悬挂 |
| IABR | 正在服务(active) |
| IPR | 优先级,每 IRQ 一字节(只用高 4 bit,即 16 档) |
| STIR | 软件触发某 IRQ(调试/测试) |
外设 IRQ 要跑起来,通常要:
- 外设产生事件(如 USART RBNE=1)
- 外设自己的中断使能位置 1
- NVIC ISER 使能该 IRQ
- PRIMASK=0
- 该优先级允许抢占当前上下文
系统异常(SysTick、Fault)不走 ISER,而走 SysTick CTRL.TICKINT 或 SCB 的 SHCSR 使能位。
4.2 进中断时的栈帧
以「Thread + MSP、未压 FPU 帧」为例,硬件自动压 8 个字、32 字节:
图 5 HardFault 时:MSP+6 字(24 字节)处附近就是 stacked PC(具体偏移以 SP 对齐后的帧为准,常用 MSP+0x18 为 stacked PC)。
进 ISR 之后 CPU 还做了:
IPSR ← 异常号(等于向量索引,数字因 IRQ 而异)
LR ← EXC_RETURN(如 0xFFFFFFF9)
PC ← *(VTOR + 4 * 索引)
进入 Handler 模式,栈仍为 MSP
ISR 返回:BX LR 识别 EXC_RETURN → 弹这 8 个字 → PC 回到被打断处,IPSR=0,Thread 模式。
ISR 必须清源。 USART 读数据寄存器才清 RBNE;TIMER 写中断标志清零;USB 由库清核标志。不清就返回,NVIC 认为还在请求,会立刻再进,像死锁。
深入阅读:FPU 惰性压栈
若 CONTROL.FPCA=1(用过硬件浮点),异常入栈可能变成 26 个字,或先占位真正用到 FPU 再压。Bootloader 的 ISR 应避免浮点运算,免得栈突然变深、延迟变大。
4.3 优先级
NVIC 优先级字段宽度由实现决定。STM32F4 的 CMSIS 头写 __NVIC_PRIO_BITS = 4(16 档);GD32F4xx 用户手册写「4 bits interrupt priority—16 priority levels」。优先级写在 NVIC->IPR[] 每字节的高位。
SCB->AIRCR 的 PRIGROUP 把这 4 bit 切成「抢占优先级」和「子优先级」。写 AIRCR 必须同时写钥匙 0x05FA,否则写入被忽略。
若分组设成「4 位全是抢占、子优先级为 0」(GD32 常叫 NVIC_PRIGROUP_PRE4_SUB0,STM32 HAL 对应 NVIC_PRIORITYGROUP_4),则数字更小的 IRQ 可以打断数字更大的。例如 USB(2) 打断 USART(8),USART 再打断 TIMER(11)。同抢占级之间不互相抢占,NVIC 用硬件排队。具体谁编成 2/8/11 由你的工程决定。
尾链(tail-chaining): 一个 ISR 返回时若已有另一个 pending,硬件可不弹栈再压栈,直接换 PC,少十多个周期。
迟到(late-arrival): 刚开始进低优先级,更高优先级来了,硬件改去进高优先级。
4.4 SysTick
映射 0xE000E010:
| 寄存器 | 作用 |
|---|---|
| CTRL | bit0 ENABLE;bit1 TICKINT;bit2 CLKSOURCE(1=核时钟) |
| LOAD | 重装值。内核 168 MHz 时 1 ms:168000−1 |
| VAL | 当前值,向下减,到 0 时重装并置 COUNTFLAG |
| CALIB | 厂家校准,不一定准 |
SysTick_Config(n) 会同时打开 ENABLE 和 TICKINT。即便 Handler 是空函数,CPU 仍每 1 ms 进一次异常 15(压栈弹栈)。有的 delay_us 不靠 Handler 计数,而是读 VAL 的差值自旋——此时若仍打开 TICKINT 且 Handler 为空,只是浪费一点周期。
跳 APP 前应关掉 ENABLE 和 TICKINT,否则 APP 尚未设置自己的 SysTick Handler(且 VTOR 可能仍指向 Bootloader)时,会周期性进 BL 的空 ISR 或错误向量。
4.5 Fault
| Fault | 典型原因 |
|---|---|
| UsageFault | 非法指令、除零(若 CCR.DIV_0_TRP)、PC bit0=0、未对齐(若开了 UNALIGN_TRP) |
| BusFault | 访问了没有从设备的地址、Flash 读错误 |
| MemManage | MPU 违例、取指于禁止执行区 |
| HardFault | 上述 Fault 未使能而被「升级」;或在 Fault 处理中再次 Fault |
默认很多芯片只开 HardFault,Usage/Bus/Mem 被升级成 HardFault。可在 SCB->SHCSR 里分别使能,再读 SCB->CFSR(可配置 Fault 状态)、HFSR、MMFAR、BFAR 定位。
HardFault 里最有用的是 stacked PC(图 5):那是「将要执行但没执行成」或「导致 Fault 的指令」附近。在 Handler 里用 MSP(若从 Thread+MSP 进来)解析帧即可。
NVIC_SystemReset():写 AIRCR.SYSRESETREQ(必须带 VECTKEY 0x05FA)。核复位后再次从启动别名地址 0 加载 MSP 和 PC。ARM 规定 VTOR 复位值是实现定义;STM32F4 / GD32F4 从主 Flash 启动时通常为 0,仍建议核对手册。
5. 双镜像与 VTOR:为什么 BL+APP
本章要回答:为什么需要 BL+APP 两张向量表?VTOR 在跳转时必须改吗?
与前后章的关系:第 2 章向量表 + 第 4 章 NVIC,在本章汇到「两张表、一个 VTOR」。
Bootloader 里会碰到:BL 运行时常 VTOR=0;跳进 APP 前应 SCB->VTOR = APP_BASE 并 DSB/ISB。
5.1 SCB 与 VTOR
基址 0xE000ED00(SCS_BASE + 0x0D00)。这是内核系统区,不是 GPIO 那种片上外设。
| 寄存器 | 偏移 | 用途 |
|---|---|---|
| CPUID | +0x00 | 读核型号 |
| ICSR | +0x04 | 当前/悬挂异常号;可挂起/清除 PendSV、SysTick |
| VTOR | +0x08 = 0xE000ED08 | 向量表基址;跳 APP 时应当改到 APP 表,有的实现却留给 APP 自己写 |
| AIRCR | +0x0C | PRIGROUP、SYSRESETREQ;写操作要 VECTKEY 0x05FA |
| SCR | +0x10 | 睡眠、SEVONPEND |
| CCR | +0x14 | 除零 trap、非对齐 trap、STKALIGN |
| SHPR1/2/3 | Usage/SVC/SysTick/PendSV 优先级 | |
| SHCSR | 使能 Usage/Bus/Mem Fault | |
| CFSR/HFSR/MMFAR/BFAR | Fault 诊断 | |
| CPACR | +0x88 | 开 FPU:CP10/CP11 写 0b11 |
VTOR 详细
CMSIS 把 SCB 当成结构体:SCB->VTOR = 0x08010000 就是向 0xE000ED08 写一个字。查表公式永远是:
表项地址 = (VTOR 的有效基址) + 4 × 向量索引
复位、NMI、HardFault、SysTick、USBFS……全部用这一个基址,没有第二张隐藏表。
位域(Cortex-M4)
31 7 6 0
┌────────────────────────────┬─────────────┐
│ TBLOFF = 表基址的 bit[31:7] │ 保留,必须为 0 │
└────────────────────────────┴─────────────┘
- 写入地址 bit[6:0] 必须为 0 → 至少 128 字节对齐。
- ARM 还要求对齐 ≥ 表大小向上取的 2 的幂。F407 类表约 98 项 × 4 ≈ 392 字节 → 按 512 字节对齐更稳妥。表更长的型号要对齐到更大的 2 的幂。
0x08000000、0x08010000、0x20000000都满足。写0x08010004则对不齐。
厂商库往往再包一层:GD32 的 nvic_vector_table_set(NVIC_VECTTAB_FLASH, offset) 内部仍是写 SCB->VTOR(掩码常见 0x1FFFFF80);STM32 HAL 则直接 SCB->VTOR = FLASH_BASE | offset 或在系统初始化里写。APP 表在 0x08010000 时应:
SCB->VTOR = 0x08010000;
__DSB();
__ISB(); /* 冲掉流水线里旧的「表在 0」的决策 */
有的库函数只做了 __DSB(),建议再加 __ISB()。Bootloader 若运行期间从不写 VTOR,复位后一直是 0。
复位值、何时会变
| 时机 | VTOR |
|---|---|
上电 / NVIC_SystemReset() | ARM 规定复位值实现定义;本类 F4 从主 Flash 启动时通常为 0 |
| 整个 Bootloader 运行期间 | 常见保持 0 → 经别名读 0x08000000 的 BL 表 |
| 跳进 APP 之后 | 应为 APP 基址(示例 0x08010000) |
VTOR = 0 时核从地址 0 取表;从主 Flash 启动时 0x00000000 镜像到 0x08000000,所以「VTOR=0」和「表在 Flash 开头」是同一张表。
它管什么、不管什么
管:下一次取向量时从哪读 4 字节。
不管:已经在跑的 PC;MSP/PSP 当前值(跳 APP 要自己 __set_MSP);表会不会拷到 RAM(拷完必须改 VTOR 才生效)。
若跳转只改 MSP、PC 而 VTOR 仍是 0,则中断仍去 Bootloader 的表里查 Handler。
VTOR 只是一个地址,Flash 或 SRAM 都行。没拷表时,RAM 里没有向量表这一段。
5.2 软件跳转相对复位缺了什么
| 硬件复位 | 软件跳到另一份固件(仅改 MSP/PC) |
|---|---|
| VTOR=0,从 BL 表取 MSP/PC(若表在 0x08000000) | 若实现不改 VTOR:只把 MSP、PC 改成 APP 表的值 |
| 外设寄存器回到复位值 | USB/USART/SPI 仍是 Bootloader 配过的脏值 |
| PRIMASK=0 | 跳的时候 PRIMASK=1 |
| LR 无意义,全新线程 | LR 指向 Bootloader 里跳转调用的返回点 |
| NVIC 使能位清掉 | 往往只关了当前升级用的那个 IRQ;其它外设的 NVIC 使能位仍可能在(靠 PRIMASK 挡住) |
所以 APP 必须:设 VTOR、重新 init 外设、自己开中断。这不是 APP「多余」,是跳转代码没有也不该完整模仿一次芯片复位(否则会把 Bootloader 自己的时钟/映射也拆掉)。更干净的做法是跳转前尽量关外设时钟、清 NVIC、写好 VTOR 再放 PRIMASK。
5.3 定位与 Flash 地图
复位入口被 Bootloader 占着。APP 是客人:只有 BL 认为 APP 基址上已有本机、完整、校验通过的镜像,才把 PC 交给它。跳走后 BL 的 main 不再运行;再升级必须再复位(或 APP 里提供「回到 BL」的复位请求)。
0x08000000 BL 向量[0] = BL 的 MSP 初值
BL 向量[1] = BL Reset ← 上电硅片从这里开始(VTOR=0)
BL 代码(示例:扇区 0–3,64 KB),升级 APP 时不擦
0x08010000 APP 向量[0] = APP 的 MSP
APP 向量[1] = APP Reset ← 软件跳转使用
APP 向量[98..] = 可选镜像信息(魔数、长度、CRC…)
APP 代码(其余 Flash)
64 KB / 0x08010000 只是 F407 上「扇区 0–3 共 64 KB」的常见切法。STM32F407 与 GD32F407 的主 Flash 扇区划分非常接近(16/16/16/16/64/128… KB),但不要把扇区号抄到 F1/F0/G0 上。链接脚本必须和擦除函数用同一张扇区表。
static assert 或运行时检查:erase_addr >= APP_BASE。升级通道可以有多条,用编译开关或一块 EEPROM/Flash 标志选择。没打开的通道不要去初始化对应外设,以免引脚冲突。
5.4 从复位到 main(对应第 2 章)
- 硅片:MSP←
[0x08000000],PC←[0x08000004](经地址 0 别名)。 SystemInit:若编译打开了 FPU 则写SCB->CPACR;配 PLL(F407 示例 168 MHz,USB 要 48 MHz)。STM32 走RCC+SystemClock_Config(Cube);GD32 走RCU+ 厂商system_gd32f4xx.c。- C 库启动:拷
.data、清.bss。若把镜像信息放在 APP 区固定偏移,这里就可以把 APP Flash 当结构体只读:const app_info_t *info = (const app_info_t *)(APP_BASE + INFO_OFFSET); - 板级
init():设 AIRCR 优先级分组;按需开 SysTick;初始化本次升级真正用到的 GPIO / USART / USB / SPI;只使能这些 IRQ。
此时:PC 在 Bootloader 的 main,VTOR 仍是 0,升级所用的中断已使能。
Reset_Handler → SystemInit → __main(__main 才拷 .data、清 .bss);STM32Cube 的 GCC 启动文件常见先拷 data / 清 bss,再调 SystemInit。无论哪一种,跳转都应放在 main(或等价入口)之后,此时全局对象已就绪。不要依赖「SystemInit 里读全局变量一定安全」。5.5 时钟、Flash 等待与外设访问
CPU 再快,总线和 Flash 跟不上也会插入等待周期。
F407 类芯片一份典型 SystemInit:8 MHz 外部晶振 + PLL。ST 写在 RCC(PLLM/N/P/Q),兆易写在 RCU(PSC/N/P/Q),数字可以相同:
- SYSCLK = 8/8×336/2 = 168 MHz
- PLLQ = 8/8×336/7 = 48 MHz → USB 全速(48 MHz)必须用这个频率(OTG FS / USBFS 都一样)
- AHB=168,APB2=AHB/2=84,APB1=AHB/4=42;APB1 上的定时器时钟通常 ×2 = 84 MHz(手册:预分频不为 1 时定时器时钟是 APB 的 2 倍)
168 MHz 读 Flash 需要等待状态。STM32F4 配 FLASH->ACR 的等待周期并打开 ART;GD32F4 配 FMC/加速相关寄存器。建议核对手册。Bootloader 体积小,主要影响的是 APP 性能。
外设访问是读改写寄存器。GPIO 原子置位:STM32 写 BSRR,GD32 写 BOP/BC,避免读-改-写被中断打断导致丢 bit。编程 Flash 前要解锁,擦扇区、按字(或按双字,视型号)写入,完后上锁——这些都是对控制器寄存器的 STORE,不是对 0x08010000 直接赋值那么简单(直接赋值在 Flash 上通常无效)。
片上 CRC:ST RM0090 与 GD32F4xx 用户手册都写固定多项式 0x4C11DB7(即 0x04C11DB7,以太网 CRC-32),并且按 32 位字写入数据寄存器,不是按字节。F4 这一代的 CRC 控制寄存器主要是复位数据寄存器(ST 的 CRC_CR.RESET / 兆易的 CRC_CTL.RST),没有 F0/L4 那种输入/输出反转位。不要用 zlib/PNG 的按字节反射 CRC 去对这个硬件值。
6. Bootloader 工程:升级、校验与跳转
本章要回答:双镜像 BL 从复位到跳 APP 的完整工程步骤?
与前后章的关系:第 5 章讲了「为什么」;本章讲「怎么做」——热路径、校验、跳转清单。
Bootloader 里会碰到:先校验后擦除;跳转前关 SysTick/NVIC、设 VTOR、换 MSP、调 APP Reset。
下面不再重复「PC 是什么」,只说双镜像升级通常怎么接前面各章的硬件契约。示例:无 RTOS、BL 占 Flash 开头 64 KB(0x08000000)、APP 从 0x08010000 起。通道以「USB 主机读 U 盘 FAT 文件」为例;UART、SPI Flash、SD 卡是同一套校验/擦写,只是搬运层不同。GD32 与 STM32 核步骤相同,外设差异见第 7 章。
6.1 升级热路径:Thread 与 ISR 抢 PC
以 USB 主机 + MSC + FatFs 为例(STM32 用 HAL USB Host / 中间件;GD32 用 USBFS Host 库,思想相同):
main 循环:usbh_process()(名字因库而异)驱动枚举状态机 + 尝试打开约定路径的文件 + 短延时。无盘时不要死等过久,也不要丢掉「安装成功」的返回值——否则已经写好 APP 仍被当成失败。
USB 事件到来时:NVIC 某 IRQn(F407 上 OTG FS / USBFS 常见为 67,以手册为准)→ 向量索引 = IRQn+16 → PC 进入 OTG_FS_IRQHandler / USBFS_IRQHandler → 库的 ISR。这是第 4 章的实例。主循环跑状态机;ISR 搬 FIFO。若大容量类读函数返回 BUSY,死等期间必须允许 USB 中断,否则永远 BUSY。
其它通道对照:
| 通道 | Thread 侧 | IRQ 侧 | 注意 |
|---|---|---|---|
| USB Host MSC | 枚举、开文件、读块 | USB 核心 ISR | 全速要 48 MHz 时钟 |
| UART Ymodem/自定义帧 | 组包、CRC、写 Flash | USART RXNE | 必须有 RX Handler;超时用定时器 |
| SPI NOR | 读 JEDEC、读页 | 通常轮询或 DMA TC | busy 轮询要有超时,禁止无限等 |
| SDIO/SDMMC | FatFs diskio | SDIO/SDMMC IRQ | 文件系统类型要与 FatFs 配置一致 |
USART 若开了接收中断却只在启动文件里留着弱符号死循环,一旦线上有数据就会卡死——这是弱符号机制的典型坑,与厂商无关。
6.2 合法 APP、可选密文、安装(PC 仍在 BL)
合法与否只看 APP 基址上的数据,不要只看「U 盘上有个文件」。一份最小约定:
| 字段(示例) | 规则 |
|---|---|
| magic | 必须等于约定值,如 0x0000A5A5(小端字节 A5 A5 00 00) |
| fileLen | 大于向量表长度,小于 APP 分区容量 |
| fileCrc | 与片上 CRC 单元一致:按字、多项式与输入方向与编程时相同 |
信息块可以放在向量表尾部预留字(示例偏移 0x188 = 索引 98),这样「头 512 字节明文」里既有向量也有元数据,校验时不必先解密整个文件。
升级包布局示例:
[0, 512) 明文:向量表 + 镜像信息
[512, len) 可选密文:按算法块大小(XXTEA 常见 8 字节;AES 16 字节)
CRC 范围 常从 offset=512 计到 fileLen,不含头,避免把元数据算进自身校验
推荐顺序:先只读校验,通过才擦 APP 扇区,再编程。失败则保留旧 APP,用户仍能启动上一份固件。I/O 返回值在工程内部要统一语义(成功/失败),不要有的函数 true=失败、旁边又 HAL_OK=0。
调试后门(例如「CRC 为某魔术且长度为最小头」)只应存在于内部构建宏里,量产关闭。
失败路径:指示灯/蜂鸣 → 延时 → NVIC_SystemReset()。这对应前文的 AIRCR.SYSRESETREQ,PC 重新从 BL 向量加载。
深入阅读:片上 CRC 与 zlib/PNG 的区别
片上 CRC 按 32 位字、多项式 0x4C11DB7,与 zlib/PNG(常按字节、反射)不是同一个数。PC 上复现必须模拟「按小端拼字再写入同一硬件状态机」。F4 级 ST 与兆易这一单元都没有输入/输出反转位;换到带 REV_IN/REV_OUT 的后续系列时再核对手册。
6.3 跳转瞬间:至少三个寄存器,建议更多
/* 示意,CMSIS 写法,GD32 / STM32 通用 */
SysTick->CTRL = 0; /* 关 ENABLE 与 TICKINT */
NVIC_DisableIRQ(OTG_FS_IRQn); /* STM32F407:OTG_FS_IRQn=67;GD32 常为 USBFS_IRQn */
/* 更好:按 NVIC->ICER 清掉本次 BL 用过的全部 IRQ,并 NVIC_ClearPendingIRQ */
__disable_irq(); /* PRIMASK = 1 */
SCB->VTOR = APP_BASE; /* 强烈建议在此完成,不要留给 APP 窗口期 */
__DSB();
__ISB();
__set_MSP(*(uint32_t *)APP_BASE);
void (*app_reset)(void) = (void (*)(void))(*(uint32_t *)(APP_BASE + 4u));
app_reset(); /* PC ← APP Reset | Thumb;LR ← BL 返回点 */
for (;;) {
NVIC_SystemReset(); /* 若 APP Reset 返回 */
}
若不写 VTOR:MSP、PC 已经是 APP 的,中断仍查 BL 表。APP 若尚未关中断就开 SysTick/USART,可能进 BL 的弱符号 B .。这是第 2 章弱符号 + 第 5 章「没改 VTOR」叠在一起的典型故障。
外设是脏的:APP 要当热启动重新 init(时钟树往往还在 168 MHz,但 GPIO 复用、USB 核、DMA 流都可能还指着 BL 的缓冲区)。Bootloader 的 Host/FatFs 对象留在 SRAM 里不再被调用,APP 不要以为 RAM 是干净的上电值。
RCC/RCU 的 AHBxEN/APBxEN 清位)、清所有 pending、可选 SCB->ICSR 里与 SysTick/PendSV 相关的悬挂位。FPU 若在 BL 里用过,注意 CONTROL.FPCA 与懒压栈,APP 的 SystemInit 应重新配 CPACR。6.4 传输栈在链上的位置(分层,便于替换通道)
存储介质(U 盘 / SPI NOR / UART 流)
→ 总线驱动(USB Host / SPI / USART)
→ 块协议(MSC BBB+SCSI / 页读取 / 自定义帧)
→ 文件系统或裸偏移(FatFs 逻辑盘 0: 或直接 LBA)
→ 512 字节(或页)缓冲
→ 可选解密(按字)
→ CRC(按字)
→ Flash 解锁 / 擦扇区 / 编程 / 上锁
FatFs 要与磁盘格式一致:只开 FAT16/32 就不要用 exFAT 盘。0: 是逻辑盘号。Bootloader 看见的是文件内字节偏移,不是物理 LBA——这是 diskio 层的职责。
USB 全速外设时钟必须是 48 MHz。引脚:F4 上 USB OTG FS 常见 PA11/PA12,具体以数据手册「复用功能」表为准(有的封装有 USB_SOF、VBUS 检测脚)。
6.5 给 APP 的合同;仿写时抄什么
- 链接在约定 APP 基址,体积不超过 APP 分区。
- 向量[1] 必须是合法 Thumb 入口(bit0=1)。
- 若使用镜像信息:魔数、长度、CRC 与 BL 同一套端序和多项式。
- 发布包格式与 BL 解析器一致(哪一段明文、哪一段密文)。
- Reset 之后尽快设 VTOR(若 BL 未设),重新 init 外设,再开中断。
- 现场升级:把文件放到约定路径后复位;其它通道同样「先校验后擦除」。
值得沿用:策略 / 搬运 / BSP 分层;先校验后擦除;元数据靠近向量表;无 RTOS 时直线 main;跳转用 CMSIS 而不是内联奇技。
不要沿用的反模式:跳转不设 VTOR;返回值真假颠倒;每次开机固定空等总线且丢返回值;RX 中断无 ISR;忙等待无超时;擦除范围与链接脚本不一致。
7. 附录:ST/GD 对照与出处
7.1 GD32F4 与 STM32F4 对照
下面这些不影响前文的 PC/SP/VTOR 故事,但会让你从一家的例程抄到另一家时编不过。
| 主题 | STM32F4(ST) | GD32F4(兆易) |
|---|---|---|
| 时钟树 | RCC,Cube 的 SystemClock_Config | RCU,system_gd32f4xx.c |
| Flash 控制器 | FLASH,HAL_FLASH_Unlock/Program/Erase | FMC,fmc_unlock / 页字程序列 |
| 等待与加速 | FLASH->ACR 等待周期 + ART | FMC/加速寄存器,建议核对手册 |
| GPIO 原子置位 | BSRR | BOP / BC |
| USB 全速 | USB OTG FS,OTG_FS_IRQHandler | USBFS,USBFS_IRQHandler |
| NVIC 封装 | CMSIS NVIC_*;HAL 再包一层 | CMSIS 同样可用;厂商 nvic_irq_enable |
| 向量表重定位 | SCB->VTOR = … | 同上,或 nvic_vector_table_set |
| 启动文件 | startup_stm32f407xx.s | startup_gd32f407_427.s |
| IRQ 编号 | 以 ST 参考手册 IRQ 表为准 | 以兆易用户手册为准;F407 类往往接近但不保证每一项相同 |
| CRC 单元 | RM0090:多项式 0x4C11DB7,按 32 位字 | 用户手册第 8 章:同样 0x4C11DB7,按字 |
| BOOT 脚 | RM0090 表 2:BOOT1+BOOT0 | 同类启动别名,以用户手册为准 |
归纳:可以共用的是 CMSIS 头里的 SCB、NVIC、SysTick、core_cm4.h 内联函数,以及你自己写的跳转函数。不要共用的是 stm32f4xx_hal_conf.h 与 gd32f4xx.h 外设基址假设、CubeMX 生成的 MX_* 与兆易标准外设库的初始化。USB 协议栈两家各做各的,只复用「MSC + FatFs + 校验 + FMC/FLASH」这一层思想。
调试器:SWD 引脚在 F4 上常见 PA13/PA14,复位后默认 SWD,改 GPIO 复用会把调试口踢掉。Bootloader 若要复用这些脚,量产再关调试,开发阶段留着。
7.2 出处与文献
核对原则:核与异常只引用 ARM;片上外设、容量、BOOT、扇区、IRQ 引用 ST / 兆易;总线协议引用 USB / IETF;镜像魔数、分区切法和加密包装是工程约定,不冒充手册条款。下列版本以本次核对所用文本为准,修订号更新后页码可能变,请按章节名检索。
权威文献
- ARM DDI0403(ARMv7-M Architecture Reference Manual):向量表内容与对齐、复位取 SP/PC、异常号、EXC_RETURN、硬件压栈、VTOR 位域、AIRCR VECTKEY、PRIMASK/BASEPRI、端序位、UsageFault INVSTATE(向量 bit0=0)。官方入口:developer.arm.com/documentation/ddi0403
- ARM DUI0553(Cortex-M4 Devices Generic User Guide):NVIC/SCB/SysTick 地址(
0xE000E100、0xE000ED00、0xE000E010)、ISER/ICER、SysTick CTRL/LOAD/VAL、DMB/DSB/ISB 用途。 - ARM IHI0042(Procedure Call Standard for the ARM Architecture, AAPCS):R0–R3 传参、R0 返回、R4–R11 callee-saved;硬件异常帧按 caller-saved 压栈与此一致。
- ST RM0090(STM32F405/407/415/417 参考手册,本次核 Rev 21):存储器别名与 BOOT 表 2(BOOT1/BOOT0)、Flash 扇区号、USB OTG FS 向量地址
0x0000014C、CRC 第 4 章(多项式0x4C11DB7、按 32 位字)、GPIO BSRR、RCC/FLASH。PDF:st.com RM0090 - ST STM32F407xx 数据手册(如 DS8626 / stm32f407ve):核 Cortex-M4+FPU、最高 168 MHz、ART、最多 1 MB Flash、192+4 KB RAM(含 64 KB CCM)。stm32f407ve.pdf
- ST CMSIS
stm32f407xx.h:OTG_FS_IRQn = 67、FPU_IRQn = 81(向量索引 97)、__NVIC_PRIO_BITS = 4、FLASH_BASE = 0x08000000。官方仓库:cmsis_device_f4 - 兆易 GD32F4xx User Manual(本次核 Rev 3.0):RCU、FMC、USBFS、CRC 第 8 章(多项式
0x4C11DB7、按字、CRC_CTL.RST)、NVIC「4 bits / 16 levels」、TCMSRAM 64 KB @0x10000000、SRAM0/1/2 划分。GD32F4xx_User_Manual_Rev3.0.pdf - GD32F407xx 数据手册:168 MHz、SRAM 常见 192 KB、Flash 分 512/1024/3072 KB 等订货型号。产品页与 PDF
- USB 2.0 Specification(2000-04-27)§8.1 Byte/Bit Ordering:标准描述符、请求与应答中的多字节字段在总线上为小端(LSB 先)。
- IETF RFC 791(Internet Protocol):网上多字节整数用网络字节序(大端)。主机上的
htonl/ntohl即为此服务。 - NIST FIPS 197(AES):分组长度 128 位。文中升级包「AES 16 字节」据此,不是硅片外设。
- 以太网 CRC-32 多项式:ST / 兆易 CRC 章节都写「Ethernet polynomial 0x4C11DB7」,与 IEEE 802.3 CRC-32 生成多项式相同;硬件按字喂入后的数值仍不等于常见软件 zlib CRC。
- FatFs(Elm-Chan):
FF_FS_EXFAT未开时不要用 exFAT 盘;0:为逻辑盘号。见 elm-chan.org/fsw/ff - CMSIS-Core:
__set_MSP、__DSB、__ISB、__disable_irq、NVIC_EnableIRQ、SysTick_Config、NVIC_SystemReset。 - Keil / Arm Compiler armasm 用户指南:
AREA、DCD、[WEAK]是汇编器/链接器伪指令,不是 Thumb 指令。GCC 对应.section .isr_vector与链接脚本,芯片行为相同。
章节与出处对照
| 章节 | 依据 |
|---|---|
| 1 地址地图与 MCU、ARMv7-E-M、无 ARM 状态、Thumb bit0 | [1];FPU CPACR 亦见 [2] |
| 2.1 小端、AIRCR ENDIANNESS | [1];本类芯片固定小端见 [4][7] |
| 2.1 网络大端、USB 小端 | [10] [9] |
| 3 地址段 0x0800_0000 / 0x2000_0000 / 0x4000_0000 / 0xE000_0000 | [1] 通用映射;F4 具体见 [4][7] |
| 3 CCM/TCM、BOOT、Flash 512 KB 示例 | [5][4] 表 2;[7][8]。容量按订货型号 |
| 3.1 / 图 2b 链接地址 | 工程 .map 示例,非手册保证 |
| 3 寄存器、AAPCS、MSP/PSP、EXC_RETURN、xPSR | [1][2][3] |
| 4.1 读 R15 = 当前指令 +4 | [1] Thumb 程序员模型 |
| 4.5 ISER 公式;IRQn=67 | [2];[6] OTG_FS_IRQn |
| 3 屏障 | [1][2] |
| 2 复位 [0]、[4] | [1] Reset behavior;F4 别名见 [4] §2.4 |
| 2 向量表、索引 = IRQn+16、弱符号 | [1];启动文件 [6] 与 [15] |
| 4 NVIC、栈帧 8 字、优先级 4 bit、尾链 | [1][2];4 bit 见 [6][7] |
| 11 SysTick | [2] |
| 12 Fault、VECTKEY | [1] |
| 5 VTOR 对齐(≥128 B 且 ≥ 表长的 2 的幂) | [1] Vector table / VTOR |
| 14 168 MHz / 48 MHz USB、ART、CRC | [5][4] RCC/FLASH/CRC;[7] RCU/FMC/CRC |
| 5–6 双镜像与 BL策略、魔数、路径 | 工程约定;跳转寄存器仍用 [1][13] |
| 5 Flash 地图 0–3 = 64 KB | F407 Flash 扇区表 [4];其它系列勿套用 |
| 6 USB 48 MHz、PA11/12 | [4] 嵌入式 bootloader / USB 章;[5] 引脚复用 |
| 6 升级包 16 字节 | [11];XXTEA 为公开算法文献,非 MCU 手册 |
| 6 FatFs / exFAT | [12] |
| 7 对照表 | [4][6] 对 ST;[7] 对兆易 |
SRAM 里 .data/.bss/堆栈的绝对地址以当时链接 .map 为准。手册修订后若与上文冲突,以你下载的官方 PDF 为准。