3. Flash读取操作:读取时序、对齐访问、缓存与预取机制
好,咱们今天聊聊Flash读取。说实话,很多工程师写Flash程序时,注意力全放在擦写上了。读取?不就是读个地址嘛,有什么好讲的?
嗯,我以前也这么想。直到有一次,我在一个电机控制项目里,发现程序明明跑在RAM里,但关键数据从Flash读取时,总会出现微秒级的抖动。排查了三天,最后发现是Flash读取的预取机制和我的中断服务程序“打架”了。
所以,读取操作看似简单,但里面的门道,真不少。
3.1 读取时序:不是你想读就能读
S32K3的Flash读取,不是像SRAM那样“地址一给,数据就来”。它有自己的时序要求。说白了,就是需要等待几个时钟周期。
核心概念:等待状态(Wait State)
Flash读取需要等待状态。你频率跑得越高,需要的等待周期就越多。比如你跑80MHz,可能需要0个等待周期;跑到160MHz,可能就需要2个等待周期了。
我个人习惯,在系统时钟初始化完成后,第一件事就是检查并配置好Flash的读取时序。别等到跑起来再调,那时候出问题很难定位。
| 系统频率 (MHz) | 等待状态 (WS) | 备注 |
|---|---|---|
| ≤ 80 | 0 | 零等待,性能最佳 |
| 80 ~ 120 | 1 | 需要1个等待周期 |
| 120 ~ 160 | 2 | 性能开始下降 |
| > 160 | 3 | 建议考虑代码在RAM中运行 |
你想想看,如果配置错了会怎样?轻则程序跑飞,重则偶尔读错数据。我在项目中遇到过一位同事,他死活找不到程序偶尔崩溃的原因。最后发现,他把等待状态少配了一个周期。频率高了,Flash跟不上,数据读出来是错的。
注意:等待状态配置必须在Flash访问之前完成。一旦开始从Flash取指令,再修改等待状态,可能会导致不可预知的行为。
3.2 对齐访问:地址不对,性能白费
Flash读取有个“脾气”——它喜欢对齐。什么意思呢?S32K3的Flash总线宽度是128位(16字节)。也就是说,它一次读取,最少也是16个字节。
如果你只读一个字节,它也会把整个16字节都读出来,然后从中提取你要的那个字节。这本身没问题,但如果你读的地址不是16字节对齐的,那就麻烦了。
举个例子:
// 对齐访问(推荐)
uint8_t data1 = *(uint8_t*)(0x00400000); // 地址是16字节对齐的
// 非对齐访问(不推荐)
uint8_t data2 = *(uint8_t*)(0x00400001); // 地址不是16字节对齐的
为什么说非对齐访问不好?因为Flash控制器需要额外做一次“拼接”操作。它会读取两个16字节的块,然后把中间那一个字节取出来。性能直接打折扣。
我记得有一次做Bootloader,需要从Flash读取配置信息。我图省事,把配置结构体定义成了紧凑模式(packed),结果每次读取都多花了好几个微秒。后来改成对齐模式,速度一下就上来了。
我的建议:在定义存储在Flash中的数据结构时,尽量使用自然对齐。如果必须使用非对齐结构,可以考虑先拷贝到RAM中再访问。
3.3 缓存机制:给Flash加点“加速器”
S32K3内部有一个Flash缓存,叫Cache。它的作用很简单:把最近读过的数据存一份。下次再读同样的地址,直接从Cache拿,不用再去慢吞吞的Flash里取了。
这个Cache通常有几种模式:
- 直写模式(Write-Through):读的时候查Cache,写的时候直接写Flash。简单,但性能一般。
- 回写模式(Write-Back):读和写都先操作Cache,等Cache满了或者需要的时候,再写回Flash。性能好,但实现复杂。
- 禁用模式:不用Cache,每次都读Flash。适合对实时性要求极高、且代码执行路径固定的场景。
我个人习惯,在大多数应用场景下,开启Cache。但有一个例外——Bootloader。在升级固件时,我通常会暂时禁用Cache,因为升级过程中Flash内容在变化,Cache里的数据可能是“脏”的。
关键点:Cache不是万能的。如果你的代码是顺序执行的,比如一个很大的循环,Cache的命中率会很高。但如果你的代码是随机跳转的,比如大量使用函数指针或虚函数,Cache的效果就会大打折扣。
3.4 预取机制:提前帮你准备好
预取(Prefetch)是另一个加速手段。它的思路很简单:当你读地址A的时候,Flash控制器猜测你接下来可能会读地址A+1、A+2...于是它提前把这些数据读出来,放在一个缓冲区里。
等你真的去读A+1时,数据已经在缓冲区里了,不需要再等等待状态。这就是所谓的“零等待”效果。
但预取也有副作用。我曾经遇到过一个坑:
我在一个中断服务程序里,读取Flash中的查表数据。中断触发的时间点很不固定。有时候,预取机制已经把数据准备好了,中断执行很快;有时候,预取没命中,中断执行就慢了。这就导致了中断响应时间的抖动。
后来怎么解决的?我把那个查表数据搬到了RAM里。中断里只做最核心的操作,不依赖Flash的预取。
避坑指南:我曾经在调试一个CAN通信程序时,发现报文偶尔会丢帧。查了很久,发现是Flash预取导致的。预取操作会占用Flash总线的带宽,如果此时DMA也在读Flash,两者就会冲突。解决办法是:在DMA传输期间,暂时降低预取的优先级,或者干脆禁用预取。
3.5 实战建议:如何配置Flash读取
好了,理论讲完了。咱们来点实际的。在S32K3上,配置Flash读取通常需要做这几步:
- 确定系统频率:先跑起来,知道你的CPU主频是多少。
- 配置等待状态:根据频率查表,设置正确的WS值。
- 使能Cache:默认一般是开启的,但建议显式配置一下。
- 配置预取:根据你的代码特性,决定是否开启预取。
- 验证:跑一个简单的测试程序,确认读取速度和正确性。
下面是一个简单的配置示例(伪代码):
void Flash_Read_Init(void)
{
uint32_t sys_clk = GetSystemClock(); // 获取系统时钟
// 1. 根据频率设置等待状态
if (sys_clk <= 80000000) {
FLASH->WSCN = 0; // 0等待
} else if (sys_clk <= 120000000) {
FLASH->WSCN = 1; // 1等待
} else {
FLASH->WSCN = 2; // 2等待
}
// 2. 使能Cache
FLASH->CACHE_CTRL |= FLASH_CACHE_CTRL_EN_MASK;
// 3. 使能预取(如果代码是顺序执行的)
FLASH->PFCTRL |= FLASH_PFCTRL_EN_MASK;
// 4. 等待配置生效
while (FLASH->STATUS & FLASH_STATUS_BUSY_MASK);
}
嗯,代码很简单。但实际项目中,我建议你在配置前后都加一些调试输出,确认配置是否真的生效了。有时候,芯片的复位状态会影响这些寄存器的默认值。
小技巧:如果你不确定当前Flash的读取配置是否最优,可以写一个简单的性能测试函数。循环读取同一个Flash地址,测量耗时。然后调整等待状态、Cache和预取配置,对比性能变化。这是最直观的方法。
最后总结一下:Flash读取不是简单的“读地址”。等待状态、对齐访问、Cache和预取,这四个要素共同决定了你的程序能不能跑得快、跑得稳。下次遇到程序性能问题,不妨先检查一下Flash的读取配置。