3. ALSA 架构核心对象

ALSA(Advanced Linux Sound Architecture)在驱动层抽象出若干核心对象,它们共同构成了声卡驱动的骨架。理解这些对象的职责、生命周期与关联关系,是编写正确、可维护的 ALSA 驱动的第一步。本章将逐一剖析 snd_card、snd_device、snd_pcm、snd_control 和 snd_rawmidi 这五个核心对象。

3.1 核心对象全景图

下面的 SVG 展示了 ALSA 驱动中核心对象的层次结构与关联关系。一个 snd_card 作为顶层容器,内部通过 snd_device 链表管理多个子组件,其中 snd_pcm、snd_control、snd_rawmidi 是最主要的三种功能组件。

ALSA 核心对象架构图 ALSA 核心对象架构图 snd_card(声卡实例) snd_device 链表(设备注册与管理) snd_pcm PCM 数据流 snd_control 混音/控制接口 snd_rawmidi MIDI 传输 snd_pcm_substream snd_pcm_ops snd_pcm_hardware DMA buffer snd_kcontrol snd_ctl_elem_id snd_ctl_elem_value put/get 回调 snd_rawmidi_substream snd_rawmidi_ops 输入/输出缓冲区 中断处理 说明 snd_card 通过 snd_device 链表统一管理所有子组件 每个子组件在注册时自动挂入链表,卸载时统一释放

3.2 snd_card:声卡实例的根容器

snd_card 是 ALSA 驱动中最高层级的抽象,代表一个物理或逻辑声卡。每个声卡驱动在 probe 阶段必须首先创建并初始化一个 snd_card 实例。

3.2.1 核心职责

  • 生命周期管理:作为整个声卡实例的根对象,负责协调所有子组件的创建、注册与销毁。
  • 资源容器:持有声卡编号(card number)、设备结构体指针、组件链表头等全局信息。
  • 用户空间接口:通过 /dev/snd/controlC0 等设备文件与用户态通信。

3.2.2 关键 API

函数 作用 调用时机
snd_card_new() 分配并初始化 snd_card 结构体 probe 函数最开头
snd_card_register() 向 ALSA 核心注册声卡,创建设备节点 所有子组件创建完成后
snd_card_free() 释放声卡及其所有子组件资源 remove 函数或错误处理路径
snd_card_disconnect() 断开声卡与用户空间的连接(热拔插场景) 设备意外移除时
最佳实践: 在 probe 函数中,建议先调用 snd_card_new(),然后依次创建 PCM、Control、MIDI 等子组件,最后调用 snd_card_register()。这种“先构造、后注册”的模式可以避免用户空间在组件未就绪时访问设备。

3.3 snd_device:统一的设备管理链表

snd_device 是 ALSA 内部用于统一管理各种子组件的链表节点。它本身不直接暴露给用户空间,而是作为驱动开发者的内部基础设施。

3.3.1 数据结构

struct snd_device {
    struct list_head list;          // 链表节点,挂入 snd_card->devices
    struct snd_card *card;          // 所属声卡
    snd_device_type_t type;         // 设备类型(PCM/Control/MIDI/...)
    void *device_data;              // 指向具体子组件实例的指针
    struct snd_device_ops *ops;     // 操作函数集(dev_free/dev_register/dev_disconnect)
};

3.3.2 设备类型枚举

类型宏 对应组件 说明
SNDRV_DEV_PCM snd_pcm PCM 播放/录音设备
SNDRV_DEV_CONTROL snd_control 混音器与控件接口
SNDRV_DEV_RAWMIDI snd_rawmidi 原始 MIDI 设备
SNDRV_DEV_TIMER snd_timer 定时器设备
SNDRV_DEV_INFO snd_info_entry procfs 信息节点
SNDRV_DEV_JACK snd_jack 插拔检测
关键理解: snd_device 链表使得 ALSA 核心可以在 snd_card_free() 时遍历链表,按注册顺序的逆序自动调用每个组件的 dev_free 回调,从而避免资源泄漏。驱动开发者通常不需要直接操作 snd_device,而是通过 snd_pcm_new()、snd_ctl_create() 等封装函数间接使用。

