4. 内存模型深度剖析:SRAM、Flash、DTCM、AXI SRAM分区策略、内存映射与链接脚本

做嵌入式AI,说白了就是跟内存打交道。模型跑不跑得动,往往不取决于算力,而是取决于内存够不够用。我见过太多人模型选好了,量化也做了,结果一部署就崩——内存爆了。

这一章,咱们就把STM32的内存模型彻底讲透。你搞懂了SRAM、Flash、DTCM、AXI SRAM这些区域怎么用,链接脚本怎么写,后面做模型移植才能游刃有余。

4.1 STM32的内存家族

先看一张总览图。STM32H7系列的内存资源最丰富,咱们就以它为例。其他系列原理类似,只是容量和区域数量不同。

核心要点:STM32的内存不是一块大平地,而是分成了多个物理区域,每个区域有各自的访问速度、总线接口和用途。选错了区域,性能直接腰斩。

STM32H7 内存架构总览 Flash (ITCM / AXIM) 2MB,代码存储 + 常量数据 ITCM总线:128位,取指最快 SRAM1 + SRAM2 512KB,默认 .data/.bss 区域 D1域,CPU直接访问 DTCM (Data TCM) 128KB,紧耦合数据内存 CPU内核直接访问,零等待 AXI SRAM 512KB,D1域,高带宽 DMA + 外设访问首选 总线矩阵 (Bus Matrix) 多主多从架构,CPU/DMA/外设并行访问不同内存区域 DMA / LTDC / MDMA ETH / USB / SDMMC

4.2 各区域特性对比

我刚开始用H7的时候,也搞混过DTCM和AXI SRAM的区别。后来踩了坑才明白——它们虽然都是RAM,但脾气完全不同。

内存区域 容量 总线宽度 访问延迟 推荐用途
Flash (ITCM) 2MB 128位 1~3等待周期 代码执行、常量表、模型权重
Flash (AXIM) 2MB 64位 1~3等待周期 数据读取、DMA访问
DTCM 128KB 64位 0等待(内核时钟) 栈、关键变量、中间缓冲区
AXI SRAM 512KB 64位 1等待周期 大缓冲区、DMA传输、帧缓存
SRAM1+SRAM2 512KB 32位 1等待周期 默认数据区、堆、全局变量
SRAM3/SRAM4 384KB 32位 1等待周期 备份数据、低功耗保留

我的经验:DTCM只有128KB,但它是真正的零等待。我习惯把中断栈、OS内核变量、以及AI推理中最频繁访问的中间层缓冲区放在DTCM。别把大数组扔进去,128KB很快就满了。

4.3 分区策略:AI推理场景怎么分?

做图像识别模型移植,内存分区直接决定了推理帧率。我总结了一套经过实战检验的分区策略,你可以直接套用。

4.3.1 模型权重放哪里?

模型权重是只读的,而且体积最大。量化后的模型通常在几百KB到几MB之间。我的建议是:

  • 首选:Flash。权重直接放在Flash的常量区,不占RAM。访问速度虽然比RAM慢一点,但CPU有Cache,实际影响不大。
  • 次选:外部QSPI Flash。如果内部Flash不够用,用内存映射模式把外部Flash映射到0x90000000地址段。速度会再慢一些,但容量可以做到64MB以上。

注意:如果你把权重放在Flash里,一定要确认链接脚本把权重段放在了Flash的AXIM区域(0x08000000),而不是ITCM区域。ITCM区域虽然取指快,但数据访问反而受限。

4.3.2 中间缓冲区放哪里?

AI推理过程中,每一层的输入输出都需要缓冲区。这部分是内存消耗的大头。我的分区建议是:

缓冲区类型 推荐区域 原因
输入图像缓冲区 AXI SRAM DMA从摄像头搬运数据,AXI SRAM带宽高
模型中间层缓冲区 DTCM 零等待,CPU频繁读写
输出结果缓冲区 SRAM1 容量充足,后续处理方便
临时计算缓冲区 DTCM 或 SRAM2 按需分配,用完即释放

我做过一个MobileNetV2的移植,输入是224x224的RGB图像。我把输入缓冲区放在AXI SRAM,中间层缓冲区放在DTCM,结果推理帧率比全部放在SRAM1提升了将近30%。

4.4 内存映射与链接脚本实战

分区策略定好了,怎么落地?靠链接脚本。很多人不敢改链接脚本,其实没那么玄乎。说白了,链接脚本就是告诉编译器:哪段代码放哪个地址,哪段数据放哪个区域。

4.4.1 默认链接脚本长什么样?

STM32CubeIDE生成的默认链接脚本,通常把所有东西都塞在Flash和SRAM1里。对于AI应用,这远远不够。我们需要手动分区。

/* 默认链接脚本片段(简化版) */
MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 2M
  RAM   (xrw) : ORIGIN = 0x24000000, LENGTH = 512K
}

SECTIONS
{
  .text : { *(.text*) } > FLASH
  .data : { *(.data*) } > RAM AT> FLASH
  .bss  : { *(.bss*)  } > RAM
}

你看,这里只有两个区域。DTCM和AXI SRAM根本没用到。这就是为什么很多人明明有硬件资源,性能却上不去。

