3. 音频DMA引擎介绍:DMA控制器工作原理、DMA传输模式与音频DMA特点
在音频驱动中,数据搬运是性能瓶颈的核心。CPU 直接参与每个采样点的搬运会严重拖累系统实时性。DMA(Direct Memory Access)控制器正是为解决此问题而设计的硬件引擎,它允许外设与内存之间直接交换数据,无需 CPU 逐字干预。本章深入剖析 DMA 控制器的工作原理、传输模式,并重点阐述音频场景下 DMA 的特殊设计。
3.1 DMA控制器工作原理
DMA 控制器本质上是一个专用的状态机,负责在源地址与目的地址之间搬运数据。其核心工作流程包含三个阶段:请求、传输与终止。
核心工作流程:
- 请求阶段:外设(如 I2S 控制器)通过硬件请求线(DMA Request)向 DMA 控制器发出传输请求。
- 传输阶段:DMA 控制器获得总线控制权,执行一次或多次总线传输,将数据从源地址读取并写入目的地址。
- 终止阶段:传输计数器减至零或收到终止信号,DMA 控制器释放总线,并向 CPU 发出中断通知传输完成。
下图展示了 DMA 控制器在系统中的位置与基本数据流向:
3.2 DMA传输模式
根据源与目的的不同,DMA 传输主要分为三种模式。音频驱动中最常用的是外设到内存(录音)和内存到外设(播放)。
| 传输模式 | 源地址 | 目的地址 | 典型应用场景 | 音频相关性 |
|---|---|---|---|---|
| 内存到内存 | 内存区域 A | 内存区域 B | 数据拷贝、缓冲区搬移、内存清零 | 低(仅用于音频缓冲区管理) |
| 外设到内存 | 外设数据寄存器 | 内存缓冲区 | ADC 采样、麦克风录音、传感器数据采集 | 高(录音路径) |
| 内存到外设 | 内存缓冲区 | 外设数据寄存器 | DAC 输出、扬声器播放、波形生成 | 高(播放路径) |
关键区别:内存到内存传输通常由软件触发,而外设到内存/内存到外设传输由硬件请求线触发,这决定了音频 DMA 必须使用硬件同步模式。
下图对比了三种传输模式的数据路径:
3.3 音频DMA特点
音频流对实时性、连续性和数据完整性有严格要求,因此音频 DMA 相比通用 DMA 有以下显著特点:
- 循环缓冲(Ring Buffer / Cyclic Buffer):音频数据是连续流,DMA 必须支持地址回绕,在缓冲区末尾自动跳转到起始地址继续传输,避免音频中断。
- 双缓冲(Double Buffering):通常使用两个缓冲区交替工作,一个用于 DMA 传输,另一个供 CPU 读写,消除竞争条件。
- 硬件同步(Hardware Synchronization):传输由外设的帧同步信号(如 LRCLK)触发,确保采样率精确稳定。
- 突发传输(Burst Transfer):音频数据通常以字(16/24/32-bit)为单位,DMA 控制器支持配置突发大小以匹配 I2S 数据宽度。
- 中断与回调:每完成半缓冲或全缓冲传输时产生中断,驱动在中断服务程序中更新指针或填充新数据。
常见陷阱:如果 DMA 控制器不支持硬件循环模式,驱动必须手动在传输完成中断中重新配置源/目的地址,这会导致短暂的音频毛刺。选择 SoC 时务必确认 DMA 控制器是否支持硬件循环(Hardware Linked List 或 Auto-Reload)。
下图展示了音频 DMA 循环缓冲与双缓冲的典型结构:
3.4 音频DMA关键参数配置
在 Linux 音频驱动中,DMA 控制器通过 dmaengine API 进行配置。以下是与音频强相关的关键参数:
| 参数 | 描述 | 音频典型值 | 配置接口 |
|---|---|---|---|
| 传输宽度 | 单次传输的数据位宽 | 16-bit / 24-bit / 32-bit | dma_set_slave_width() |
| 突发大小 | 每次总线请求传输的数据单元数 | 4 / 8 / 16(与 FIFO 深度匹配) | dmaengine_slave_config() |
| 源/目的地址 | 外设数据寄存器物理地址 | I2S 的 TX/RX 寄存器地址 | dmaengine_slave_config() |
| 循环模式 | 是否启用地址自动回绕 | 必须启用 | dmaengine_prep_dma_cyclic() |
| 传输方向 | DMA 数据流向 | DMA_DEV_TO_MEM / DMA_MEM_TO_DEV | dmaengine_slave_config() |
代码示例:音频 DMA 循环缓冲配置(伪代码)
struct dma_slave_config config = {
.direction = DMA_MEM_TO_DEV, // 播放方向
.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
.src_maxburst = 4, // 每次突发4个字
.dst_maxburst = 4,
.slave_id = i2s->dma_tx_id, // 外设DMA请求ID
};
dmaengine_slave_config(chan, &config);
// 准备循环传输:缓冲区、周期大小、周期数
txdesc = dmaengine_prep_dma_cyclic(chan, buf_phys, buf_size,
period_size, DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
txdesc->callback = audio_dma_complete; // 周期完成回调
dmaengine_submit(txdesc);
dma_async_issue_pending(chan); // 启动传输