4. ALSA中的PCM设备模型:PCM设备注册、PCM流(playback/capture)、PCM硬件参数(hw_params)

ALSA 的 PCM 设备模型是音频数据传输的核心抽象。本章深入剖析 PCM 设备的注册流程、Playback/Capture 流的区分机制,以及硬件参数(hw_params)的协商与配置过程。理解这些概念是进行 DMA 传输优化和环形缓冲区设计的基础。

4.1 PCM设备注册流程

在 ALSA 驱动中,PCM 设备的注册遵循一套标准化的步骤。驱动开发者需要填充 snd_pcm_ops 结构体,并调用核心 API 完成注册。

核心注册步骤:
  1. 分配 PCM 实例: 调用 snd_pcm_new() 或 snd_pcm_new_internal() 创建 PCM 设备实例。
  2. 设置操作函数集: 填充 snd_pcm_ops 结构体,包括 open、close、hw_params、prepare、trigger、pointer 等回调。
  3. 设置硬件约束: 通过 snd_pcm_hw_constraint_* 系列函数定义硬件支持的参数范围(如采样率、格式、通道数)。
  4. 注册设备: 调用 snd_card_register() 将 PCM 设备注册到 ALSA 核心层,暴露给用户空间。

以下是一个典型的 PCM 设备注册代码片段:

static const struct snd_pcm_ops my_pcm_ops = {
    .open      = my_pcm_open,
    .close     = my_pcm_close,
    .hw_params = my_pcm_hw_params,
    .hw_free   = my_pcm_hw_free,
    .prepare   = my_pcm_prepare,
    .trigger   = my_pcm_trigger,
    .pointer   = my_pcm_pointer,
};

static int my_pcm_new(struct snd_card *card, int device)
{
    struct snd_pcm *pcm;
    int err;

    // 1. 分配 PCM 实例,支持 playback 和 capture
    err = snd_pcm_new(card, "My PCM Device", device, 1, 1, &pcm);
    if (err < 0)
        return err;

    // 2. 绑定操作函数集
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &my_pcm_ops);
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &my_pcm_ops);

    // 3. 设置硬件约束(示例)
    snd_pcm_hw_constraint_list(pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream,
                               0, SNDRV_PCM_HW_PARAM_RATE, &my_rates);

    return 0;
}
提示: snd_pcm_new() 的第三个参数 playback_count 和第四个参数 capture_count 分别指定该设备支持的 playback 和 capture 子流数量。如果某个方向不支持,则传入 0。

4.2 PCM流:Playback 与 Capture

ALSA 将 PCM 数据流分为两个方向:Playback(播放) 和 Capture(录音)。每个方向对应一个独立的 snd_pcm_substream 实例,拥有各自的 DMA 缓冲区、硬件参数和状态机。

特性 Playback 流 Capture 流
数据方向 用户空间 → 硬件(DAC) 硬件(ADC) → 用户空间
DMA 操作 内存 → 外设(DMA 写) 外设 → 内存(DMA 读)
环形缓冲区角色 用户填充数据,DMA 消费 DMA 填充数据,用户消费
典型应用 音频播放器、语音合成 录音机、语音识别

在驱动内部,snd_pcm_substream 通过 stream 字段区分方向:

struct snd_pcm_substream {
    struct snd_pcm *pcm;
    struct snd_pcm_str *pstr;   // 指向所属的流方向
    int number;                 // 子流编号
    int stream;                 // SNDRV_PCM_STREAM_PLAYBACK 或 SNDRV_PCM_STREAM_CAPTURE
    struct snd_pcm_runtime *runtime;
    // ... 其他字段
};
注意: 虽然 Playback 和 Capture 共享同一个 snd_pcm 设备,但它们的硬件参数(如采样率、格式)可以独立配置。全双工设备需要确保两个方向的参数兼容,否则可能导致硬件冲突。

下面的 SVG 图展示了 Playback 和 Capture 流在 ALSA 框架中的位置与数据流向:

ALSA PCM 流:Playback 与 Capture 数据路径 用户空间应用 ALSA 核心层 Playback 子流 (DMA 写: 内存→DAC) Capture 子流 (DMA 读: ADC→内存) 音频硬件 DAC / ADC 说明: • Playback 流:用户空间写入数据 → ALSA 核心 → DMA 将数据从内存传输到 DAC → 硬件播放。 • Capture 流:ADC 采集数据 → DMA 将数据从硬件传输到内存 → ALSA 核心 → 用户空间读取。 • 两个流共享同一个 PCM 设备,但拥有独立的 DMA 缓冲区和硬件参数。

4.3 PCM硬件参数(hw_params)

hw_params 是 ALSA 驱动中最重要的回调之一。它在用户空间调用 snd_pcm_hw_params() 时被触发,用于协商和配置硬件相关的参数。这些参数直接影响 DMA 传输的效率和音频质量。

4.3.1 关键硬件参数

