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和内存池。一步一步来,稳扎稳打。

下一章咱们聊聊中断管理,那也是座舱系统里容易出幺蛾子的地方。