4、Flash映射与缓存:内存映射Flash、XIP(原地执行)原理、缓存一致性
好,咱们接着聊Flash。前面几章我们把文件系统、磨损均衡、掉电保护都过了一遍。但有个问题一直没细说——Flash到底怎么跟CPU打交道?
你想想看,CPU跑代码,最舒服的方式是直接从内存里取指令。但Flash呢?它是个块设备,读写都得按页来,速度还慢。这就尴尬了。我早年做第一个Zephyr项目时,就踩过这个坑——代码放在外部Flash上,启动慢得像蜗牛爬。后来才搞明白,原来得用内存映射和XIP这两招。
4.1 内存映射Flash:把Flash“伪装”成内存
说白了,内存映射就是把Flash的地址空间,直接映射到CPU的寻址空间里。CPU读某个内存地址,实际上是在读Flash里的某个位置。对CPU来说,它根本不知道背后是Flash还是RAM——反正都是读地址嘛。
在Zephyr里,这通常由SPI控制器或QSPI控制器的硬件模块来完成。比如STM32的QuadSPI,它内部有个查找表(LUT),你配置好命令序列,它就能自动把内存读请求转换成Flash的读命令。
关键点:内存映射模式下,CPU读Flash就像读RAM一样——直接load指令就行。但写操作呢?嗯,这里要注意,大多数Flash不支持直接写映射地址。你得通过SPI命令去写,或者用XIP模式下的特殊写操作。
我在项目中遇到过一个问题:某款Flash芯片,内存映射读没问题,但一写就死机。查了半天,发现是芯片的写保护引脚没拉高。所以啊,硬件设计时一定要把Flash的控制引脚留出来,别图省事全接地。
4.2 XIP(原地执行):代码直接在Flash上跑
XIP,全称eXecute In Place。意思就是代码不搬到RAM里,直接在Flash上执行。
你可能会问:Flash那么慢,怎么跑代码?嗯,这里有两个前提:
- Flash支持高速读:比如QSPI Flash,四线模式下时钟能跑到100MHz以上,读速度跟慢速RAM差不多。
- CPU有指令缓存:代码执行有局部性,缓存命中率高的话,实际性能损失很小。
Zephyr对XIP的支持很成熟。你只要在设备树里配好Flash的reg属性,把代码段链接到Flash地址上就行。举个例子:
// 设备树片段
&qspi {
pinctrl-0 = <&qspi_pins>;
status = "okay";
mx25r6435f: flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <104000000>;
size = <0x800000>; // 8MB
// 关键:映射到CPU地址空间
memory-map = <0x90000000 0x800000>;
};
};
然后链接脚本里,把text段放到0x90000000开始的区域:
SECTIONS {
.text : {
*(.text*)
} > FLASH_MMAP // 这个FLASH_MMAP就是0x90000000
}
这样编译出来的固件,启动时CPU直接从Flash取指令执行。我习惯在调试阶段先用RAM运行,稳定后再切到XIP——毕竟XIP下断点调试不太方便。
小技巧:XIP模式下,如果代码里频繁访问全局变量,性能会下降。因为全局变量在RAM里,每次访问都要跨总线。我一般把频繁用的变量放到紧耦合内存(TCM)里,或者用__attribute__((section(".ramfunc")))把关键函数搬到RAM里执行。
4.3 缓存一致性:别让CPU和Flash“打架”
好,现在代码在Flash上跑了,CPU也开了缓存。问题来了——缓存里的数据跟Flash里的数据不一致怎么办?
举个例子:你通过SPI命令往Flash里写了一段新代码,然后CPU从映射地址去读。如果缓存里还存着旧数据,CPU读到的就是错的。这就是缓存一致性问题。
在Zephyr里,处理方式分几种情况:
| 场景 | 处理方法 | 我踩过的坑 |
|---|---|---|
| 写Flash后读映射地址 | 手动无效化缓存行 | 忘了做,读回旧数据,程序跑飞 |
| DMA写Flash后CPU读 | DMA完成回调里清缓存 | 回调里没做,数据错乱 |
| 多核共享Flash映射 | 用内存屏障+缓存维护指令 | 两个核同时读,一个读到旧的 |
具体到代码,ARM Cortex-M系列有专门的指令:
// 无效化某个地址的缓存行
void invalidate_dcache_by_addr(uint32_t addr, uint32_t size) {
uint32_t line_size = 32; // 通常32字节
uint32_t start = addr & ~(line_size - 1);
uint32_t end = (addr + size + line_size - 1) & ~(line_size - 1);
for (uint32_t a = start; a < end; a += line_size) {
__asm volatile("DCIMVAC %0" : : "r"(a)); // 数据缓存无效化
}
__DSB(); // 数据同步屏障
__ISB(); // 指令同步屏障
}
我曾经在OTA升级时吃过这个亏。升级完成后,新固件写到Flash里,然后复位重启。结果CPU从映射地址读到的还是旧代码——因为指令缓存没清。后来在复位前加了段汇编,把整个指令缓存刷一遍,问题才解决。
警告:千万不要在中断服务函数里做缓存维护操作!ISR里调用DCIMVAC这类指令,可能会触发异常。我建议在任务上下文里统一处理,或者用硬件自带的缓存一致性协议(比如某些SoC的ACE总线)。
4.4 实战建议:怎么选?
最后给点个人经验。做项目时,到底用不用XIP,得看场景:
- 代码量小(< 256KB):直接全放RAM里跑,省心。Zephyr的ROM/RAM分区很灵活。
- 代码量大(> 1MB):必须用XIP,不然RAM不够用。这时候要选高速QSPI Flash,最好带DTR(双倍数据率)模式。
- 实时性要求高:把中断处理函数、RTOS调度器这些关键代码放到RAM里。我习惯用链接脚本的.ramfunc段来管理。
嗯,Flash映射和缓存这块,说白了就是用硬件特性来掩盖Flash的慢。你只要把映射配好、缓存管好,Zephyr就能跑得又快又稳。下一章我们聊聊更高级的话题——多分区管理和安全启动,到时候这些知识还会用上。