引言

STM32H7 系列(如 STM32H743/750)最高运行在 480MHz,但片内 Flash 的访问速度远低于 CPU 频率。为了弥补差距,ST 引入了 ART(Adaptive Real-Time Accelerator)缓存和预取机制。然而,很多开发者误以为“零等待”是绝对的,实际却存在缓存未命中、预取失效等代价。本文将从硬件原理出发,分析真实性能损耗,并给出可落地的优化方案。

一、Flash 访问延迟的硬件真相

1.1 主频与 Flash 速度的鸿沟

  • STM32H7 的 Flash 接口最高支持约 120MHz 的读取频率(取决于电压和等待状态)。
  • 在 480MHz 下,CPU 需要插入至少 3 个等待周期(WS=3)才能完成一次 Flash 读取。
  • 这意味着:如果每次取指都直接访问 Flash,CPU 将浪费约 75% 的周期在等待上。

1.2 ART 缓存:核心加速机制

  • ART 是一个 128 位的缓存行(cache line)预取引擎,位于 CPU 和 Flash 之间。
  • 它支持两种模式:
    • 预取(Prefetch):顺序取指时,提前将后续指令读入缓存。
    • 缓存(Cache):将已访问的指令/数据保存在 SRAM 中,供后续快速访问。
  • 关键参数:缓存大小(通常 8KB 或 16KB,取决于型号),以及缓存行大小(128 位 = 16 字节)。

1.3 零等待的“真实”含义

  • 当指令命中 ART 缓存时,CPU 可以零等待访问。
  • 但缓存未命中时,仍需等待 Flash 读取(至少 3 个周期),且预取可能失效。
  • 因此,实际性能取决于缓存命中率,而非绝对零等待。

二、缓存命中率的影响因素

2.1 代码布局

  • 顺序执行:预取机制对顺序代码友好,命中率极高。
  • 分支跳转:if/else、循环、函数调用等会打断预取流,导致缓存未命中。
  • 数据访问:常量数组、查找表等数据访问同样占用缓存,与指令争抢空间。

2.2 缓存冲突

  • ART 缓存采用直接映射(direct-mapped)或组相联(set-associative)结构(具体取决于型号)。
  • 如果两个热代码段映射到同一缓存行,会互相驱逐,导致频繁未命中。

2.3 编译优化

  • 编译器默认优化可能忽略缓存友好性,例如过度内联导致代码膨胀。
  • 未使用 -falign-functions 等选项,函数边界不对齐,可能增加缓存行占用。

三、量化实际代价

3.1 测试方法

  • 使用 DWT 计数器(CYCCNT)测量指令执行周期。
  • 对比:代码在 Flash 中运行 vs 在 RAM 中运行(将关键函数拷贝到 DTCM)。

3.2 典型结果

  • 对于顺序执行的简单循环,命中率高,性能接近零等待。
  • 对于复杂分支或随机跳转,性能可能下降 20%-50%。
  • 数据密集型应用(如查表)可能因缓存争用而进一步恶化。

四、优化策略实战

4.1 代码布局优化

  • 将热函数放入 ITCM:ITCM(指令紧耦合内存)是零等待 SRAM,直接连接 CPU,无缓存开销。
  • 使用 __attribute__((section(".itcm"))) 将关键函数放入 ITCM。
  • 避免过大的循环体:保持循环体在 16 字节对齐内,减少缓存行跨越。

4.2 链接脚本调整

  • 将频繁调用的函数放在连续地址,减少缓存冲突。
  • 使用 --function-sections--gc-sections 配合,按需放置。

4.3 编译选项

  • 开启 -falign-functions=4(或 8),使函数入口对齐到缓存行边界。
  • 使用 -fno-prefetch-loop-arrays 避免预取数据数组干扰指令缓存。
  • 对于关键循环,使用 #pragma GCC optimize("O3") 但注意代码膨胀。

4.4 数据缓存策略

  • 将只读数据放入 D1 域(如 AXI SRAM),并开启 D-Cache(如果支持)。
  • 避免在中断处理函数中访问大数组,减少缓存抖动。

五、完整代码示例

以下示例演示如何将关键函数放入 ITCM,并测量执行周期。

// main.c
#include "stm32h7xx.h"
#include <stdio.h>

// 定义 DWT 计数器访问宏
#define DWT_CYCCNT  (*(volatile uint32_t *)0xE0001004)
#define DWT_CTRL    (*(volatile uint32_t *)0xE0001000)
#define DWT_LAR     (*(volatile uint32_t *)0xE0001FB0)

// 启用 DWT
void DWT_Init(void) {
    DWT_LAR = 0xC5ACCE55; // 解锁
    DWT_CTRL |= (1 << 0);  // 使能 CYCCNT
}

// 关键函数:放入 ITCM 段
__attribute__((section(".itcm"))) int critical_func(int a, int b) {
    int sum = 0;
    for (int i = 0; i < 1000; i++) {
        sum += a * b + i;
    }
    return sum;
}

// 普通函数:留在 Flash
int normal_func(int a, int b) {
    int sum = 0;
    for (int i = 0; i < 1000; i++) {
        sum += a * b + i;
    }
    return sum;
}

int main(void) {
    DWT_Init();
    uint32_t t1, t2;
    volatile int result;

    // 测量普通函数
    t1 = DWT_CYCCNT;
    result = normal_func(3, 4);
    t2 = DWT_CYCCNT;
    printf("Normal func cycles: %lu\n", t2 - t1);

    // 测量 ITCM 函数
    t1 = DWT_CYCCNT;
    result = critical_func(3, 4);
    t2 = DWT_CYCCNT;
    printf("ITCM func cycles: %lu\n", t2 - t1);

    while (1);
}

链接脚本片段(.icf 或 .ld):

// 对于 IAR(.icf)
place in ITCMRAM { section .itcm };

// 对于 GCC(.ld)
.itcm : {
    *(.itcm*)
} > ITCMRAM

六、注意事项

  • ITCM 空间有限:通常 128KB,仅放置最关键的代码。
  • 缓存一致性:如果同时使用 D-Cache,注意数据一致性(如 DMA 操作)。
  • 中断处理:中断函数应避免复杂逻辑,或放入 ITCM 以保证实时性。
  • 调试时:使用 -O0 会大幅降低性能,测试时应使用 -O2-O3
  • 不同型号差异:H7 系列有 H743、H750、H723 等,缓存大小和 ITCM 容量不同,需查阅参考手册。

七、总结

STM32H7 的 480MHz 性能并非免费午餐,Flash 零等待执行依赖 ART 缓存的高命中率。通过理解缓存机制,合理布局代码,利用 ITCM 和编译优化,可以显著提升实际执行效率。建议开发者使用 DWT 计数器进行性能剖析,针对热点函数进行专项优化,才能发挥这颗芯片的真正实力。