第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);
这个函数会做三件事:
- 遍历card下所有注册的设备,逐个调用它们的析构函数。
- 从ALSA核心中注销这个card。
- 释放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的生命周期:
- 创建:snd_card_new()分配结构体,设置索引和ID。
- 组装:用snd_device_new()或更高级的API(如snd_pcm_new())添加组件。
- 注册:snd_card_register()让card对用户空间可见。
- 注销:snd_card_free()清理所有资源。
这个流程,你写100个ALSA驱动,100个都逃不掉。所以,把它刻在脑子里。
下面这张图,是我自己画的一个snd_card管理流程图,帮你理清思路:
这张图把整个流程串起来了。你写驱动的时候,就照着这个顺序来,准没错。
下一章,我们会深入PCM设备的创建和配置。不过在那之前,先把snd_card玩熟。万丈高楼平地起,card就是你的地基。
公众号:蓝海资料掘金营,微信deep3321