3、SRAM与DRAM:静态RAM与动态RAM的工作原理,STM32的SRAM分区

说到单片机里的存储器,RAM 是绕不开的话题。很多初学者觉得 RAM 就是“内存”,能存变量就行。但实际项目中,RAM 选不对、分不好,程序跑飞、死机、数据丢失都是家常便饭。

这一章,我们就把 SRAM 和 DRAM 彻底讲透。然后结合 STM32 的 SRAM 分区,聊聊实际项目中堆栈和变量区到底怎么分配。

3.1 SRAM 与 DRAM:本质区别在哪?

先看一张对比表,心里有个底:

特性 SRAM(静态RAM) DRAM(动态RAM)
存储单元 6个晶体管(锁存器) 1个晶体管 + 1个电容
是否需要刷新 不需要 需要(约64ms刷新一次)
速度 快(10ns级) 较慢(50ns级)
功耗 静态功耗较高 刷新有额外功耗
集成度 低(同样面积容量小) 高(同样面积容量大)
成本 高 低
典型应用 CPU缓存、单片机片内RAM 电脑内存条、大容量存储

说白了,SRAM 靠锁存器“记住”数据,只要不断电,数据就一直在。DRAM 靠电容存电荷,电荷会漏,所以必须定期“刷新”——就像给快没电的电池充电。

为什么单片机里几乎都用 SRAM?因为单片机不需要大容量 RAM,但要求速度快、时序简单、不用操心刷新。你想想看,如果让 Cortex-M 内核每 64ms 去刷一次 DRAM,那实时性还怎么保证?

我的经验: 有一次做低功耗项目,我图便宜用了外挂 DRAM。结果发现每次从休眠唤醒后,DRAM 里的数据全丢了——因为休眠期间刷新停止了。从那以后,低功耗场景我坚决只用 SRAM 或备份 SRAM。

3.2 STM32 的 SRAM 分区:不止一块 RAM

STM32 的 SRAM 不是一整块。以 STM32H743 为例,它的 SRAM 被分成了好几个区域:

  • TCM RAM:紧耦合存储器,直接挂在 CPU 内核上,速度最快(零等待)。
  • SRAM1:主 RAM 区,通过总线矩阵访问,速度稍慢。
  • SRAM2:另一块独立 RAM,可以用于 DMA 或 USB 缓冲。
  • 备份 SRAM:在 Vbat 引脚供电下,主电源掉电后数据依然保留。

为什么要分这么多块?说白了,是为了解决“速度”和“冲突”的问题。CPU 要取指令、读写数据,DMA 要搬运数据,USB 要缓冲包——如果都挤在同一块 RAM 上,总线就堵死了。

下面这张图能帮你快速理解 STM32 的 SRAM 分区结构:

STM32H743 SRAM 分区结构图 Cortex-M7 内核 DTCM RAM (128KB) 数据紧耦合,零等待 ITCM RAM (64KB) 指令紧耦合,零等待 AXI 总线矩阵 SRAM1 (256KB) SRAM2 (256KB) 备份 SRAM (4KB) TCM 直接挂在 CPU 上,速度最快;SRAM1/2 通过总线矩阵访问;备份 SRAM 由 Vbat 独立供电

3.3 实际项目中如何分配堆栈与变量区

这块是实战重点。很多工程师把代码写好了,但一跑就死机,查到最后发现是堆栈溢出或者变量放错了位置。

3.3.1 堆栈放哪?

我个人习惯把主堆栈(MSP)放在 DTCM RAM 里。为什么?因为函数调用、中断嵌套、局部变量都在堆栈上操作,速度要求最高。DTCM 零等待,最适合干这个。

举个例子,在 STM32H7 的链接脚本里,可以这样指定:

/* 链接脚本片段 */
_estack = ORIGIN(DTCMRAM) + LENGTH(DTCMRAM);  /* 堆栈顶在 DTCM 末尾 */

/* 确保 .stack 段放在 DTCM */
.stack (NOLOAD) : {
  . = ALIGN(8);
  _sstack = .;
  . = . + 0x4000;  /* 16KB 堆栈 */
  _estack = .;
} >DTCMRAM
注意: 堆栈大小不是越大越好。我曾经见过有人给堆栈分配了 64KB,结果 DTCM 被占满,其他变量没地方放。一般 RTOS 任务栈 1-4KB 足够,主栈 4-8KB 也够了。具体看你的函数嵌套深度。

3.3.2 变量区怎么分?

变量区通常分三类:

  • .bss 段:未初始化的全局变量、静态变量。放在 SRAM1 或 SRAM2 都行。
  • .data 段:已初始化的全局变量、静态变量。同样放 SRAM1/2。
  • 堆区(Heap):malloc 动态分配的内存。建议放 SRAM2,避免和堆栈争抢同一块空间。

我一般这样分配:

区域 用途 推荐放置 说明
DTCM RAM 主堆栈、RTOS 内核变量 速度敏感型数据 零等待,适合频繁读写
ITCM RAM 关键中断函数、实时性要求高的代码 指令紧耦合 通过 ICACHE 加速取指
SRAM1 .bss、.data、大数组 普通全局变量 容量大,速度尚可
SRAM2 堆区、DMA 缓冲、USB FIFO DMA 和动态分配 避免与 CPU 争抢总线
备份 SRAM 掉电保持数据、RTC 参数 关键配置参数 主电源掉电后数据不丢

3.3.3 一个实际案例

我之前做一款工业控制器,要求掉电后保存最后 100 条日志。当时我直接把日志数组放在了备份 SRAM 里:

/* 使用 __attribute__ 指定段 */
#define BACKUP_RAM __attribute__((section(".backup_ram")))

BACKUP_RAM uint8_t log_buffer[1024];  /* 1KB 日志缓冲区 */
BACKUP_RAM uint32_t log_index = 0;

/* 写日志函数 */
void log_write(uint8_t *data, uint32_t len) {
  /* 注意:备份 SRAM 写入速度较慢,不要频繁操作 */
  memcpy(&log_buffer[log_index], data, len);
  log_index = (log_index + len) % 1024;
}

同时,在链接脚本里要确保 .backup_ram 段落在备份 SRAM 的地址范围:

.backup_ram (NOLOAD) : {
  . = ALIGN(4);
  *(.backup_ram)
  . = ALIGN(4);
} >BKPSRAM
关键点: 备份 SRAM 的写入速度比普通 SRAM 慢,而且有写入次数限制(虽然寿命很长)。所以只放“掉电后必须保留”的数据,别把频繁更新的变量也塞进去。

3.4 避坑指南

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

  • 堆栈溢出检测:一定要在堆栈末尾放一个“哨兵值”,比如 0xDEADBEEF。程序跑一段时间后检查这个值还在不在。我吃过亏,堆栈溢出后程序表现诡异,查了三天才发现。
  • DMA 缓冲对齐:如果 DMA 缓冲放在 SRAM2,注意地址要 32 字节对齐。否则 DMA 传输会出错。
  • 备份 SRAM 的初始化:备份 SRAM 上电后内容随机。第一次使用前必须手动清零,否则读到的是乱码。
  • 不要把所有变量都放 TCM:TCM 容量有限,而且不支持 DMA 访问。把大数组放 TCM 纯属浪费。

嗯,这一章的内容就到这里。SRAM 和 DRAM 的原理是基础,STM32 的分区是工具,怎么分配堆栈和变量区才是真功夫。希望你在下一个项目里,能把这些知识用上。