2. DMA引擎基础
2.1 DMA控制器工作原理
DMA(Direct Memory Access)控制器是嵌入式系统中负责在内存与外设之间、或内存与内存之间直接搬运数据的硬件引擎。CPU只需配置传输参数,DMA控制器便独立完成数据移动,从而将CPU从繁重的数据拷贝任务中解放出来,专注于音频算法与协议处理。
核心价值:在ALSA音频驱动中,DMA负责将音频采样数据从内存缓冲区搬运到I2S(Inter-IC Sound)发送FIFO,或将接收FIFO数据搬回内存。一次典型的48kHz/16位/双声道音频流,每秒需传输192,000字节,若由CPU逐字节搬运,将消耗大量算力。
DMA控制器内部架构
一个典型的DMA控制器包含以下关键模块:
| 模块 | 功能描述 | 音频场景作用 |
|---|---|---|
| 控制寄存器组 | 存储源地址、目的地址、传输长度、控制标志 | 配置音频缓冲区地址与采样大小 |
| 状态寄存器 | 指示传输完成、错误、空闲等状态 | 触发中断通知CPU缓冲区已空/满 |
| 地址生成器 | 自动递增/递减源和目的地址 | 顺序搬运音频帧,支持循环缓冲区 |
| 传输计数器 | 记录剩余待传输字节数 | 精确控制每次DMA burst大小 |
| 仲裁器 | 管理多个DMA通道的优先级与总线请求 | 保证音频DMA通道获得实时性保障 |
| 中断控制器 | 传输完成/错误时向CPU发送中断信号 | 触发ALSA period elapsed事件 |
关键概念:DMA传输的最小单位是burst(突发传输)。音频场景下,burst大小通常设置为与I2S FIFO深度匹配(如4×32位),以平衡总线占用与实时性。
DMA传输基本流程
- CPU配置DMA通道:设置源地址(内存缓冲区)、目的地址(外设FIFO)、传输宽度(8/16/32位)、burst大小。
- CPU启动DMA:写控制寄存器使能通道,DMA控制器开始工作。
- DMA请求总线:通过系统总线仲裁器获取总线控制权。
- DMA执行传输:从源地址读取数据,写入目的地址,地址生成器自动更新指针。
- 传输完成:计数器归零,状态寄存器置位,可选产生中断。
- CPU响应中断:在中断服务程序中更新缓冲区指针或重新配置下一轮传输。
2.2 DMA传输模式:单次传输 vs 循环传输
在ALSA音频驱动中,DMA传输模式的选择直接影响音频流的连续性与中断频率。两种模式各有适用场景:
| 特性 | 单次传输(Single-shot) | 循环传输(Circular/Auto-reload) |
|---|---|---|
| 传输行为 | 完成指定长度后停止,需CPU重新配置 | 完成后自动重置计数器,从头开始 |
| 中断频率 | 每次传输完成产生一次中断 | 可配置半满/全满中断,频率可控 |
| CPU负载 | 高(频繁中断+重配置) | 低(中断少,无重配置开销) |
| 适用场景 | 非实时数据块传输、初始化阶段 | 连续音频流播放/录音 |
| 缓冲区管理 | 线性缓冲区,需手动切换 | 环形缓冲区,硬件自动回绕 |
| ALSA典型用法 | snd_pcm_period_elapsed 每次触发 | 半中断触发 period,全中断做同步 |
性能陷阱:若使用单次传输模式播放48kHz音频,每传输一个period(如1024帧)就产生一次中断并重配置DMA。在低延迟场景(period=64帧)下,中断频率高达750Hz,CPU将花费大量时间在中断上下文中,导致音频卡顿或xrun。
循环传输在ALSA中的实现
循环传输依赖DMA控制器的自动重载(auto-reload)能力。配置时需设置:
- 源地址:环形缓冲区的起始地址
- 目的地址:外设FIFO地址(固定不变)
- 传输长度:整个环形缓冲区大小(如period_size × period_count)
- 循环使能位:传输完成后自动重置地址和计数器
/* 循环传输配置示例(伪代码) */
dma_channel->src_addr = (uint32_t)ring_buffer;
dma_channel->dst_addr = (uint32_t)&i2s_regs->tx_fifo;
dma_channel->transfer_len = period_size * period_count;
dma_channel->ctrl = DMA_CTRL_CIRCULAR | DMA_CTRL_INC_SRC;
dma_channel->burst_size = 4; /* 4 words per burst */
2.3 DMA描述符链
描述符链(Descriptor Chain)是DMA控制器的高级特性,允许将多个传输任务链接成链表,由硬件自动顺序执行。每个描述符包含下一次传输的配置信息,DMA完成当前传输后自动加载下一个描述符,无需CPU介入。
描述符结构
每个DMA描述符通常包含以下字段:
| 字段 | 位宽 | 说明 |
|---|---|---|
| src_addr | 32/64位 | 源数据地址(内存或外设) |
| dst_addr | 32/64位 | 目的数据地址 |
| transfer_len | 16/32位 | 本次传输的字节数 |
| next_desc | 32/64位 | 下一个描述符的物理地址(NULL表示结束) |
| ctrl_flags | 16/32位 | 中断使能、地址递增、传输宽度等控制位 |
描述符链在ALSA中的应用
在音频驱动中,描述符链常用于实现多缓冲区乒乓操作:
- 描述符0:搬运缓冲区A → I2S FIFO,完成后产生中断
- 描述符1:搬运缓冲区B → I2S FIFO,完成后产生中断
- 描述符2:搬运缓冲区C → I2S FIFO,完成后产生中断
- 描述符3:搬运缓冲区D → I2S FIFO,完成后产生中断
- 回环:描述符3的next_desc指向描述符0,形成无限循环
优化技巧:描述符链配合中断聚合(interrupt coalescing)技术,可以只在关键节点(如半缓冲或整缓冲)产生中断,进一步降低CPU负载。例如每4个描述符才产生一次中断,CPU有更多时间处理音频算法。
/* 描述符链配置示例(伪代码) */
struct dma_descriptor desc[4];
/* 初始化描述符链 */
for (int i = 0; i < 4; i++) {
desc[i].src_addr = (uint32_t)&audio_buffers[i * PERIOD_SIZE];
desc[i].dst_addr = (uint32_t)&i2s_regs->tx_fifo;
desc[i].transfer_len = PERIOD_SIZE;
desc[i].ctrl_flags = DMA_CTRL_INC_SRC | DMA_CTRL_INT_EN;
desc[i].next_desc = (uint32_t)&desc[(i + 1) % 4]; /* 循环链接 */
}
/* 启动DMA,从描述符0开始 */
dma_start_chain(&desc[0]);
2.4 三种传输模式对比总结
| 特性 | 单次传输 | 循环传输 | 描述符链 |
|---|---|---|---|
| 硬件复杂度 | 低 | 中 | 高 |
| CPU干预频率 | 每次传输完成 | 仅半/全中断时 | 可配置(聚合中断) |
| 缓冲区灵活性 | 低(线性) | 中(环形) | 高(多缓冲区乒乓) |
| 适用音频场景 | 初始化、短数据块 | 连续播放/录音 | 多通道、低延迟、复杂路由 |
| 中断优化空间 | 小 | 中(调整period大小) | 大(中断聚合、批量处理) |
工程建议:对于大多数嵌入式Linux ALSA音频驱动,推荐使用循环传输 + 描述符链的组合方案。循环传输保证音频流的连续性,描述符链提供灵活的缓冲区管理能力。在低延迟场景(如专业音频设备)中,描述符链的乒乓操作可将延迟控制在1ms以内。