前言:
本文档描述了如何编写 ALSA(高级 Linux 音频架构)驱动程序。文档主要聚焦于 PCI 声卡的实现。对于其他类型的设备,可能会使用不同的 API。不过,至少 ALSA 的内核 API 是一致的,因此本文档在编写这些驱动时仍然具有一定的参考价值。 本指南面向那些已经具备足够 C 语言技能并且掌握基本 Linux 内核编程知识的开发者。本文档不会讲解 Linux 内核编程的一般性内容,也不会涉及底层驱动实现的细节。它仅介绍在 ALSA 框架下编写 PCI 声卡驱动的标准方法。
内容参考学习链接文档:
https://kernel.org/doc/html/latest/sound/kernel-api/writing-an-alsa-driver.html#header-files
File Tree Structure:
下面的文件结构是Linux内核关于sound目录下的介绍:
sound/core/oss/seq/oss/include/drivers/mpu401/opl3/i2c/synth/emux/pci/(cards)/isa/(cards)/arm/ppc/sparc/usb/pcmcia /(cards)/soc/oss
-
core 目录 : 该目录包含 ALSA 驱动的中间层,是 ALSA 驱动的核心。此目录中存放的是 ALSA 的原生模块。其子目录包含了不同的模块,并依赖于内核配置选项。
-
core/oss : 该目录存放 OSS PCM 和混音器(mixer)仿真模块的代码。由于 OSS 的 rawmidi 仿真部分非常小,因此被直接包含在 ALSA 的 rawmidi 代码中。序列器(sequencer)相关的 OSS 仿真代码则位于 core/seq/oss 目录(见下文)。
-
core/seq : 该目录及其子目录存放 ALSA 序列器(sequencer)相关代码。它包含了序列器核心以及主要模块,如 snd-seq-midi、snd-seq-virmidi 等。这些模块仅在内核配置中启用了 CONFIG_SND_SEQUENCER 时才会被编译。
-
core/seq/oss : 此目录包含 OSS 序列器仿真代码。
-
include 目录 : 该目录用于存放 ALSA 驱动对用户空间公开的头文件,或供多个不同目录中的文件共享使用。一般来说,私有头文件不应放在这个目录中,但你仍可能会在其中看到一些私有头文件,这是历史遗留问题所致。:)
-
drivers 目录 :该目录包含在不同平台间共享的驱动代码,因此它们不应具有架构相关性。例如,dummy PCM 驱动和串口 MIDI 驱动都位于此目录。其子目录中存放的是与总线或 CPU 架构无关的组件代码。 drivers/mpu401 包含 MPU401 和 MPU401-UART 模块。 drivers/opl3 与 opl4 包含 OPL3 和 OPL4 FM 合成器相关代码。
-
i2c 目录 :该目录包含 ALSA 的 i2c 相关组件。虽然 Linux 系统本身有标准的 i2c 层,但某些声卡只需要简单的操作,而标准的 i2c API 过于复杂,因此 ALSA 对某些声卡实现了自己的 i2c 代码。
-
synth 目录 :此目录包含中间层的合成器模块。目前,只有 Emu8000/Emu10k1 合成器驱动存放在 synth/emux 子目录中。
-
pci 目录 :此目录及其子目录存放 PCI 声卡的顶层驱动模块,以及与 PCI 总线相关的专用代码。如果驱动只包含单个源文件,则直接放在 pci 目录下;如果驱动包含多个源文件,则会单独创建子目录(例如 emu10k1、ice1712)。
-
isa 目录 : 此目录及其子目录存放 ISA 声卡的顶层驱动模块。
-
arm、ppc 和 sparc 目录 : 这些目录用于存放特定于某一架构的顶层声卡驱动模块。
-
usb 目录 : 该目录包含 USB 音频驱动。USB MIDI 驱动已经整合进了 USB 音频驱动中。
-
pcmcia 目录 : 该目录用于存放 PCMCIA(尤其是 PCCard)驱动。由于 CardBus 的 API 与标准 PCI 卡相同,因此其驱动位于 pci 目录中。
-
soc 目录 : 该目录包含 ASoC(ALSA SoC,系统级芯片)层的代码,包括 ASoC 核心、编解码器(codec)和 machine 驱动等。
-
oss 目录 : 该目录包含 OSS/Lite 代码。截止目前,除了 m68k 架构下的 dmasound 之外,其他代码均已被移除。
basic Flow for PCI Drivers:
PCI 声卡驱动的最小实现流程如下:
-
定义 PCI ID 表(参见“PCI 条目”部分)。
-
创建 probe 回调函数(用于设备检测和初始化)。
-
创建 remove 回调函数(用于设备移除时的清理操作)。
-
创建一个 struct pci_driver 结构体,包含上述三个函数指针(即:PCI ID 表、probe 和 remove 函数)。
-
创建初始化函数,该函数调用 pci_register_driver() 来注册上面定义的 pci_driver 表。
-
创建退出函数,该函数调用 pci_unregister_driver() 来注销该驱动。
Full Code Example:
下面展示了一个代码示例。目前有些部分尚未实现,但会在接下来的章节中补充完成。 在 snd_mychip_probe() 函数中的注释行里标注的数字,对应的是下一节中将详细解释的内容。
#include <linux/init.h>
#include <linux/pci.h>
#include <linux/slab.h>
#include <sound/core.h>
#include <sound/initval.h>/* module parameters (see "Module Parameters") */
/* SNDRV_CARDS: maximum number of cards supported by this module */
static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
static bool enable[SNDRV_CARDS] = SNDRV_DEFAULT_ENABLE_PNP;/* definition of the chip-specific record */
struct mychip {struct snd_card *card;/* the rest of the implementation will be in section* "PCI Resource Management"*/
};/* chip-specific destructor* (see "PCI Resource Management")*/
static int snd_mychip_free(struct mychip *chip)
{.... /* will be implemented later... */
}/* component-destructor* (see "Management of Cards and Components")*/
static int snd_mychip_dev_free(struct snd_device *device)
{return snd_mychip_free(device->device_data);
}/* chip-specific constructor* (see "Management of Cards and Components")*/
static int snd_mychip_create(struct snd_card *card,struct pci_dev *pci,struct mychip **rchip)
{struct mychip *chip;int err;static const struct snd_device_ops ops = {.dev_free = snd_mychip_dev_free,};*rchip = NULL;/* check PCI availability here* (see "PCI Resource Management")*/..../* allocate a chip-specific data with zero filled */chip = kzalloc(sizeof(*chip), GFP_KERNEL);if (chip == NULL)return -ENOMEM;chip->card = card;/* rest of initialization here; will be implemented* later, see "PCI Resource Management"*/....err = snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);if (err < 0) {snd_mychip_free(chip);return err;}*rchip = chip;return 0;
}/* constructor -- see "Driver Constructor" sub-section */
static int snd_mychip_probe(struct pci_dev *pci,const struct pci_device_id *pci_id)
{static int dev;struct snd_card *card;struct mychip *chip;int err;/* (1) */if (dev >= SNDRV_CARDS)return -ENODEV;if (!enable[dev]) {dev++;return -ENOENT;}/* (2) */err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,0, &card);if (err < 0)return err;/* (3) */err = snd_mychip_create(card, pci, &chip);if (err < 0)goto error;/* (4) */strcpy(card->driver, "My Chip");strcpy(card->shortname, "My Own Chip 123");sprintf(card->longname, "%s at 0x%lx irq %i",card->shortname, chip->port, chip->irq);/* (5) */.... /* implemented later *//* (6) */err = snd_card_register(card);if (err < 0)goto error;/* (7) */pci_set_drvdata(pci, card);dev++;return 0;error:snd_card_free(card);return err;
}/* destructor -- see the "Destructor" sub-section */
static void snd_mychip_remove(struct pci_dev *pci)
{snd_card_free(pci_get_drvdata(pci));
}
驱动构造函数:
PCI 驱动的真正构造函数是 probe 回调函数。由于 PCI 设备可能是热插拔设备,因此 probe 回调函数及其调用的其他组件构造函数不能使用 __init 前缀。 在 probe 回调函数中,通常会使用以下流程:
-
1)检查并递增设备索引。
static int dev;
....
if (dev >= SNDRV_CARDS)return -ENODEV;
if (!enable[dev]) {dev++;return -ENOENT;
}
其中 enable[dev] 是模块参数选项。 每次调用 probe 回调时,都要检查该设备是否可用。如果不可用,则只需递增设备索引并返回。dev 变量稍后(在第 7 步)也会继续递增。
-
2)创建一个 sound card 实例
struct snd_card *card;
int err;
....
err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,0, &card);
详细内容将在“声卡与组件的管理”章节中进行说明。
-
3)创建主组件 在这一部分,将分配 PCI 资源
struct mychip *chip;
....
err = snd_mychip_create(card, pci, &chip);
if (err < 0)goto error;
详细内容将在“PCI 资源管理”章节中进行说明。 当出现错误时,probe 函数需要进行错误处理。在本示例中,我们采用统一的错误处理路径,该路径被放置在函数的末尾:
error:snd_card_free(card);return err;
于每个组件都可以被正确释放,因此在大多数情况下,只需调用一次 snd_card_free() 就足够了。
-
4)设置驱动的 ID 和名称字符串。
strcpy(card->driver, "My Chip");
strcpy(card->shortname, "My Own Chip 123");
sprintf(card->longname, "%s at 0x%lx irq %i",card->shortname, chip->port, chip->irq);
driver 字段保存芯片的最小 ID 字符串。这个 ID 会被 alsa-lib 的配置器使用,因此应保持简洁但唯一。即使是同一个驱动,也可以使用不同的 driver ID 来区分不同类型芯片的功能。 shortname 字段是一个简短但更具描述性的名称字符串,用于展示。 longname 字段包含的是将在 /proc/asound/cards 中显示的信息。
-
5)创建其他组件,如混音器、MIDI 等 在此步骤中定义基本组件,例如 PCM、混音器(如 AC97)、MIDI(如 MPU-401)以及其他接口。如果需要定义 proc 文件,也应在这里进行。
-
6)注册该声卡实例
err = snd_card_register(card);
if (err < 0)goto error;
这一部分也将在“声卡与组件的管理”章节中进行说明。
-
7) 设置 PCI 驱动的数据,并返回 0
pci_set_drvdata(pci, card);
dev++;
return 0;
在上述步骤中,声卡记录被保存了下来。该指针同样会在 remove 回调函数 和 电源管理回调函数 中使用。
Destructor(析构函数):
析构函数,即 remove 回调函数,仅需释放声卡实例即可。随后,ALSA 中间层会自动释放所有附加的组件。 通常只需调用一行代码即可:
static void snd_mychip_remove(struct pci_dev *pci)
{snd_card_free(pci_get_drvdata(pci));
}
上述代码的前提是:声卡指针已通过 PCI 驱动的数据接口(即 pci_set_drvdata())保存。
Header Files:
对于上述示例,至少需要包含以下头文件:
#include <linux/init.h>
#include <linux/pci.h>
#include <linux/slab.h>
#include <sound/core.h>
#include <sound/initval.h>
最后一个头文件(指 <linux/moduleparam.h>)仅在源码中定义了模块参数时才是必须的。如果代码被拆分为多个文件,那么没有模块参数定义的文件就不需要包含该头文件。 除了这些头文件之外:
-
如果要处理中断,需要包含 <linux/interrupt.h>;
-
如果需要进行 I/O 操作访问,需要包含 <linux/io.h>;
-
如果使用了 mdelay() 或 udelay() 延时函数,还需要包含 <linux/delay.h>。
ALSA 接口,例如 PCM 和控制接口(control API),则定义在其他的 <sound/xxx.h> 头文件中。 这些头文件必须在 <sound/core.h> 之后包含。
声卡与组件的管理(Management of Cards and Components):
-
Card Instance
对于每一块声卡,必须分配一个 “card” 记录。 “card” 记录是声卡的核心管理结构,它负责管理该声卡上的所有设备(组件),例如 PCM、混音器(Mixer)、MIDI、合成器等。同时,该记录还保存声卡的 ID 和名称字符串,管理与之相关的 proc 文件入口,控制电源管理状态以及处理热插拔断开等情况。卡记录中的组件列表用于在驱动销毁时正确释放所有资源。 如前所述,要创建一个声卡实例,可以调用 snd_card_new() 函数:
struct snd_card *card;
int err;
err = snd_card_new(&pci->dev, index, id, module, extra_size, &card);
该函数接受六个参数:父设备指针、声卡索引号、ID 字符串、模块指针(通常为 THIS_MODULE)、额外数据空间的大小,以及用于返回声卡实例的指针。 其中,extra_size 参数用于为芯片相关的私有数据分配 card->private_data 空间。需要注意的是,这块私有数据是由 snd_card_new() 函数自动分配的。 第一个参数是 struct device 的指针,用于指定父设备。对于 PCI 设备,通常传入 &pci->dev
-
Components
在创建好声卡之后,你可以将各个组件(设备)附加到该声卡实例上。在 ALSA 驱动中,每个组件由一个 struct snd_device 对象表示。组件可以是一个 PCM 实例、控制接口、Raw MIDI 接口等。每个这样的实例对应一个组件条目。 组件可以通过 snd_device_new() 函数来创建:
snd_device_new(card, SNDRV_DEV_XXX, chip, &ops);
该函数接收以下参数:声卡指针、设备级别(SNDRV_DEV_XXX)、数据指针以及回调函数指针(&ops)。
其中,设备级别用于定义组件的类型,并决定其注册和注销的顺序。对于大多数组件,设备级别已在 ALSA 中预定义。对于用户自定义的组件,可以使用 SNDRV_DEV_LOWLEVEL。
此函数本身不会分配数据空间,数据需要在调用之前手动分配,并将其指针作为参数传入。在上述示例中,该指针(如 chip)会作为该实例的标识符使用。
对于 ALSA 中预定义的组件(如 AC97、PCM 等),它们在各自的构造函数内部会调用 snd_device_new()。组件的析构函数由回调函数中的指针指定,因此开发者无需手动调用组件的析构函数。
如果你希望创建自定义组件,需要在 ops 结构中设置 dev_free 回调指针为对应的析构函数,这样在调用 snd_card_free() 时该组件就能自动被释放。 接下来的示例将展示如何实现与芯片相关的私有数据结构。
-
Chip-Specific Data
与芯片相关的特定信息,例如 I/O 端口地址、资源指针或中断号(irq number),会被存储在芯片专用的数据结构(record)中:
struct mychip {....
};
一般来说,分配芯片记录有两种方式。
-
通过 snd_card_new() 分配。
如前所述,你可以将额外数据的长度作为 snd_card_new() 的第 5 个参数传入,例如:
err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,sizeof(struct mychip), &card);
struct mychip 是芯片记录的结构体类型。 作为返回值,分配好的记录可以通过以下方式访问:
struct mychip *chip = card->private_data;
使用这种方法,你无需进行两次内存分配。该记录会随着声卡实例一起被释放。
-
Allocating an extra device.
在通过 snd_card_new() 分配声卡实例(第 4 个参数设为 0)之后,调用 kzalloc():
struct snd_card *card;
struct mychip *chip;
err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,0, &card);
.....
chip = kzalloc(sizeof(*chip), GFP_KERNEL);
芯片记录结构体中至少应包含一个字段用于保存声卡指针:
struct mychip {struct snd_card *card;....
};
然后,在返回的芯片实例中设置该声卡指针:
chip->card = card;
接下来,初始化各个字段,并使用指定的 ops 将该芯片记录作为一个低层设备(low-level device)进行注册:
static const struct snd_device_ops ops = {.dev_free = snd_mychip_dev_free,
};
....
snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);
snd_mychip_dev_free() 是设备的析构函数,它将调用真正的析构操作:
static int snd_mychip_dev_free(struct snd_device *device)
{return snd_mychip_free(device->device_data);
}
其中,snd_mychip_free() 才是真正的析构函数。 这种方法的缺点是:代码量明显更大。 但它的优点在于:你可以通过在 snd_device_ops 中设置相关回调函数,在声卡注册和断开连接时触发自定义的回调操作。 关于声卡的注册与断开连接,请参见下文的子章节。
注册与释放
在所有组件都分配完成后,通过调用 snd_card_register() 来注册声卡实例。从这一刻起,设备文件的访问才会被启用。
也就是说,在调用 snd_card_register() 之前,外部无法访问这些组件,因此是安全的。
如果该函数调用失败,应在调用 snd_card_free() 释放声卡资源后退出 probe 函数。
要释放声卡实例,只需调用 snd_card_free()。
如前所述,该调用会自动释放所有附加的组件。
对于支持热插拔的设备,可以使用 snd_card_free_when_closed()。 该函数将在所有设备文件被关闭后,再延迟销毁声卡实例。