Skip to content

LV065-外部SRAM-APP方案分析 ​

一、 方案背景与可行性 ​

1. 背景与动机 ​

LV035-SRAM-APP设计 把 APP 装进内部 SRAM 运行,容器固定 24KB——.text 一旦超过这个数,链接阶段就报 region 溢出。而这块板(STM32F103ZET6 + IS62WV51216)通过 FSMC 扩展了 1MB 外部 SRAM(NE3,基址 0x68000000),见 LV015-外部SRAM读写实例。一个自然的想法是:把 APP 的运行容器从内部 SRAM 的 0x20007000 换到外部 SRAM 的 0x68000000,代码体积上限直接从 24KB 提到接近 1MB。

本文分析这个方案在机制上是否成立、需要哪些改动、以及放到这块板的具体资源格局下会付出什么代价。结论先行:机制成立,但板级代价不小,是否采用取决于性能实测与分区取舍。

2. 取指可行性原理 ​

Cortex-M3 的取指只看地址落在哪个总线区域,不挑介质。ARMv7-M 默认内存映射中,0x60000000 ~ 0x9FFFFFFF 是"外部 RAM"Normal 存储区,不是 XN(Execute Never)区,允许取指。FSMC 把 IS62WV51216 映射到 0x68000000 后,它就是一块可随机访问的普通存储器,与内部 SRAM 的 0x20000000 在取指视角下没有本质区别。真正禁止取指的是 0x40000000 ~ 0x5FFFFFFF 外设区与 0xE0000000 起的系统区,FSMC 映射区不在其列。

与 LV035 相同的三个执行条件,一条都不能少:

(1)链接时的执行地址就是实际装载地址——位置相关代码,Bootloader 必须把镜像拷到链接器编址的那个位置;

(2)镜像的向量表在运行时被 SCB->VTOR 指向,中断才能找到处理函数——0x68000000 满足 VTOR 的 128 字节对齐要求;

(3)装载完成后按向量表把 MSP 换成镜像自己的栈顶。

3. 与内部 SRAM-APP 的同构性 ​

跳转机制一行都不用改:iap_load_app() 读目标处向量表、设 VTOR、__set_MSP()、跳 Reset_Handler(详见 LV050-IAP跳转与向量表原理)。差异只在两处:地址从 0x20007000 换成 0x68000000;新增一条"FSMC 必须已初始化"的启动依赖。

维度内部 SRAM-APP外部 SRAM-APP
运行位置0x200070000x68000000
容器大小24KB(内部 SRAM 分区限制)最高接近 1MB(实际受暂存区约束,见四、1)
取指路径I-bus / D-busSystem 总线 + FSMC
启动依赖无(SRAM 上电即可用)FSMC 已初始化、时钟一致
执行速度与 Flash 无异慢数倍(FSMC 等待周期,见四、3)
掉电行为丢失,重新 go sram丢失,重新 go sram

二、 整体框架与装载流程 ​

1. 硬件框架与地址分区 ​

图一是整个方案的硬件框架:Bootloader 在内部 FLASH 里经 ICode/DCode 总线执行;装载命令 go sram 把暂存区镜像经 System 总线、FSMC 搬到 NE3 的外部 SRAM;跳转后 APP 的每条指令都经同一条链路取回。

外部 SRAM-APP 的硬件框架与装载路径

对应的地址分区建议:

区域地址用途
Bootloader0x08000000 ~ 0x08020000(128KB)上电入口,负责 FSMC 初始化与装载
flash-app0x08020000 ~ 0x08040000(128KB)主线交付镜像,不动
暂存区0x08040000 ~ 0x08080000(256KB)app.img 落盘位置,掉电保留
APP 数据区0x2000D000 起的内部 SRAM.data/.bss/堆/栈,保持内部 SRAM 的访问速度
代码容器0x68000000 起(≤256KB)镜像装载与执行位置
剩余外部 SRAM容器之后到 1MB既有大缓冲区迁移至此(见四、2)

分区设计的关键取舍:代码容器放外部 SRAM,RW 数据区继续留在内部 SRAM。这样 .data/.bss/堆/栈的访问速度不变、FSMC 流量最小,1MB 外存全部让给代码;栈溢出时也会先撞到 NOINIT 区而不是覆盖指令,沿用 LV035 的边界设计思想。

2. 装载与启动流程 ​

图二按时间顺序给出从复位到业务运行的完整链路,Bootloader 侧六步、APP 侧两步,任何一环校验失败都留在 Bootloader 等待重发。

外部 SRAM-APP 的装载与启动流程

与 LV035 的装载流程相比,有三处实质差异:

