4、ADSP内存系统:L1/L2/L3缓存层次结构、TCM(紧耦合内存)使用、DMA传输机制、内存屏障与一致性

各位同学,今天我们来啃一块硬骨头——ADSP的内存系统。说实话,这块内容我当年刚接触时也头大,但搞懂了之后,很多性能问题就迎刃而解了。

4.1 缓存层次结构:L1/L2/L3

ADSP的缓存设计,说白了就是一套分级加速体系。我习惯把它想象成一个工具箱:最常用的螺丝刀放口袋里(L1),次常用的放工具箱上层(L2),不常用的扔后备箱(L3)。

层级 典型大小 访问延迟 用途
L1 I-Cache 32KB 1-2 cycles 指令缓存
L1 D-Cache 32KB 1-2 cycles 数据缓存
L2 Cache 256KB-1MB 8-12 cycles 统一缓存
L3 Cache 2MB-8MB 30-50 cycles 多核共享

我在项目中遇到过一个问题:某个音频算法在L1里跑得飞快,但一上系统就卡顿。查了半天,原来是数据被挤到L2去了。你想想看,L1和L2的延迟差了将近10倍,这性能能不掉吗?

关键点:ADSP的L1缓存是软件可配置的。你可以把它全部当缓存用,也可以划出一部分当TCM用。这个灵活性,既是优势也是陷阱。

4.2 TCM:紧耦合内存的正确打开方式

TCM,全称Tightly Coupled Memory。嗯,这里要注意,它和缓存最大的区别是——确定性。缓存有miss,TCM没有。缓存延迟不确定,TCM延迟固定。

我个人习惯把TCM用在三个地方:

  • 中断向量表:中断来了,你总不希望还要等缓存加载吧?
  • 实时性要求高的数据:比如音频buffer、传感器数据流
  • 栈空间:特别是嵌套中断的场景
// 在ADSP上配置TCM的典型代码
// 注意:不同平台API可能不同
void configure_tcm() {
    // 将L1缓存的一部分配置为TCM
    // 0x2D800000 是TCM的基地址
    uint32_t* tcm_base = (uint32_t*)0x2D800000;
    
    // 配置TCM大小为64KB
    L1CACHE_CFG |= (0x1 << TCM_SIZE_BIT);
    
    // 将关键数据放在TCM中
    __attribute__((section(".tcm_data"))) 
    volatile int32_t audio_buffer[16000];
    
    // 将关键函数放在TCM中
    __attribute__((section(".tcm_text")))
    void isr_audio_handler() {
        // 中断处理逻辑
    }
}

避坑指南:我曾经把TCM配得太大,结果L1缓存不够用,性能反而下降了。TCM不是越大越好,要根据你的实时任务和缓存需求来平衡。

4.3 DMA传输机制

DMA,Direct Memory Access,直译就是直接内存访问。说白了,就是让数据自己跑,CPU别管。我刚开始做ADSP开发时,总觉得DMA可有可无,直到有一次要处理4路麦克风数据...

ADSP上的DMA有几个特点:

  • 支持链式传输:可以预先配置多个传输任务,DMA自己排队执行
  • 支持环形buffer:音频处理最常用的模式
  • 支持2D传输:处理图像数据时特别有用
// DMA配置示例 - 音频数据采集
void setup_audio_dma() {
    // 配置DMA通道
    DMA_CH_CFG ch_cfg = {
        .src = &I2S_RX_REG,      // 从I2S外设读
        .dst = audio_buffer,      // 写到内存
        .size = 4,                // 每次传输4字节
        .count = 16000,           // 传输16000次
        .mode = DMA_MODE_CIRCULAR // 环形模式
    };
    
    // 启动DMA
    dma_channel_start(DMA_CH_0, &ch_cfg);
    
    // 注册DMA完成中断
    dma_register_callback(DMA_CH_0, dma_done_callback);
}

小技巧:DMA传输时,尽量使用burst模式。一次传一大块,比一次传一个字节效率高得多。我在项目中试过,burst模式能提升30%以上的传输效率。

4.4 内存屏障与一致性

内存一致性,这是个容易踩坑的地方。为什么会这样?因为ADSP里有多个master:CPU、DMA、硬件加速器。它们各自有缓存,数据可能不同步。

我遇到过最典型的场景:CPU写了一个buffer,通知DMA去读。结果DMA读到的还是旧数据。为什么?因为CPU写的数据还在L1缓存里,没刷到内存。

解决这个问题,就需要内存屏障:

// 内存屏障使用示例
void process_audio_data() {
    // CPU写入数据
    audio_buffer[0] = sample_1;
    audio_buffer[1] = sample_2;
    
    // 内存屏障:确保之前的写操作完成
    // 并且刷出L1缓存
    __asm__ volatile("dsb" ::: "memory");
    
    // 通知DMA开始传输
    DMA_TRIGGER_REG = 1;
    
    // DMA传输完成后
    while(!(DMA_STATUS_REG & DMA_DONE));
    
    // 内存屏障:确保DMA写入的数据对CPU可见
    __asm__ volatile("dsb" ::: "memory");
    
    // 现在可以安全读取数据了
    int32_t result = audio_buffer[0];
}
屏障类型 作用 使用场景
DMB 数据内存屏障 保证数据访问顺序
DSB 数据同步屏障 等待所有内存操作完成
ISB 指令同步屏障 刷新指令流水线

避坑指南:我曾经在一个项目中,因为少加了一个DSB屏障,导致DMA和CPU的数据不同步,音频出现爆音。查了两天才找到问题。从那以后,我在所有DMA操作前后都加上了内存屏障,宁可多刷几次,也不能漏掉。

最后说一句,内存系统这块,理论要懂,但更重要的是实践。我建议你在开发板上跑几个实验:试试不同TCM配置对性能的影响,试试不加内存屏障会出什么问题。只有亲手踩过坑,才能真正理解这些机制。