第2章:内存架构分析——STM32内部SRAM与外部SDRAM的分配策略

做TouchGFX双缓冲,说白了就是跟内存打交道。你想想看,一个1024×600的16位色帧缓冲区,光一帧就要占1.2MB。STM32内部SRAM通常只有几百KB,根本塞不下。所以外部SDRAM几乎是标配。

但问题来了——SDRAM和内部SRAM速度差多少?帧缓冲区放哪里最合适?对齐要求到底多严格?这些坑,我早年做第一个TouchGFX项目时全踩过。今天咱们一条一条捋清楚。

2.1 STM32的内存家族

先看一张总览图,把STM32内部SRAM和外部SDRAM的层级关系搞清楚。

STM32双缓冲内存架构总览 内部SRAM DTCM / ITCM / SRAM1/2/3 容量:192KB ~ 1MB 速度:CPU时钟频率访问 外部SDRAM FMC接口挂载 容量:8MB ~ 64MB 速度:通常 90~167MHz FMC总线桥接 帧缓冲区部署策略 双缓冲全部放在外部SDRAM(容量充足) 或:一个缓冲在内部SRAM,一个在SDRAM(速度均衡) 对齐要求:帧缓冲区首地址必须 32字节对齐 LTDC/DMA2D硬件要求,否则显示花屏或性能下降

2.2 内部SRAM:快但小

STM32内部SRAM分好几块。以H743为例,有DTCM(128KB)、ITCM(64KB)、SRAM1(128KB)、SRAM2(128KB)、SRAM3(32KB),总共480KB。看着不少,但扣掉系统堆栈、RTOS任务栈、中间件缓冲区,能剩给UI的也就一两百KB。

内部SRAM的优势:CPU和DMA2D可以直接全速访问,零等待。我在一个项目里把部分频繁更新的小图标缓存放在DTCM,刷新速度比SDRAM快了将近一倍。

内部SRAM的劣势:容量太小。一个1024×600的16位色帧缓冲区就要1.2MB,内部SRAM根本放不下。所以双缓冲方案里,至少有一个缓冲必须放在外部SDRAM。

我的经验:内部SRAM适合放「热数据」——比如字体缓存、频繁更新的小控件纹理、或者作为DMA2D的临时渲染目标。别把大块帧缓冲区往里塞,那是自找麻烦。

2.3 外部SDRAM:大但慢

外部SDRAM通过FMC接口挂载,容量从8MB到64MB不等。对于TouchGFX来说,SDRAM是帧缓冲区的主战场。

但SDRAM有个硬伤——访问延迟高。内部SRAM一个时钟周期就能读完的数据,SDRAM可能要等好几个周期。再加上FMC总线的时钟频率通常只有CPU主频的几分之一,差距就更明显了。

对比项 内部SRAM 外部SDRAM
典型容量 192KB ~ 1MB 8MB ~ 64MB
访问速度 CPU时钟频率(如400MHz) FMC时钟频率(如100MHz)
延迟 1~2个时钟周期 5~15个时钟周期
适合存放 堆栈、热数据、小缓存 帧缓冲区、大纹理、资源池
对齐要求 4字节对齐(ARM推荐) 32字节对齐(LTDC/DMA2D要求)
注意:我曾经在一个项目里把两个帧缓冲区都放在SDRAM,结果发现DMA2D从SDRAM读取源数据时,因为SDRAM的page miss导致带宽骤降,帧率从60fps掉到35fps。后来把其中一个缓冲挪到内部SRAM,问题才解决。

2.4 帧缓冲区的对齐要求

这是最容易踩坑的地方。TouchGFX的帧缓冲区,或者说LTDC和DMA2D使用的缓冲区,有严格的对齐要求。

为什么需要对齐?LTDC从内存读取像素数据时,是以32字节(256位)为单位的突发传输。如果缓冲区首地址不是32字节对齐的,硬件会做额外的对齐操作,轻则性能下降,重则直接花屏。

我见过一个案例:工程师把帧缓冲区定义在SDRAM的某个偏移地址,没注意对齐,结果屏幕上半部分正常,下半部分全是乱码。查了两天才发现是地址没对齐。

对齐规则:帧缓冲区首地址必须满足 addr % 32 == 0。对于双缓冲方案,两个缓冲区的首地址都要各自32字节对齐。

在代码里怎么保证?看下面这个例子:

// 正确做法:使用 __attribute__((aligned(32))) 强制对齐
static uint16_t framebuffer1[1024 * 600] __attribute__((aligned(32)));
static uint16_t framebuffer2[1024 * 600] __attribute__((aligned(32)));

// 或者用内存池分配,手动对齐
#define ALIGN_32(x)  (((x) + 31) & ~31)

uint32_t buffer_addr = (uint32_t)malloc(size1 + size2 + 32);
uint32_t fb1_addr = ALIGN_32(buffer_addr);
uint32_t fb2_addr = ALIGN_32(fb1_addr + size1);

2.5 分配策略:怎么分最合理?

根据我多年的项目经验,总结出三种主流分配策略:

  1. 双缓冲全放SDRAM:最简单,容量管够。适合分辨率不高(如480×272)或SDRAM带宽充裕的场景。
  2. 一内一外:一个缓冲在内部SRAM,一个在外部SDRAM。DMA2D渲染时从SDRAM读数据,写到内部SRAM;LTDC从内部SRAM读取显示。性能最优,但内部SRAM要够大。
  3. 三缓冲混合:两个渲染缓冲在SDRAM,一个显示缓冲在内部SRAM。适合高帧率动画场景,但实现复杂。
策略 内部SRAM占用 SDRAM占用 性能 适用场景
双缓冲全SDRAM 0 2×帧大小 中等 低分辨率、简单UI
一内一外 1×帧大小 1×帧大小 高 中高分辨率、流畅动画
三缓冲混合 1×帧大小 2×帧大小 最高 高分辨率、60fps动画
我的建议:如果你的STM32内部SRAM有512KB以上,优先选「一内一外」策略。把显示缓冲放在内部SRAM,渲染缓冲放在SDRAM。这样LTDC读取显示数据时零等待,DMA2D渲染时虽然要访问SDRAM,但因为是后台操作,不影响显示帧率。

2.6 实际项目中的避坑指南

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

  • SDRAM初始化顺序:FMC和SDRAM的初始化必须在TouchGFX启动之前完成。我见过有人把SDRAM初始化放在HAL_Init之后,结果LTDC一启动就访问未初始化的SDRAM,直接HardFault。
  • Cache一致性:如果开启了CPU Cache(D-Cache),SDRAM区域必须配置为不可缓存或使用MPU设置成强序内存。否则DMA2D写完了数据,CPU读到的还是Cache里的旧数据,画面会撕裂。
  • SDRAM刷新周期:SDRAM需要定时刷新,刷新期间不能访问。如果刷新周期配置不当,会导致帧缓冲区访问出现周期性延迟,表现为画面偶尔卡顿。
我曾经踩过的坑:一个项目里用了外部SDRAM做帧缓冲区,但忘了配置MPU。结果DMA2D渲染完一帧,LTDC读到的还是Cache里的旧数据,画面一直不更新。查了三天,最后发现是Cache一致性没处理好。从那以后,我每次初始化SDRAM后第一件事就是配MPU。