3. 内核音频驱动框架:核心数据结构

Linux ALSA(Advanced Linux Sound Architecture)驱动框架以层次化、面向对象的方式组织音频设备。驱动开发者通过操作四个核心数据结构——snd_card、snd_pcm、snd_control、snd_rawmidi——来完成声卡的探测、注册与数据传输。本章深入剖析这些结构体的职责、关联关系与初始化流程。

核心要点:ALSA 驱动模型遵循“一个 snd_card 对应一个物理声卡,一个 snd_pcm 对应一个音频流方向”的原则。理解这四个结构体的生命周期与互操作关系,是编写稳定音频驱动的基石。

3.1 snd_card:声卡实例的根容器

snd_card 是 ALSA 驱动中最顶层的结构体,代表一个物理声卡或逻辑音频设备。每个声卡驱动在探测阶段必须创建且仅创建一个 snd_card 实例,作为所有子组件的挂载点。

3.1.1 结构体核心字段

字段 类型 说明
number int 声卡编号,由 ALSA 核心自动分配,对应 /dev/snd/controlC0 中的数字
id char[16] 声卡标识字符串,如 "HDA-Intel",用于用户空间识别
driver char[16] 驱动名称,如 "hda-intel"
devices struct list_head 挂载所有子设备(pcm、control、rawmidi 等)的链表头
controls_list struct list_head 挂载所有 control 元素的链表
private_data void * 驱动私有数据指针,通常指向芯片专有结构体

3.1.2 创建与注册流程

  1. 分配:调用 snd_card_new() 分配 snd_card 实例,传入父设备指针、声卡索引、驱动 ID 等参数。
  2. 挂载子设备:依次创建 PCM、Control、RawMIDI 等子设备,并通过 snd_device_new() 注册到 snd_card 的 devices 链表。
  3. 注册:调用 snd_card_register() 将声卡暴露给用户空间,此时 /dev/snd/ 下生成对应的设备节点。
  4. 释放:在驱动卸载时调用 snd_card_free() 递归释放所有子设备与自身内存。
工程经验:在 snd_card_new() 与 snd_card_register() 之间,驱动应完成所有子设备的创建与初始化。一旦注册,用户空间即可访问设备,未就绪的组件可能导致应用程序崩溃。
snd_card 生命周期与子设备挂载 snd_card snd_pcm (播放) snd_pcm (捕获) snd_control snd_rawmidi snd_hwdep snd_card 通过 devices 链表管理所有子设备,每个子设备通过 snd_device_new() 注册

3.2 snd_pcm:音频数据流的核心引擎

snd_pcm 是 ALSA 驱动中处理音频数据传输的核心结构体。每个 snd_pcm 实例代表一个方向(播放或捕获)的 PCM 设备,驱动通常为每个声卡创建两个 snd_pcm 实例(一个播放、一个捕获)。

3.2.1 结构体层次

snd_pcm 内部包含两个关键子结构体:

  • snd_pcm_str:表示一个 PCM 流方向(播放或捕获),包含硬件参数约束、运行时状态、DMA 缓冲区描述等。
  • snd_pcm_substream:表示一个 PCM 子流,对应一个打开的文件描述符。一个 snd_pcm_str 可以包含多个 substream,用于多路复用。

3.2.2 关键操作回调

回调函数 触发时机 典型实现
open 用户打开 PCM 设备节点时 分配 substream 私有数据,初始化硬件指针
hw_params 用户设置采样率、格式、通道数时 验证参数合法性,配置 DMA 引擎,分配 DMA 缓冲区
prepare 每次开始传输前 重置 DMA 指针,清空 FIFO,配置中断
trigger 开始/停止/暂停传输时 启动或停止 DMA 传输,使能/禁用中断
pointer ALSA 核心轮询当前硬件位置时 返回当前 DMA 传输位置(字节偏移)
常见陷阱:hw_params 回调中分配的 DMA 缓冲区必须在 hw_free 回调中释放。如果驱动在 hw_params 中申请了内存但未在 hw_free 中释放,会导致内存泄漏。此外,trigger 回调运行在原子上下文中,禁止睡眠或调用可能睡眠的函数。

