4、内存与缓存优化:内存池管理、DMA传输优化、Cache一致性维护、TCM紧耦合内存使用

内存和缓存,说白了就是嵌入式系统的「血液」和「高速公路」。很多实时性卡顿,根源都在这里。我做了十几年嵌入式,见过太多项目因为内存碎片导致系统崩溃,或者因为Cache没处理好,数据死活不对。

这一章,咱们就聊聊怎么把这几个硬骨头啃下来。

4.1 内存池管理:告别碎片化

标准库的 malloc/free 用起来方便,但在实时系统里,它是个定时炸弹。为什么?因为频繁分配释放会产生大量内存碎片,最终导致分配失败。

我个人习惯,在实时任务里,一律使用内存池。

4.1.1 固定大小内存池

这是最常用的方式。预先划分一块连续内存,切成固定大小的块。分配和释放都是 O(1) 复杂度,没有碎片。

/* 固定大小内存池示例 */
#define POOL_SIZE  10
#define BLOCK_SIZE 64

typedef struct {
    uint8_t  buffer[POOL_SIZE][BLOCK_SIZE];
    uint32_t free_mask;  /* 位图标记空闲块 */
} mem_pool_t;

void* pool_alloc(mem_pool_t *pool) {
    /* 找到第一个空闲位 */
    uint32_t idx = __CLZ(__RBIT(pool->free_mask));
    if (idx >= POOL_SIZE) return NULL;
    pool->free_mask &= ~(1UL << idx);
    return &pool->buffer[idx][0];
}

void pool_free(mem_pool_t *pool, void *ptr) {
    uint32_t idx = ((uint32_t)ptr - (uint32_t)pool->buffer) / BLOCK_SIZE;
    pool->free_mask |= (1UL << idx);
}
经验之谈: 我建议把内存池放在 .noinit 段,这样系统复位后数据还在,调试时特别有用。

4.1.2 变长内存池

如果数据包大小不固定,可以用伙伴算法(Buddy System)。它把内存按 2 的幂次分割,分配和释放效率也很高。

特性 固定大小池 伙伴算法池
分配速度 O(1) O(log N)
内存利用率 低(有内碎片) 中等
适用场景 固定长度消息、任务栈 变长网络包、文件系统缓存
避坑指南: 我曾经在一个项目中,把内存池大小设得刚刚好,结果协议升级后包长增加了 8 字节,整个系统崩溃。记住:池大小一定要留余量,至少 20%。

4.2 DMA传输优化:让CPU喘口气

DMA(直接存储器访问)是实时系统的「隐形助手」。它能在不占用 CPU 的情况下,完成数据搬运。

4.2.1 双缓冲(Ping-Pong)模式

这是我最喜欢的 DMA 用法。两个缓冲区轮流工作:一个给 DMA 写,一个给 CPU 读。没有锁,没有等待。

/* 双缓冲DMA示例 */
#define BUF_SIZE 1024

static uint8_t ping_buf[BUF_SIZE] __attribute__((aligned(32)));
static uint8_t pong_buf[BUF_SIZE] __attribute__((aligned(32)));
static volatile uint32_t active_buf = 0;

void dma_callback(void) {
    /* DMA完成中断:切换缓冲区 */
    active_buf ^= 1;
    /* 重新配置DMA到另一个缓冲区 */
    DMA->SAR = (uint32_t)(active_buf ? pong_buf : ping_buf);
    DMA->CR  |= DMA_CR_EN;
}

void process_data(void) {
    uint8_t *data = active_buf ? ping_buf : pong_buf;
    /* CPU处理 data,DMA同时在写另一个缓冲区 */
    /* ... */
}

4.2.2 链表DMA(Scatter-Gather)

当数据不连续时,链表 DMA 可以一次传输多个不连续的内存块。我曾在视频采集项目中用这个,把帧率从 15fps 提到了 30fps。

关键指标: DMA 传输延迟 = 源地址建立时间 + 目的地址建立时间 + 传输时间。对于高速外设(如以太网),建议使用 4 字节对齐的缓冲区,否则会触发总线错误。

