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执行,这是终极加速方案。
具体怎么做?
- 标记关键函数:在代码里用属性或段名,把需要搬运的函数标记出来。
- 修改链接脚本:在链接脚本里,给这些函数分配一个RAM地址段。
- 启动时搬运:在固件启动流程里,把标记好的代码从Flash复制到RAM。
- 跳转执行: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少等、多命中、快执行。调等待周期是基础,预取解决顺序执行,缓存解决重复执行,代码搬运解决关键路径。
嗯,这一节的内容就到这里。下一节咱们聊聊中断向量表的配置——别小看它,配置不好,系统连中断都进不去。