(1)装载前新增 FSMC 初始化。Bootloader 要往 0x68000000 写数据,FSMC 必须先配好——这是外部 SRAM 方案独有的、绕不开的前置步骤;

(2)搬运长度从"固定容器大小"改为"帧头实际长度"。LV035 固定搬满 24KB 容器是因为代价小;外部 SRAM 容器接近 1MB,整容器搬运每次 go sram 都要白搬几十万字节 0xFF,必须改为按帧头记录的实际长度搬运;

(3)向量表判据范围更新。栈顶判据仍是"落在内部 SRAM",但复位向量判据从 0x20007000 ~ 0x2000D000 改为"落在 0x68000000 起的容器内且最低位为 1"。

APP 侧的启动顺序沿用 LV035 的约束:main() 里 SCB->VTOR = APP_BASE 必须早于 HAL_Init(),否则 SysTick 中断仍会按 Bootloader 的向量表跳进旧代码。

3. 地址同步点 ​

沿用 LV035"三处必须同步"的方法论,同步点从三处扩到五处:

位置内容不同步的后果
sram-app.uvprojx 的 IROM10x68000000 / 实际容量镜像按错误地址编址,装载后跑飞
iap.h 的 IAP_SRAM_APP_BASE/MAX_SIZE同上帧头地址校验拒绝,或搬运越界
pack_iap.py 的 SRAM_APP_BASE/END同上打包时报 address or size out of range
iap_load_app() 向量表判据外部 SRAM 地址范围合法镜像被拒或非法数据被放行
外部 SRAM 内部再分区容器起点、缓冲区起点代码容器与既有缓冲区互相覆盖(见四、2)

三、 启动契约:FSMC 初始化与跳转 ​

这一节回答两个最容易被问到的疑问:跳转后 FSMC 相关寄存器会不会出问题?APP 还需不需要再初始化一次外部 SRAM?

1. 跳转不会破坏 FSMC 寄存器 ​

跳转是"硬跳":Bootloader 只改 PC、MSP、VTOR 三个东西,不碰任何外设寄存器。FSMC 的 GPIO 复用、FSMC_BCRx/FSMC_BTRx 配置原样移交给 APP。所以"跳转这个动作会不会弄坏 FSMC"的答案是:不会,寄存器值一位都不会变。

但要立刻补一句:APP 从第一条指令起就依赖这份配置——Reset_Handler、SystemInit、__main 的 scatterload 全部从 0x68000000 取指。这是外部 SRAM 执行与内部 SRAM 执行最本质的差别:内部 SRAM-APP 跳转后对外设零依赖,外部 SRAM-APP 把"FSMC 已配置"变成了隐形的启动契约。

2. APP 是否需要重新初始化 ​

分三层看:

(1)APP 没有"运行前自己初始化"的机会。第一条指令就在外部 SRAM 里执行,此时 FSMC 必须已经工作——鸡生蛋问题,初始化权天然在 Bootloader 手里;

(2)main() 里用相同参数再跑一次 FSMC_Init 是可以且无害的。相同值写回去不改变时序,不影响正在进行的取指,属于防御性编程,建议保留;

(3)APP 修改 FSMC 时序参数是禁止项。APP 正在外存里执行,改错的瞬间后续取指就是垃圾数据,直接 HardFault,没有第二次机会。

标准库也佐证了这个契约:SystemInit() 里预留了 DATA_IN_ExtSRAM 宏,定义后会调用 SystemInit_ExtMemCtl() 初始化 FSMC——官方对"代码在外存执行"的标准做法就是启动最早期初始化外存。IAP 场景下这份职责前移到了 Bootloader,但"谁让代码落进外存,谁负责先配好 FSMC"的契约不变。APP 对其余外设为何必须全量重配、FSMC 为何是唯一例外,一般性原理见 LV050-IAP跳转与向量表原理。

3. 复位路径的必然失效 ​

系统复位(软复位、看门狗复位、硬复位)后 RCC->AHBENR 的 FSMCEN 复位值为 0,FSMC 时钟被关断,外部 SRAM 立刻不可访问。由此得到两条推论:

(1)每次装载前必须重新 FSMC_Init,禁止"检测到已初始化就跳过"的优化——寄存器值可能还在,但时钟一定没了;

(2)reboot 命令(NVIC_SystemReset(),见 LV025-reboot命令实例)回 Bootloader 的路径是安全的:复位后 PC 回到 0x08000000,Bootloader 反正每次都会重新初始化 FSMC 再装载。看门狗复位同理,见 LV055-看门狗问题与解决方案。

4. 时钟一致性约束 ​

