4. DSP 内存架构:哈佛架构与冯诺依曼架构、片内 RAM/片外 RAM、DMA 传输、Cache 一致性

做语音算法移植,说白了就是在跟内存打交道。你算法写得再漂亮,跑在 DSP 上如果内存没安排好,照样卡成狗。我见过太多工程师,PC 上仿真跑得飞起,一上 DSP 就崩,十有八九是内存踩坑了。

这一章,咱们把 DSP 内存架构的几个核心概念掰开揉碎。搞懂了这些,你写代码的时候心里就有底了。

4.1 哈佛架构 vs 冯诺依曼架构

先问个问题:你的程序指令和数据,是放在同一个地方,还是分开存放?

这就是两种架构的根本区别。

  • 冯诺依曼架构:指令和数据共用一套总线、一个存储空间。CPU 取指令和读写数据不能同时进行,得排队。这叫“冯诺依曼瓶颈”。
  • 哈佛架构:指令总线和数据总线是分开的,程序存储器和数据存储器也是独立的。CPU 可以一边取指令,一边读写数据,并行操作。

大部分 DSP 都采用哈佛架构或其改进型。为什么?因为 DSP 的核心运算——乘加操作(MAC),需要在一个时钟周期内同时读取指令、两个操作数,还要写回结果。冯诺依曼根本扛不住。

核心差异一句话:冯诺依曼是单车道,哈佛架构是双车道。DSP 要跑得快,必须上双车道。

我刚开始用 TI C55x 系列时,习惯性地把数据和代码混放在一个段里,结果程序跑起来比预期慢了一半。查了半天才发现,是链接脚本没配好,数据和指令抢了同一套总线。改成哈佛模式后,性能直接翻倍。

现在很多高端 DSP 用的是改进型哈佛架构——数据和指令总线物理上分开,但允许数据总线去读程序存储器里的常数表。这个特性在语音算法里特别有用,比如查正弦表、窗函数系数,可以直接走数据总线,不用绕路。

4.2 片内 RAM vs 片外 RAM

DSP 的存储系统,通常分两层:片内(On-Chip)和片外(Off-Chip)。

特性 片内 RAM 片外 RAM
访问速度 1~2 个时钟周期 几十到上百个时钟周期
容量 几十 KB ~ 几 MB 几十 MB ~ 几 GB
功耗 低 高
成本 高(每 bit 贵) 低
典型用途 关键代码、高频数据、堆栈 大缓冲区、离线数据、日志

做语音算法移植时,我有个原则:能放片内,绝不放片外。语音信号处理是实时系统,每一帧数据都有严格的 deadline。片外 RAM 的延迟,足以让你的音频出现“咔咔”的爆音。

举个例子,一个 16kHz 采样率的 AEC(回声消除)算法,帧长 10ms,也就是 160 个采样点。如果参考信号和近端信号的缓冲区都放在片外,每次读写多花 50 个周期,一帧下来可能多出几千个周期的额外开销。在低功耗 DSP 上,这直接导致处理不完一帧数据。

我的习惯:先把算法里最频繁访问的数组——比如自适应滤波器的系数、环形缓冲区——用关键字或链接脚本强制放到片内 RAM。剩下的低频数据,比如统计信息、调试日志,才放到片外。

但片内 RAM 就那么点,不够用怎么办?两个思路:

  • 分时复用:不同处理阶段共用同一块片内缓冲区。比如 AEC 处理完,这块内存马上给 NR(降噪)用。
  • DMA 搬运:把片外数据用 DMA 批量搬到片内,处理完再搬回去。这就是下一节要聊的。

4.3 DMA 传输:让 CPU 喘口气

DMA(Direct Memory Access),直译就是“直接内存访问”。它的作用很简单:在不需要 CPU 干预的情况下,在内存和外设之间、或者内存和内存之间搬数据。

你想想看,语音算法里最耗时的操作之一就是搬数据。比如麦克风采集进来的 160 个样点,要从 I2S 接口的 FIFO 搬到内存里。如果每次都用 CPU 去搬,那 CPU 啥也别干了,光搬砖了。

DMA 就是干这个的。你配好源地址、目的地址、数据长度,启动它,它就自己在那搬。搬完了给你发个中断,告诉你“活干完了”。

我做过一个项目,双麦克风波束成形,每通道 48kHz 采样。一开始用 CPU 轮询搬数据,CPU 占用率直接飙到 60%。后来改成 DMA 乒乓缓冲,CPU 占用率降到 15%,剩下的算力全用来做算法处理。

DMA 配置要点:

  • 传输粒度:8-bit、16-bit、32-bit,要和数据类型对齐。语音数据一般是 16-bit,用半字传输最省带宽。
  • 传输模式:单次传输、连续传输、链表传输。语音帧处理用连续传输+乒乓缓冲最常用。
  • 优先级:DMA 通道有优先级,音频数据流通常设最高优先级,别被其他低优先级 DMA 堵住。

