理解存储器
理解存储器
引言
理解存储器,除了知道其类型和用途,更需要从 CPU 访问的视角出发,了解访问路径上经过的硬件层次、所需的时钟周期数以及是否需要总线仲裁。这直接决定了程序的执行效率和实时性。
1. CPU 访问存储器的核心视角
对于嵌入式工程师而言,理解存储器的核心不是记住各种存储器的物理特性,而是从 CPU/DMA 访问的角度思考:
- CPU/DMA 能不能访问:地址空间是否映射,MPU/MMU 配置
- 从哪里访问:访问路径经过哪些硬件层次
- 延迟是否稳定:访问时间是否可预测(确定性)
- 是否经过 Cache:Cache 命中/未命中的影响
- 是否需要仲裁:多主设备访问时的竞争
- 链接脚本如何放置:代码/数据最终落在哪个存储区域
- 启动阶段如何初始化:RAM 上电后的初始化流程
- 运行时是否会踩内存:Stack/Heap 溢出、数组越界等
1.1 寄存器(Register)
- 位置与特性:位于 CPU 内核内部,是速度最快的存储单元,与 ALU(算术逻辑单元)直接相连。
- 访问路径:CPU 内核直接访问,不经过任何总线。
- 时钟周期:通常为 1 个时钟周期(单周期访问)。在流水线架构中,访问甚至可以被"隐藏"在指令解码阶段。
- 总线仲裁:不需要。寄存器是 CPU 私有的资源。
- 容量:非常有限,由指令集架构(ISA)定义,例如 ARM Cortex-M 系列通常有 13 个通用寄存器(R0-R12)加上 SP、LR、PC 等特殊寄存器。
- 开发者视角:开发者通常不直接管理寄存器分配,现代编译器会自动完成优化。理解寄存器的存在有助于编写更高效的代码,但具体分配由编译器决定。
1.2 TCM / SPRAM / Local RAM:确定性延迟存储
TCM(Tightly Coupled Memory)和 SPRAM(Scratch Pad RAM)/Local RAM 都属于低延迟 SRAM,核心目标是提供确定性访问延迟,避免 Cache Miss 带来的性能抖动。具体访问方式和是否支持共享访问,需要参考芯片架构。
概念澄清:TCM vs SPRAM/Local RAM
- TCM:更靠近 CPU Core、接口更专用的 Scratchpad Memory,通常有独立的指令/数据总线直接连接 CPU 内核。
- SPRAM/Local RAM:更泛化的叫法,指由软件显式管理的高速本地 SRAM。具体是否等同于 TCM,需要查阅芯片手册。
- 共同点:都由软件管理、无 Cache Miss、延迟稳定可预测。
- 差异点:TCM 通常有更专用的物理连接和更低的延迟,但本质都是为实时性要求高的场景设计。
访问路径
CPU Core
│
├─ ITCM (专用指令总线,如存在)
└─ DTCM (专用数据总线,如存在)
│
└─ Local Bus / High-Speed Bus (SPRAM/Local RAM)
时钟周期
具体周期数取决于 CPU 架构、总线频率、Wait States 和芯片实现,不能脱离手册直接定论。典型范围:
- TCM:通常 1-3 个时钟周期,延迟极低且固定
- SPRAM/Local RAM:通常 2-5 个时钟周期,延迟稳定可预测
关键不是绝对速度最快,而是延迟确定性,不会因 Cache Miss 突然增加。
总线仲裁
- 如果是 CPU 私有 TCM/SPRAM,通常无系统总线仲裁
- 如果允许 DMA 或其他 Master 访问,则仍可能存在局部仲裁
- 多核系统中,每个核可能有自己的 TCM,核间访问可能需要仲裁
开发者视角:如何选择和使用
TCM 适合:对延迟极其敏感、必须保证执行时间的代码/数据
- 中断服务函数(ISR)
- 实时控制循环
- 高频调用函数
- 调度器关键路径
SPRAM/Local RAM 适合:需要稳定带宽、避免 Cache 一致性问题
- DSP 算法中间结果
- Audio Buffer
- Video Buffer
- DMA Buffer(特别是与 Cache 一致性相关的场景)
使用方式:
// TCM 示例
__attribute__((section(".itcm"))) void critical_isr(void);
__attribute__((section(".dtcm"))) uint32_t audio_buffer[1024];
// SPRAM 示例
__attribute__((section(".spram_data"))) int16_t fft_temp[512];
链接脚本配置:
MEMORY {
ITCM (rx) : ORIGIN = 0x00000000, LENGTH = 64K
DTCM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
SPRAM (rwx): ORIGIN = 0x30000000, LENGTH = 64K
}
SECTIONS {
.itcm : { *(.itcm) } > ITCM
.dtcm : { *(.dtcm) } > DTCM
.spram_data : { *(.spram_data) } > SPRAM
}
1.3 高速缓存(Cache)
- Cache 分类:
- I-Cache(Instruction Cache):缓存指令,通常只读,一致性管理相对简单。
- D-Cache(Data Cache):缓存数据,支持读写,需要复杂的一致性管理。
- 位置与特性:位于 CPU 内核和主存之间,由高速 SRAM 构成,自动缓存最近访问过的指令和数据。
- 访问路径:CPU → Cache 控制器 → (若命中)Cache → CPU;(若未命中)Cache 控制器 → 系统总线 → 主存 → 填充 Cache → CPU。
- 时钟周期:
- 命中(Hit):1-3 个时钟周期,速度极快。
- 未命中(Miss):需要访问主存,延迟可能高达 几十甚至上百个时钟周期。
- 总线仲裁:需要。当 Cache 未命中时,需要发起系统总线事务访问主存,此时可能与其他总线主设备(如 DMA、另一个 CPU 内核)竞争总线使用权。
- 关键概念:
- 缓存行(Cache Line):Cache 与主存交换数据的最小单位(通常 32/64 字节)。
- 局部性原理:时间局部性(最近访问的很可能再次访问)和空间局部性(访问一个地址,其邻近地址也可能被访问)是 Cache 有效的基础。
- 写策略:直写(Write-Through,同时写 Cache 和主存)和回写(Write-Back,只写 Cache,脏数据被替换时才写回主存)。
- Cache 一致性(重点):
-
DMA 与 Cache 一致性问题:当 DMA 访问 Cacheable 内存时,CPU 修改的数据可能仍在 D-Cache 中,DMA 看到的是内存中的旧数据。注意:DMA 一致性问题通常只涉及 D-Cache,I-Cache 通常由硬件自动管理。
-
解决方案:
// DMA 传输前:确保 CPU 写的数据已从 D-Cache 刷到内存 CleanDCache_by_Addr(buffer, size); // DMA 传输后:确保 CPU 能从内存读到 DMA 写入的新数据 InvalidateDCache_by_Addr(buffer, size); -
最佳实践:
- DMA Buffer 使用 Non-Cacheable 内存区域(通过 MPU/MMU 配置)。
- 或使用 TCM/SPRAM(通常标记为 Non-Cacheable)。
- 必须 Cacheable 时,严格管理 D-Cache 的 Clean/Invalidate 操作。
-
DMA Cache 操作原则:
- DMA 传输前:
- 如果目的地址是 Memory,DMA 之前,目的地址段要 Clean Cache。
- 如果源地址是 Memory,DMA 之前,源地址段要 Clean Cache。
- DMA 传输后:
- 如果目的地址是 Memory,DMA 结束后,目的地址段要 Invalidate Cache。
- Buffer 对齐要求:
- 传给 DMA 的 Buffer,首地址和大小都要 Cache Line 对齐。
- 传输的数据按 DMA 控制器要求对齐即可,但 Buffer 地址和大小必须 Cache Line 对齐。
- DMA 传输前:
-
Cache Line 对齐的重要性:
// DMA Buffer 对齐宏定义示例 #define __DMA_ALIGNED_ADDR_SIZE 32U // 地址对齐边界 #define __DMA_ALIGNED_LINE_SIZE 32U // Cache Line 大小 #define I2C_DMA_MIN_BUF_SIZE __DMA_ALIGNED_LINE_SIZE // 最小 Buffer 大小 // 向上对齐宏 #define round_boundary(value, boundary) \ ((-(typeof(value))((boundary) - 1))) #define round_up(value, boundary) \ ((((value) - 1) | round_boundary(value, boundary)) + 1) // 正确:Buffer 起始地址和长度都按 Cache Line 对齐 __attribute__((aligned(__DMA_ALIGNED_LINE_SIZE))) uint8_t dma_buffer[round_up(1024, __DMA_ALIGNED_LINE_SIZE)]; // DMA 操作前清理 Cache(确保数据从 Cache 刷到内存) CleanDCache_by_Addr(dma_buffer, sizeof(dma_buffer)); // DMA 操作后无效化 Cache(确保 CPU 从内存读取新数据) InvalidateDCache_by_Addr(dma_buffer, sizeof(dma_buffer));为什么需要对齐:
- 性能优化:对齐的地址可以充分利用 Cache Line,减少不必要的内存访问。
- 操作正确性:Cache Maintenance 操作要求地址和长度按 Cache Line 对齐。
- 避免数据不一致:非对齐操作可能只清理/无效化部分 Cache Line。
- 硬件要求:部分 DMA 控制器要求 Buffer 地址按特定边界对齐。
-
DMA 前未 Clean Cache 的典型错误场景:
假设目的地址里有一块数据在 Cache Line A 中,并且这个 Cache Line 是 Dirty 的。
开始 DMA 传输后,当前线程阻塞等待结束,切换到其他线程执行。
其他线程申请 Cache Line 时,Cache 算法碰巧选中 Cache Line A。
由于 Cache Line A 是 Dirty 的,Cache 算法先将 Cache Line A 的数据写入内存,
然后再将 Cache Line A 分配给线程。此时即出现 DMA 目的地址内数据被意外改写的错误。
-
1.4 内部 SRAM:运行时数据
访问路径
内部 SRAM 不一定经过 Cache,访问路径取决于 MPU/MMU 的 Memory Attribute 配置:
CPU
↓
Bus Matrix / Interconnect
↓
SRAM (Cacheable 或 Non-Cacheable)
是否经过 Cache 由内存属性决定,工程师需要通过 MPU/MMU 配置明确指定。
内存布局与关键段
内存布局与关键段
典型的 SRAM 内存布局示意图:
0x20000000 ┌─────────────────┐
│ .data │ ← 已初始化全局/静态变量,启动时从 Flash 拷贝
├─────────────────┤
│ .bss │ ← 未初始化全局/静态变量,启动时清零
├─────────────────┤
│ Heap │ ← 动态内存分配区域,向上增长
│ ↑ │
│ │ │
│ │ │
│ ↓ │
│ Stack │ ← 函数调用、局部变量、中断现场,向下增长
0x20020000 └─────────────────┘
关键理解:
- Heap 向上增长:
malloc()从低地址向高地址分配 - Stack 向下增长:函数调用时栈指针递减
- Heap 和 Stack 相向生长:两者相遇意味着内存耗尽
- .data 和 .bss 大小固定:编译链接时确定
1.5 Flash / XIP:代码与常量存储
访问延迟与 Wait States
Flash 访问速度通常低于 CPU 主频,需要配置正确的 Wait States:
CPU 168MHz → Flash 可能需要 5 Wait States
配置错误可能导致系统无法启动或运行不稳定。
XIP(Execute In Place)
- 通常只有 NOR Flash 支持 XIP
- Flash Controller 必须支持 Memory Mapping
- 并非所有 NOR Flash 系统都支持 XIP
- XIP 性能受 Flash 读取速度限制,通常需要配合 Prefetch 和 I-Cache
Firmware 视角的存储映射
Flash
├─ .text (代码段)
└─ .rodata (只读数据,如常量字符串、查找表)
将常量放入 .rodata 可节省宝贵的 SRAM 空间。
启动过程:从 Reset 到 main()
理解 Flash 中的代码如何被 CPU 执行,以及数据如何加载到 SRAM:
Reset
↓
执行 Startup Code(启动代码)
↓
初始化 Stack Pointer(设置栈顶)
↓
拷贝 .data 段从 Flash 到 SRAM
↓
清零 .bss 段
↓
初始化 TCM/SPRAM(如果需要)
↓
调用 SystemInit()(初始化时钟、Flash Wait States 等)
↓
跳转到 main()
这是最经典的启动流程,每个嵌入式工程师都必须理解。
1.6 External SDRAM / DDR / PSRAM:容量扩展
访问延迟对比
内部 SRAM:几个时钟周期
↓
几十倍
↓
SDRAM/DDR:几十到数百个时钟周期
初始化要求
SDRAM 上电后不能直接使用,必须完成:
- 时钟和电源稳定
- 模式寄存器配置(MRS)
- 时序参数配置(tRCD、tRP、tRAS、tWR、CAS Latency)
- 刷新配置
性能优化要点
- 必须配合 Cache 使用,否则性能极差
- 大数据搬运使用 DMA,避免 CPU 参与
- 顺序访问优于随机访问,充分利用 Burst Transfer
- 严格管理 Cache 一致性(DMA 场景)
总线仲裁与竞争
DDR 最大的问题通常不是容量不足,而是多个 Master 同时访问带来的仲裁和带宽竞争。在复杂系统中,多个主设备可能同时竞争 DDR 访问:
CPU Core 0 ───┐
CPU Core 1 ───┤
DMA Engine ───┼─── DDR Controller ─── DDR Memory
GPU/NPU ──────┤
ISP/Codec ────┘
性能瓶颈往往来自总线竞争,而不是 DDR 本身带宽不足。工程师需要:
- 合理分配带宽优先级
- 使用内存控制器提供的 QoS(服务质量)配置
- 避免多个主设备同时访问同一 Bank 的不同 Row(导致频繁的 Row 切换)
2. 嵌入式工程师真正关心什么
2.1 地址空间和 Memory Map
理解芯片的 Memory Map 是基础:
- 哪些地址范围对应 Flash、SRAM、TCM、外设
- 地址是否连续,是否有空洞
- 不同存储区域的访问属性(Cacheable、Bufferable、Shareable)
实际案例
对于新人来说,理解 Memory Map 最好的方式是通过真实项目案例。以下是一个实际工程的内存配置分析:
1. Memory Configuration(链接脚本定义)
.flash.text 0x900056d0 0xec9b
0x900056d0 _stext = ABSOLUTE (.)
0x900056d0 _text_start = ABSOLUTE (.)
*(.literal .text .literal.* .text.*)
.flash.rodata 0x90014370 0x1f60
0x90014370 _srodata = ABSOLUTE (.)
*(.rodata)
.data 0x90005310 0x3c0
0x90005330 . = (_stack_top + 0x20)
*fill* 0x90005310 0x20
0x90005330 _sdata = ABSOLUTE (.)
0x90005330 _data_start = ABSOLUTE (.)
*(.data)
Memory Configuration
Name Origin Length
iram0_0_seg 0x90c00000 0x00080000 ; 512KB IRAM
dram0_0_seg 0x90400000 0x00380000 ; 3.5MB DRAM
ROM 0x90000000 0x00019000 ; 100KB ROM
关键信息:
-
ROM 区域只有 100KB(0x90000000~0x90019000),实际使用 88.7KB(0x162d0)
-
整个系统都运行在这 100KB 区域内
2. 实际内存布局
0x90000000 ┌─────────────────┐
│ Vector Table │
├─────────────────┤
│ .bss │ ; 0x90003490 ~ 0x90004710 (4.6KB)
├─────────────────┤
│ Idle Stack │ ; 0x90004710 ~ 0x90005310 (3KB)
├─────────────────┤
│ .data │ ; 0x90005330 ~ 0x900056d0
├─────────────────┤
│ .text │ ; 0x900056d0 ~ 0x9001436b
├─────────────────┤
│ .rodata │ ; 0x90014370 ~ 0x900162d0
├─────────────────┤
│ Heap │ ; 0x900162d0 ~ 0x90019000 (11.3KB)
│ ↑ │
│ │ │
│ │ │
│ │ │
├─────────────────┤
│ Stack │ ; 向下增长
│ ↓ │
0x90019000 └─────────────────┘
3. 各段详细分析
- .bss 段:0x90003490~0x90004710,大小 4.6KB
- Idle Stack:0x90004710~0x90005310,大小 3KB
- .data 段:0x90005330~0x900056d0,存放已初始化全局/静态变量
- .text 段:0x900056d0~0x9001436b,存放代码
- .rodata 段:0x90014370~0x900162d0,存放只读数据
- Heap 区域:0x900162d0~0x90019000,大小 11.3KB
5. 工程意义
这个案例展示了:
- Memory Map 不是理论:实际工程可能只使用芯片部分内存区域
- 链接脚本决定一切:程序最终落在哪里由链接脚本决定,不是所有可用内存都会被使用
- 调试必备技能:通过 Map 文件分析内存布局是工程师的基本功
- 资源规划:在有限内存下(本例仅 100KB),需要精心规划 .text、.data、.bss、Heap、Stack 的分配### 2.2 Linker Script:程序如何落地
链接脚本定义了代码/数据最终落在哪个物理存储区域:
MEMORY {
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
DTCM (rwx): ORIGIN = 0x20000000, LENGTH = 128K
SRAM (rwx): ORIGIN = 0x24000000, LENGTH = 320K
}
SECTIONS {
.text : { *(.text*) } > FLASH
.rodata : { *(.rodata*) } > FLASH
.data : { *(.data*) } > SRAM AT> FLASH
.bss : { *(.bss*) } > SRAM
.heap : { . = ALIGN(8); __heap_start = .; . += 0x2000; __heap_end = .; } > SRAM
.stack : { . = ALIGN(8); __stack_start = .; . += 0x1000; __stack_end = .; } > SRAM
}
2.3 .text/.rodata/.data/.bss/Heap/Stack 的生命周期
- .text/.rodata:编译时确定,烧录到 Flash,运行时只读
- .data:编译时确定初值,烧录到 Flash,启动时拷贝到 SRAM
- .bss:编译时确定大小,启动时在 SRAM 中清零
- Heap:运行时动态分配,需防止碎片和越界
- Stack:函数调用时动态使用,需防止溢出
2.4 Boot 阶段如何初始化 RAM
启动代码(Startup Code)的关键任务:
- 初始化 Stack Pointer
- 拷贝 .data 段从 Flash 到 SRAM
- 清零 .bss 段
- 初始化 TCM/SPRAM(如果需要)
- 调用 SystemInit() 初始化时钟、Flash Wait States 等
- 跳转到 main()
2.5 DMA Buffer 与 Cache Coherency
这是最常踩的坑:
场景 1:DMA 从外设读取数据到 CPU 要处理的内存
// 错误:DMA 完成后,数据在内存中,但 Cache 中是旧数据
dma_start_receive(buffer, size);
// 此时 CPU 读取 buffer 可能从 Cache 拿到旧数据
// 正确:DMA 完成后无效化 Cache
dma_start_receive(buffer, size);
wait_dma_complete();
InvalidateDCache_by_Addr(buffer, size); // 让 CPU 看到新数据
场景 2:CPU 准备数据后由 DMA 发送
// 错误:CPU 写的数据可能在 Cache 中,未刷到内存
prepare_data(buffer, size);
dma_start_transmit(buffer, size); // DMA 从内存读取,可能是旧数据
// 正确:DMA 传输前清理 Cache
prepare_data(buffer, size);
CleanDCache_by_Addr(buffer, size); // 将 Cache 数据刷到内存
dma_start_transmit(buffer, size);
总结
看待存储器,本质是在回答这些问题:
- CPU/DMA 能不能访问 → 地址映射、MPU/MMU 配置
- 从哪里访问 → 访问路径、是否经过 Cache
- 延迟是否稳定 → TCM/SPRAM 的确定性 vs Cache 的不确定性
- 是否需要仲裁 → 多主设备竞争、总线矩阵
- 链接脚本如何放置 → .text/.data/.bss 最终落在哪里
- 启动阶段如何初始化 → 拷贝 .data、清零 .bss、初始化 TCM
- 运行时是否会踩内存 → Stack/Heap 管理、数组边界检查
说明:本文由 AI 辅助生成,内容基于公开技术资料整理,仅作为学习记录和讨论参考。文中描述可能存在不准确或过时之处,实际开发请以芯片手册和官方文档为准。欢迎大家在评论区讨论指正!
更多推荐



所有评论(0)