FSMC 的 FSMC_BTRx 时序参数(ADDSET、DATAST)是按特定 HCLK 配置的。如果 Bootloader 按 72MHz 配时序、APP 的 SystemInit 却把时钟改了,外部 SRAM 访问就违时序——症状是运行中随机 HardFault 或数据错乱,极难定位。所以工程约定是:Bootloader 与 APP 的时钟配置保持完全一致,APP 不改系统时钟。这条约定保护的也不止 FSMC:Bootloader 移交的其他外设配置同样依赖时钟不变。

5. 契约清单汇总 ​

契约责任方违反后果
装载前完成 FSMC_Init(每次必做)Bootloader写入或取指直接 HardFault
时钟配置两边完全一致双方FSMC 时序静默错配,随机跑飞
APP 不改 FSMC 时序参数APP取指变垃圾数据,立即 HardFault
APP 不复用 FSMC 引脚做 GPIOAPP总线信号被破坏
APP 不关 RCC->AHBENR.FSMCENAPP外存瞬间不可访问
APP 不进改频类低功耗模式APP时钟变化导致时序失配

四、 板级资源冲突与风险分析 ​

方案能不能落地,最终取决于这块板的具体资源格局。按严重程度列出三个问题。

1. 暂存区容量给"大 APP"封了顶 ​

镜像必须先进暂存区再搬运——这是"掉电后重发一条 go 就能复活"收益的前提,而暂存区只有 256KB(0x08040000 ~ 0x08080000,分区见 LV030-FLASH-APP设计)。外部 SRAM 容器虽然接近 1MB,APP 实际上限仍是 256KB——"解除 24KB 限制"的核心收益被 Flash 分区卡住了。想突破只有两条路:重分区(压缩 Bootloader 或砍掉 flash-app),或者跳过暂存直接流入外部 SRAM——后者会丢掉暂存收益,方案就变味了。

2. 外部 SRAM 在这块板上不空 ​

LV001-以太网与DM9000硬件接口 已经确认:IAP 现在就把 128KB 网络载荷缓冲放在外部 SRAM,访问互斥还要靠临界区保护;LV015 的示例还用 __attribute__((at(0x68000000))) 把数组钉在这个地址。代码容器一旦从 0x68000000 开始铺,这些缓冲全部要挪——外部 SRAM 必须重新分区(代码容器在前、缓冲区在后),链接脚本和所有 at() 地址同步修改。这还是"多处同步"的坑,只是同步点更多。

3. 总线竞争被"取指"放大数倍 ​

现在的设计里只有数据面(缓冲读写、网卡收发)碰 FSMC,代码在 Flash 走 ICode 总线。APP 挪到外部 SRAM 之后:

(1)ICode 总线闲置,每条取指、每次中断向量表查询都挤上 System 总线,与内部 SRAM 数据、外设、FSMC 缓冲访问互相竞争;

(2)DM9000(NE2)与 IS62WV51216(NE3)共享 NOE/NWE/A7 信号线,FSMC 控制器在两片之间串行化;

(3)FSMC 异步时序加 16 位数据线,意味着一次 32 位取指要两次 FSMC 访问,粗估计算密集代码慢 4 ~ 10 倍,中断入口延迟明显增加。

这个数字必须实测(用 DWT 周期计数器做基准),中断密集的场合很可能不可接受。这也是本节三个问题里唯一可能直接否决方案的。

五、 结论与路线建议 ​

1. 三条路线对比 ​

路线适用场景代价
Flash 重分区,改用大容量 flash-app目标只是解除 24KB 限制需要重排 Flash 布局;执行速度不变、零 FSMC 依赖、契约最简,首选
外部 SRAM 方案(本文)需要运行期反复装载、Flash 擦写敏感四节的三个问题全部要处理,性能实测过关才可上线
混合:热代码放内部 SRAM,冷代码放外部 SRAM坚持外部 SRAM 且有实时性要求链接脚本复杂度上升,中断处理函数需单独归段

2. 结论 ​

外部 SRAM-APP 在机制上完全成立:取指区域合法、跳转流程同构、启动契约可落实。但放到这块板上,它解决的是代码体积问题,而这个问题用 Flash 重分区解决得更便宜、更快、更稳;外部 SRAM 方案的真正代价——暂存区封顶、缓冲区迁移、总线竞争——恰好落在这块板最拥挤的资源上。

给后续实施的两条建议:

(1)先做 DWT 性能实测:同一段业务代码分别在 Flash、内部 SRAM、外部 SRAM 运行,测指令吞吐与中断延迟,数据过关再谈采用;

(2)若采用,把 三、5 的契约清单原样写进 Bootloader 与 APP 的设计文档,每条契约都配上"违反后会看到什么现象"的描述,方便日后排查。


本文档由 markdowncli 技能辅助生成