4. ALSA PCM DMA驱动框架
ALSA PCM 中间层通过 snd_pcm_ops 结构体将底层 DMA 硬件操作抽象为一组标准回调函数。驱动开发者只需填充该结构体并注册到 PCM 设备,即可完成音频数据流的硬件控制。本章深入解析 snd_pcm_ops 的每个成员、三个关键回调(hw_params、prepare、trigger)的执行语义,以及 DMA 缓冲区的管理策略。
核心要点: snd_pcm_ops 是驱动与 ALSA 核心的契约,hw_params 配置硬件资源,prepare 准备传输环境,trigger 控制启停;DMA 缓冲区管理直接决定中断频率与延迟性能。
4.1 snd_pcm_ops 结构体全景
snd_pcm_ops 定义在 include/sound/pcm.h 中,是 PCM 设备驱动的核心接口。下表列出所有回调函数及其典型用途:
| 回调函数 | 原型 | 调用时机与作用 |
|---|---|---|
open | int (*open)(struct snd_pcm_substream *substream) | 打开 PCM 设备时调用,初始化 substream 私有数据 |
close | int (*close)(struct snd_pcm_substream *substream) | 关闭设备时调用,释放 open 中分配的资源 |
ioctl | int (*ioctl)(struct snd_pcm_substream *substream, unsigned int cmd, void *arg) | 处理特殊 ioctl 命令,通常用于非标准控制 |
hw_params | int (*hw_params)(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params) | 配置硬件参数:采样率、格式、通道数、DMA 缓冲区大小 |
hw_free | int (*hw_free)(struct snd_pcm_substream *substream) | 释放 hw_params 分配的硬件资源 |
prepare | int (*prepare)(struct snd_pcm_substream *substream) | 在每次启动前准备 DMA 传输,重置指针、配置 DMA 描述符 |
trigger | int (*trigger)(struct snd_pcm_substream *substream, int cmd) | 控制 DMA 启停:SNDRV_PCM_TRIGGER_START/STOP/PAUSE_PUSH/PAUSE_RELEASE |
pointer | snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream) | 返回当前 DMA 硬件指针位置(帧数),用于计算延迟和中断 |
copy_user | int (*copy_user)(struct snd_pcm_substream *substream, int channel, unsigned long pos, void __user *buf, unsigned long count) | 用户空间与 DMA 缓冲区之间的数据拷贝(可选) |
copy_kernel | int (*copy_kernel)(struct snd_pcm_substream *substream, int channel, unsigned long pos, void *buf, unsigned long count) | 内核空间与 DMA 缓冲区之间的数据拷贝(可选) |
fill_silence | int (*fill_silence)(struct snd_pcm_substream *substream, int channel, unsigned long pos, unsigned long count) | 用静音数据填充 DMA 缓冲区(可选) |
mmap | int (*mmap)(struct snd_pcm_substream *substream, struct vm_area_struct *vma) | 实现 DMA 缓冲区到用户空间的 mmap 映射 |
开发建议: 大多数 DMA 驱动只需实现
open/close/hw_params/hw_free/prepare/trigger/pointer 七个回调。copy_user/copy_kernel 仅在非 mmap 模式下需要,现代音频应用通常使用 mmap 零拷贝路径。
4.2 hw_params 回调:硬件资源契约
hw_params 是驱动与 ALSA 核心协商硬件参数的唯一入口。ALSA 核心通过 snd_pcm_hw_params 结构体传递用户空间请求的参数集,驱动必须从中提取有效配置并分配 DMA 缓冲区。
static int my_pcm_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
{
struct my_runtime *runtime = substream->runtime;
struct snd_dma_buffer *dma_buf = &runtime->dma_buffer;
size_t buffer_bytes, period_bytes;
int err;
/* 1. 提取参数 */
buffer_bytes = params_buffer_bytes(params);
period_bytes = params_period_bytes(params);
/* 2. 分配 DMA 缓冲区(一致性内存) */
err = snd_dma_alloc_pages(SNDRV_DMA_TYPE_DEV,
substream->dma_device,
buffer_bytes, dma_buf);
if (err < 0)
return err;
/* 3. 配置 DMA 控制器:地址、大小、传输方向 */
runtime->dma_addr = dma_buf->addr;
runtime->dma_area = dma_buf->area;
runtime->dma_bytes = buffer_bytes;
runtime->period_size = period_bytes / frames_to_bytes(runtime, 1);
/* 4. 设置硬件 FIFO 阈值、采样率分频器等 */
my_set_samplerate(substream, params_rate(params));
my_set_format(substream, params_format(params));
return 0;
}
常见错误: 在 hw_params 中不要启动 DMA 传输,也不要操作硬件指针。该回调只负责资源分配和参数固化,DMA 启动必须延迟到 trigger(START) 阶段。
4.3 prepare 回调:传输前状态重置
prepare 在每次 trigger(START) 之前被调用,用于将 DMA 硬件恢复到初始就绪状态。典型操作包括:
- 重置 DMA 描述符链表:将 DMA 通道的当前描述符指针指向缓冲区起始位置。
- 清零硬件指针:将 DMA 控制器的传输计数器复位到 0。
- 配置中断阈值:设置 DMA 完成中断触发的周期边界(period boundary)。
- 预填充静音数据:对于 playback 方向,在缓冲区中写入初始静音帧,避免启动时的爆音。
static int my_pcm_prepare(struct snd_pcm_substream *substream)
{
struct my_runtime *runtime = substream->runtime;
/* 1. 重置 DMA 描述符环 */
my_dma_reset_descriptors(substream);
/* 2. 设置 DMA 传输参数:源/目的地址、传输宽度、突发长度 */
my_dma_config(substream,
runtime->dma_addr,
runtime->dma_bytes,
runtime->period_size);
/* 3. 使能周期中断 */
my_dma_enable_period_irq(substream, runtime->period_size);
/* 4. 对于 playback,填充静音 */
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
my_fill_silence(runtime->dma_area, runtime->dma_bytes);
return 0;
}
性能提示: prepare 回调应尽量轻量,避免在每次启动时重新分配内存。DMA 描述符的构建建议在 hw_params 中一次性完成,prepare 仅做指针复位和使能操作。
4.4 trigger 回调:DMA 启停控制
trigger 是唯一允许操作 DMA 硬件传输状态的回调。ALSA 核心通过 cmd 参数传递控制命令,驱动必须在原子上下文(atomic context)中执行,不能睡眠。
| 命令 | 值 | 驱动行为 |
|---|---|---|
SNDRV_PCM_TRIGGER_START | 0 | 启动 DMA 传输,使能中断 |
SNDRV_PCM_TRIGGER_STOP | 1 | 停止 DMA 传输,禁止中断 |
SNDRV_PCM_TRIGGER_PAUSE_PUSH | 2 | 暂停 DMA(保留 FIFO 数据) |
SNDRV_PCM_TRIGGER_PAUSE_RELEASE | 3 | 恢复暂停的 DMA 传输 |
static int my_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
/* 使能 DMA 通道,启动传输 */
my_dma_channel_enable(substream);
my_dma_start(substream);
break;
case SNDRV_PCM_TRIGGER_STOP:
/* 立即停止 DMA,等待当前传输完成 */
my_dma_stop(substream);
my_dma_channel_disable(substream);
break;
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
my_dma_pause(substream);
break;
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
my_dma_resume(substream);
break;
default:
return -EINVAL;
}
return 0;
}
原子性约束: trigger 回调在 spinlock 或本地中断禁用上下文中执行,禁止调用
kmalloc、msleep、mutex_lock 等可能睡眠的函数。所有硬件寄存器操作必须使用 readl/writel 等原子访问。
4.5 DMA 缓冲区管理策略
DMA 缓冲区是音频数据在内存与硬件之间的传输载体。ALSA 提供 snd_dma_buffer 结构体统一管理,驱动需根据硬件特性选择合适的分配策略。
4.5.1 缓冲区分配方式
| 分配类型 | API | 适用场景 |
|---|---|---|
| 一致性 DMA | snd_dma_alloc_pages(SNDRV_DMA_TYPE_DEV, ...) | 大多数 SoC 内置 DMA 控制器,无需 cache 同步 |
| 流式 DMA | dma_alloc_coherent() + 手动 sync | 外置 DMA 控制器或需要 cache 协同的场景 |
| 连续物理内存 | snd_dma_alloc_pages(SNDRV_DMA_TYPE_CONTINUOUS, ...) | 不支持 scatter-gather 的简单 DMA 引擎 |
| Scatter-Gather | snd_dma_alloc_pages(SNDRV_DMA_TYPE_SG, ...) | 支持 SG 模式的 DMA 引擎,允许非连续物理页 |
4.5.2 周期(Period)与缓冲区大小关系
DMA 缓冲区被划分为多个周期(period),每个周期传输完成触发一次中断。周期大小直接影响中断频率和延迟:
- 小周期(如 64 帧):中断频率高,延迟低(适合低延迟录音/播放),但 CPU 占用率上升。
- 大周期(如 1024 帧):中断频率低,CPU 开销小,但延迟增大(适合音乐播放)。
- 典型配置:缓冲区大小 = 周期大小 × 周期数(通常 2~4 个周期)。
优化建议: 在 hw_params 中通过
snd_pcm_hw_constraint_pow2 约束周期大小为 2 的幂,便于 DMA 描述符对齐。对于支持硬件 FIFO 的控制器,可将周期大小设置为 FIFO 深度的一半,减少下溢/上溢风险。
4.6 回调执行流程与 DMA 中断关系
下图展示了从用户空间调用 snd_pcm_prepare 到 DMA 中断处理的完整回调链:
关键理解: hw_params 只执行一次(参数不变时),prepare 和 trigger 在每次 start/stop 周期中重复执行。DMA 中断处理函数中调用
snd_pcm_period_elapsed() 通知 ALSA 核心更新 appl_ptr,触发下一次数据搬运。
4.7 典型 DMA 缓冲区布局与指针管理
ALSA 使用两个指针管理 DMA 传输:
- hw_ptr(硬件指针):由 DMA 控制器自动更新,表示当前传输到缓冲区的哪个位置。
- appl_ptr(应用指针):由用户空间通过
snd_pcm_mmap_commit更新,表示应用程序已写入/读取的位置。
驱动通过 pointer 回调返回当前 hw_ptr 值,ALSA 核心据此计算延迟和可用帧数:
static snd_pcm_uframes_t my_pcm_pointer(struct snd_pcm_substream *substream)
{
struct my_runtime *runtime = substream->runtime;
u32 dma_pos;
/* 读取 DMA 控制器的当前传输位置(字节偏移) */
dma_pos = my_dma_get_current_position(substream);
/* 转换为帧数并返回 */
return bytes_to_frames(runtime, dma_pos) % runtime->buffer_size;
}
中断优化: 如果 DMA 控制器支持硬件指针寄存器,优先使用
pointer 回调读取,避免在中断处理中计算位置。对于环形缓冲区,确保 hw_ptr 在 wrap-around 时正确取模。
4.8 本章小结
| 回调/组件 | 核心职责 | 性能关键点 |
|---|---|---|
hw_params | 分配 DMA 缓冲区,固化硬件参数 | 缓冲区大小选择影响中断频率 |
prepare | 重置 DMA 状态,配置描述符 | 避免重复分配,预构建描述符 |
trigger | 原子启停 DMA 传输 | 必须无睡眠,寄存器操作要快 |
pointer | 返回当前硬件指针 | 直接读取寄存器,避免计算 |
| DMA 缓冲区 | 音频数据在内存与硬件间的桥梁 | 周期大小决定延迟与 CPU 负载 |