3. 内核音频驱动框架:snd_card、snd_pcm、snd_control 结构体关系

在 Linux ALSA 音频驱动中,snd_card、snd_pcm 和 snd_control 是三个最核心的内核结构体。它们共同构成了声卡驱动的骨架:snd_card 是顶层容器,代表一个物理或逻辑声卡;snd_pcm 负责音频数据流的传输(播放/录音);snd_control 负责混音器、音量、开关等控制通路。理解三者之间的层次关系与生命周期,是编写和调试声卡驱动的第一步。

调试要点: 在驱动探测(probe)阶段,如果 snd_card 创建失败,后续的 PCM 和 Control 设备均无法注册。排查时优先检查 snd_card_new 的返回值以及 card->number 是否冲突。

3.1 核心结构体概述

结构体 头文件 主要职责 生命周期
struct snd_card sound/core.h 声卡顶层管理:设备号、ID、组件列表、电源管理 从 snd_card_new 到 snd_card_free
struct snd_pcm sound/pcm.h PCM 设备:播放/捕获流、硬件参数、DMA 缓冲区 从 snd_pcm_new 到 snd_device_free
struct snd_kcontrol sound/control.h 控制通路:混音器、音量、开关、枚举控件 从 snd_ctl_new1 到 snd_ctl_remove
关键关系: 一个 snd_card 可以包含多个 snd_pcm 实例(例如:播放 + 捕获,或 HDMI + 模拟),也可以包含多个 snd_kcontrol 实例。所有子设备通过 snd_device 链表挂接到 snd_card 的 devices 字段上。

3.2 结构体关系全景图

下面的 SVG 图展示了 snd_card、snd_pcm、snd_kcontrol 之间的包含与引用关系,以及它们与底层硬件、用户空间设备节点的映射。

snd_card / snd_pcm / snd_kcontrol 结构体关系图 ALSA 核心结构体关系图 struct snd_card number, id, driver, devices 链表 private_data → 硬件私有数据 struct snd_pcm streams[2] (playback/capture) ops → open/close/hw_params/prepare struct snd_kcontrol id (iface/name/index) get/put/info 回调函数 snd_device 链表 snd_device 链表 硬件 DMA / I2S / 编解码器 通过 private_data 或 ops 访问 /dev/snd/pcmC0D0p 用户空间访问 /dev/snd/controlC0 用户空间访问 说明:snd_card 通过 snd_device 链表管理 PCM 和 Control 子设备;PCM 负责数据流,Control 负责参数调节;两者最终映射到 /dev/snd/ 下的设备节点。 虚线表示逻辑映射关系,实线表示内核对象包含关系。

3.3 snd_card:声卡顶层容器

struct snd_card 是 ALSA 驱动中最高级别的抽象。每个声卡驱动在探测函数中必须首先创建 snd_card 实例,然后在其上挂载 PCM、Control、MIDI 等子设备。

3.3.1 关键字段

字段 类型 说明
number int 声卡编号,对应 /dev/snd/controlCX 中的 X
id char[16] 声卡标识字符串,如 "HDA Intel"
driver char[16] 驱动名称,用于匹配
devices struct list_head 挂载所有子设备(snd_device)的链表头
private_data void * 指向驱动私有数据结构,如硬件描述符
power_state unsigned int 电源状态管理

3.3.2 创建与释放流程

/* 创建 snd_card */
struct snd_card *card;
int err;

err = snd_card_new(&pdev->dev, index, id, THIS_MODULE,
                   sizeof(struct my_chip), &card);
if (err < 0) {
    dev_err(&pdev->dev, "snd_card_new failed: %d\n", err);
    return err;
}
/* card->private_data 自动指向 my_chip 实例 */

/* ... 注册 PCM、Control 等子设备 ... */

/* 注册声卡,暴露给用户空间 */
err = snd_card_register(card);
if (err < 0) {
    snd_card_free(card);
    return err;
}

/* 释放时 */
snd_card_free(card);
常见调试陷阱: snd_card_new 的 index 参数如果传入 -1,内核会自动分配一个空闲编号。但如果驱动强制指定了一个已被占用的编号,snd_card_new 会返回 -ENODEV。调试时可通过 /proc/asound/cards 查看当前已注册的声卡编号。

3.4 snd_pcm:音频数据流引擎

struct snd_pcm 是 ALSA 中负责音频数据读写的核心对象。每个 PCM 实例可以包含最多两个流:播放(playback) 和 捕获(capture)。每个流又包含一个或多个 substream,对应不同的声道或设备实例。

3.4.1 PCM 层次结构

