4. 内存模型深度剖析:SRAM、Flash、DTCM、AXI SRAM分区策略、内存映射与链接脚本
做嵌入式AI,说白了就是跟内存打交道。模型跑不跑得动,往往不取决于算力,而是取决于内存够不够用。我见过太多人模型选好了,量化也做了,结果一部署就崩——内存爆了。
这一章,咱们就把STM32的内存模型彻底讲透。你搞懂了SRAM、Flash、DTCM、AXI SRAM这些区域怎么用,链接脚本怎么写,后面做模型移植才能游刃有余。
4.1 STM32的内存家族
先看一张总览图。STM32H7系列的内存资源最丰富,咱们就以它为例。其他系列原理类似,只是容量和区域数量不同。
核心要点:STM32的内存不是一块大平地,而是分成了多个物理区域,每个区域有各自的访问速度、总线接口和用途。选错了区域,性能直接腰斩。
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 验证分区是否生效
分区做完后,怎么确认真的生效了?我一般用两种方法:
- 查看.map文件。编译完成后,打开生成的.map文件,搜索你的段名。看地址是不是落在你期望的区域。比如
.dtcm_data的地址应该是0x20000000开头。 - 运行时打印地址。在代码里打印关键变量的地址,跟.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 是不是写错了。
小技巧:我习惯在链接脚本开头加一段注释,把每个区域的用途和容量写清楚。这样过几个月回头看,不用重新分析。团队协作时,别人也能一眼看懂你的分区意图。