
1. SRAM运行程序不是稀有操作但很多人第一步就理解错了1.1 你需要SRAM运行的真实场景先说清楚一件事把应用从 Flash 挪到 SRAM 里跑不是炫技也不是嵌入式发烧友的怪癖而是一类非常刚需的操作。我最初碰到这个需求是在做 IAPIn-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 看成了一块。区域起始地址大小特性Flash0x080000001MBF407VGART 加速器支持预取和缓存SRAM10x20000000112KB主 SRAM可通过总线矩阵被 DMA 访问SRAM20x2001C00016KB紧接 SRAM1也可被 DMA 访问CCM RAM0x1000000064KBCPU 专享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 的出厂设定那么 VTORVector Table Offset Register就是程序员手里最关键的旋钮。它在 Core Peripheral 的地址 0xE000ED08CMSIS 里直接用 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_HandlerSP 的值也可能是旧的一压栈就写到非法地址。即便 SP 也对了中断一来VTOR 还指向 Flash废了。所以你会看到各种诡异的失败姿势有的卡在启动文件里有的进 main 就崩有的正常运行几十毫秒一开中断就 HardFault。它们看着不一样根因都是这三个环节中的一个或多个。2.4 用调试器的寄存器窗口和反汇编窗口验证启动完整性排查的时候别靠猜直接打开调试器的寄存器窗口看几个关键值寄存器期望值说明MSP0x2001xxxx 或 0x1001xxxx必须落在你的 RAM 区域内PC0x2000xxxx 指向 Reset_Handler不能是 0x00000000 或 0x08000000SCB-VTOR0x20000000通过 Watch 窗口读 0xE000ED08CF SR / HFSR0如果有异常这里会有标志再打开反汇编窗口确认 Reset_Handler 的第一条指令和你编译出来的机器码一致。很多人忽略了这个最笨也最有效