3、声卡创建与注册:snd_card_new()、snd_card_register() 流程详解,实战编写一个虚拟声卡驱动

好,咱们今天来啃一块硬骨头——声卡的创建与注册。说实话,很多做音频驱动的朋友,搞了很久都搞不清楚 snd_card_new() 和 snd_card_register() 到底干了什么。我当年刚接触 ALSA 时也踩过不少坑,有一次注册完声卡发现设备节点死活不出来,查了两天才发现是传参顺序搞反了……嗯,咱们今天就把这事彻底说透。

3.1 声卡的生命周期:从出生到上岗

一个声卡在 ALSA 框架里,要经历三个关键阶段:

  1. 创建:调用 snd_card_new(),分配内存,初始化基础结构
  2. 组装:添加 PCM、控制、MIDI 等子设备
  3. 注册:调用 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);

它内部干了这些事:

  1. 创建设备节点 /dev/snd/controlC0、/dev/snd/pcmC0D0p 等
  2. 在 sysfs 中创建声卡目录
  3. 触发 uevent,让用户空间知道新声卡来了
  4. 调用所有子设备的注册回调

注意: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 缓冲区管理、指针更新、中断处理这些硬核内容。到时候我会拿一个真实的虚拟声卡代码来拆解,保证让你看完就能自己写一个能发声的虚拟声卡。