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,那实时性还怎么保证?
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 分区结构:
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
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
3.4 避坑指南
最后分享几个我踩过的坑:
- 堆栈溢出检测:一定要在堆栈末尾放一个“哨兵值”,比如 0xDEADBEEF。程序跑一段时间后检查这个值还在不在。我吃过亏,堆栈溢出后程序表现诡异,查了三天才发现。
- DMA 缓冲对齐:如果 DMA 缓冲放在 SRAM2,注意地址要 32 字节对齐。否则 DMA 传输会出错。
- 备份 SRAM 的初始化:备份 SRAM 上电后内容随机。第一次使用前必须手动清零,否则读到的是乱码。
- 不要把所有变量都放 TCM:TCM 容量有限,而且不支持 DMA 访问。把大数组放 TCM 纯属浪费。
嗯,这一章的内容就到这里。SRAM 和 DRAM 的原理是基础,STM32 的分区是工具,怎么分配堆栈和变量区才是真功夫。希望你在下一个项目里,能把这些知识用上。