XIP 地址映射原理

说白了,XIP 地址映射就是让 CPU 把 Flash 或 ROM 当成普通内存来访问。CPU 取指令时,直接通过地址总线从 Flash 里读,跟读 RAM 没什么两样。省掉了一次拷贝,也省掉了 RAM 空间。我在做第一颗基于 Cortex-M 的 IoT 芯片时,就靠这个把 512KB Flash 直接映射到 0x08000000 起始地址,跑起来毫无压力。

为何要搞地址映射?

你想想看,传统嵌入式方案里,代码得先从 Flash 复制到 RAM,然后 CPU 从 RAM 里取指。这叫“加载后执行”——慢,还费内存。XIP 的核心思路是:“为什么不让 CPU 直接从 Flash 里读指令?”

  • 省 RAM:代码不占 RAM,只占 Flash 空间。RAM 留给数据和堆栈。
  • 省时间:上电就能跑,不用等搬运。我见过一个项目,把 2MB 代码从 SPI Flash 搬到 SDRAM,光搬运就花了 200ms——用 XIP 就没这个问题。
  • 简化设计:不需要额外的加载器,CPU 复位后直接从映射地址跳过去。

当然代价也有——Flash 的读取速度通常比 RAM 慢,所以需要缓存(Cache)来救场,那是后面章节要聊的。

地址映射的基本方式

我习惯把映射方式分成三类,表格列在这,方便你对照:

映射方式描述典型场景缺点
线性直接映射Flash 物理地址连续映射到 CPU 地址空间的一段固定区间大多数 MCU(STM32、EFM32)Flash 大小固定,无法动态扩展
Bank 切换映射把大 Flash 分成多个 Bank,通过寄存器选择一个 Bank 映射到固定窗口8/16 位单片机,大容量 NOR Flash切换 Bank 有额外开销,中断响应可能受影响
动态重映射通过 MMU 或 MPU 把不同 Flash 物理块映射到虚拟地址Linux 系统级 SoC、ARM A 系列需要 MMU,实时性不好控制
个人习惯:能走线性映射就别折腾 Bank 切换。有一次我帮客户调试一个 Bootloader,他用 Bank 方式把两个 Firmware 放不同 Bank 里切换,结果一次中断发生在 Bank 切换瞬间,程序直接跑飞。纯粹是自己找麻烦。

地址映射的实际连接

映射本质上是地址线的连接。CPU 发出地址后,地址总线的高位线决定了哪个外设被选中,低位线决定了外设内部的偏移位置。

举个例子,一个 32 位地址的 CPU,Flash 挂在 0x08000000~0x080FFFFF(1MB 空间)。地址线 A[31:20] 决定片选:当 A[31:20] = 0x080 时,Flash 芯片使能。地址线 A[19:0] 直接送给 Flash 作为内部地址偏移,Flash 内部通过这个偏移找到具体的字节。

如果你的 Flash 是 16 位数据总线,那 A[0] 可能被忽略,CPU 发出 A[1:0] 控制字节使能信号——这个时候要格外小心对齐问题。我曾在项目中遇到一个坑:CPU 做未对齐的 16 位访问,Flash 控制器直接抛异常,因为 Flash 不支持非对齐读取。后来我强制所有代码和常量 4 字节对齐,问题才解决。

切记:XIP 映射的 Flash 通常只支持对齐访问。如果不确定,请查阅芯片手册的“AHB 从设备支持”章节。我见过有人因为没对齐,整个系统跑出“HardFault”,查了三天才发现是地址最低位没清零。

映射初始化流程

上电后 CPU 要做的映射初始化,通常就这么几步:

  1. 配置 Flash 控制器时序:设置等待周期、预取使能等。Flash 速度一般比 CPU 时钟慢,需要插入等待状态。
  2. 配置地址解码器(如果采用 Bank 切换):选定映射窗口和 Bank 编号。
  3. 启用 Cache(如果有):让 Flash 读取通过 Cache 加速。
  4. 跳转到 Flash 地址:CPU 复位向量指向映射后的 Flash 起始地址即可。
// 伪代码示例:STM32F4 系列启用 XIP 映射并设置等待周期
void XIP_Init(void)
{
    // 1. 使能 Flash 预取和指令缓存 (ICache)
    FLASH->ACR |= FLASH_ACR_PRFTEN | FLASH_ACR_ICEN;

    // 2. 根据系统时钟设置等待周期 (例如 168 MHz 时 5 个等待)
    FLASH->ACR = (FLASH->ACR & ~FLASH_ACR_LATENCY_Msk) 
                 | FLASH_ACR_LATENCY_5WS;

    // 3. 跳转到 Flash 起始地址 (已经在链接脚本中设置好)
    // 实际上 Reset_Handler 已经在 0x08000000 处
}

避坑指南:我曾经把等待周期设少了,系统运行几分钟后偶尔出现指令错误。原因嘛——Flash 内部读出的数据还没等其稳定就被 CPU 锁存了。后来我往大了加了一个等待周期,就再没出过问题。等待周期宁可多不可少。

映射空间的边界与保护

记住一点:映射空间的大小受限于 Flash 芯片的容量,也受限于 CPU 地址空间的地址窗口大小。大多数 MCU 会预留一块连续的地址区域给外部存储器映射,比如 Cortex-M 的 0x60000000~0x9FFFFFFF(外部存储器区域)。

地址窗口不够用怎么办?我常用的土办法:在硬件上把 Flash 的 CS 引脚接到 GPIO,然后用软件控制 Bank 切换,但需要保证切换时不执行代码。我记得有个同事的解决思路更野:他把两片 Flash 的地址线通过 NAND 门组合,做成“地址折叠”来扩展窗口——当时我们都说他是调通了玄学。

另外,注意 MPU 或 MMU 的保护。XIP 区域通常只读,如果意外向映射地址写数据,可能会触发异常甚至损坏 Flash。我习惯在初始化时就把该区域设置为“只读 and 不可执行”之外的权限。

  • 只读保护:防止意外写入。
  • 可执行保护:确保该区域可以被取指。
  • Cache 策略:写回还是写直通?我一般用“写直通+读缓存”以保证一致性。

下面这张图展示了典型的线性直接映射关系。你可以看到 CPU 地址空间中有专门给 Flash 的窗口,Flash 内部地址通过低位地址线直接映射。

CPU 地址空间 → Flash 映射示意 CPU 地址空间(32位) 映射窗口(固定基址) 基址 0x08000000 窗口大小 1MB 地址线分解:A[31:20] → 片选决定 A[19:0] → 低位偏移送给 Flash Flash / ROM 芯片 物理容量 512KB~1MB A[19:0] 偏移 CS (A[31:20]) 实际映射需要 等待周期 + Cache 配合

让我再唠叨几句

映射原理并不复杂,但每个芯片的地址映射细微差别很大。我建议你拿到新平台时,先做两个实验:

  1. 读取映射地址的第一个字,看看是不是复位向量内容。
  2. 在映射地址末尾写一个常量,然后读回来,验证是否循环(地址回绕)。

做完这两个实验,你对这块芯片的映射机制心里就有底了。遇到奇奇怪怪的问题,十有八九是映射窗口大小设错了,或者等待周期没配好。下次我们谈谈 Cache 一致性的问题,那是 XIP 性能的关键。