3、声卡创建与注册:snd_card_new()、snd_card_register() 流程详解,实战编写一个虚拟声卡驱动
好,咱们今天来啃一块硬骨头——声卡的创建与注册。说实话,很多做音频驱动的朋友,搞了很久都搞不清楚 snd_card_new() 和 snd_card_register() 到底干了什么。我当年刚接触 ALSA 时也踩过不少坑,有一次注册完声卡发现设备节点死活不出来,查了两天才发现是传参顺序搞反了……嗯,咱们今天就把这事彻底说透。
3.1 声卡的生命周期:从出生到上岗
一个声卡在 ALSA 框架里,要经历三个关键阶段:
- 创建:调用
snd_card_new(),分配内存,初始化基础结构 - 组装:添加 PCM、控制、MIDI 等子设备
- 注册:调用
snd_card_register(),向系统暴露设备节点
说白了,snd_card_new() 是「造房子打地基」,snd_card_register() 是「挂招牌开门营业」。中间那步「组装」才是真正干活的地方。
3.2 snd_card_new():地基怎么打?
先看原型:
int snd_card_new(struct device *parent, int idx, const char *xid,
struct module *module, int extra_size,
struct snd_card **card_ret);
每个参数我都吃过亏,一个一个说:
| 参数 | 说明 | 我的经验 |
|---|---|---|
| parent | 父设备指针,通常是 platform_device 或 pci_dev | 传 NULL 也能跑,但 sysfs 层级会乱 |
| idx | 声卡编号,-1 表示自动分配 | 我建议永远传 -1,除非你有特殊需求 |
| xid | 声卡标识字符串,比如 "MyVirtualCard" | 长度别超过 32 字节,否则会被截断 |
| module | 所属模块,传 THIS_MODULE | 这个忘了传会导致引用计数问题 |
| extra_size | 额外私有数据大小 | 我习惯把自定义结构体放这里,省得再分配 |
| card_ret | 返回声卡指针 | 记得检查返回值,别直接解引用 |
核心要点:snd_card_new() 内部会做三件事:
- 分配
snd_card结构体内存 - 初始化链表头、锁、引用计数
- 如果 extra_size > 0,在 card 结构体尾部追加私有数据
3.3 实战:虚拟声卡的私有数据
我个人习惯把虚拟声卡的状态信息放在 extra 区域里。你看这个写法:
struct my_virtual_card {
struct snd_card *card;
struct snd_pcm *pcm;
void __iomem *reg_base;
int irq;
spinlock_t lock;
};
static int my_card_create(struct device *dev, struct snd_card **card_ret)
{
struct snd_card *card;
struct my_virtual_card *mycard;
int err;
// 注意:extra_size 传 sizeof(struct my_virtual_card)
err = snd_card_new(dev, -1, "VirtualCard", THIS_MODULE,
sizeof(struct my_virtual_card), &card);
if (err < 0)
return err;
// 直接拿到私有数据指针,不用再 kmalloc
mycard = (struct my_virtual_card *)card->private_data;
mycard->card = card;
spin_lock_init(&mycard->lock);
*card_ret = card;
return 0;
}
我的小技巧:用 extra_size 传私有数据,能省一个 kmalloc/free 对。而且 card 释放时,extra 区域会自动释放,不会内存泄漏。我曾经在一个项目里看到有人又单独 kmalloc 了一个结构体,结果释放顺序搞反了,导致 use-after-free……嗯,用 extra 就简单多了。
3.4 snd_card_register():挂招牌营业
注册函数就简单多了:
int snd_card_register(struct snd_card *card);
它内部干了这些事:
- 创建设备节点
/dev/snd/controlC0、/dev/snd/pcmC0D0p等 - 在 sysfs 中创建声卡目录
- 触发 uevent,让用户空间知道新声卡来了
- 调用所有子设备的注册回调
注意:snd_card_register() 必须在所有子设备添加完成后调用。如果你先注册再添加 PCM 设备,那 PCM 设备节点就不会出现。我曾经犯过这个错,调试了一下午才发现顺序反了。
3.5 完整虚拟声卡驱动框架
来,咱们拼一个完整的虚拟声卡驱动骨架。你想想看,一个最简单的虚拟声卡,只需要做三件事:创建、添加 PCM、注册。
static int my_virtual_probe(struct platform_device *pdev)
{
struct snd_card *card;
struct snd_pcm *pcm;
int err;
// 第一步:创建声卡
err = my_card_create(&pdev->dev, &card);
if (err < 0)
return err;
// 第二步:添加 PCM 设备
err = snd_pcm_new(card, "Virtual PCM", 0, 1, 1, &pcm);
if (err < 0) {
snd_card_free(card);
return err;
}
// 设置 PCM 操作函数集
strcpy(pcm->name, "Virtual PCM Device");
pcm->private_data = card->private_data;
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &my_pcm_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &my_pcm_ops);
// 分配 DMA 缓冲区(虚拟声卡用内存模拟)
err = snd_pcm_lib_preallocate_pages_for_all(pcm,
SNDRV_DMA_TYPE_CONTINUOUS,
snd_dma_continuous_data(GFP_KERNEL),
64 * 1024, 64 * 1024);
if (err < 0) {
snd_card_free(card);
return err;
}
// 第三步:注册声卡
err = snd_card_register(card);
if (err < 0) {
snd_card_free(card);
return err;
}
platform_set_drvdata(pdev, card);
return 0;
}
static int my_virtual_remove(struct platform_device *pdev)
{
struct snd_card *card = platform_get_drvdata(pdev);
snd_card_free(card);
return 0;
}
3.6 避坑指南:我踩过的雷
做虚拟声卡驱动,有几个地方特别容易出问题:
- snd_card_free() 和 snd_card_free_when_closed() 的区别:前者立即释放,后者等所有文件句柄关闭后再释放。我建议在 remove 里用
snd_card_free_when_closed(),避免正在播放时拔设备导致崩溃。 - 别忘了设置 card->private_free:如果你在 extra 区域里又分配了其他内存,记得在 private_free 回调里释放。
- 注册失败要回滚:
snd_card_register()失败后,一定要调用snd_card_free(),否则内存泄漏。
总结一下:
snd_card_new()是分配 + 初始化,extra_size 用来放私有数据- 中间步骤添加各种子设备(PCM、控制、MIDI 等)
snd_card_register()最后调用,向用户空间暴露设备- 卸载时用
snd_card_free_when_closed()更安全
好了,声卡的创建与注册就讲到这里。下一章咱们会深入 PCM 设备的实现,包括 dma 缓冲区管理、指针更新、中断处理这些硬核内容。到时候我会拿一个真实的虚拟声卡代码来拆解,保证让你看完就能自己写一个能发声的虚拟声卡。