2. 嵌入式系统内存基础
在嵌入式C++开发中,内存不是“无限大”的抽象资源,而是由若干物理介质、地址区间和分配策略共同构成的有限系统。理解内存层次结构,是写出稳定、可预测、可优化代码的前提。本节从物理介质出发,逐步过渡到分配方式、对齐规则与碎片问题,为后续容器选型和内存池设计打基础。
一、内存层次结构:速度、容量与成本的三角关系
嵌入式系统的内存通常呈现明显的层次结构:越靠近CPU的存储,速度越快、容量越小、单位成本越高;越远离CPU的存储,速度越慢、容量越大、单位成本越低。典型层次从内到外依次为:CPU寄存器 → Cache(部分MCU有)→ 片上SRAM → 片外SRAM/PSRAM → 片上Flash → 片外Flash/EEPROM → 外部存储卡。C++程序运行时,代码通常驻留在Flash中,而变量、栈、堆则位于RAM中。
这种层次结构决定了优化方向:频繁访问的热数据应尽量放在高速小容量区域,冷数据和大块常量可放在Flash或外部存储。例如,中断服务程序中的状态变量应放在片上SRAM,而字库、音频采样等只读大数组可声明为const并留在Flash中,避免占用宝贵的RAM。
二、RAM、ROM、Flash、栈与堆的角色分工
RAM是可读写的易失性存储,断电后数据丢失,用于存放运行时变量、栈和堆。ROM通常指掩膜只读存储器,出厂固化、不可修改,适合存放启动代码和固定常量。Flash是可电擦写的非易失存储,兼具ROM的持久性和可更新性,是嵌入式程序代码和只读数据的主要载体。
栈由编译器自动管理,用于保存函数参数、返回地址和局部变量,遵循后进先出。栈空间通常很小(几KB到几十KB),溢出会导致程序跑飞。堆用于动态内存分配,由malloc/free或new/delete管理,空间较大但分配和释放时机由程序员控制,容易产生碎片和泄漏。
| 区域 | 介质 | 生命周期 | 管理方式 | 典型风险 |
|---|---|---|---|---|
| 栈 Stack | RAM | 函数调用期间 | 编译器自动 | 溢出、递归过深 |
| 堆 Heap | RAM | 手动申请到释放 | malloc/new | 碎片、泄漏、越界 |
| .data/.bss | RAM | 程序运行全程 | 编译器静态分配 | RAM占用过大 |
| .text/.rodata | Flash | 程序运行全程 | 链接器固化 | 误写Flash、寿命 |
| EEPROM | 非易失 | 掉电保持 | 驱动读写 | 擦写次数有限 |
三、静态分配与动态分配:确定性与灵活性的取舍
静态分配在编译期或链接期确定内存位置和大小,包括全局变量、静态变量、固定数组和栈上局部变量。它的优点是确定性高、无碎片、执行速度快,适合实时性要求强的嵌入式场景。动态分配在运行期从堆中申请内存,灵活性高,但分配时间不确定、可能失败、易产生碎片,在安全关键系统中通常被限制甚至禁用。
// 静态分配:编译期确定,无碎片
static uint8_t rx_buffer[512];
// 动态分配:运行期申请,需检查失败
uint8_t* p = new (std::nothrow) uint8_t[512];
if (p == nullptr) {
// 处理内存不足
}
// 嵌入式常见替代:内存池 + placement new
alignas(4) static uint8_t pool[512];
MyObj* obj = new (pool) MyObj();
std::array,把动态分配限制在启动阶段或明确可控的路径中。若必须使用堆,应配合std::nothrow并检查返回值。四、内存对齐与碎片:看不见的性能与稳定性代价
内存对齐要求变量的地址是其对齐值的整数倍。例如,uint32_t通常要求4字节对齐,double可能要求8字节对齐。对齐能保证CPU单次访问完成读写,未对齐访问在某些架构上会触发异常,在另一些架构上则被拆成多次访问,降低性能。结构体成员之间可能插入填充字节,导致sizeof大于成员之和。
struct Bad {
uint8_t a; // 1字节
uint32_t b; // 需4字节对齐,a后填充3字节
uint8_t c; // 1字节
}; // sizeof 通常为12
struct Good {
uint32_t b; // 4字节
uint8_t a; // 1字节
uint8_t c; // 1字节
}; // sizeof 通常为8
碎片分为内部碎片和外部碎片。内部碎片来自对齐填充或分配器按固定块大小分配,导致块内未被充分利用;外部碎片来自频繁申请和释放不同大小内存,使堆中出现大量不连续的小空闲块,最终无法满足较大请求。嵌入式系统中,碎片往往在长时间运行后突然导致分配失败,排查困难。
std::pmr或自定义分配器。五、案例与复盘:从一次通信缓冲失败看内存布局
某设备在连续运行数小时后,串口接收大包时偶发失败。排查发现,接收缓冲区使用new动态申请,且每次收包后释放,长时间运行后堆中形成外部碎片,虽然总空闲量足够,但无法分配连续的大块。复盘结论:将接收缓冲区改为静态双缓冲,配合DMA和内存池,问题消失。该案例说明,嵌入式内存优化不仅是“省空间”,更是“保确定性”。