4.3 Cache一致性维护:别让数据「打架」

Cache 是双刃剑。用好了,性能翻倍;用不好,数据错乱。特别是在多核系统或 DMA 场景下,Cache 一致性问题能把人逼疯。

4.3.1 问题根源

CPU 读写 Cache,DMA 直接读写内存。两边数据不一致了,程序就跑飞了。

/* 典型问题场景 */
uint8_t buffer[1024];

/* CPU写buffer */
buffer[0] = 0xAA;  /* 数据在Cache里,还没写回内存 */

/* DMA从内存读buffer */
DMA->SAR = (uint32_t)buffer;
DMA->CR |= DMA_CR_EN;  /* DMA读到的是旧数据! */

4.3.2 解决方案

方法 操作 开销
Clean(清空) 将Cache写回内存 中
Invalidate(失效) 丢弃Cache内容,下次从内存读 低
Clean+Invalidate 先写回再失效 高
我的习惯: DMA 读内存前,先 Invalidate 对应区域;DMA 写内存后,先 Clean 再让 CPU 读。别偷懒,少一步都可能出问题。

4.3.3 非Cache区域

对于 DMA 缓冲区,我建议直接配置为非 Cache 区域。ARM Cortex-M7 的 MPU 可以做到:

/* 配置MPU:DMA缓冲区为非Cache */
MPU->RNR  = 0;
MPU->RBAR = (uint32_t)buffer;
MPU->RASR = (0x03 << 0)  |  /* 全权限 */
             (0x01 << 16) |  /* 1区 */
             (0x01 << 18) |  /* 可执行 */
             (0x00 << 19) |  /* 非共享 */
             (0x00 << 24) |  /* 非Cache */
             (0x1F << 1);    /* 大小 */
血的教训: 我曾经在一个项目中,DMA 和 CPU 共享一个缓冲区,忘了做 Cache 维护。结果数据偶尔对、偶尔错,查了三天才发现是 Cache 一致性问题。从那以后,我所有 DMA 缓冲区都放在非 Cache 区域。

4.4 TCM紧耦合内存使用:零等待的「黄金地段」

TCM(Tightly Coupled Memory)是挂在 CPU 内部总线上的内存,访问速度跟 CPU 一样快,没有 Cache Miss 的惩罚。

4.4.1 放什么到TCM

  • 中断服务程序(ISR):要求最低延迟
  • 实时控制循环:如电机 FOC、PID 计算
  • 关键数据:如任务栈、调度器变量
  • Bootloader:启动阶段 Cache 还没初始化
/* 将关键函数放到ITCM */
__attribute__((section(".itcm")))
void critical_isr(void) {
    /* 零等待执行 */
    /* ... */
}

/* 将关键数据放到DTCM */
__attribute__((section(".dtcm")))
volatile uint32_t system_tick;

4.4.2 TCM vs Cache

特性 TCM Cache
访问延迟 1个时钟周期 1-10个周期(命中/未命中)
确定性 完全确定 不确定(依赖命中率)
大小 通常 64KB-512KB 通常 16KB-2MB
功耗 低 较高

核心原则: TCM 放「确定性」代码和数据,Cache 放「大块但非实时」的内容。别把整个程序塞进 TCM,它没那么大。

4.5 知识体系总览

下面这张图,把本章的核心逻辑串起来了。你想想看,从内存分配开始,到数据搬运,再到缓存一致性,最后到紧耦合内存——这是一条完整的优化链路。

内存与缓存优化知识体系 内存与缓存优化知识体系 内存池管理 DMA传输优化 Cache一致性 TCM紧耦合内存 提供缓冲区 数据一致性 零等待传输 互补关系 关键原则 • 实时任务用内存池 • DMA用双缓冲模式 • 共享缓冲区非Cache • ISR和关键循环放TCM 常见陷阱 • 忘记Cache维护 • 内存池大小不留余量 • DMA缓冲区未对齐 • TCM放太多代码

嗯,到这里,内存与缓存优化的核心内容就讲完了。记住:没有银弹,每种技术都有它的适用场景。关键是要理解你的系统瓶颈在哪里,然后对症下药。