4.4.2 自定义分区链接脚本

下面是我实际项目中用过的分区脚本。我把内存分成了5个区域,每个区域用途明确。

/* 自定义分区链接脚本 - AI推理优化版 */
MEMORY
{
  FLASH_ITCM (rx)  : ORIGIN = 0x00200000, LENGTH = 64K   /* 关键代码段 */
  FLASH_AXIM (rx)  : ORIGIN = 0x08000000, LENGTH = 1984K /* 模型权重 + 常量 */
  DTCM       (xrw) : ORIGIN = 0x20000000, LENGTH = 128K  /* 零等待数据区 */
  AXI_SRAM   (xrw) : ORIGIN = 0x24000000, LENGTH = 512K  /* 大缓冲区 */
  SRAM1      (xrw) : ORIGIN = 0x30000000, LENGTH = 256K  /* 通用数据区 */
}

SECTIONS
{
  /* 中断向量表 + 启动代码放ITCM Flash */
  .isr_vector : { *(.isr_vector*) } > FLASH_ITCM
  .text       : { *(.text*) } > FLASH_ITCM

  /* 模型权重放AXIM Flash */
  .model_weights : { *(.model_weights*) } > FLASH_AXIM

  /* 关键数据放DTCM */
  .dtcm_data :
  {
    *(.dtcm_data*)
    *(.stack*)
  } > DTCM

  /* 大缓冲区放AXI SRAM */
  .axi_buffers (NOLOAD) :
  {
    *(.axi_buffers*)
  } > AXI_SRAM

  /* 默认数据放SRAM1 */
  .data : { *(.data*) } > SRAM1 AT> FLASH_AXIM
  .bss  : { *(.bss*)  } > SRAM1
}

关键点:注意看 .model_weights 段和 .dtcm_data 段。这些段不是标准段名,需要你在代码里用 __attribute__((section(".model_weights"))) 来指定。我习惯在头文件里定义几个宏,用起来方便。

4.4.3 代码里怎么用?

链接脚本写好了,代码里要配合。下面是我常用的做法:

/* memory_regions.h */
#define DTCM_DATA   __attribute__((section(".dtcm_data")))
#define AXI_BUFFER  __attribute__((section(".axi_buffers")))
#define MODEL_WEIGHTS __attribute__((section(".model_weights")))

/* 使用示例 */
DTCM_DATA float layer_buffer[1024];        // 中间层缓冲区 → DTCM
AXI_BUFFER uint8_t frame_buffer[640*480*3]; // 图像帧缓冲区 → AXI SRAM
MODEL_WEIGHTS const uint8_t model_data[] = { /* 量化后的权重 */ };

嗯,这里要注意一点:const 修饰的变量如果放在Flash里,默认就在 .rodata 段。如果你想让模型权重单独占一个段,必须用 MODEL_WEIGHTS 宏强制指定。我刚开始就忘了加,结果权重跟其他常量混在一起,想单独调整位置都调不了。

4.5 避坑指南

做内存分区,有几个坑我替你们踩过了。直接列出来:

  • DTCM不能放代码。DTCM是数据TCM,只能放数据。代码放进去会触发HardFault。ITCM才是放代码的。
  • AXI SRAM的Cache问题。AXI SRAM默认是Cacheable的。如果你用DMA往AXI SRAM写数据,CPU读的时候可能读到Cache里的旧数据。记得用 SCB_CleanInvalidateDCache() 刷Cache。
  • Flash等待周期。H7跑400MHz时,Flash需要配置等待周期。如果配错了,程序直接跑飞。在SystemClock_Config()里检查 FLASH_ACR 寄存器。
  • 链接脚本的NOLOAD。大缓冲区用 NOLOAD 关键字,这样启动时不会清零,节省启动时间。但要注意,里面的数据是随机的,用之前必须初始化。

我曾经踩过的坑:有一次我把模型权重放在Flash的ITCM区域(0x00200000),结果CPU从ITCM取指没问题,但DMA读权重数据时死活读不到。查了两天才发现,DMA只能访问AXIM区域的Flash(0x08000000)。从那以后,我所有只读数据都放在AXIM区域。

4.6 验证分区是否生效

分区做完后,怎么确认真的生效了?我一般用两种方法:

  1. 查看.map文件。编译完成后,打开生成的.map文件,搜索你的段名。看地址是不是落在你期望的区域。比如 .dtcm_data 的地址应该是 0x20000000 开头。
  2. 运行时打印地址。在代码里打印关键变量的地址,跟.map文件对照。我习惯在初始化时加一段调试输出:
printf("DTCM buffer addr: 0x%08X\n", (uint32_t)layer_buffer);
printf("AXI buffer addr:  0x%08X\n", (uint32_t)frame_buffer);
printf("Model weights addr:0x%08X\n", (uint32_t)model_data);

如果地址落在对应的区域里,说明分区成功了。如果地址不对,回去检查链接脚本里的 ORIGIN 和 LENGTH 是不是写错了。

小技巧:我习惯在链接脚本开头加一段注释,把每个区域的用途和容量写清楚。这样过几个月回头看,不用重新分析。团队协作时,别人也能一眼看懂你的分区意图。