2. ALSA核心数据结构:snd_card、snd_device、snd_pcm、snd_control,以及它们之间的关系

好,咱们今天来啃ALSA驱动里最核心的几个数据结构。说实话,我刚接触ALSA那会儿,看着这些结构体互相嵌套,头都大了。但后来我发现,只要搞清楚了它们之间的"亲戚关系",整个音频框架的脉络就清晰了。

这章我会重点讲四个结构体:snd_card、snd_device、snd_pcm、snd_control。它们就像音频驱动的"四大家族",各司其职又紧密相连。

2.1 snd_card:音频卡的"户口本"

snd_card是整个ALSA驱动的顶层结构体。你可以把它想象成一张音频卡的"户口本"——它记录了这张卡的基本信息,比如编号、名字、驱动模块,以及它下面挂载的所有子设备。

核心要点:每个物理音频设备(或者虚拟设备)在ALSA里都对应一个snd_card。它是所有其他数据结构的"根"。

来看看它的关键字段(我挑最常用的说):

struct snd_card {
    int number;               // 声卡编号,从0开始
    char id[16];              // 声卡ID,比如"Generic"
    char driver[16];          // 驱动名字
    char shortname[32];       // 短名称
    char longname[80];        // 长名称
    struct list_head devices;  // 挂载所有snd_device的链表
    struct list_head controls; // 挂载所有control的链表
    struct snd_pcm *pcms;     // PCM设备链表
    // ... 还有很多其他字段
};

我个人习惯在写驱动时,第一步就是创建snd_card。用snd_card_new()函数,传入设备指针、编号、驱动ID等参数。嗯,这里要注意:snd_card_new()的第四个参数是额外私有数据的大小,很多新手会传0,结果后面想存点自定义数据发现没地方放。

我的经验:建议在snd_card_new()时预留至少sizeof(struct my_chip)大小的私有数据空间。这样你的芯片相关数据就能直接挂在card上,省得再搞全局变量。

2.2 snd_device:统一管理的"容器"

snd_device是个容易被忽略的结构体。它本质上是一个通用容器,用来把PCM、Control、MIDI等各种子设备统一挂到snd_card上。

为什么会需要这个?你想想看,一张声卡上可能有PCM播放设备、PCM录音设备、混音器控制、MIDI接口……如果每种设备都自己搞一套注册和销毁逻辑,那驱动代码就乱套了。ALSA的设计者很聪明,搞了个snd_device来统一管理生命周期。

struct snd_device {
    struct list_head list;    // 链表节点,挂到card->devices
    struct snd_card *card;    // 所属的card
    snd_device_type_t type;   // 设备类型:SNDRV_DEV_PCM, SNDRV_DEV_CONTROL等
    void *device_data;        // 指向具体设备结构体(比如snd_pcm)
    struct snd_device_ops *ops; // 操作函数集(init、free、register等)
};

说白了,snd_device就是个"包装盒"。你把snd_pcm或者snd_control放进去,然后通过snd_device_register()注册到card上。销毁时,ALSA会遍历card上的所有snd_device,调用各自的free回调。

我曾经踩过的坑:在写一个USB音频驱动时,我忘了在snd_device的ops里实现free函数。结果卸载模块时,PCM设备的内存没释放,导致内核报"slab object leaked"。从那以后,我每次注册snd_device都会再三确认ops是否完整。

2.3 snd_pcm:音频数据的"高速公路"

snd_pcm是ALSA里最核心的数据结构之一。它负责管理音频数据的传输——说白了,就是用户空间和硬件之间搬数据的那条"高速公路"。

一个snd_pcm实例代表一个PCM设备,比如/dev/snd/pcmC0D0p(播放)或/dev/snd/pcmC0D0c(录音)。它内部包含两个重要的子结构:

  • snd_pcm_str:代表一个数据流方向(播放或录音)
  • snd_pcm_substream:代表一个数据流实例(可以理解为一个打开的音频流)
struct snd_pcm {
    struct snd_card *card;           // 所属的card
    struct list_head list;           // 挂到card->pcms链表
    unsigned int streams;            // 支持的流方向数量(1或2)
    struct snd_pcm_str streams[2];   // 播放和录音两个方向
    struct snd_pcm_ops *ops;         // 硬件操作函数集
    // ...
};