参数 ALSA 宏定义 说明 典型值
采样率 SNDRV_PCM_HW_PARAM_RATE 每秒采样点数 44100, 48000, 96000
采样格式 SNDRV_PCM_HW_PARAM_FORMAT 每个采样的位宽和编码 S16_LE, S24_LE, FLOAT_LE
通道数 SNDRV_PCM_HW_PARAM_CHANNELS 音频通道数量 1 (Mono), 2 (Stereo), 6 (5.1)
周期大小 SNDRV_PCM_HW_PARAM_PERIOD_SIZE 一次中断传输的帧数 256, 512, 1024
周期数 SNDRV_PCM_HW_PARAM_PERIODS 环形缓冲区中的周期数量 2, 4, 8
缓冲区大小 SNDRV_PCM_HW_PARAM_BUFFER_SIZE 环形缓冲区总帧数 1024, 2048, 4096

4.3.2 hw_params 回调实现

在驱动中,hw_params 回调负责验证用户请求的参数,并配置硬件寄存器。典型的实现包括:

  • 参数验证: 检查请求的采样率、格式、通道数是否在硬件支持范围内。
  • DMA 缓冲区分配: 根据 buffer_size 和 period_size 分配 DMA 内存。
  • 硬件寄存器配置: 设置 I2S 接口的位时钟、帧时钟、数据格式等。
  • DMA 引擎配置: 配置 DMA 传输的源/目的地址、传输宽度、突发长度等。
static int my_pcm_hw_params(struct snd_pcm_substream *substream,
                            struct snd_pcm_hw_params *hw_params)
{
    struct my_pcm_runtime *prtd = substream->runtime->private_data;
    int err;

    // 1. 获取参数
    unsigned int rate = params_rate(hw_params);
    snd_pcm_format_t format = params_format(hw_params);
    unsigned int channels = params_channels(hw_params);
    unsigned int buffer_size = params_buffer_size(hw_params);
    unsigned int period_size = params_period_size(hw_params);

    // 2. 验证参数(示例:仅支持 48000Hz)
    if (rate != 48000)
        return -EINVAL;

    // 3. 分配 DMA 缓冲区
    err = snd_pcm_lib_malloc_pages(substream, buffer_size * 
                                   snd_pcm_format_physical_width(format) / 8 * channels);
    if (err < 0)
        return err;

    // 4. 配置硬件寄存器(伪代码)
    my_hw_set_rate(rate);
    my_hw_set_format(format);
    my_hw_set_channels(channels);

    // 5. 保存参数到运行时结构
    prtd->period_size = period_size;
    prtd->buffer_size = buffer_size;

    return 0;
}
优化建议: 在 hw_params 中尽早分配 DMA 缓冲区,并使用 snd_pcm_lib_malloc_pages() 确保内存对齐。对于嵌入式系统,建议使用 dma_alloc_coherent() 分配非缓存内存,避免 DMA 与 CPU 缓存一致性问题。

4.3.3 硬件参数协商流程

用户空间通过 snd_pcm_hw_params_any() 初始化参数,然后调用 snd_pcm_hw_params_set_* 系列函数设置期望值。ALSA 核心层会调用驱动的 hw_params 回调,并返回最终确定的参数。

下面的 SVG 图展示了从用户空间到驱动的完整参数协商流程:

hw_params 协商流程 1. 用户空间初始化 2. 设置期望参数 3. 调用 hw_params 4. 驱动验证参数 5. 配置硬件寄存器 6. 返回最终参数 详细流程说明: 1. 用户空间调用 snd_pcm_hw_params_any() 初始化参数结构体。 2. 通过 snd_pcm_hw_params_set_rate/set_format/set_channels 设置期望值。 3. 调用 snd_pcm_hw_params() 触发内核驱动的 hw_params 回调。 4. 驱动验证参数是否在硬件支持范围内,若不支持则返回错误。 5. 驱动配置硬件寄存器(时钟、格式、DMA 引擎)并分配 DMA 缓冲区。 6. 最终确定的参数通过 params_rate/params_format 等宏返回给用户空间。 7. 用户空间根据返回的参数调整应用逻辑(如缓冲区大小、延迟计算)。

4.4 硬件约束与参数范围

为了简化用户空间的参数选择,驱动可以通过 硬件约束(Constraints) 定义参数的有效范围。ALSA 提供了多种约束函数:

约束函数 用途 示例
snd_pcm_hw_constraint_list 限制参数为离散值列表 采样率仅支持 44100, 48000
snd_pcm_hw_constraint_minmax 限制参数在最小/最大值之间 缓冲区大小 1024 ~ 65536
snd_pcm_hw_constraint_pow2 限制参数为 2 的幂 周期大小必须是 2^n
snd_pcm_hw_constraint_step 限制参数为步长的整数倍 通道数必须是 2 的倍数
最佳实践: 在驱动初始化时(通常在 probe 函数中)设置硬件约束,而不是在 hw_params 回调中。这样可以提前告知用户空间支持的参数范围,减少参数协商失败的概率。

4.5 本章小结

  • PCM 设备注册遵循 分配 → 设置 ops → 设置约束 → 注册 的标准流程。
  • Playback 和 Capture 流是独立的子流,拥有各自的 DMA 缓冲区和硬件参数。
  • hw_params 回调是参数协商的核心,负责验证、配置和资源分配。
  • 硬件约束机制帮助用户空间快速找到支持的参数组合,提升兼容性。