3.4 snd_pcm:PCM 数据流引擎

snd_pcm 是 ALSA 驱动中最核心、最复杂的对象,负责管理音频数据的数字采样流(Pulse Code Modulation)。它直接面向音频数据的传输,是驱动开发中工作量最大的部分。

3.4.1 层次结构

一个 snd_pcm 实例内部包含两个关键子结构:

  • snd_pcm_substream:代表一个单向数据流(播放或录音)。一个 PCM 设备通常包含两个 substream(一个播放、一个录音),也可能有多个。
  • snd_pcm_ops:操作函数集,驱动开发者需要实现 open、close、hw_params、prepare、trigger、pointer 等回调。

3.4.2 核心回调函数

回调 触发时机 驱动职责
open 用户打开 PCM 设备时 初始化硬件、分配运行时数据结构
hw_params 用户设置采样参数时 验证并配置采样率、格式、通道数、DMA 缓冲区
prepare 每次启动传输前 重置 DMA 指针、清空 FIFO、准备硬件
trigger 启动/停止/暂停时 操作硬件寄存器开始或停止 DMA 传输
pointer 周期性查询当前位置 返回当前 DMA 传输的硬件指针位置

3.4.3 创建流程

// 1. 创建 PCM 设备实例
struct snd_pcm *pcm;
snd_pcm_new(card, "My PCM", 0, 1, 1, &pcm);
// 参数:card, 名称, 设备编号, 播放流数, 录音流数, 输出指针

// 2. 设置操作函数集
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. 设置硬件描述信息
snd_pcm_hardware my_hw = {
    .info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_INTERLEAVED,
    .formats = SNDRV_PCM_FMTBIT_S16_LE,
    .rates = SNDRV_PCM_RATE_48000,
    .channels_min = 2,
    .channels_max = 2,
};
// 在 open 回调中赋值给 runtime->hw

3.5 snd_control:混音与控制接口

snd_control 提供了用户空间访问硬件控制能力的通道,例如音量调节、静音开关、输入源选择、均衡器设置等。它通过 /dev/snd/controlC0 设备文件与用户态交互。

3.5.1 控件(kcontrol)模型

每个控制功能被抽象为一个 snd_kcontrol 实例,包含:

  • ID 信息:名称(如 "Master Volume")、索引、类型(布尔/整数/枚举)。
  • 值访问回调:get 回调读取当前值,put 回调写入新值。
  • 信息回调:info 回调返回控件的取值范围、枚举列表等元数据。

3.5.2 常用控件类型

类型 典型用途 值范围
SNDRV_CTL_ELEM_TYPE_BOOLEAN 静音开关、通道切换 0 或 1
SNDRV_CTL_ELEM_TYPE_INTEGER 音量、增益、均衡器 自定义 min/max/step
SNDRV_CTL_ELEM_TYPE_ENUMERATED 输入源选择、输出路由 枚举字符串列表
SNDRV_CTL_ELEM_TYPE_BYTES 私有数据、效果器参数 字节数组
SNDRV_CTL_ELEM_TYPE_IEC958 S/PDIF 数字音频配置 IEC958 结构体

3.5.3 创建控件示例

static int my_ctl_info(struct snd_kcontrol *kcontrol,
                       struct snd_ctl_elem_info *uinfo) {
    uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
    uinfo->count = 2;  // 左右声道
    uinfo->value.integer.min = 0;
    uinfo->value.integer.max = 100;
    return 0;
}

static int my_ctl_get(struct snd_kcontrol *kcontrol,
                      struct snd_ctl_elem_value *ucontrol) {
    ucontrol->value.integer.value[0] = 80;  // 左声道
    ucontrol->value.integer.value[1] = 80;  // 右声道
    return 0;
}

static int my_ctl_put(struct snd_kcontrol *kcontrol,
                      struct snd_ctl_elem_value *ucontrol) {
    int left = ucontrol->value.integer.value[0];
    int right = ucontrol->value.integer.value[1];
    // 写入硬件寄存器
    return 1;  // 返回值 1 表示值已改变
}

