3. ALSA 核心数据结构:snd_card、snd_pcm、snd_control、snd_rawmidi、snd_device

ALSA 框架在 Linux 内核中通过一组核心数据结构来抽象和管理音频硬件。理解这些结构是编写音频驱动的基础。本章将深入剖析 snd_card、snd_pcm、snd_control、snd_rawmidi 和 snd_device 这五个关键对象,阐明它们在驱动中的角色、生命周期以及相互之间的关联。

3.1 顶层容器:snd_card

snd_card 是整个声卡的顶层管理结构,代表一个物理或逻辑音频设备。每个声卡驱动在初始化时,必须首先创建并注册一个 snd_card 实例。它负责管理该声卡下的所有子组件(PCM、Control、MIDI 等),并提供与用户空间交互的入口。

核心职责:
  • 作为声卡的唯一标识(card number),对应 /dev/snd/controlC0 中的索引。
  • 维护声卡名称、ID、驱动程序信息等元数据。
  • 持有子组件链表,统一管理组件的创建、注册与释放。
  • 提供电源管理回调接口(suspend/resume)。

驱动中通常通过 snd_card_new() 创建实例,并在所有子组件初始化完成后调用 snd_card_register() 使其生效。释放时使用 snd_card_free()。

struct snd_card *card;
int err;

err = snd_card_new(&pdev->dev, index, id, THIS_MODULE, 0, &card);
if (err < 0) {
    dev_err(&pdev->dev, "snd_card_new failed: %d\n", err);
    return err;
}
/* ... 创建 PCM、Control 等子组件 ... */
err = snd_card_register(card);
if (err < 0) {
    snd_card_free(card);
    return err;
}

3.2 音频流核心:snd_pcm

snd_pcm 是 ALSA 中最重要的数据流接口,负责管理音频数据的播放(playback)与录制(capture)。每个 snd_pcm 实例对应一个 PCM 设备文件(如 /dev/snd/pcmC0D0p),并包含两个关键子结构:snd_pcm_substream 和 snd_pcm_ops。

子结构 作用
snd_pcm_substream 代表一个数据流方向(播放或录制),管理硬件参数(采样率、格式、通道数)、运行时状态(snd_pcm_runtime)以及 DMA 缓冲区。
snd_pcm_ops 驱动必须实现的回调函数集合,包括 open、close、hw_params、prepare、trigger、pointer 等,用于控制硬件数据流。
开发要点: 驱动通常调用 snd_pcm_new() 创建 PCM 实例,然后通过 snd_pcm_set_ops() 绑定操作函数。注意区分 playback 和 capture 的 substream 数量,它们可以不同。
struct snd_pcm *pcm;
err = snd_pcm_new(card, "My PCM", 0, 1, 1, &pcm);
if (err < 0) {
    snd_card_free(card);
    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);

3.3 控制通路:snd_control

snd_control 是 ALSA 的控制接口,用于调节音量、静音、切换输入源、配置滤波器等非数据流操作。用户空间的 alsamixer 和 amixer 工具就是通过 Control 接口与驱动交互的。

每个控制项由 snd_kcontrol 结构体表示,包含名称、类型(如 SNDRV_CTL_ELEM_TYPE_INTEGER)、取值范围以及 info、get、put 回调函数。驱动通过 snd_ctl_add() 将控制项注册到声卡。

static struct snd_kcontrol_new my_volume_control = {
    .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
    .name = "Master Playback Volume",
    .info = my_volume_info,
    .get = my_volume_get,
    .put = my_volume_put,
};

err = snd_ctl_add(card, snd_ctl_new1(&my_volume_control, chip));
if (err < 0)
    return err;

3.4 传统 MIDI 接口:snd_rawmidi

snd_rawmidi 用于支持 MIDI 输入/输出设备。它提供原始 MIDI 字节流的传输能力,不解析 MIDI 事件,适合连接合成器、MIDI 键盘等设备。每个 snd_rawmidi 实例包含输入和输出两个子流,分别对应 snd_rawmidi_substream。

驱动通过 snd_rawmidi_new() 创建实例,并实现 snd_rawmidi_ops 中的 open、close、trigger 等回调。数据通过 snd_rawmidi_receive() 和 snd_rawmidi_transmit() 在驱动与 ALSA 中间层之间传递。

注意: 现代 USB MIDI 设备通常使用 snd_usbmidi 驱动,但嵌入式平台上的 UART MIDI 或 FPGA 实现仍常直接使用 snd_rawmidi 接口。

3.5 组件管理基类:snd_device

snd_device 是 ALSA 内部用于统一管理各类子组件的抽象基类。它不直接暴露给用户空间,而是作为链表节点,将 PCM、Control、RawMIDI 等组件串联在 snd_card 的设备链表中。每个组件在创建时,ALSA 核心会自动为其分配一个 snd_device 实例,并注册相应的析构函数。

这种设计使得 snd_card_free() 可以遍历设备链表,按逆序自动释放所有子组件,无需驱动手动管理。驱动开发者通常不需要直接操作 snd_device,但理解其存在有助于把握 ALSA 的组件生命周期。

3.6 核心数据结构关系图

下图展示了五个核心数据结构之间的层次关系与数据流向:

ALSA 核心数据结构关系图 snd_card snd_device 链表(管理所有子组件生命周期) snd_pcm PCM 数据流 snd_control 混音器/控制项 snd_rawmidi MIDI 原始流 snd_pcm_substream 播放/录制方向 snd_pcm_ops 回调 DMA 缓冲区 snd_kcontrol 名称/类型/范围 info/get/put 回调 硬件寄存器映射 snd_rawmidi_substream 输入/输出方向 snd_rawmidi_ops 回调 字节流缓冲区 用户空间接口 /dev/snd/pcmC0D0p | /dev/snd/controlC0 | /dev/snd/midiC0D0

3.7 数据结构对比总结

数据结构 功能定位 用户空间设备节点 关键回调/操作
snd_card 声卡顶层容器 /dev/snd/controlC0 snd_card_new/register/free
snd_pcm 音频数据流(播放/录制) /dev/snd/pcmC0D0p/c open/close/hw_params/trigger/pointer
snd_control 混音器与设备控制 通过 /dev/snd/controlC0 访问 info/get/put
snd_rawmidi 原始 MIDI 字节流 /dev/snd/midiC0D0 open/close/trigger,receive/transmit
snd_device 组件生命周期管理基类 无直接设备节点 内部链表管理,自动析构
驱动开发建议: 编写新驱动时,应优先确定 snd_card 的创建时机,然后按需添加 PCM 和 Control 组件。对于不支持 MIDI 的简单 Codec,可以省略 snd_rawmidi。所有组件注册完成后,务必调用 snd_card_register() 使设备对用户空间可见。