3.2.3 创建 PCM 实例的代码模板

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

    // 创建 PCM 实例,方向为播放+捕获
    err = snd_pcm_new(card, "My PCM", device, 1, 1, &pcm);
    if (err < 0)
        return err;

    // 设置操作回调集
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &my_playback_ops);
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &my_capture_ops);

    // 分配 DMA 缓冲区(预分配)
    snd_pcm_lib_preallocate_pages_for_all(pcm,
        SNDRV_DMA_TYPE_DEV_SG,
        card->dev,
        64*1024,    // 最大缓冲区大小
        64*1024);   // 最小缓冲区大小

    return 0;
}
snd_pcm 内部层次与数据流 snd_pcm snd_pcm_str (播放) snd_pcm_str (捕获) substream #0 substream #1 substream #0 substream #1 每个 substream 包含独立的运行时状态(snd_pcm_runtime)和 DMA 缓冲区

3.3 snd_control:混音器与参数控制接口

snd_control 是 ALSA 中用于控制音频参数(音量、静音、采样率选择、路由等)的接口。每个 control 元素(snd_kcontrol)代表一个可读写的控制点,用户空间通过 alsamixer 或 amixer 工具访问。

3.3.1 snd_kcontrol 结构体

字段 说明
id 控制元素的唯一标识,包含名称、索引、设备号等
vd 值域描述,定义可读/写权限、最小值、最大值、步进
get 回调函数,读取当前控制值
put 回调函数,写入控制值
private_value 驱动私有数据,通常用于存储硬件寄存器地址或掩码

3.3.2 创建 control 元素的步骤

  1. 定义 snd_kcontrol_new 模板,填充名称、回调函数、权限标志。
  2. 调用 snd_ctl_new1() 分配 snd_kcontrol 实例。
  3. 调用 snd_ctl_add() 将 control 元素注册到 snd_card 的 controls_list 链表。
static int mycard_add_volume_control(struct snd_card *card)
{
    struct snd_kcontrol *kctl;
    struct snd_kcontrol_new vol_control = {
        .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
        .name = "Master Playback Volume",
        .index = 0,
        .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
        .info = my_volume_info,
        .get = my_volume_get,
        .put = my_volume_put,
        .private_value = (unsigned long)&my_hw_reg,
    };

    kctl = snd_ctl_new1(&vol_control, card);
    if (!kctl)
        return -ENOMEM;

    return snd_ctl_add(card, kctl);
}
命名规范:Control 元素的名称遵循 ALSA 约定,如 "Master Playback Volume"、"Capture Switch"、"PCM Playback Volume"。用户空间工具依赖名称字符串来识别控制点,命名错误会导致混音器无法正确映射。

3.4 snd_rawmidi:MIDI 设备支持

snd_rawmidi 结构体用于支持 MIDI 输入/输出设备。虽然现代音频驱动中 MIDI 使用率下降,但在专业音频设备和合成器中仍然是必备功能。

3.4.1 核心字段与操作

组件 说明
snd_rawmidi 顶层结构体,包含输入/输出流列表
snd_rawmidi_str 表示输入或输出方向,包含 substream 链表
snd_rawmidi_substream 表示一个打开的 MIDI 连接,包含缓冲区与回调
open/close 子流打开/关闭回调
trigger 启动/停止 MIDI 传输

3.4.2 创建 rawmidi 设备

static int mycard_rawmidi_new(struct snd_card *card, int device)
{
    struct snd_rawmidi *rmidi;
    int err;

    err = snd_rawmidi_new(card, "My MIDI", device,
                          1,    // 输出 substream 数量
                          1,    // 输入 substream 数量
                          &rmidi);
    if (err < 0)
        return err;

    // 设置操作回调
    snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_OUTPUT, &my_midi_output_ops);
    snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_INPUT, &my_midi_input_ops);

    rmidi->private_data = my_midi_chip;
    return 0;
}

3.5 四者关系总结

四个核心数据结构在 ALSA 驱动中形成树形依赖关系:

  • snd_card 是根节点,负责生命周期管理与设备注册。
  • snd_pcm 处理音频 PCM 数据流,是驱动中性能最关键的组件。
  • snd_control 提供参数控制接口,与 snd_pcm 配合实现音量、路由等控制。
  • snd_rawmidi 处理 MIDI 数据,独立于 PCM 数据路径。
ALSA 核心数据结构关系总览 snd_card snd_pcm snd_control snd_rawmidi 音频数据流 混音器/控制 MIDI 数据
驱动开发要点:在 probe 函数中,正确的创建顺序是:snd_card_new → snd_pcm_new → snd_ctl_add(多个)→ snd_rawmidi_new → snd_card_register。任何子设备创建失败都应调用 snd_card_free 回滚,避免留下半初始化的声卡。