1. SRAM运行程序不是稀有操作,但很多人第一步就理解错了
1.1 你需要SRAM运行的真实场景
先说清楚一件事:把应用从 Flash 挪到 SRAM 里跑,不是炫技,也不是嵌入式发烧友的怪癖,而是一类非常刚需的操作。我最初碰到这个需求,是在做 IAP(In-Application Programming)升级程序。STM32F407 支持在应用运行的同时擦写 Flash,但擦写 Flash 的这段时间里,如果 CPU 还在从 Flash 取指,而那个 Flash 控制器正忙着执行擦除算法,总线就会互相卡住。最稳妥的做法就是:把 Flash 编程的那一小段代码放到 SRAM 里,让 CPU 从 SRAM 取指,Flash 专心做擦写。
除此之外,还有几种常见场景:
- 调试阶段不想反复擦写 Flash。F407 的 Flash 虽然标称擦写寿命是 1 万次,但开发迭代多了,真能把一个扇区写到报废,尤其是不小心写了死循环在 Flash 里,就只能靠 ST-Link 连上去擦。
- Bootloader 需要自我更新。主 Bootloader 在 Flash 里运行,要更新自己的某个功能,必须先把更新代码放到 SRAM 再跳过去执行,否则连自己所在的 Flash 都不敢动。
- 固件加密传输。有些产品从串口或网口收到加密固件,先解压到 SRAM 校验,再决定要不要写入 Flash,整个过程不想依赖 Flash。
- 追求确定性的执行时间。Flash 有等待周期和预取机制,SRAM 是零等待的,对时序敏感的中断服务函数放 SRAM 里,延迟会更稳定。
所以,"Failed to run application loaded into SRAM on STM32F407"这个报错,其实出现在很多人刚把编译目标从 Flash 切到 SRAM 的时候。程序编译通过了,数据也通过调试器写进内存了,但一按运行,芯片就是不动,或者直接飞进 HardFault。这个问题的本质,不在编译,不在下载,而在你对 Cortex-M4 启动机制的理解上。
1.2 STM32F407 的 SRAM 不是一块而是三块
要排查这类问题,第一步得认识 STM32F407 的存储布局。很多初学者以为它只有 128KB SRAM,地址从 0x20000000 开始,其实那是把三块物理 RAM 看成了一块。
| 区域 | 起始地址 | 大小 | 特性 |
|---|---|---|---|
| Flash | 0x08000000 | 1MB(F407VG) | ART 加速器,支持预取和缓存 |
| SRAM1 | 0x20000000 | 112KB | 主 SRAM,可通过总线矩阵被 DMA 访问 |
| SRAM2 | 0x2001C000 | 16KB | 紧接 SRAM1,也可被 DMA 访问 |
| CCM RAM | 0x10000000 | 64KB | CPU 专享,DMA 等总线外设无法访问 |
SRAM1 和 SRAM2 在地址上是连续的,0x20000000 到 0x2001FFFF 正好拼成 128KB,所以链接脚本里可以把它们当一个连续区域来用。CCM RAM 是一个特立独行的角色,它贴着 CPU 核心,取指和数据访问延迟都很低,但代价是只有 CPU 能碰它,DMA、USB、以太网这些总线主设备全都够不着。
这个"三块 RAM"的差异,在 SRAM 运行应用时会导致一个非常隐蔽的坑:如果你把整个程序的 .bss 段或者 DMA 缓冲区丢进了 CCM RAM,程序跑起来后外设 DMA 读写的内存区域是 CCM,数据要么全零,要么随机错乱。这个问题我在后面的事故复盘里会专门讲。
1.3 "加载成功"只是数据进去了,离"跑起来"还差三步
回到那个典型的报错。你看到的"加载成功",是调试器通过 SWD/JTAG 接口把二进制镜像写进了目标地址的 RAM 里。这个过程本身非常简单,就像用写字板往一块白板上抄了一篇文章。但 CPU 能不能读这篇文章、从哪里开始读、读到的第一句是什么,完全由另一套机制决定。
一个可运行的程序,在 Cortex-M4 上至少要满足三件事:
- 栈指针(MSP)的值必须指向有效的 RAM 空间,CPU 才能压栈、调用函数。
- 程序计数器(PC)必须指向第一条要执行的指令,通常是 Reset_Handler。
- 向量表必须放在 CPU 能够找到的位置,而且中断偏移寄存器 SCB->VTOR 要指向它。
这三样东西,任何一样不对,"加载成功"都只是个假象。程序要么压根不启动,要么跑两步就异常,要么一进中断就瞬间 HardFault。你看到的所有"加载进 SRAM 跑不起来"的现象,归根结底都可以回溯到这三件事上。
2. 先把 Cortex-M4 的启动链路讲透,才知道该在哪一步下手
2.1 复位后 CPU 第一件事:从向量表读两个值
Cortex-M4 上电复位后,处理器核心会做一个非常原始的操作:它把存储器地址 0x00000000 处的 32 位数据读出来,作为主栈指针 MSP;再把地址 0x00000004 处的 32 位数据读出来,作为第一条要执行的指令地址,也就是 Reset_Handler。然后它直接跳过去执行。
注意,这里的关键是"地址 0x00000000"。在 STM32F407 上,这个地址并不是固定的物理存储器,而是一个映射窗口。默认情况下(BOOT0 拉低),0x00000000 映射到主 Flash 的 0x08000000。如果你把 BOOT0 和 BOOT1 都拉高,它才会映射到 SRAM 的 0x20000000。这就是为什么很多人把程序下载进 SRAM 后一按复位就死——复位后 CPU 根本不认识 SRAM 里的程序,它还是从 Flash 的向量表开始执行,而那片 Flash 要么是空的,要么是旧的 Bootloader。
如果你使用调试器直接加载并运行,调试器通常会帮你设置 PC 到镜像入口,但这只是调试器层面的事,芯片自身并不"记得"SRAM 里有程序。
2.2 VTOR:决定中断向量去哪查表的关键寄存器
如果说复位读向量表是 CPU 的"出厂设定",那么 VTOR(Vector Table Offset Register)就是程序员手里最关键的旋钮。它在 Core Peripheral 的地址 0xE000ED08,CMSIS 里直接用 SCB->VTOR 访问。
它的作用非常直白:告诉 CPU,向量表到底放在哪个地址。默认值是 0x00000000,也就是说 CPU 中断的时候,默认去 0x00000000 的映射区域找向量。当程序从 SRAM 运行时,你必须把 SCB->VTOR 改成 0x20000000(或你实际的 SRAM 运行基址),否则一旦有任何中断产生——串口、定时器、SysTick——CPU 都会跑到 Flash 的向量表里取中断服务函数地址,取到空的或者乱的数据,结果就是 HardFault。
这里还有一个对齐要求:Cortex-M4 要求 VTOR 的值必须对齐到向量表大小的上取整 2 的幂。STM32F407 的向量表有 16 个内核异常加 82 个外设中断,总共 98 个字,约 392 字节,上取整到 2 的幂就是 512 字节。所以只要你把向量表放在 0x20000000 这样天然对齐的地方,问题不大,但如果你把运行基址挪到 0x20000200 之类的偏移位置,就会踩中这个坑。我习惯把向量表 64 字节对齐起跳,链接脚本里直接 ALIGN(8) 不够保险,建议 ALIGN(512) 或干脆 ALIGN(1024)。
2.3 为什么"加载成功"不等于"运行成功"
把这两节连起来,你就能完整解释那个报错了。调试器把镜像写进 SRAM,这件事只完成了"数据存在"。但 CPU 继续执行的前提是:
- 复位后从 0x00000000 映射区读 MSP 和 PC,如果你没有把启动映射切到 SRAM,它读到的还是 Flash 的东西。
- 即便调试器帮你把 PC 指到了 Reset_Handler,SP 的值也可能是旧的,一压栈就写到非法地址。
- 即便 SP 也对了,中断一来,VTOR 还指向 Flash,废了。
所以你会看到各种诡异的失败姿势:有的卡在启动文件里,有的进 main 就崩,有的正常运行几十毫秒,一开中断就 HardFault。它们看着不一样,根因都是这三个环节中的一个或多个。
2.4 用调试器的寄存器窗口和反汇编窗口验证启动完整性
排查的时候,别靠猜,直接打开调试器的寄存器窗口,看几个关键值:
| 寄存器 | 期望值 | 说明 |
|---|---|---|
| MSP | 0x2001xxxx 或 0x1001xxxx | 必须落在你的 RAM 区域内 |
| PC | 0x2000xxxx 指向 Reset_Handler | 不能是 0x00000000 或 0x08000000 |
| SCB->VTOR | 0x20000000 | 通过 Watch 窗口读 0xE000ED08 |
| CF SR / HFSR | 0 | 如果有异常,这里会有标志 |
再打开反汇编窗口,确认 Reset_Handler 的第一条指令和你编译出来的机器码一致。很多人忽略了这个最笨也最有效