4、Flash读取加速:Flash预取缓冲、等待周期配置、零等待执行技巧

说到GD32V的性能优化,Flash读取往往是第一个瓶颈。我见过不少工程师,CPU频率配得很高,但实际跑起来总觉得差口气。查到最后,问题多半出在Flash读取上。

说白了,CPU跑得再快,指令喂不进去也是白搭。今天我们就聊聊怎么让Flash读取不再拖后腿。

4.1 为什么Flash会成为瓶颈?

GD32V的内核最高能跑到200MHz以上,但Flash本身的速度远没这么快。常规嵌入式Flash的读取速度通常在30-50MHz左右。你想想看,CPU要读指令,Flash却跟不上,那CPU只能干等着。

这个等待的时间,就是所谓的等待周期(Wait State)。

CPU频率范围 所需等待周期 实际指令吞吐率
0 - 48 MHz 0 WS(零等待) 100%
48 - 96 MHz 1 WS 50% - 100%
96 - 144 MHz 2 WS 33% - 100%
144 - 200 MHz 3 WS 25% - 100%

看到没?频率越高,等待周期越多,实际吞吐率反而可能下降。这就是为什么有时候降频反而跑得更快——等待周期少了。

4.2 Flash预取缓冲:硬件帮你"猜"指令

为了解决等待周期的问题,GD32V内部集成了一个Flash预取缓冲器。它的工作原理很简单:

  • CPU当前正在执行地址A的指令
  • 预取器自动把地址A+1、A+2...的指令提前读出来
  • 放到一个小的SRAM缓冲区里
  • CPU下次取指时,直接从缓冲区拿,不用等Flash

我个人习惯,只要CPU频率超过48MHz,一定会把预取缓冲打开。这个操作几乎没有副作用,白捡的性能提升。

关键配置寄存器: Flash控制寄存器(FMC_CTL)的PRFTB位
操作: 置1使能预取缓冲,置0关闭
/* 使能Flash预取缓冲 */
FMC_CTL |= FMC_CTL_PRFTB;

/* 配置等待周期(以108MHz为例) */
FMC_CTL &= ~FMC_CTL_WS_MASK;
FMC_CTL |= FMC_CTL_WS_2;  /* 2个等待周期 */

4.3 等待周期配置:找到最佳平衡点

等待周期的配置不是越少越好,也不是越多越好。关键是要匹配你的实际工作频率。

我曾经在一个项目中,把等待周期设少了,结果程序跑飞了。查了半天才发现,Flash读取不稳定,偶尔读到错误数据。嗯,这里要注意:等待周期不足会导致数据损坏,而不是性能下降。

警告: 等待周期配置过少会导致Flash读取错误,程序可能随机崩溃。务必按照数据手册中的频率-等待周期对照表来配置。

配置等待周期的步骤:

  1. 确定系统时钟频率(例如:108MHz)
  2. 查数据手册,找到对应频率所需的等待周期数
  3. 修改FMC_CTL寄存器的WS位段
  4. 等待Flash就绪(检查FMC_STAT的BUSY位)
/* 完整的Flash初始化函数 */
void flash_init(uint32_t sys_clk_mhz)
{
    uint8_t ws;
    
    if (sys_clk_mhz <= 48)      ws = 0;
    else if (sys_clk_mhz <= 96) ws = 1;
    else if (sys_clk_mhz <= 144)ws = 2;
    else                        ws = 3;
    
    /* 配置等待周期 */
    FMC_CTL = (FMC_CTL & ~FMC_CTL_WS_MASK) | ws;
    
    /* 使能预取缓冲 */
    FMC_CTL |= FMC_CTL_PRFTB;
    
    /* 等待配置生效 */
    while (FMC_STAT & FMC_STAT_BUSY);
}

4.4 零等待执行技巧:把关键代码搬到SRAM

预取缓冲虽然好,但它不是万能的。遇到跳转密集的代码(比如中断服务函数、循环体),预取器经常猜错,还是要等Flash。

真正的零等待执行,是把代码放到SRAM里跑。GD32V的SRAM是零等待的,CPU可以全速读取。

技巧: 把中断服务函数、实时性要求高的算法、频繁调用的库函数放到SRAM中执行。我一般会预留4-8KB的SRAM作为代码运行区。

具体做法:

  • 在链接脚本中定义SRAM代码段
  • 用__attribute__((section(".sram_code")))标记函数
  • 启动时从Flash拷贝到SRAM
/* 链接脚本片段(.ld文件) */
.sram_code : 
{
    _sram_code_start = .;
    *(.sram_code)
    _sram_code_end = .;
} > SRAM AT> FLASH

/* C代码中标记函数 */
__attribute__((section(".sram_code")))
void critical_isr(void)
{
    /* 这里执行零等待的中断处理 */
}

4.5 知识结构总览

下面这张图把Flash读取加速的整个体系串起来了,你可以对照着看:

Flash读取加速知识体系 Flash读取速度 < CPU速度 预取缓冲 等待周期配置 SRAM零等待执行 实现方式 • 使能FMC_CTL.PRFTB位 • 硬件自动预取后续指令 实现方式 • 根据频率设置WS位段 • 查表匹配频率与等待周期 实现方式 • 链接脚本定义SRAM段 • 关键函数加section属性 效果:减少CPU等待周期,提升指令吞吐率 25% - 75% 注意:三种方案可同时使用 预取缓冲 + 等待周期配置 + SRAM执行 = 最佳性能

4.6 实战避坑指南

最后分享几个我踩过的坑:

我曾经犯过的错:
  • 在系统时钟切换前没有先配置好等待周期,导致Flash读取时序错乱
  • 预取缓冲使能后没有检查BUSY位就继续操作,配置没生效
  • 把大数组放到SRAM代码段里,结果SRAM溢出,程序直接挂掉

正确的操作顺序应该是:先配等待周期 → 再开预取缓冲 → 最后切系统时钟。这个顺序不能乱,否则容易出问题。

至于SRAM执行,我建议只放那些真正对实时性敏感的函数。别一股脑全塞进去,SRAM总共就那么点,还要给变量和堆栈留空间。