第1章:snd_card的创建与管理

各位同学,今天我们来聊聊ALSA驱动里最基础、也最核心的东西——snd_card

你可以把snd_card想象成一张“声卡的总控台”。所有的PCM设备、控制接口、MIDI设备、混音器……统统挂在这个card下面。没有它,你的声卡驱动就是一堆散沙。

我个人习惯,写一个新的ALSA驱动时,第一件事就是先把snd_card搞出来。为什么?因为后面所有的组件都要往它身上挂。你想想看,没有根,枝叶往哪儿长?

1.1 snd_card_new()——创建声卡实例

创建snd_card,用的是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。说白了,告诉内核这张卡挂在哪个硬件总线上。
  • idx:卡的索引号。传-1表示让内核自动分配。我建议新手一律传-1,省得跟其他声卡冲突。
  • xid:卡的ID字符串。比如"my_audio_card"。这个ID后面会出现在/proc/asound/cards里。
  • module:传THIS_MODULE就行,不用多想。
  • extra_size:额外分配的内存大小。你可以把私有数据结构跟snd_card一起分配,省一次kmalloc。我个人习惯传sizeof(struct my_chip),方便。
  • card_ret:输出参数,返回创建好的snd_card指针。

返回值:0成功,负数出错。

核心要点:snd_card_new()只是分配了结构体,还没注册到系统里。这时候你的声卡还是“隐身”的。

我在项目中遇到过一个问题:有个同事传了idx=0,结果跟系统自带的声卡冲突了,导致alsa-lib打开设备时总是报错。后来改成-1,世界清净了。

1.2 snd_card_free()——释放声卡资源

有创建就有释放。函数原型:

int snd_card_free(struct snd_card *card);

这个函数会做三件事:

  1. 遍历card下所有注册的设备,逐个调用它们的析构函数。
  2. 从ALSA核心中注销这个card。
  3. 释放snd_card本身占用的内存。

注意:snd_card_free()不能在中断上下文调用。它可能会睡眠。我曾经在中断处理里顺手调了一下,结果内核直接panic了。血的教训。

另外,如果你在驱动卸载时忘了调snd_card_free(),那你的声卡会变成“僵尸卡”——用户空间还能看到它,但一操作就死机。嗯,这个坑我踩过。

1.3 card的索引与ID设置

索引(index)和ID(id)是两个容易混淆的概念。

属性 含义 设置方式
index 数字编号,如0、1、2 snd_card_new()的idx参数,或模块参数
id 字符串标识,如"Generic"、"HDMI" snd_card_new()的xid参数,或snd_card_set_id()

index决定了声卡的加载顺序。id则用于用户空间的设备节点命名。比如id为"Generic"的卡,它的设备节点是/dev/snd/controlC0(如果index=0)。

你可以在模块加载时通过参数指定:

modprobe snd-my-driver index=2 id="MyCard"

这样即使系统里已经有其他声卡,你的卡也能排在固定位置。我个人习惯在驱动里支持这两个模块参数,方便调试。

1.4 card的注册与注销流程

创建完snd_card之后,还不能直接用。你得先往里面塞组件,最后调用snd_card_register()让它“活过来”。

标准流程如下:

// 1. 创建card
snd_card_new(parent, -1, "mycard", THIS_MODULE, sizeof(my_chip), &card);

// 2. 创建各种组件(PCM、控制、MIDI等)
snd_pcm_new(card, ...);
snd_ctl_add(card, ...);
// ... 其他组件

// 3. 注册card
snd_card_register(card);

注销时反过来:

snd_card_free(card);

就这么简单。但要注意顺序:先创建组件,再注册card。我曾经试过先注册再创建组件,结果组件创建失败时,card已经暴露给用户空间了,用户一访问就崩溃。

小技巧:在snd_card_new()和snd_card_register()之间,你的驱动可以随时返回错误。只要没注册,调用snd_card_free()就能干净地回滚。所以,把所有可能失败的初始化都放在注册之前做。

1.5 card的组件管理——snd_device_new()

ALSA驱动里,每个组件(PCM、控制、定时器等)都是一个snd_device。你可以通过snd_device_new()手动创建并挂到card上。

函数原型:

int snd_device_new(struct snd_card *card,
                   enum snd_device_type type,
                   void *device_data,
                   const struct snd_device_ops *ops);

参数说明:

  • type:设备类型,比如SNDRV_DEV_PCM、SNDRV_DEV_CONTROL。
  • device_data:你的设备私有数据指针。
  • ops:操作函数集,包含dev_register、dev_disconnect、dev_free三个回调。

举个例子,假设你要注册一个自定义组件:

static int my_dev_register(struct snd_device *dev) {
    printk("my device registered\n");
    return 0;
}

static int my_dev_free(struct snd_device *dev) {
    printk("my device freed\n");
    return 0;
}

static struct snd_device_ops my_ops = {
    .dev_register = my_dev_register,
    .dev_free = my_dev_free,
};

// 在驱动初始化中
struct my_dev *mydev = kzalloc(sizeof(*mydev), GFP_KERNEL);
snd_device_new(card, SNDRV_DEV_LOWLEVEL, mydev, &my_ops);

这样,当card注册时,ALSA核心会自动调用my_dev_register();当card释放时,自动调用my_dev_free()。你不用手动管理生命周期。

为什么需要snd_device_new()? 因为ALSA核心需要知道card下面挂了哪些组件,以便在注册/注销时统一管理。说白了,就是让核心帮你做“家务活”。

我见过一些驱动,自己维护一个链表来管理组件,然后在snd_card_free()里手动释放。这种做法不仅麻烦,还容易漏掉。用snd_device_new(),让核心帮你搞定,省心。

1.6 本章小结

好了,我们来捋一下snd_card的生命周期:

  1. 创建:snd_card_new()分配结构体,设置索引和ID。
  2. 组装:用snd_device_new()或更高级的API(如snd_pcm_new())添加组件。
  3. 注册:snd_card_register()让card对用户空间可见。
  4. 注销:snd_card_free()清理所有资源。

这个流程,你写100个ALSA驱动,100个都逃不掉。所以,把它刻在脑子里。

下面这张图,是我自己画的一个snd_card管理流程图,帮你理清思路:

snd_card 生命周期流程图 snd_card_new() 添加组件 (snd_device_new / snd_pcm_new) snd_card_register() 用户空间交互 (alsa-lib / aplay) snd_card_free() 分配内存 挂载组件 暴露设备 正常使用 释放资源 可传-1自动分配索引 注册前完成所有初始化 之后不可回滚 不能在中断中调用

这张图把整个流程串起来了。你写驱动的时候,就照着这个顺序来,准没错。

下一章,我们会深入PCM设备的创建和配置。不过在那之前,先把snd_card玩熟。万丈高楼平地起,card就是你的地基。


公众号:蓝海资料掘金营,微信deep3321