4、内存管理抽象:DDR、SRAM、DMA的HAL封装与内存池设计
各位同学,咱们今天聊点硬核的——内存管理。
说实话,座舱系统里最让人头疼的往往不是CPU算力不够,而是内存怎么分配、怎么用、怎么不出错。我见过太多项目,功能逻辑写得漂漂亮亮,一跑起来就崩,查到最后全是内存踩踏、碎片化、DMA地址不对齐这些破事。
所以这一章,咱们把DDR、SRAM、DMA的HAL封装讲透,再聊聊内存池设计。这些东西搞明白了,你的系统稳定性至少上一个台阶。
4.1 为什么需要内存管理抽象层?
你想想看,座舱系统里通常有几种内存?
- DDR:大容量,几百MB到几GB,但延迟高,适合存大块数据、帧缓冲、导航地图。
- SRAM:容量小(几十KB到几MB),但速度快,适合放关键变量、中断栈、实时数据。
- DMA专用内存:要求物理连续、地址对齐,不然DMA传着传着就崩了。
如果每个模块都直接调用底层API去申请释放,那代码就乱成一锅粥了。而且不同芯片的内存映射还不一样,换个平台就得重写一遍。
所以我们需要一个硬件抽象层(HAL),把内存操作统一封装起来。上层应用只管调用 mem_alloc()、mem_free(),底层怎么映射、怎么对齐,那是HAL的事。
核心目标:
- 统一接口:不管DDR还是SRAM,申请方式一样
- 平台无关:换芯片只需改HAL层,应用代码不动
- 安全可靠:防止内存泄漏、碎片化、越界访问
4.2 DDR的HAL封装
DDR是座舱系统的主力内存。我习惯把它分成几个区域:
- 系统堆区:给应用层动态分配
- 帧缓冲池:给显示系统用,固定大小,提前分配
- 多媒体缓冲区:给视频解码、音频处理用
封装的时候,我一般会提供一个初始化函数,把DDR的物理地址和大小传进去,然后内部维护一个内存分配器。
/* ddr_hal.h */
typedef struct {
uint32_t phys_addr; /* 物理基地址 */
uint32_t size; /* 总大小 */
uint32_t block_size; /* 最小分配粒度 */
} ddr_config_t;
int ddr_hal_init(ddr_config_t *cfg);
void *ddr_alloc(uint32_t size);
void ddr_free(void *ptr);
uint32_t ddr_get_free_size(void);
这里有个坑——DDR的物理地址和虚拟地址映射。在带MMU的系统里,应用看到的是虚拟地址,但DMA控制器要的是物理地址。所以我通常会提供一对转换函数:
uint32_t ddr_virt_to_phys(void *virt_addr);
void *ddr_phys_to_virt(uint32_t phys_addr);
经验之谈: 我在一个项目中遇到过DDR分配碎片化严重,导致大块帧缓冲申请失败。后来改用伙伴算法(Buddy System)管理DDR,问题就解决了。如果你的系统对实时性要求高,可以考虑用固定大小块分配。
4.3 SRAM的HAL封装
SRAM虽然小,但金贵。它通常用来放:
- 中断服务程序的栈
- 实时性要求高的变量(比如CAN报文缓冲区)
- 低功耗模式下保持的数据
SRAM的封装和DDR类似,但有个区别——SRAM一般不需要动态分配,而是静态划分。我建议在编译时就定好每个模块用多少SRAM。
/* sram_hal.h */
typedef enum {
SRAM_REGION_ISR_STACK, /* 中断栈区 */
SRAM_REGION_CAN_BUF, /* CAN缓冲区 */
SRAM_REGION_RT_DATA, /* 实时数据区 */
SRAM_REGION_COUNT
} sram_region_t;
int sram_hal_init(void);
void *sram_get_region(sram_region_t region);
uint32_t sram_get_region_size(sram_region_t region);
嗯,这里要注意:SRAM的访问速度虽然快,但容量有限。我曾经见过一个同事把整个导航地图缓存扔到SRAM里,结果系统直接启动不了。所以SRAM里只放真正需要快的东西。
警告: 不要把SRAM和DDR的地址空间搞混了。有些芯片的SRAM映射在特定地址范围,如果DMA操作时地址给错了,轻则数据错误,重则硬件异常。我建议在HAL层做地址范围检查。
4.4 DMA内存的HAL封装
DMA是座舱系统的数据搬运工。但DMA有个脾气——它要的内存必须物理连续,而且地址要对齐(通常是32字节或64字节对齐)。
所以DMA内存的HAL封装,核心就是保证这两点。
/* dma_hal.h */
typedef struct {
uint32_t phys_addr; /* 物理地址 */
void *virt_addr; /* 虚拟地址 */
uint32_t size; /* 实际大小 */
uint32_t align; /* 对齐要求 */
} dma_buf_t;
int dma_hal_init(void);
dma_buf_t *dma_alloc_buffer(uint32_t size, uint32_t align);
void dma_free_buffer(dma_buf_t *buf);
void dma_flush_cache(dma_buf_t *buf); /* 刷新缓存,保证一致性 */
为什么要有 dma_flush_cache?因为CPU和DMA看到的缓存可能不一致。CPU写了一块内存,DMA去读的时候可能读的是缓存里的旧数据。反过来也一样。所以每次DMA传输前后,都要做缓存同步。
避坑指南: 我曾经在一个项目中,DMA传输视频数据时画面出现撕裂。查了两天才发现是缓存一致性问题。从那以后,我每次DMA操作前后都强制刷缓存,再也没出过类似问题。
4.5 内存池设计
动态分配虽然灵活,但频繁的malloc/free会导致碎片化,而且实时性不可控。在座舱系统里,我更推荐用内存池。
内存池说白了就是提前划分好一块大内存,然后切成固定大小的块。应用需要时直接拿一块,用完还回去。没有碎片,分配速度是O(1)。
/* mem_pool.h */
typedef struct {
void *pool_start; /* 池起始地址 */
uint32_t block_size; /* 每个块的大小 */
uint32_t block_count; /* 块总数 */
uint32_t free_count; /* 空闲块数 */
uint32_t free_list; /* 空闲块链表头 */
} mem_pool_t;
int mem_pool_init(mem_pool_t *pool, void *mem, uint32_t block_size, uint32_t block_count);
void *mem_pool_alloc(mem_pool_t *pool);
void mem_pool_free(mem_pool_t *pool, void *ptr);
设计内存池时,有几个参数要仔细斟酌:
| 参数 | 说明 | 建议 |
|---|---|---|
| 块大小 | 每个内存块的大小 | 根据典型数据大小设定,比如音频帧512字节、视频行缓存1920字节 |
| 块数量 | 池中总块数 | 根据峰值需求 + 20%余量 |
| 对齐方式 | 地址对齐要求 | 至少4字节对齐,DMA内存需要32或64字节对齐 |
我一般会为不同用途创建多个内存池:
- 小对象池:16字节、32字节块,给消息队列、事件标志用
- 中对象池:512字节块,给音频帧、小图片用
- 大对象池:4KB或更大,给视频帧、导航数据用
个人习惯: 我会在内存池里加一个调试接口,可以打印当前使用率、最大使用量、分配失败次数。这在系统联调时特别有用,能快速定位内存不足的问题。
4.6 统一内存管理接口
最后,我们把DDR、SRAM、DMA和内存池整合到一个统一接口下。上层应用不用关心底层是哪种内存,只需要告诉系统:我要多大、要什么特性。
/* mem_hal.h */
typedef enum {
MEM_TYPE_DDR, /* 普通DDR */
MEM_TYPE_SRAM, /* 快速SRAM */
MEM_TYPE_DMA, /* DMA专用(物理连续、对齐) */
MEM_TYPE_POOL /* 内存池(固定大小块) */
} mem_type_t;
void *mem_alloc(mem_type_t type, uint32_t size);
void *mem_alloc_aligned(mem_type_t type, uint32_t size, uint32_t align);
void mem_free(mem_type_t type, void *ptr);
void mem_get_info(mem_type_t type, mem_info_t *info);
这样做的好处是:
- 应用代码里只出现
mem_alloc(MEM_TYPE_DMA, 1024),清晰明了 - 换平台时,只需要重写HAL层的实现,接口不变
- 调试时可以在HAL层统一加日志、加保护
再次提醒: 统一接口不代表可以乱用。DMA内存不能随便申请释放,最好在初始化阶段就分配好。SRAM要省着用,别把大块数据往里塞。内存池的块大小要选对,不然内部碎片也很浪费。
好了,这一章的内容就到这儿。内存管理抽象层看起来简单,但真正做好不容易。我建议你在自己的项目里先搭一个最小原型,跑通DDR和SRAM的分配,再加DMA和内存池。一步一步来,稳扎稳打。
下一章咱们聊聊中断管理,那也是座舱系统里容易出幺蛾子的地方。