4. 分区读写性能优化:DMA传输、Cache策略、双缓冲机制、预取与流水线
分区读写,说白了就是Bootloader从Flash里把数据搬来搬去。很多人觉得这有什么难的?直接memcpy不就完了?嗯,我刚开始做Bootloader时也这么想,直到有一次升级固件,用户等了整整两分钟……从那以后,我就再也不敢小看这块了。
这一章,我把压箱底的四板斧掏出来:DMA传输、Cache策略、双缓冲机制、预取与流水线。每个都是实战中踩过坑才真正理解的。
4.1 DMA传输:让硬件替你搬砖
我见过太多Bootloader代码,读Flash用轮询,写Flash也用轮询。CPU全程傻等,啥也不干。这太浪费了。
DMA(直接存储器访问)就是专门干这个的。你告诉DMA控制器:从地址A搬N个字节到地址B,然后它自己搞定,搬完了给你发个中断。CPU呢?该算校验和算校验和,该解压缩解压缩,两不耽误。
配置DMA时,有几个坑要注意:
- 源地址和目的地址的对齐——很多DMA控制器要求32位对齐,不对齐就降速甚至报错
- 传输宽度匹配——Flash读通常是32位,RAM写也是32位,别配成8位模式,速度差4倍
- 中断优先级——DMA完成中断别设太低,否则数据准备好了CPU还在忙别的
// 一个典型的DMA配置示例(伪代码)
void dma_flash_to_ram(uint32_t flash_addr, uint32_t ram_addr, uint32_t size) {
// 1. 关闭DMA通道,防止配置冲突
DMA_Cmd(DMA_CHANNEL, DISABLE);
// 2. 配置源:Flash地址,32位宽度,地址递增
DMA_SetSource(DMA_CHANNEL, flash_addr, DMA_WIDTH_32, DMA_INC_ENABLE);
// 3. 配置目的:RAM地址,32位宽度,地址递增
DMA_SetDest(DMA_CHANNEL, ram_addr, DMA_WIDTH_32, DMA_INC_ENABLE);
// 4. 设置传输大小(字节数)
DMA_SetSize(DMA_CHANNEL, size);
// 5. 开启传输完成中断
DMA_ITConfig(DMA_CHANNEL, DMA_IT_TC, ENABLE);
// 6. 启动DMA
DMA_Cmd(DMA_CHANNEL, ENABLE);
// CPU可以去做别的事了
// 等中断来了再处理数据
}
4.2 Cache策略:该缓存的缓存,不该缓存的别碰
Cache这东西,用好了是神器,用不好是灾难。尤其是在Bootloader里,Flash和RAM的访问模式完全不同。
我个人习惯把Cache策略分成三种场景:
| 场景 | Cache策略 | 原因 |
|---|---|---|
| 读固件镜像(顺序访问) | 开启Cache,预取使能 | 顺序读命中率高,Cache能大幅提速 |
| 写分区数据(随机写入) | 关闭Cache或写透模式 | Cache写回会导致数据不一致,万一掉电就完了 |
| 校验计算(SHA/CRC) | 开启Cache,但注意DMA一致性 | 计算密集,Cache加速明显,但要处理好DMA和Cache的同步 |
你想想看,如果写Flash时用了写回(Write-Back)Cache,数据先留在Cache里,还没写到Flash就掉电了——嗯,固件就废了。所以写操作我坚决用写透(Write-Through)或者干脆关掉Cache。
// DMA传输前后的Cache维护
void dma_with_cache_sync(uint32_t flash_addr, uint32_t ram_addr, uint32_t size) {
// 传输前:将RAM区域的Cache写回(Clean)
// 防止Cache里的脏数据没写回RAM
SCB_CleanDCache_by_Addr((uint32_t*)ram_addr, size);
// 启动DMA传输
start_dma(flash_addr, ram_addr, size);
// 等待DMA完成
while(!dma_done);
// 传输后:将RAM区域的Cache失效(Invalidate)
// 确保CPU读的是DMA写进来的新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)ram_addr, size);
// 现在可以安全地访问ram_addr了
}
4.3 双缓冲机制:流水线式读写
单缓冲的问题很明显:你读一块数据,处理完,再读下一块。中间有大量空闲时间。双缓冲就是解决这个的。
原理很简单:两个缓冲区,一个给DMA读数据,一个给CPU处理数据。DMA填满缓冲区A,发中断;CPU处理A的同时,DMA已经在填缓冲区B了。交替进行,谁也别等谁。
我在项目中用过的一个典型配置:
- 缓冲区大小:4KB(刚好一个Flash扇区大小)
- 缓冲区A:0x20000000
- 缓冲区B:0x20001000
- DMA传输完成中断里,切换当前缓冲区
// 双缓冲机制核心代码
#define BUF_SIZE 4096
uint8_t buf_a[BUF_SIZE] __attribute__((aligned(32)));
uint8_t buf_b[BUF_SIZE] __attribute__((aligned(32)));
volatile uint32_t current_buf = 0;
void dma_double_buffer_isr(void) {
if (current_buf == 0) {
// 缓冲区A已填满,处理A
process_data(buf_a, BUF_SIZE);
// 同时DMA已经在填B(由硬件自动切换)
current_buf = 1;
} else {
// 缓冲区B已填满,处理B
process_data(buf_b, BUF_SIZE);
current_buf = 0;
}
}
void start_double_buffer_read(uint32_t flash_addr, uint32_t total_size) {
// 配置DMA为循环模式,双缓冲
DMA_DoubleBufferConfig(DMA_CHANNEL,
(uint32_t)buf_a, // 内存地址1
(uint32_t)buf_b, // 内存地址2
BUF_SIZE); // 每次传输大小
// 启动DMA,硬件会自动交替使用两个缓冲区
DMA_Cmd(DMA_CHANNEL, ENABLE);
}
4.4 预取与流水线:让数据等CPU,别让CPU等数据
预取说白了就是:猜你下一步要读什么,提前帮你准备好。Flash读是有延迟的,尤其是NOR Flash,读第一个字节可能要等几十个周期,但后续字节可以流水线读出。
我一般会做两件事:
- 开启Flash预取缓冲——大多数MCU的Flash控制器都有预取功能,默认可能没开。翻翻手册,找到FLASH_ACR寄存器,把PRFTEN位置1。
- 指令预取对齐——代码和数据尽量放在连续的地址空间,让预取逻辑能命中。别东一块西一块。
流水线呢?就是把一个大的读操作拆成多个阶段:发送地址→等待延迟→读取数据。每个阶段独立进行,就像工厂流水线一样。
具体到代码层面,很多MCU的Flash控制器会自动做流水线。你需要做的只是:
- 配置正确的等待周期(根据主频和Flash速度算)
- 开启预取使能
- 如果支持,开启指令Cache和数据Cache
// STM32系列Flash预取配置示例
void flash_prefetch_config(void) {
// 假设HCLK=168MHz,Flash等待周期设为5
FLASH->ACR = FLASH_ACR_LATENCY_5WS | // 等待周期
FLASH_ACR_PRFTEN | // 预取使能
FLASH_ACR_ICEN | // 指令Cache使能
FLASH_ACR_DCEN; // 数据Cache使能
// 就这么几行,性能能差30%以上
}
4.5 四板斧组合实战
单独用哪个都有效果,但组合起来才是王炸。我整理了一个典型的优化流程:
| 步骤 | 操作 | 预期提升 |
|---|---|---|
| 1 | 开启Flash预取和Cache | 基础读性能提升30% |
| 2 | DMA替代CPU轮询 | CPU利用率降低40% |
| 3 | 双缓冲机制 | 吞吐量提升50% |
| 4 | 流水线读优化 | 延迟降低20% |
加起来,整体分区读写性能可以提升2-3倍。这不是理论值,是我在好几个项目里实测出来的。