4. Flash与存储优化:XIP、缓存、压缩与布局
好,咱们聊聊存储这块。说实话,Bootloader的启动时间,很大一部分都耗在Flash读取上。你想想看,CPU跑得飞快,但Flash读取速度就那么点,这中间的不匹配就是性能瓶颈所在。我这些年调过的板子,十有八九都在这里吃过亏。
4.1 XIP(就地执行)技术
XIP,全称是Execute In Place。说白了,就是代码直接在Flash里跑,不用先拷到RAM里。这听起来很美好,对吧?省掉了加载时间,也省了RAM空间。
但事情没那么简单。我遇到过不少新手,一听说XIP好,就把所有代码都扔Flash里了。结果呢?启动是快了,但跑起来慢得像蜗牛。为什么?因为Flash的随机读取延迟太高了。
核心要点:XIP适合存放那些执行频率高、但代码量小的关键路径。比如中断向量表、复位处理函数、以及一些频繁调用的底层驱动。
具体怎么用?以ARM Cortex-M为例,你需要在链接脚本里把代码段放到Flash地址空间:
/* 链接脚本示例 - XIP区域 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.text : {
*(.isr_vector) /* 中断向量表 - 必须XIP */
*(.text.boot) /* 启动代码 - 必须XIP */
*(.text.xip) /* 标记为XIP的函数 */
} > FLASH
.data : {
*(.text.ram) /* 需要拷到RAM执行的函数 */
*(.data)
} > RAM AT> FLASH
}
这里有个坑:不是所有函数都适合XIP。那些被频繁调用的、对延迟敏感的函数,我建议还是拷到RAM里跑。我曾经在一个项目里,把中断处理函数也放Flash里XIP,结果中断响应时间直接翻了三倍。嗯,从那以后我就学乖了。
4.2 Flash缓存与预取
现代MCU基本都带Flash缓存和预取单元。这东西的原理很简单:猜你下一步要读哪,提前把数据准备好。
但猜也有猜不准的时候。我见过一个案例,代码里到处都是跳转和分支,预取命中率低得可怜。最后怎么解决的?把关键路径上的分支改成了查表,预取命中率从40%提到了85%。
个人经验:开启预取后,记得把代码中的循环展开。循环体越小,预取越容易命中。我一般把循环体控制在16字节以内,这样预取一次就能覆盖整个循环。
配置示例(以STM32为例):
/* 开启Flash预取和缓存 */
void flash_optimize(void)
{
/* 开启指令缓存 */
FLASH->ACR |= FLASH_ACR_ICEN;
/* 开启数据缓存 */
FLASH->ACR |= FLASH_ACR_DCEN;
/* 设置预取缓冲区 */
FLASH->ACR |= FLASH_ACR_PRFTEN;
/* 根据主频设置等待周期 */
if (SystemCoreClock > 48000000) {
FLASH->ACR = (FLASH->ACR & ~FLASH_ACR_LATENCY_Msk) |
FLASH_ACR_LATENCY_1WS;
}
}
注意:开启缓存后,如果代码有自修改功能(比如OTA升级),一定要先清缓存再写Flash。否则读到的可能是旧数据。我曾经因为这个bug排查了整整两天。
4.3 压缩与解压缩策略
压缩,听起来是省空间用的。但在Bootloader里,压缩其实也能省时间。为什么?因为Flash读取速度慢,但解压缩速度快。如果压缩比够高,读压缩数据的时间 + 解压时间,可能比直接读原始数据还短。
我常用的压缩算法有几种:
| 算法 | 压缩比 | 解压速度 | 适用场景 |
|---|---|---|---|
| LZSS | 2:1 ~ 3:1 | 极快 | 代码段压缩 |
| LZ4 | 2:1 ~ 2.5:1 | 非常快 | 数据段压缩 |
| RLE | 1.5:1 ~ 2:1 | 极快 | 初始化数据 |
我个人偏爱LZSS。它解压速度快,代码量也小,很适合Bootloader这种资源受限的环境。下面是一个轻量级的解压实现:
/* LZSS解压 - 轻量级实现 */
void lzss_decompress(uint8_t *src, uint8_t *dst, uint32_t size)
{
uint32_t flags = 0;
uint32_t mask = 0;
while (size > 0) {
/* 每8个字节读取一次标志位 */
if (mask == 0) {
flags = *src++;
mask = 0x80;
}
if (flags & mask) {
/* 未压缩字节 - 直接拷贝 */
*dst++ = *src++;
size--;
} else {
/* 压缩块 - 从历史缓冲区拷贝 */
uint8_t lo = *src++;
uint8_t hi = *src++;
uint16_t offset = ((hi & 0xF0) << 4) | lo;
uint16_t length = (hi & 0x0F) + 3;
uint8_t *ref = dst - offset - 1;
while (length-- && size) {
*dst++ = *ref++;
size--;
}
}
mask >>= 1;
}
}
避坑指南:我曾经把整个Bootloader都压缩了,结果发现解压代码本身也需要解压才能运行。这是个死循环。正确的做法是:把解压代码放在不压缩的区域,或者用汇编手写一个极简的解压器。
4.4 存储布局优化
存储布局,说白了就是怎么安排Flash里的内容。布局合理,启动就快;布局不合理,启动就慢。我见过最夸张的一个项目,Bootloader大小只有32KB,但启动花了3秒。为什么?因为关键代码分散在Flash的各个角落,每次读取都要跨页。
我的布局原则是这样的:
- 热代码集中存放:把启动路径上的所有函数放在一个连续的Flash区域。这样预取和缓存都能发挥最大作用。
- 冷热分离:不常用的功能(比如调试接口、错误处理)放到Flash末尾。别让它们污染了热区。
- 对齐到Flash页边界:每个功能模块都从Flash页的起始地址开始。这样读取时不会跨页,减少等待周期。
一个典型的布局示例:
Flash布局 (512KB Flash)
+------------------+ 0x08000000
| 中断向量表 | 256字节 - 必须XIP
+------------------+ 0x08000100
| 启动代码 | 4KB - 热区,XIP
+------------------+ 0x08001100
| 核心驱动 | 8KB - 热区,XIP
+------------------+ 0x08003100
| 解压代码 | 2KB - 必须XIP
+------------------+ 0x08003900
| 压缩的应用数据 | 剩余空间
+------------------+ 0x08080000
重要提醒:布局确定后,一定要用objdump或者readelf工具检查实际布局。我遇到过链接脚本写对了,但编译器优化把函数挪到了奇怪的位置。嗯,这种事情发生过不止一次。
最后说一句:存储优化没有银弹。每个项目都有自己的特点,关键是要理解你的瓶颈在哪。是Flash读取慢?还是解压耗时长?还是缓存命中率低?找到瓶颈,对症下药,才能把启动时间真正压下来。