4. 声卡创建与注册:snd_card_new、snd_device_new、snd_card_register 的调用流程与参数详解

在 ALSA 驱动框架中,声卡的创建与注册是驱动初始化的核心步骤。一个完整的声卡生命周期包含三个关键阶段:分配声卡对象(snd_card_new)、挂载子设备(snd_device_new)、注册声卡(snd_card_register)。本章将深入解析这三个 API 的调用流程、参数含义及典型使用模式。

核心要点: ALSA 采用“先构造、后注册”的设计哲学。在 snd_card_register 调用之前,所有子设备(PCM、Control、MIDI 等)必须通过 snd_device_new 挂载到声卡对象上,否则注册后无法动态添加。
声卡创建与注册三阶段流程 阶段一:分配 snd_card_new() 创建 snd_card 对象 阶段二:挂载 snd_device_new() 添加 PCM/Control 等 阶段三:注册 snd_card_register() 暴露给用户空间 ⚠ 关键约束 • snd_card_new → snd_device_new(可多次)→ snd_card_register 顺序不可颠倒 • 注册后不可通过 snd_device_new 添加新设备,必须使用 snd_card_register 前完成所有挂载 • 每个 snd_card 对应 /dev/snd/ 下的一个 controlCx 设备节点

4.1 snd_card_new:声卡对象的分配与初始化

snd_card_new 是声卡生命周期的起点,负责在内存中分配 struct snd_card 结构体,并完成基本字段的初始化。该函数通常在驱动的 probe 函数中第一个调用。

4.1.1 函数原型与参数

int snd_card_new(struct device *parent, int idx, const char *xid,
                 struct module *module, int extra_size,
                 struct snd_card **card_ret);
参数 类型 说明 典型值 / 注意事项
parent struct device * 父设备指针,通常是平台设备或 PCI 设备 不能为 NULL,用于设备模型层级关联
idx int 声卡编号(card index) -1 表示由 ALSA 自动分配;非负值强制指定
xid const char * 声卡标识字符串(ID string) 用于 ALSA 配置文件匹配,如 "hw:0,0" 中的标识
module struct module * 所属内核模块 通常传入 THIS_MODULE
extra_size int 额外私有数据大小(字节) 驱动私有结构体大小,分配在 snd_card 之后连续内存
card_ret struct snd_card ** 输出参数,返回分配的 snd_card 指针 后续所有操作都依赖此指针
最佳实践: 推荐将 idx 设为 -1,让 ALSA 子系统自动管理声卡编号,避免与其他驱动冲突。extra_size 通常设置为 sizeof(struct my_chip),然后通过 snd_card_get_private_data(card) 获取私有数据指针。

4.1.2 典型调用示例

static int my_audio_probe(struct platform_device *pdev)
{
    struct snd_card *card;
    struct my_chip *chip;
    int ret;

    /* 第一步:创建声卡对象 */
    ret = snd_card_new(&pdev->dev, -1, NULL, THIS_MODULE,
                       sizeof(struct my_chip), &card);
    if (ret < 0) {
        dev_err(&pdev->dev, "snd_card_new failed: %d\n", ret);
        return ret;
    }

    /* 获取私有数据指针 */
    chip = snd_card_get_private_data(card);
    chip->card = card;
    chip->pdev = pdev;

    /* ... 后续初始化 ... */
}

4.2 snd_device_new:子设备挂载机制

声卡对象创建完成后,需要将 PCM 流、Control 接口、MIDI 端口等子设备挂载到声卡上。snd_device_new 是 ALSA 提供的通用设备注册接口,它将一个设备对象与声卡关联,并指定设备类型和操作回调。

4.2.1 函数原型与参数

int snd_device_new(struct snd_card *card, enum snd_device_type type,
                   void *device_data, const struct snd_device_ops *ops);
参数 类型 说明
card struct snd_card * 目标声卡对象指针(由 snd_card_new 返回)
type enum snd_device_type 设备类型枚举,如 SNDRV_DEV_PCM、SNDRV_DEV_CONTROL
device_data void * 设备实例指针,如 struct snd_pcm *
ops const struct snd_device_ops * 设备操作回调结构体,包含 dev_register、dev_disconnect、dev_free

4.2.2 snd_device_ops 回调结构

struct snd_device_ops {
    int (*dev_register)(struct snd_device *dev);
    int (*dev_disconnect)(struct snd_device *dev);
    int (*dev_free)(struct snd_device *dev);
};
  • dev_register:在 snd_card_register 时被调用,用于创建设备文件节点或注册到 ALSA 核心
  • dev_disconnect:设备热拔插时调用,清理硬件资源
  • dev_free:声卡释放时调用,释放设备私有内存
常见错误: 开发者常忘记设置 dev_free 回调,导致声卡卸载时内存泄漏。即使设备不需要特殊清理,也应提供一个空函数或使用 ALSA 提供的默认回调。

4.2.3 挂载 PCM 设备的典型流程

/* 定义 PCM 设备操作回调 */
static const struct snd_device_ops pcm_ops = {
    .dev_register = snd_pcm_dev_register,
    .dev_disconnect = snd_pcm_dev_disconnect,
    .dev_free = snd_pcm_dev_free,
};