snd_pcm 层次结构 snd_pcm 层次结构 struct snd_pcm name, device, flags streams[SNDRV_PCM_STREAM_PLAYBACK] 播放流 streams[SNDRV_PCM_STREAM_CAPTURE] 捕获流 substream #0 hw_params, runtime substream #1 hw_params, runtime substream #0 hw_params, runtime ... 更多 每个 substream 对应一个独立的音频流,拥有自己的 hw_params、sw_params、runtime 和 DMA 缓冲区。

3.4.2 PCM 操作集

驱动通过 struct snd_pcm_ops 向 ALSA 核心注册硬件操作回调:

struct snd_pcm_ops {
    int (*open)(struct snd_pcm_substream *substream);
    int (*close)(struct snd_pcm_substream *substream);
    int (*ioctl)(struct snd_pcm_substream *substream,
                 unsigned int cmd, void *arg);
    int (*hw_params)(struct snd_pcm_substream *substream,
                     struct snd_pcm_hw_params *params);
    int (*hw_free)(struct snd_pcm_substream *substream);
    int (*prepare)(struct snd_pcm_substream *substream);
    snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);
    int (*trigger)(struct snd_pcm_substream *substream, int cmd);
};
调试关键: trigger 回调是中断处理的入口点。当 DMA 传输启动或停止时,trigger 被调用,驱动应在此时使能或禁用硬件中断。pointer 回调返回当前 DMA 硬件指针位置,用于 snd_pcm_period_elapsed 的中断处理。

3.5 snd_kcontrol:控制通路接口

struct snd_kcontrol 是 ALSA 控制接口的核心,用于暴露混音器、音量、开关、枚举选择器等用户可调节的参数。每个 kcontrol 通过 get、put、info 三个回调与硬件交互。

3.5.1 控制类型与回调

回调 功能 调用时机
info 返回控件的类型、取值范围、名称等信息 用户空间调用 ioctl(..., SNDRV_CTL_IOCTL_ELEM_INFO)
get 读取当前硬件状态,填充到 snd_ctl_elem_value 用户空间调用 ioctl(..., SNDRV_CTL_IOCTL_ELEM_READ)
put 将用户设置的值写入硬件寄存器 用户空间调用 ioctl(..., SNDRV_CTL_IOCTL_ELEM_WRITE)

3.5.2 创建 kcontrol 示例

static const struct snd_kcontrol_new my_control = {
    .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
    .name = "Master Playback Volume",
    .index = 0,
    .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
    .info = my_ctl_info,
    .get = my_ctl_get,
    .put = my_ctl_put,
};

/* 在探测函数中注册 */
err = snd_ctl_add(card, snd_ctl_new1(&my_control, chip));
if (err < 0) {
    dev_err(card->dev, "failed to add control: %d\n", err);
    return err;
}
调试技巧: 使用 amixer 或 alsamixer 可以列出所有注册的 kcontrol。如果驱动注册了控件但用户空间看不到,检查 snd_ctl_add 的返回值以及 iface 字段是否设置正确(常见错误:混音器控件使用了 SNDRV_CTL_ELEM_IFACE_CARD 而非 _MIXER)。

3.6 三者的协同工作流程

在声卡探测函数中,三个结构体的典型创建顺序如下:

  1. snd_card_new:创建顶层声卡容器,分配编号。
  2. snd_pcm_new:创建 PCM 设备,指定播放/捕获流数量,挂载到 card。
  3. snd_ctl_add:逐个添加控制控件(音量、开关等)。
  4. snd_card_register:将所有子设备注册到 ALSA 核心,创建设备节点。

在中断处理中,三者的协作关系如下:

  • 硬件 DMA 完成一个 period 后触发中断。
  • 中断服务程序调用 snd_pcm_period_elapsed(substream),通知 PCM 核心层。
  • PCM 核心层唤醒等待的进程,触发 pointer 回调更新硬件指针。
  • 如果中断与控件相关(如音量旋钮中断),则调用对应 kcontrol 的 put 回调更新状态,并通过 snd_ctl_notify 通知用户空间。
中断上下文注意事项: snd_pcm_period_elapsed 可以在中断上下文中调用,但 snd_ctl_notify 需要谨慎——它可能会触发用户空间事件,建议在中断下半部(tasklet 或 threaded IRQ)中调用。

3.7 调试检查清单

检查项 命令/方法 预期结果
声卡是否注册 cat /proc/asound/cards 显示声卡编号和名称
PCM 设备是否可见 cat /proc/asound/pcm 列出所有 PCM 设备及流方向
控件是否注册 amixer controls 显示所有 kcontrol 名称
设备节点是否存在 ls -l /dev/snd/ 存在 pcmC0D0p、controlC0 等
中断是否触发 cat /proc/interrupts | grep audio 中断计数持续增长