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 挂载到声卡对象上,否则注册后无法动态添加。
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 内部执行的操作
- 设备编号分配: 如果
snd_card_new时idx为 -1,此时分配最终的 card number - proc 文件系统创建: 在
/proc/asound/cardX/下创建信息节点 - 设备文件创建: 在
/dev/snd/下创建controlCx设备节点 - 子设备注册回调: 遍历声卡上所有通过
snd_device_new挂载的设备,依次调用其dev_register回调 - 添加到 ALSA 全局列表: 将声卡加入
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 回调中自行释放。