/* 创建 PCM 实例 */
struct snd_pcm *pcm;
ret = snd_pcm_new(card, "my_pcm", 0, 1, 1, &pcm);
if (ret < 0)
    goto err_pcm;

/* snd_pcm_new 内部已调用 snd_device_new,无需手动调用 */
/* 但如果是自定义设备类型,则需要手动调用: */
/* ret = snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &my_ops); */
注意: 对于标准设备类型(PCM、Control、MIDI、Timer 等),ALSA 提供了专用的创建函数(如 snd_pcm_new、snd_ctl_create),这些函数内部已经封装了 snd_device_new 调用。只有在注册自定义低层级设备(SNDRV_DEV_LOWLEVEL)时才需要直接调用 snd_device_new。

4.3 snd_card_register:声卡注册与用户空间暴露

当所有子设备挂载完成后,调用 snd_card_register 将声卡正式注册到 ALSA 核心,并创建对应的设备文件节点。此调用之后,用户空间的应用程序(如 aplay、arecord)才能访问该声卡。

4.3.1 函数原型与参数

int snd_card_register(struct snd_card *card);
参数 类型 说明
card struct snd_card * 已挂载完子设备的声卡对象

返回值: 0 表示成功,负数表示错误码。

4.3.2 注册过程中 ALSA 内部执行的操作

  1. 设备编号分配: 如果 snd_card_new 时 idx 为 -1,此时分配最终的 card number
  2. proc 文件系统创建: 在 /proc/asound/cardX/ 下创建信息节点
  3. 设备文件创建: 在 /dev/snd/ 下创建 controlCx 设备节点
  4. 子设备注册回调: 遍历声卡上所有通过 snd_device_new 挂载的设备,依次调用其 dev_register 回调
  5. 添加到 ALSA 全局列表: 将声卡加入 snd_cards 全局数组,使其可被用户空间发现
snd_card_register 内部执行流程 snd_card_register() ① 分配最终 card number(若 idx==-1) ② 创建 /proc/asound/cardX 节点 ③ 创建 /dev/snd/controlCx 设备节点 ④ 遍历子设备,调用每个设备的 dev_register 回调 ⑤ 加入 snd_cards 全局列表

4.3.3 完整 probe 函数示例

static int my_audio_probe(struct platform_device *pdev)
{
    struct snd_card *card;
    struct my_chip *chip;
    struct snd_pcm *pcm;
    int ret;

    /* 1. 创建声卡 */
    ret = snd_card_new(&pdev->dev, -1, NULL, THIS_MODULE,
                       sizeof(struct my_chip), &card);
    if (ret < 0)
        return ret;

    chip = snd_card_get_private_data(card);
    chip->card = card;

    /* 2. 创建 Control 接口(内部调用 snd_device_new) */
    ret = snd_ctl_create(card);
    if (ret < 0)
        goto err_ctl;

    /* 3. 创建 PCM 设备(内部调用 snd_device_new) */
    ret = snd_pcm_new(card, "my_pcm", 0, 1, 1, &pcm);
    if (ret < 0)
        goto err_pcm;

    /* 4. 设置 PCM 操作函数集 */
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &my_playback_ops);
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &my_capture_ops);

    /* 5. 注册声卡 */
    ret = snd_card_register(card);
    if (ret < 0)
        goto err_register;

    platform_set_drvdata(pdev, card);
    return 0;

err_register:
    snd_pcm_free(pcm);
err_pcm:
err_ctl:
    snd_card_free(card);
    return ret;
}

4.4 三阶段调用流程总结

阶段 函数 核心作用 调用时机 失败处理
分配 snd_card_new 分配 snd_card 结构体及私有数据内存 probe 函数最开头 直接返回错误码
挂载 snd_device_new(或专用封装函数) 将 PCM、Control 等子设备挂载到声卡 snd_card_new 之后,register 之前 跳转到错误标签,调用 snd_card_free
注册 snd_card_register 暴露声卡给用户空间,创建设备文件 所有子设备挂载完成后 清理已挂载的子设备,调用 snd_card_free
调试技巧: 在 snd_card_register 成功后,可以通过 cat /proc/asound/cards 查看声卡是否被正确识别。如果声卡未出现,检查 dmesg 中的错误信息,常见原因包括:snd_device_new 参数错误、dev_register 回调失败、设备编号冲突等。

4.5 声卡释放与错误路径

当驱动卸载或 probe 过程中发生错误时,需要正确释放声卡资源。ALSA 提供了两个释放函数:

  • snd_card_free:同步释放声卡,等待所有引用释放后销毁
  • snd_card_free_when_closed:延迟释放,等待所有打开的文件句柄关闭后再销毁
static int my_audio_remove(struct platform_device *pdev)
{
    struct snd_card *card = platform_get_drvdata(pdev);
    snd_card_free(card);  /* 自动调用所有子设备的 dev_free 回调 */
    return 0;
}
注意事项: snd_card_free 会自动遍历声卡上所有通过 snd_device_new 挂载的设备,并调用其 dev_free 回调。因此,驱动不需要手动释放 PCM 或 Control 实例,只需调用 snd_card_free 即可。但私有数据(如 DMA 缓冲区、中断号等)需要在 dev_free 回调中自行释放。