下面是一个典型的 DMA 乒乓缓冲配置伪代码:

// 定义两个缓冲区
int16_t ping_buffer[160];
int16_t pong_buffer[160];

// 配置 DMA 通道 0
DMA_Config config;
config.src_addr = (uint32_t)&I2S_RX_FIFO;   // 源:I2S 接收 FIFO
config.dst_addr = (uint32_t)ping_buffer;    // 目的:Ping 缓冲区
config.transfer_size = 160;                 // 160 个半字
config.transfer_mode = DMA_MODE_PINGPONG;   // 乒乓模式
config.alternate_dst = (uint32_t)pong_buffer; // 交替缓冲区
config.interrupt_enable = TRUE;             // 传输完成产生中断

DMA_init(0, &config);
DMA_start(0);

// 中断服务函数
void DMA_ISR() {
    if (DMA_get_status(0) == DMA_PING_COMPLETE) {
        process_audio_frame(ping_buffer);  // 处理 Ping 缓冲区
    } else {
        process_audio_frame(pong_buffer);  // 处理 Pong 缓冲区
    }
}

这个模式的好处是:DMA 在后台往一个缓冲区写数据,CPU 同时处理另一个缓冲区里的数据。两者互不干扰,流水线拉满。

我曾经踩过的坑:DMA 传输完成中断里,千万别做耗时操作。中断服务函数应该只设置一个标志位,或者切换一下缓冲区指针。真正的算法处理,放到主循环或者任务里去干。否则中断嵌套一多,音频就断了。

4.4 Cache 一致性:看不见的坑

很多高性能 DSP 引入了 Cache,目的是弥合 CPU 和片外 RAM 之间的速度差距。但 Cache 有个臭名昭著的问题——一致性问题。

什么叫 Cache 一致性?简单说:CPU 在 Cache 里改了数据,但片外 RAM 里还是旧值。或者 DMA 往片外 RAM 写了新数据,但 Cache 里还是老数据。两边对不上了。

为什么会这样?因为 Cache 和 DMA 是两个独立的硬件模块,它们互相不知道对方在干什么。CPU 读写 Cache,DMA 直接读写片外 RAM,两者没有自动同步机制。

语音算法里最容易出问题的场景:

  • DMA 把新数据写入片外 RAM,CPU 从 Cache 读数据:CPU 读到的是 Cache 里的旧数据,音频就变成“重复播放上一帧”。
  • CPU 把处理结果写到 Cache,DMA 从片外 RAM 搬数据:DMA 搬走的是旧数据,输出音频就“丢帧”。

解决方案就三个字:刷、无、配。

  • 刷(Flush/Invalidate):手动刷 Cache。CPU 写完数据后,执行 Cache Flush,把 Cache 内容写回片外 RAM。CPU 读数据前,执行 Cache Invalidate,让 Cache 失效,强制从片外 RAM 重新加载。
  • 无(Non-cacheable):把 DMA 要访问的缓冲区配置为不可缓存。这样 CPU 和 DMA 都直接操作片外 RAM,没有一致性问题。代价是 CPU 访问变慢。
  • 配(MPU 配置):通过内存保护单元(MPU)把共享数据区域配成“共享”属性,硬件自动维护一致性。这个最省心,但不是所有 DSP 都支持。

我个人的习惯是:音频数据缓冲区一律配成不可缓存。因为语音算法对实时性要求高,缓冲区访问模式是流式的——写一次,读一次,不会反复读写同一个位置。Cache 对这种流式访问几乎没有加速效果,反而引入一致性问题,得不偿失。

而算法内部的系数表、查找表,这些是反复读的,可以放心地放进 Cache,加速效果明显。

一个小技巧:如果你的 DSP 支持 Cache 锁定(Cache Lock),可以把最常用的代码段或数据表锁定在 Cache 里,防止被其他数据踢出去。我在做实时 FFT 时,就把旋转因子表锁在了 Cache 里,每次 FFT 计算省了从片外 RAM 加载的时间。

4.5 本章知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作内存架构设计的检查清单。

DSP 内存架构知识体系 DSP 内存架构核心知识点 哈佛 vs 冯诺依曼 片内 RAM vs 片外 RAM DMA 传输 Cache 一致性 指令/数据总线分离 并行取指与读写 改进型:数据可读程序区 速度:片内 1~2 周期 容量:片外更大更便宜 策略:关键数据放片内 乒乓缓冲模式 CPU 零开销搬运 中断只设标志位 刷:Flush / Invalidate 无:配成 Non-cacheable 配:MPU 共享属性 核心原则:高频数据放片内,流式缓冲区用 DMA 并配成 Non-cacheable,关键代码锁 Cache

这张图把四个知识点串成了一个整体。你从上往下看:先选架构,再定存储层级,然后用 DMA 做搬运,最后用 Cache 策略兜底。每一步都有对应的工程决策。