1. 存储器概述:单片机存储体系分层
做单片机开发这么多年,我见过不少新手上来就对着数据手册里的存储器容量发懵——RAM 多大、Flash 多大、EEPROM 又是什么?其实搞懂存储体系,是选型的第一步,也是避坑的关键。
今天咱们就把这层窗户纸捅破。
1.1 存储体系为什么需要分层?
你想想看,一个单片机既要跑得快,又要存得多,还得便宜。这本身就是个矛盾。速度快的存储器(比如寄存器)贵得离谱,容量大的存储器(比如 Flash)又慢得不行。怎么办?
分层。把不同速度、不同成本的存储器组合起来,形成一个金字塔结构。
存储金字塔(从顶到底):
- 寄存器 — 最快,但容量极小(几十字节),CPU 直接操作
- Cache(缓存) — 高速 SRAM,用于暂存常用指令/数据(部分高端 MCU 才有)
- RAM(SRAM) — 主存,存放变量、堆栈,掉电丢失
- Flash — 非易失,存程序代码和常量,可擦写
- EEPROM — 非易失,按字节擦写,存配置参数
说白了,这就是用钱换速度,用速度换容量。我在一个工业控制项目里就吃过亏——选了片只有 2KB RAM 的 MCU,结果一个环形缓冲区就把内存撑爆了。嗯,从那以后我选型第一件事就是算 RAM 余量。
1.2 RAM / ROM / Flash / Cache 到底有什么区别?
很多教科书把 ROM 和 Flash 混为一谈,其实在单片机里,它们分工很明确。
| 类型 | 特性 | 典型用途 | 掉电后 |
|---|---|---|---|
| RAM(SRAM) | 读写快,按字节访问,容量小 | 变量、堆栈、堆 | 丢失 |
| ROM(Mask ROM) | 只读,出厂固化,不可改 | 量产固件(极少用了) | 保留 |
| Flash | 非易失,按扇区擦写,寿命 1 万~10 万次 | 程序代码、常量表 | 保留 |
| EEPROM | 非易失,按字节擦写,寿命 100 万次 | 校准参数、设备配置 | 保留 |
| Cache | 高速 SRAM,由硬件管理 | 加速 CPU 取指/取数 | 丢失 |
我的经验: 别把 Flash 当 EEPROM 用。Flash 擦写一次要几毫秒,而且按扇区擦(通常 1KB~4KB)。如果你要频繁修改几个字节的参数,老老实实用 EEPROM 或者模拟 EEPROM(在 Flash 里做磨损均衡)。
1.3 哈佛结构与冯诺依曼结构对比
这是计算机体系结构里最经典的一对「双胞胎」。搞单片机不懂这个,你连程序怎么跑的都说不清。
- 冯诺依曼结构(普林斯顿结构): 指令和数据共用一条总线,共用一个存储空间。典型代表:x86、ARM Cortex-M3 之前的某些内核。
- 哈佛结构: 指令总线和数据总线分开,程序存储器和数据存储器物理独立。典型代表:8051、AVR、PIC、ARM Cortex-M3/M4。
为什么哈佛结构在单片机里更流行?因为可以同时取指令和读写数据,效率更高。我当年用 8051 做项目时,就靠这个特性在单周期内完成了「读指令 + 读数据」的操作,比同频的冯诺依曼结构快了一倍。
注意: 现代很多 MCU 用的是「改进型哈佛结构」——内部总线分离,但外部看是统一编址。比如 STM32,虽然指令和数据走不同总线,但你可以用指针读 Flash 里的数据。别被数据手册的「哈佛」两个字骗了,实际用起来灵活得多。
我画了一张对比图,帮你理清思路:
1.4 存储器的选型原则
选存储器不是看哪个参数大就选哪个。我总结了一套「三步走」原则,这些年用下来没出过大错。
- 算容量: 先估算你的代码体积(.text + .rodata)和 RAM 需求(全局变量 + 堆栈 + 堆)。留 20%~30% 余量。我曾经因为堆栈只留了 128 字节,结果函数嵌套一深就死机——血的教训。
- 看速度: 如果你的算法有大量查表或实时计算,Flash 读取速度可能成为瓶颈。这时候要么选带 Cache 的 MCU,要么把关键数据搬到 RAM 里跑。
- 考虑寿命: 频繁擦写的参数别放 Flash,用 EEPROM 或外挂 FRAM。FRAM 虽然贵,但写次数近乎无限,而且不用等擦除时间。
选型速查表:
| 应用场景 | 推荐存储器 | 理由 |
|---|---|---|
| 程序代码 | Flash | 容量大、非易失、成本低 |
| 运行变量 | SRAM | 速度快、按字节访问 |
| 配置参数(频繁改) | EEPROM / FRAM | 按字节擦写、寿命长 |
| 大数据缓存(如 LCD 显存) | PSRAM / SDRAM | 容量大、成本低(需外挂) |
| 高速取指(如 DSP 算法) | Cache + Flash | 兼顾速度和容量 |
一个小技巧: 选型时别只看数据手册的「最大容量」。实际可用的 RAM 往往比标称少——因为启动代码、中断向量表、堆栈都会占用一部分。我习惯在工程里加一段内存检测代码,上电时打印出实际可用 RAM 大小,心里才有底。