你可能会问:为什么要有substream这层?我记得有一次调试一个多通道音频设备,同一个PCM设备要同时支持多个音频流(比如多个进程同时播放)。如果没有substream这层抽象,那并发控制就麻烦了。ALSA通过substream实现了"一个PCM设备,多个打开实例"的能力。

关键关系:snd_pcm通过snd_device包装后挂到snd_card上。一个snd_card可以有多个snd_pcm(比如一个用于播放,一个用于录音,或者多个不同的音频接口)。

2.4 snd_control:用户控制的"遥控器"

snd_control是ALSA里最灵活的数据结构。它不像PCM那样传输音频数据,而是用来控制音频硬件的各种参数——音量、静音、采样率选择、混音器开关等等。

你可以把snd_control想象成音频设备的"遥控器"。用户空间的alsamixer或者amixer命令,就是通过control接口来调节音量的。

struct snd_kcontrol {
    struct list_head list;           // 挂到card->controls链表
    struct snd_ctl_elem_id id;       // control的唯一标识(名字、索引等)
    struct snd_kcontrol_new *new;    // 创建时的模板
    struct snd_kcontrol_ops *ops;    // 操作函数(get、put、info)
    unsigned long private_value;     // 私有数据,常用于传硬件寄存器地址
    // ...
};

这里有个容易混淆的地方:内核里snd_control其实是用snd_kcontrol这个结构体表示的。每个snd_kcontrol代表一个"控制项"——比如"Master Volume"就是一个控制项,"Mic Boost"是另一个。

我的建议:写control驱动时,善用private_value字段。我通常用它来存储硬件寄存器的地址和位偏移,这样get/put回调里直接操作寄存器,代码非常简洁。

2.5 它们之间的关系:一张图说清楚

好了,四个结构体都介绍完了。它们之间的关系其实可以用一句话概括:

snd_card是根,snd_device是枝干,snd_pcm和snd_control是叶子。

具体来说:

  • snd_card 是顶层容器,管理所有子设备
  • snd_device 是通用包装层,把snd_pcm和snd_control统一挂到card上
  • snd_pcm 负责音频数据流传输,通过snd_device注册
  • snd_control 负责控制参数调节,直接挂在card的controls链表上(也通过snd_device)

用代码来展示一下典型的注册流程:

// 1. 创建card
struct snd_card *card;
snd_card_new(&pdev->dev, index, id, THIS_MODULE, sizeof(struct my_chip), &card);

// 2. 创建PCM设备
struct snd_pcm *pcm;
snd_pcm_new(card, "my_pcm", 0, 1, 1, &pcm);
// 这里snd_pcm_new内部会创建snd_device并挂到card上

// 3. 创建Control设备
struct snd_kcontrol *kctl;
kctl = snd_ctl_new1(&my_control_template, chip);
snd_ctl_add(card, kctl);
// snd_ctl_add内部会创建snd_device并挂到card上

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

你看,整个流程非常清晰:先有card,然后在card上创建各种子设备,最后注册card。销毁时反过来:先注销card,ALSA会自动清理所有子设备。

注意:snd_pcm_new()和snd_ctl_add()都会自动创建snd_device并挂到card上。你不需要手动创建snd_device——除非你要注册自定义类型的设备。我见过有人手动搞snd_device,结果和ALSA内部的管理冲突了,导致double free。

2.6 实战中的一些体会

最后分享几个我在项目中积累的经验:

  1. 先规划好card的私有数据结构。我习惯在struct my_chip里放所有硬件相关的信息(寄存器基址、中断号、锁等),然后通过card->private_data访问。这样整个驱动都围绕card展开,结构清晰。
  2. PCM和Control的命名要有规律。比如PCM设备叫"my_audio playback",Control叫"Master Playback Volume"。用户空间的工具依赖这些名字来查找设备,命名混乱会导致兼容性问题。
  3. 善用链表遍历宏。ALSA提供了list_for_each_entry来遍历card上的devices和controls。调试时我经常用这个来打印所有注册的设备信息,确认注册顺序是否正确。
  4. 注意生命周期管理。snd_device的free回调里要释放所有硬件资源(DMA缓冲区、中断等)。我曾经因为free回调里漏了释放DMA缓冲区,导致内存泄漏,系统跑几天就OOM了。

嗯,这章的内容就到这里。数据结构之间的关系搞清楚了,后面讲PCM操作、Control操作时就会轻松很多。下一章我们深入snd_pcm_ops,看看音频数据到底是怎么在硬件和用户空间之间流动的。