// 注册控件
static struct snd_kcontrol_new my_control = {
    .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
    .name = "My Volume",
    .info = my_ctl_info,
    .get = my_ctl_get,
    .put = my_ctl_put,
};
snd_ctl_add(card, snd_ctl_new1(&my_control, NULL));

3.6 snd_rawmidi:MIDI 传输通道

snd_rawmidi 提供了原始 MIDI 字节流的传输能力,用于连接 MIDI 键盘、合成器、音序器等设备。它支持双向通信,每个方向独立管理。

3.6.1 核心特性

  • 双方向流:每个 snd_rawmidi 实例包含输入(接收)和输出(发送)两个 substream。
  • 字节流模式:传输的是原始 MIDI 字节(状态字节 + 数据字节),不解析 MIDI 协议。
  • 中断驱动:通常基于 UART 或 USB 中断接收/发送数据。

3.6.2 操作函数集

回调 说明
open 打开 MIDI 子流,初始化硬件
close 关闭 MIDI 子流,释放资源
trigger 启动/停止 MIDI 传输
drain 等待输出缓冲区数据全部发送完成

3.6.3 创建示例

struct snd_rawmidi *rmidi;
snd_rawmidi_new(card, "My MIDI", 0, 1, 1, &rmidi);
// 参数:card, 名称, 设备编号, 输出流数, 输入流数, 输出指针

// 设置操作函数集
static struct snd_rawmidi_ops my_midi_output_ops = {
    .open = my_midi_open,
    .close = my_midi_close,
    .trigger = my_midi_output_trigger,
    .drain = my_midi_drain,
};
snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_OUTPUT,
                    &my_midi_output_ops);

3.7 对象生命周期与交互流程

下面的 SVG 展示了从驱动加载到声卡注册完成的核心对象创建与交互流程,帮助理解各对象的创建顺序和依赖关系。

ALSA 核心对象创建流程 ALSA 核心对象创建与注册流程 1. probe() 入口 2. snd_card_new() 3. 创建子组件(snd_pcm_new / snd_ctl_add / snd_rawmidi_new) snd_pcm snd_control snd_rawmidi 4. snd_card_register() 至此,声卡设备对用户空间可见,应用程序可通过 ALSA lib 访问

3.8 核心对象对比总结

对象 职责 用户空间接口 驱动开发工作量 关键回调数
snd_card 声卡容器与生命周期管理 /dev/snd/controlC* 低(框架调用) 0(无回调)
snd_device 内部链表管理,统一注册/释放 无直接接口 极低(间接使用) 3(free/register/disconnect)
snd_pcm 音频数据流传输 /dev/snd/pcmC*D* 高(核心工作量) 6+(open/close/hw_params/prepare/trigger/pointer)
snd_control 混音与硬件控制 /dev/snd/controlC* 中(控件数量决定) 3(info/get/put)
snd_rawmidi MIDI 字节流传输 /dev/snd/midiC*D* 中低(协议简单) 4(open/close/trigger/drain)
常见误区: 初学者容易混淆 snd_card 和 snd_pcm 的关系。一个 snd_card 可以包含多个 snd_pcm 实例(例如 HDMI 音频和模拟音频各一个 PCM 设备),而每个 snd_pcm 又包含多个 substream。务必理清“声卡 → PCM 设备 → 子流”的三层嵌套关系。

3.9 本章小结

  • snd_card 是声卡的根容器,负责生命周期管理,通过 snd_card_new() 创建、snd_card_register() 注册。
  • snd_device 是内部链表节点,实现子组件的统一管理,驱动开发者通常不直接操作。
  • snd_pcm 是音频数据传输的核心,驱动开发工作量最大,需要实现 hw_params、trigger、pointer 等关键回调。
  • snd_control 提供混音与控制能力,通过 snd_kcontrol 抽象每个控制项,支持布尔、整数、枚举等多种类型。
  • snd_rawmidi 处理原始 MIDI 字节流,结构相对简单,适合作为入门 ALSA 驱动的练习对象。