4、Flash控制器配置:优化读取时序,开启预取与缓存,加速代码搬运

好,咱们接着聊。上一节我们把时钟树给理顺了,系统能跑起来了。但有个问题——代码还在Flash里躺着呢。

你想想看,CPU从Flash读指令,Flash的读取速度通常比CPU主频慢得多。这就好比CPU是个高速跑车,Flash是条乡间小路。不优化一下,车跑得再快也得堵在路上。

所以这一节,咱们专门来调教Flash控制器。说白了,就是让CPU读Flash的时候,尽量少等、少堵、少浪费时间。

4.1 读取时序:别让CPU干等

Flash读取是有延迟的。CPU发一个读请求,Flash需要几个时钟周期才能把数据准备好。这个等待的周期数,就叫等待周期(Wait State)

我刚开始做项目时,图省事,直接把等待周期设成最大值。结果系统跑起来慢得离谱。后来才发现,等待周期设得太高,CPU大部分时间都在空转。

那怎么设才合适?

  • 看主频:主频越高,每个时钟周期越短,Flash跟不上,就需要更多等待周期。
  • 看Flash速度等级:不同芯片的Flash速度不一样,数据手册里会写。
  • 看电压:电压低时,Flash速度也会下降。

我一般会先查数据手册里的表格,找到对应主频下的推荐值。然后留一点余量,但别太多。

核心原则:在保证稳定性的前提下,等待周期越小越好。

举个例子,某款芯片在72MHz主频下,推荐等待周期是2。你设成3也能跑,但性能就差了。设成1呢?可能偶尔读错数据。嗯,这个平衡点要找好。

4.2 预取缓冲区:提前把指令搬过来

光调等待周期还不够。CPU读指令是有规律的——大部分时候是顺序执行。那能不能提前把下一条指令也读出来?

这就是预取(Prefetch)的作用。

Flash控制器里通常有个预取缓冲区,大小一般是几个字(比如64位或128位)。CPU读地址A时,控制器不光把A的数据取回来,还把A后面连续的数据也一并取到缓冲区里。

这样,CPU执行完当前指令,想读下一条时,大概率已经在缓冲区里了。直接命中,不用再等Flash。

我在项目中遇到过一个问题:预取开启后,系统跑得飞快,但偶尔会跑飞。查了半天,发现是预取缓冲区在中断处理时没清空,导致读到脏数据。后来在中断入口处加了个预取缓冲区刷新操作,问题就解决了。

我的习惯:预取默认开启,但中断服务函数里记得刷新预取缓冲区。

4.3 指令缓存:把常用代码存起来

预取解决的是顺序执行的问题。但代码里还有循环、跳转、函数调用。这些地方,指令地址是跳跃的,预取就帮不上忙了。

这时候需要指令缓存(I-Cache)

缓存的工作原理很简单:把最近用过的指令存到一块高速SRAM里。下次再执行同一段代码时,直接从缓存读,不用再去Flash。

你想想看,一个延时函数,可能被调用几万次。第一次从Flash读,后面几万次都从缓存读,这速度提升可不是一星半点。

缓存的大小通常不大,几KB到几十KB。但命中率很高,因为代码有时间局部性空间局部性

  • 时间局部性:刚执行过的代码,很可能马上再执行(比如循环)。
  • 空间局部性:刚执行过的代码附近的代码,很可能马上要执行(比如顺序执行)。

开启缓存后,我建议你做个简单的性能测试。比如跑一个算法,对比开启前后的执行时间。我见过一个项目,开启缓存后,算法执行时间缩短了40%。

注意:缓存不是万能的。如果代码量太大,缓存频繁换入换出,反而可能降低性能。这叫“缓存颠簸”。

4.4 代码搬运:把关键代码放到RAM里跑

有些代码对速度要求极高,比如中断服务函数、实时控制算法。就算开了预取和缓存,从Flash读还是不够快。

怎么办?直接把代码搬到RAM里跑。

RAM的速度比Flash快得多,而且没有等待周期。把关键代码复制到RAM里,然后让CPU从RAM执行,这是终极加速方案。

具体怎么做?

  1. 标记关键函数:在代码里用属性或段名,把需要搬运的函数标记出来。
  2. 修改链接脚本:在链接脚本里,给这些函数分配一个RAM地址段。
  3. 启动时搬运:在固件启动流程里,把标记好的代码从Flash复制到RAM。
  4. 跳转执行:CPU从RAM里执行这些函数。

我做过一个电机控制项目,中断服务函数里要做PID计算。原来在Flash里跑,中断响应时间太长,电机控制精度不够。后来把PID计算函数搬到RAM里,中断响应时间缩短了60%,控制效果立竿见影。

避坑指南:我曾经把整个程序都搬到RAM里,结果RAM不够用,系统直接崩溃。后来只搬运最关键的几个函数,效果最好。

4.5 配置示例:以STM32为例

说了这么多理论,咱们看个实际配置。以STM32F4系列为例,Flash控制器的配置代码大概长这样:

void Flash_Config(void)
{
    // 1. 设置等待周期
    // 主频168MHz时,推荐等待周期为5
    FLASH->ACR |= FLASH_ACR_LATENCY_5WS;
    
    // 2. 开启预取缓冲区
    FLASH->ACR |= FLASH_ACR_PRFTEN;
    
    // 3. 开启指令缓存
    FLASH->ACR |= FLASH_ACR_ICEN;
    
    // 4. 开启数据缓存(如果有)
    FLASH->ACR |= FLASH_ACR_DCEN;
    
    // 5. 等待配置生效
    while((FLASH->ACR & FLASH_ACR_LATENCY_MASK) != FLASH_ACR_LATENCY_5WS);
}

这段代码在系统时钟配置之后、主函数之前执行。顺序很重要:先设等待周期,再开预取和缓存。

为什么?因为预取和缓存依赖正确的时序配置。如果时序不对,预取的数据可能是错的,缓存里存的也是脏数据。

小技巧:配置完成后,可以读一下Flash控制器的状态寄存器,确认配置是否生效。我习惯在调试输出里打印这些信息,方便排查问题。

4.6 性能对比:优化前后的差距

咱们用数据说话。假设主频168MHz,Flash读取速度约25MHz,CPU执行一个简单循环:

配置 循环执行时间 提升比例
默认(等待周期最大,无预取无缓存) 100% 基准
优化等待周期 75% 提升25%
开启预取 55% 提升45%
开启缓存 35% 提升65%
代码搬运到RAM 20% 提升80%

你看,每一步优化都有实实在在的收益。但要注意,这些数据是在特定场景下测的。实际项目中,提升幅度取决于你的代码特点。

我个人建议:先调等待周期,再开预取,再开缓存,最后考虑代码搬运。每一步都做性能测试,看看实际效果。别一股脑全开,万一出问题,排查起来很麻烦。

4.7 小结

Flash控制器配置,说白了就是让CPU少等、多命中、快执行。调等待周期是基础,预取解决顺序执行,缓存解决重复执行,代码搬运解决关键路径。

嗯,这一节的内容就到这里。下一节咱们聊聊中断向量表的配置——别小看它,配置不好,系统连中断都进不去。