编写 ALSA 驱动程序¶
- 作者:
Takashi Iwai <tiwai@suse.de>
前言¶
本文档描述了如何编写 ALSA (高级 Linux 声音架构) 驱动程序。该文档主要关注 PCI 声卡。对于其他设备类型,API 可能也会有所不同。但是,至少 ALSA 内核 API 是一致的,因此它仍然对编写它们有所帮助。
本文档面向已经具有足够的 C 语言技能并且具有基本的 Linux 内核编程知识的人。本文档不解释 Linux 内核编码的一般主题,也不涵盖低级驱动程序实现的细节。它仅描述了在 ALSA 上编写 PCI 声音驱动程序的标准方法。
文件树结构¶
概述¶
ALSA 驱动程序的文件树结构如下所示
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 模块。子目录包含不同的模块,并且依赖于内核配置。
core/oss¶
OSS PCM 和混音器模拟模块的代码存储在此目录中。OSS rawmidi 模拟包含在 ALSA rawmidi 代码中,因为它非常小。音序器代码存储在 core/seq/oss
目录中(请参阅 下方)。
core/seq¶
此目录及其子目录用于 ALSA 音序器。此目录包含音序器核心和主要音序器模块,例如 snd-seq-midi, snd-seq-virmidi 等。仅当在内核配置中设置了 CONFIG_SND_SEQUENCER
时才编译它们。
core/seq/oss¶
这包含 OSS 音序器模拟代码。
include 目录¶
这是 ALSA 驱动程序的公共头文件的地方,这些头文件要导出到用户空间,或者由不同目录中的多个文件包含。基本上,私有头文件不应放在此目录中,但由于历史原因,您仍然可能会在那里找到文件 :)。
drivers 目录¶
此目录包含在不同架构上的不同驱动程序之间共享的代码。因此,它们不应该是特定于架构的。例如,虚拟 PCM 驱动程序和串行 MIDI 驱动程序在此目录中找到。在子目录中,存在独立于总线和 CPU 架构的组件的代码。
drivers/mpu401¶
MPU401 和 MPU401-UART 模块存储在此处。
drivers/opl3 and opl4¶
OPL3 和 OPL4 FM 合成器内容在此处找到。
i2c 目录¶
这包含 ALSA i2c 组件。
尽管 Linux 上有一个标准的 i2c 层,但 ALSA 为某些卡提供了自己的 i2c 代码,因为声卡只需要一个简单的操作,并且标准的 i2c API 对于这样的目的来说太复杂了。
synth 目录¶
这包含合成器中间级模块。
到目前为止,在 synth/emux
子目录下只有 Emu8000/Emu10k1 合成器驱动程序。
pci 目录¶
此目录及其子目录保存 PCI 声卡的顶级卡模块和特定于 PCI 总线的代码。
从单个文件编译的驱动程序直接存储在 pci 目录中,而具有多个源文件的驱动程序存储在它们自己的子目录中 (例如 emu10k1, ice1712)。
isa 目录¶
此目录及其子目录保存 ISA 声卡的顶级卡模块。
arm, ppc 和 sparc 目录¶
它们用于特定于这些架构之一的顶级卡模块。
usb 目录¶
此目录包含 USB 音频驱动程序。USB MIDI 驱动程序已集成到 usb 音频驱动程序中。
pcmcia 目录¶
PCMCIA,特别是 PCCard 驱动程序将进入此处。CardBus 驱动程序将位于 pci 目录中,因为它们的 API 与标准 PCI 卡的 API 相同。
soc 目录¶
此目录包含 ASoC(片上 ALSA 系统)层的代码,包括 ASoC 核心、编解码器和机器驱动程序。
oss 目录¶
这包含 OSS/Lite 代码。在编写时,除了 m68k 上的 dmasound 之外,所有代码都已被删除。
PCI 驱动程序的基本流程¶
概述¶
PCI 声卡的最小流程如下
定义 PCI ID 表(请参阅 PCI 条目 部分)。
创建
probe
回调。创建
remove
回调。创建一个
struct pci_driver
结构,其中包含上述三个指针。创建一个
init
函数,该函数仅调用pci_register_driver()
来注册上面定义的 pci_driver 表。创建一个
exit
函数来调用pci_unregister_driver()
函数。
完整代码示例¶
代码示例如下所示。某些部分目前尚未实现,但将在下一节中填充。 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
回调。 probe
回调和从 probe
回调调用的其他组件构造函数不能与 __init
前缀一起使用,因为任何 PCI 设备都可能是热插拔设备。
在 probe
回调中,经常使用以下方案。
1) 检查并递增设备索引。¶
static int dev;
....
if (dev >= SNDRV_CARDS)
return -ENODEV;
if (!enable[dev]) {
dev++;
return -ENOENT;
}
其中 enable[dev]
是模块选项。
每次调用 probe
回调时,检查设备的可用性。如果不可用,只需递增设备索引并返回。dev 稍后也会递增 (步骤 7)。
2) 创建卡实例¶
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 字符串。alsa-lib 的配置器使用此字符串,因此请保持简单但唯一。即使是同一个驱动程序也可以具有不同的驱动程序 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 驱动程序数据并返回零。¶
pci_set_drvdata(pci, card);
dev++;
return 0;
在上面,存储了卡记录。此指针也用于 remove 回调和电源管理回调中。
析构函数¶
析构函数,remove 回调,只是释放卡实例。然后,ALSA 中间层将自动释放所有附加的组件。
通常只是调用 snd_card_free()
static void snd_mychip_remove(struct pci_dev *pci)
{
snd_card_free(pci_get_drvdata(pci));
}
上面的代码假设卡指针已设置为 PCI 驱动程序数据。
头文件¶
对于上面的示例,至少需要以下包含文件
#include <linux/init.h>
#include <linux/pci.h>
#include <linux/slab.h>
#include <sound/core.h>
#include <sound/initval.h>
其中最后一个仅在源文件中定义了模块选项时才需要。如果代码拆分为多个文件,则没有模块选项的文件不需要它们。
除了这些头文件,您还需要 <linux/interrupt.h>
用于中断处理,以及 <linux/io.h>
用于 I/O 访问。如果您使用 mdelay()
或 udelay()
函数,您还需要包含 <linux/delay.h>
。
ALSA 接口(如 PCM 和控制 API)在其他 <sound/xxx.h>
头文件中定义。它们必须在 <sound/core.h>
之后包含。
卡和组件的管理¶
卡实例¶
对于每张声卡,必须分配一个“卡”记录。
卡记录是声卡的总部。它管理声卡上设备(组件)的整个列表,例如 PCM、混音器、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->
。
组件¶
创建卡后,您可以将组件(设备)附加到卡实例。在 ALSA 驱动程序中,组件表示为 struct snd_device 对象。组件可以是 PCM 实例、控制接口、原始 MIDI 接口等。每个此类实例都有一个组件条目。
可以通过 snd_device_new()
函数创建组件
snd_device_new(card, SNDRV_DEV_XXX, chip, &ops);
这需要卡指针、设备级别 (SNDRV_DEV_XXX
)、数据指针和回调指针 (&ops
)。设备级别定义了组件的类型以及注册和取消注册的顺序。对于大多数组件,已经定义了设备级别。对于用户定义的组件,可以使用 SNDRV_DEV_LOWLEVEL
。
此函数本身不分配数据空间。数据必须事先手动分配,并且其指针作为参数传递。此指针(在上面的示例中为 chip
)用作实例的标识符。
每个预定义的 ALSA 组件(如 AC97 和 PCM)都在其构造函数内部调用 snd_device_new()
。每个组件的析构函数都在回调指针中定义。因此,您无需注意为此类组件调用析构函数。
如果您希望创建自己的组件,则需要在 ops
中将析构函数设置为 dev_free 回调,以便可以通过 snd_card_free()
自动释放它。下一个示例将显示特定于芯片的数据的实现。
特定于芯片的数据¶
特定于芯片的信息(例如,I/O 端口地址、其资源指针或 irq 编号)存储在特定于芯片的记录中
struct mychip {
....
};
一般来说,有两种分配芯片记录的方法。
1. 通过 snd_card_new()
分配。¶
如上所述,您可以将 extra-data-length 传递给 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;
使用此方法,您不必分配两次。该记录与卡实例一起释放。
2. 分配额外的设备。¶
在通过 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
的低级设备
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()
。这将推迟销毁,直到所有设备都关闭。
PCI 资源管理¶
完整代码示例¶
在本节中,我们将完成特定于芯片的构造函数、析构函数和 PCI 条目。首先显示示例代码,如下所示
struct mychip {
struct snd_card *card;
struct pci_dev *pci;
unsigned long port;
int irq;
};
static int snd_mychip_free(struct mychip *chip)
{
/* disable hardware here if any */
.... /* (not implemented in this document) */
/* release the irq */
if (chip->irq >= 0)
free_irq(chip->irq, chip);
/* release the I/O ports & memory */
pci_release_regions(chip->pci);
/* disable the PCI entry */
pci_disable_device(chip->pci);
/* release the data */
kfree(chip);
return 0;
}
/* chip-specific constructor */
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;
/* initialize the PCI entry */
err = pci_enable_device(pci);
if (err < 0)
return err;
/* check PCI availability (28bit DMA) */
if (pci_set_dma_mask(pci, DMA_BIT_MASK(28)) < 0 ||
pci_set_consistent_dma_mask(pci, DMA_BIT_MASK(28)) < 0) {
printk(KERN_ERR "error to set 28bit mask DMA\n");
pci_disable_device(pci);
return -ENXIO;
}
chip = kzalloc(sizeof(*chip), GFP_KERNEL);
if (chip == NULL) {
pci_disable_device(pci);
return -ENOMEM;
}
/* initialize the stuff */
chip->card = card;
chip->pci = pci;
chip->irq = -1;
/* (1) PCI resource allocation */
err = pci_request_regions(pci, "My Chip");
if (err < 0) {
kfree(chip);
pci_disable_device(pci);
return err;
}
chip->port = pci_resource_start(pci, 0);
if (request_irq(pci->irq, snd_mychip_interrupt,
IRQF_SHARED, KBUILD_MODNAME, chip)) {
printk(KERN_ERR "cannot grab irq %d\n", pci->irq);
snd_mychip_free(chip);
return -EBUSY;
}
chip->irq = pci->irq;
card->sync_irq = chip->irq;
/* (2) initialization of the chip hardware */
.... /* (not implemented in this document) */
err = snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);
if (err < 0) {
snd_mychip_free(chip);
return err;
}
*rchip = chip;
return 0;
}
/* PCI IDs */
static struct pci_device_id snd_mychip_ids[] = {
{ PCI_VENDOR_ID_FOO, PCI_DEVICE_ID_BAR,
PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0, },
....
{ 0, }
};
MODULE_DEVICE_TABLE(pci, snd_mychip_ids);
/* pci_driver definition */
static struct pci_driver driver = {
.name = KBUILD_MODNAME,
.id_table = snd_mychip_ids,
.probe = snd_mychip_probe,
.remove = snd_mychip_remove,
};
/* module initialization */
static int __init alsa_card_mychip_init(void)
{
return pci_register_driver(&driver);
}
/* module clean up */
static void __exit alsa_card_mychip_exit(void)
{
pci_unregister_driver(&driver);
}
module_init(alsa_card_mychip_init)
module_exit(alsa_card_mychip_exit)
EXPORT_NO_SYMBOLS; /* for old kernels only */
一些 Hafta's¶
PCI 资源的分配在 probe
函数中完成,通常为此目的编写一个额外的 xxx_create()
函数。
对于 PCI 设备,您首先必须在分配资源之前调用 pci_enable_device()
函数。此外,您需要设置正确的 PCI DMA 掩码以限制访问的 I/O 范围。在某些情况下,您可能还需要调用 pci_set_master()
函数。
假设一个 28 位掩码,要添加的代码如下所示
err = pci_enable_device(pci);
if (err < 0)
return err;
if (pci_set_dma_mask(pci, DMA_BIT_MASK(28)) < 0 ||
pci_set_consistent_dma_mask(pci, DMA_BIT_MASK(28)) < 0) {
printk(KERN_ERR "error to set 28bit mask DMA\n");
pci_disable_device(pci);
return -ENXIO;
}
资源分配¶
I/O 端口和 irq 的分配通过标准的内核函数完成。这些资源必须在析构函数中释放(见下文)。
现在假设 PCI 设备有一个 8 字节的 I/O 端口和一个中断。然后 struct mychip 将具有以下字段
struct mychip {
struct snd_card *card;
unsigned long port;
int irq;
};
对于 I/O 端口(以及内存区域),你需要拥有用于标准资源管理的资源指针。对于 irq,你只需要保留 irq 号码(整数)。但是你需要在实际分配之前将该号码初始化为 -1,因为 irq 0 是有效的。端口地址及其资源指针可以通过 kzalloc()
自动初始化为 null,因此你无需担心重置它们。
I/O 端口的分配方式如下:
err = pci_request_regions(pci, "My Chip");
if (err < 0) {
kfree(chip);
pci_disable_device(pci);
return err;
}
chip->port = pci_resource_start(pci, 0);
它将保留给定 PCI 设备的 8 字节 I/O 端口区域。返回值 chip->res_port
通过 kmalloc()
由 request_region()
分配。该指针必须通过 kfree()
释放,但这里存在一个问题。这个问题稍后会解释。
中断源的分配方式如下:
if (request_irq(pci->irq, snd_mychip_interrupt,
IRQF_SHARED, KBUILD_MODNAME, chip)) {
printk(KERN_ERR "cannot grab irq %d\n", pci->irq);
snd_mychip_free(chip);
return -EBUSY;
}
chip->irq = pci->irq;
其中 snd_mychip_interrupt()
是稍后定义的中断处理程序。请注意,只有当 request_irq()
成功时,才应该定义 chip->irq
。
在 PCI 总线上,中断可以共享。因此,IRQF_SHARED
被用作 request_irq()
的中断标志。
request_irq()
的最后一个参数是传递给中断处理程序的数据指针。通常,芯片特定的记录用于此目的,但你也可以使用你喜欢的任何东西。
我不会在此处提供有关中断处理程序的详细信息,但至少现在可以解释它的外观。中断处理程序通常如下所示:
static irqreturn_t snd_mychip_interrupt(int irq, void *dev_id)
{
struct mychip *chip = dev_id;
....
return IRQ_HANDLED;
}
在请求 IRQ 之后,你可以将其传递给 card->sync_irq
字段。
card->irq = chip->irq;
这允许 PCM 核心在正确的时间自动调用 synchronize_irq()
,例如在 hw_free
之前。有关详细信息,请参见后面的章节sync_stop callback。
现在,让我们为上面的资源编写相应的析构函数。析构函数的作用很简单:禁用硬件(如果已经激活)并释放资源。到目前为止,我们没有硬件部分,因此此处未编写禁用代码。
要释放资源,“检查和释放”方法是一种更安全的方法。对于中断,执行以下操作:
if (chip->irq >= 0)
free_irq(chip->irq, chip);
由于 irq 号码可以从 0 开始,因此应使用负值(例如 -1)初始化 chip->irq
,以便可以检查上述 irq 号码的有效性。
当你通过 pci_request_region()
或 pci_request_regions()
请求 I/O 端口或内存区域时(如本示例中所示),请使用相应的函数 pci_release_region()
或 pci_release_regions()
释放资源。
pci_release_regions(chip->pci);
当你通过 request_region()
或 request_mem_region()
手动请求时,可以通过 release_resource()
释放它。假设你将从 request_region()
返回的资源指针保存在 chip->res_port 中,则释放过程如下所示:
release_and_free_resource(chip->res_port);
在结束之前,请不要忘记调用 pci_disable_device()
。
最后,释放芯片特定的记录:
kfree(chip);
我们没有实现上面的硬件禁用部分。如果需要这样做,请注意,即使在完成芯片的初始化之前,也可以调用析构函数。最好有一个标志来跳过硬件禁用(如果尚未初始化硬件)。
当使用 SNDRV_DEV_LOWLELVEL
通过 snd_device_new()
将芯片数据分配给卡时,其析构函数最后被调用。也就是说,可以确保所有其他组件(例如 PCM 和控件)都已释放。你不必显式停止 PCM 等,只需调用低级硬件停止即可。
内存映射区域的管理几乎与 I/O 端口的管理相同。你需要以下两个字段:
struct mychip {
....
unsigned long iobase_phys;
void __iomem *iobase_virt;
};
分配方式如下:
err = pci_request_regions(pci, "My Chip");
if (err < 0) {
kfree(chip);
return err;
}
chip->iobase_phys = pci_resource_start(pci, 0);
chip->iobase_virt = ioremap(chip->iobase_phys,
pci_resource_len(pci, 0));
相应的析构函数将是:
static int snd_mychip_free(struct mychip *chip)
{
....
if (chip->iobase_virt)
iounmap(chip->iobase_virt);
....
pci_release_regions(chip->pci);
....
}
当然,使用 pci_iomap()
的现代方法也会使事情变得更容易:
err = pci_request_regions(pci, "My Chip");
if (err < 0) {
kfree(chip);
return err;
}
chip->iobase_virt = pci_iomap(pci, 0, 0);
它与析构函数中的 pci_iounmap()
配对。
PCI 条目¶
到目前为止,一切都很好。让我们完成缺少的 PCI 内容。首先,我们需要此芯片组的 struct pci_device_id
表。它是 PCI 供应商/设备 ID 号码的表,以及一些掩码。
例如:
static struct pci_device_id snd_mychip_ids[] = {
{ PCI_VENDOR_ID_FOO, PCI_DEVICE_ID_BAR,
PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0, },
....
{ 0, }
};
MODULE_DEVICE_TABLE(pci, snd_mychip_ids);
struct pci_device_id
的第一个和第二个字段是供应商和设备 ID。如果没有理由过滤匹配的设备,则可以按上述方式保留其余字段。struct pci_device_id
的最后一个字段包含此条目的私有数据。你可以在此处指定任何值,例如,为受支持的设备 ID 定义特定操作。在 intel8x0 驱动程序中可以找到这样的示例。
此列表的最后一个条目是终止符。你必须指定此全零条目。
然后,准备 struct pci_driver
记录:
static struct pci_driver driver = {
.name = KBUILD_MODNAME,
.id_table = snd_mychip_ids,
.probe = snd_mychip_probe,
.remove = snd_mychip_remove,
};
probe
和 remove
函数已在前几节中定义。name
字段是此设备的名称字符串。请注意,你不得在此字符串中使用斜杠(“/”)。
最后,是模块条目:
static int __init alsa_card_mychip_init(void)
{
return pci_register_driver(&driver);
}
static void __exit alsa_card_mychip_exit(void)
{
pci_unregister_driver(&driver);
}
module_init(alsa_card_mychip_init)
module_exit(alsa_card_mychip_exit)
请注意,这些模块条目带有 __init
和 __exit
前缀。
就这样!
PCM 接口¶
常规¶
ALSA 的 PCM 中间层非常强大,每个驱动程序只需要实现低级函数来访问其硬件。
要访问 PCM 层,你需要首先包含 <sound/pcm.h>
。此外,如果访问与 hw_param 相关的一些函数,则可能需要 <sound/pcm_params.h>
。
每个卡设备最多可以有四个 PCM 实例。PCM 实例对应于 PCM 设备文件。实例数量的限制仅来自 Linux 设备号码的可用位大小。一旦使用 64 位设备号码,我们将有更多可用的 PCM 实例。
PCM 实例由 PCM 回放和捕获流组成,每个 PCM 流由一个或多个 PCM 子流组成。一些声卡支持多个回放功能。例如,emu10k1 有一个 32 立体声子流的 PCM 回放。在这种情况下,在每次打开时,(通常)自动选择并打开一个空闲子流。同时,当只有一个子流存在并且已经被打开时,根据文件打开模式,后续的打开将阻塞或出错,并显示 EAGAIN
。但是你无需关心驱动程序中的此类详细信息。PCM 中间层将处理此类工作。
完整代码示例¶
下面的示例代码不包含任何硬件访问例程,但仅显示了框架,如何构建 PCM 接口:
#include <sound/pcm.h>
....
/* hardware definition */
static struct snd_pcm_hardware snd_mychip_playback_hw = {
.info = (SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_MMAP_VALID),
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rates = SNDRV_PCM_RATE_8000_48000,
.rate_min = 8000,
.rate_max = 48000,
.channels_min = 2,
.channels_max = 2,
.buffer_bytes_max = 32768,
.period_bytes_min = 4096,
.period_bytes_max = 32768,
.periods_min = 1,
.periods_max = 1024,
};
/* hardware definition */
static struct snd_pcm_hardware snd_mychip_capture_hw = {
.info = (SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_MMAP_VALID),
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rates = SNDRV_PCM_RATE_8000_48000,
.rate_min = 8000,
.rate_max = 48000,
.channels_min = 2,
.channels_max = 2,
.buffer_bytes_max = 32768,
.period_bytes_min = 4096,
.period_bytes_max = 32768,
.periods_min = 1,
.periods_max = 1024,
};
/* open callback */
static int snd_mychip_playback_open(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
runtime->hw = snd_mychip_playback_hw;
/* more hardware-initialization will be done here */
....
return 0;
}
/* close callback */
static int snd_mychip_playback_close(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
/* the hardware-specific codes will be here */
....
return 0;
}
/* open callback */
static int snd_mychip_capture_open(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
runtime->hw = snd_mychip_capture_hw;
/* more hardware-initialization will be done here */
....
return 0;
}
/* close callback */
static int snd_mychip_capture_close(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
/* the hardware-specific codes will be here */
....
return 0;
}
/* hw_params callback */
static int snd_mychip_pcm_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *hw_params)
{
/* the hardware-specific codes will be here */
....
return 0;
}
/* hw_free callback */
static int snd_mychip_pcm_hw_free(struct snd_pcm_substream *substream)
{
/* the hardware-specific codes will be here */
....
return 0;
}
/* prepare callback */
static int snd_mychip_pcm_prepare(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
/* set up the hardware with the current configuration
* for example...
*/
mychip_set_sample_format(chip, runtime->format);
mychip_set_sample_rate(chip, runtime->rate);
mychip_set_channels(chip, runtime->channels);
mychip_set_dma_setup(chip, runtime->dma_addr,
chip->buffer_size,
chip->period_size);
return 0;
}
/* trigger callback */
static int snd_mychip_pcm_trigger(struct snd_pcm_substream *substream,
int cmd)
{
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
/* do something to start the PCM engine */
....
break;
case SNDRV_PCM_TRIGGER_STOP:
/* do something to stop the PCM engine */
....
break;
default:
return -EINVAL;
}
}
/* pointer callback */
static snd_pcm_uframes_t
snd_mychip_pcm_pointer(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
unsigned int current_ptr;
/* get the current hardware pointer */
current_ptr = mychip_get_hw_pointer(chip);
return current_ptr;
}
/* operators */
static struct snd_pcm_ops snd_mychip_playback_ops = {
.open = snd_mychip_playback_open,
.close = snd_mychip_playback_close,
.hw_params = snd_mychip_pcm_hw_params,
.hw_free = snd_mychip_pcm_hw_free,
.prepare = snd_mychip_pcm_prepare,
.trigger = snd_mychip_pcm_trigger,
.pointer = snd_mychip_pcm_pointer,
};
/* operators */
static struct snd_pcm_ops snd_mychip_capture_ops = {
.open = snd_mychip_capture_open,
.close = snd_mychip_capture_close,
.hw_params = snd_mychip_pcm_hw_params,
.hw_free = snd_mychip_pcm_hw_free,
.prepare = snd_mychip_pcm_prepare,
.trigger = snd_mychip_pcm_trigger,
.pointer = snd_mychip_pcm_pointer,
};
/*
* definitions of capture are omitted here...
*/
/* create a pcm device */
static int snd_mychip_new_pcm(struct mychip *chip)
{
struct snd_pcm *pcm;
int err;
err = snd_pcm_new(chip->card, "My Chip", 0, 1, 1, &pcm);
if (err < 0)
return err;
pcm->private_data = chip;
strcpy(pcm->name, "My Chip");
chip->pcm = pcm;
/* set operators */
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK,
&snd_mychip_playback_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE,
&snd_mychip_capture_ops);
/* pre-allocation of buffers */
/* NOTE: this may fail */
snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV,
&chip->pci->dev,
64*1024, 64*1024);
return 0;
}
PCM 构造函数¶
PCM 实例由 snd_pcm_new()
函数分配。最好为 PCM 创建一个构造函数,即:
static int snd_mychip_new_pcm(struct mychip *chip)
{
struct snd_pcm *pcm;
int err;
err = snd_pcm_new(chip->card, "My Chip", 0, 1, 1, &pcm);
if (err < 0)
return err;
pcm->private_data = chip;
strcpy(pcm->name, "My Chip");
chip->pcm = pcm;
...
return 0;
}
snd_pcm_new()
函数接受六个参数。第一个参数是分配此 PCM 的卡指针,第二个参数是 ID 字符串。
第三个参数(index
,在上面为 0)是此新 PCM 的索引。它从零开始。如果创建多个 PCM 实例,请在此参数中指定不同的数字。例如,对于第二个 PCM 设备,index = 1
。
第四个和第五个参数分别是回放和捕获的子流数量。此处,两个参数都使用 1。当没有可用的回放或捕获子流时,将 0 传递给相应的参数。
如果芯片支持多个回放或捕获,则可以指定更多数字,但必须在 open/close 等回调中正确处理它们。当需要知道你正在引用哪个子流时,可以从传递给每个回调的 struct snd_pcm_substream 数据中获取它,如下所示:
struct snd_pcm_substream *substream;
int index = substream->number;
创建 PCM 后,你需要为每个 PCM 流设置运算符:
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK,
&snd_mychip_playback_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE,
&snd_mychip_capture_ops);
运算符通常定义如下:
static struct snd_pcm_ops snd_mychip_playback_ops = {
.open = snd_mychip_pcm_open,
.close = snd_mychip_pcm_close,
.hw_params = snd_mychip_pcm_hw_params,
.hw_free = snd_mychip_pcm_hw_free,
.prepare = snd_mychip_pcm_prepare,
.trigger = snd_mychip_pcm_trigger,
.pointer = snd_mychip_pcm_pointer,
};
所有回调都在 运算符 小节中描述。
设置运算符后,你可能想要预先分配缓冲区并设置托管分配模式。为此,只需调用以下内容:
snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV,
&chip->pci->dev,
64*1024, 64*1024);
默认情况下,它将分配高达 64kB 的缓冲区。缓冲区管理详细信息将在后面的章节 缓冲区和内存管理 中描述。
此外,你可以在 pcm->info_flags
中为此 PCM 设置一些额外的信息。可用值在 <sound/asound.h>
中定义为 SNDRV_PCM_INFO_XXX
,它用于硬件定义(稍后描述)。当你的声卡芯片仅支持半双工时,请像这样指定它:
pcm->info_flags = SNDRV_PCM_INFO_HALF_DUPLEX;
... 还有析构函数?¶
PCM 实例的析构函数并非总是必要的。由于 PCM 设备将由中间层代码自动释放,因此你不必显式调用析构函数。
如果你在内部创建了特殊记录并且需要释放它们,则析构函数是必要的。在这种情况下,将析构函数设置为 pcm->private_free
:
static void mychip_pcm_free(struct snd_pcm *pcm)
{
struct mychip *chip = snd_pcm_chip(pcm);
/* free your own data */
kfree(chip->my_private_pcm_data);
/* do what you like else */
....
}
static int snd_mychip_new_pcm(struct mychip *chip)
{
struct snd_pcm *pcm;
....
/* allocate your own data */
chip->my_private_pcm_data = kmalloc(...);
/* set the destructor */
pcm->private_data = chip;
pcm->private_free = mychip_pcm_free;
....
}
运行时指针 - PCM 信息的宝库¶
打开 PCM 子流时,将分配一个 PCM 运行时实例并将其分配给该子流。可以通过 substream->runtime
访问此指针。此运行时指针保存着控制 PCM 所需的大部分信息:hw_params 和 sw_params 配置的副本、缓冲区指针、mmap 记录、自旋锁等。
运行时实例的定义位于 <sound/pcm.h>
中。以下是此文件的相关部分:
struct _snd_pcm_runtime {
/* -- Status -- */
struct snd_pcm_substream *trigger_master;
snd_timestamp_t trigger_tstamp; /* trigger timestamp */
int overrange;
snd_pcm_uframes_t avail_max;
snd_pcm_uframes_t hw_ptr_base; /* Position at buffer restart */
snd_pcm_uframes_t hw_ptr_interrupt; /* Position at interrupt time*/
/* -- HW params -- */
snd_pcm_access_t access; /* access mode */
snd_pcm_format_t format; /* SNDRV_PCM_FORMAT_* */
snd_pcm_subformat_t subformat; /* subformat */
unsigned int rate; /* rate in Hz */
unsigned int channels; /* channels */
snd_pcm_uframes_t period_size; /* period size */
unsigned int periods; /* periods */
snd_pcm_uframes_t buffer_size; /* buffer size */
unsigned int tick_time; /* tick time */
snd_pcm_uframes_t min_align; /* Min alignment for the format */
size_t byte_align;
unsigned int frame_bits;
unsigned int sample_bits;
unsigned int info;
unsigned int rate_num;
unsigned int rate_den;
/* -- SW params -- */
struct timespec tstamp_mode; /* mmap timestamp is updated */
unsigned int period_step;
unsigned int sleep_min; /* min ticks to sleep */
snd_pcm_uframes_t start_threshold;
/*
* The following two thresholds alleviate playback buffer underruns; when
* hw_avail drops below the threshold, the respective action is triggered:
*/
snd_pcm_uframes_t stop_threshold; /* - stop playback */
snd_pcm_uframes_t silence_threshold; /* - pre-fill buffer with silence */
snd_pcm_uframes_t silence_size; /* max size of silence pre-fill; when >= boundary,
* fill played area with silence immediately */
snd_pcm_uframes_t boundary; /* pointers wrap point */
/* internal data of auto-silencer */
snd_pcm_uframes_t silence_start; /* starting pointer to silence area */
snd_pcm_uframes_t silence_filled; /* size filled with silence */
snd_pcm_sync_id_t sync; /* hardware synchronization ID */
/* -- mmap -- */
volatile struct snd_pcm_mmap_status *status;
volatile struct snd_pcm_mmap_control *control;
atomic_t mmap_count;
/* -- locking / scheduling -- */
spinlock_t lock;
wait_queue_head_t sleep;
struct timer_list tick_timer;
struct fasync_struct *fasync;
/* -- private section -- */
void *private_data;
void (*private_free)(struct snd_pcm_runtime *runtime);
/* -- hardware description -- */
struct snd_pcm_hardware hw;
struct snd_pcm_hw_constraints hw_constraints;
/* -- timer -- */
unsigned int timer_resolution; /* timer resolution */
/* -- DMA -- */
unsigned char *dma_area; /* DMA area */
dma_addr_t dma_addr; /* physical bus address (not accessible from main CPU) */
size_t dma_bytes; /* size of DMA area */
struct snd_dma_buffer *dma_buffer_p; /* allocated buffer */
#if defined(CONFIG_SND_PCM_OSS) || defined(CONFIG_SND_PCM_OSS_MODULE)
/* -- OSS things -- */
struct snd_pcm_oss_runtime oss;
#endif
};
对于每个声卡驱动程序的运算符(回调),这些记录中的大多数都应该是只读的。只有 PCM 中间层才能更改/更新它们。例外情况是硬件描述 (hw) DMA 缓冲区信息和私有数据。此外,如果使用标准托管缓冲区分配模式,则无需自己设置 DMA 缓冲区信息。
在下面的章节中,将解释重要的记录。
硬件描述¶
硬件描述符(struct snd_pcm_hardware)包含基本硬件配置的定义。首先,你需要 PCM open callback 中定义它。请注意,运行时实例保存描述符的副本,而不是指向现有描述符的指针。也就是说,在 open 回调中,你可以根据需要修改复制的描述符(runtime->hw
)。例如,如果只有在某些芯片型号上通道的最大数量为 1,你仍然可以使用相同的硬件描述符并稍后更改 channels_max:
struct snd_pcm_runtime *runtime = substream->runtime;
...
runtime->hw = snd_mychip_playback_hw; /* common definition */
if (chip->model == VERY_OLD_ONE)
runtime->hw.channels_max = 1;
通常,你将具有如下所示的硬件描述符:
static struct snd_pcm_hardware snd_mychip_playback_hw = {
.info = (SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_MMAP_VALID),
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rates = SNDRV_PCM_RATE_8000_48000,
.rate_min = 8000,
.rate_max = 48000,
.channels_min = 2,
.channels_max = 2,
.buffer_bytes_max = 32768,
.period_bytes_min = 4096,
.period_bytes_max = 32768,
.periods_min = 1,
.periods_max = 1024,
};
info
字段包含此 PCM 的类型和功能。位标志在<sound/asound.h>
中定义为SNDRV_PCM_INFO_XXX
。在此处,至少你必须指定是否支持 mmap 以及支持哪些交错格式。当硬件支持 mmap 时,在此处添加SNDRV_PCM_INFO_MMAP
标志。当硬件支持交错格式或非交错格式时,必须分别设置SNDRV_PCM_INFO_INTERLEAVED
或SNDRV_PCM_INFO_NONINTERLEAVED
标志。如果同时支持两者,你也可以同时设置这两个标志。在上面的示例中,为 OSS mmap 模式指定了
MMAP_VALID
和BLOCK_TRANSFER
。通常,两者都设置。当然,只有在真正支持 mmap 时才设置MMAP_VALID
。其他可能的标志是
SNDRV_PCM_INFO_PAUSE
和SNDRV_PCM_INFO_RESUME
。PAUSE
位表示 PCM 支持“暂停”操作,而RESUME
位表示 PCM 支持完整的“挂起/恢复”操作。如果设置了PAUSE
标志,则下面的trigger
回调必须处理相应的(暂停推送/释放)命令。即使没有RESUME
标志,也可以定义挂起/恢复触发命令。有关详细信息,请参见 电源管理 部分。当可以同步 PCM 子流时(通常,同步回放和捕获流的启动/停止),你也可以提供
SNDRV_PCM_INFO_SYNC_START
。在这种情况下,你需要在触发回调中检查 PCM 子流的链接列表。这将在后面的章节中描述。formats
字段包含支持的格式的位标志(SNDRV_PCM_FMTBIT_XXX
)。如果硬件支持多种格式,请提供所有或运算后的位。在上面的示例中,指定了带符号的 16 位小端格式。rates
字段包含支持的速率的位标志(SNDRV_PCM_RATE_XXX
)。当芯片支持连续速率时,请额外传递CONTINUOUS
位。预定义的速率位仅为典型速率提供。如果你的芯片支持非常规速率,则需要添加KNOT
位并手动设置硬件约束(稍后解释)。rate_min
和rate_max
定义最小和最大采样率。这应以某种方式对应于rates
位。正如你可能已经预料到的,
channels_min
和channels_max
定义了最小和最大通道数。buffer_bytes_max
定义了以字节为单位的最大缓冲区大小。没有buffer_bytes_min
字段,因为它可以通过最小周期大小和最小周期数计算得出。同时,period_bytes_min
和period_bytes_max
定义了以字节为单位的周期的最小和最大大小。periods_max
和periods_min
定义了缓冲区中周期的最大和最小数量。“周期”是一个术语,对应于 OSS 世界中的片段。周期定义了生成 PCM 中断的点。此点在很大程度上取决于硬件。通常,较小的周期大小将为你提供更多的中断,这导致能够更及时地填充/耗尽缓冲区。在捕获的情况下,此大小定义了输入延迟。另一方面,整个缓冲区大小定义了回放方向的输出延迟。
还有一个字段
fifo_size
。这指定了硬件 FIFO 的大小,但目前驱动程序和 alsa-lib 均未使用它。因此,你可以忽略此字段。
PCM 配置¶
好的,让我们再次回到 PCM 运行时记录。运行时实例中最常引用的记录是 PCM 配置。在应用程序通过 alsa-lib 发送 hw_params
数据后,PCM 配置存储在运行时实例中。有许多从 hw_params 和 sw_params 结构体复制的字段。例如,format
保存应用程序选择的格式类型。此字段包含枚举值 SNDRV_PCM_FORMAT_XXX
。
需要注意的是,配置的缓冲区和周期大小以运行时的“帧”存储。在 ALSA 世界中,1 frame = channels * samples-size
。要在帧和字节之间进行转换,可以使用 frames_to_bytes()
和 bytes_to_frames()
辅助函数:
period_bytes = frames_to_bytes(runtime, runtime->period_size);
此外,许多软件参数 (sw_params) 也以帧存储。请检查字段的类型。snd_pcm_uframes_t
用于作为无符号整数的帧,而 snd_pcm_sframes_t
用于作为有符号整数的帧。
DMA 缓冲区信息¶
DMA 缓冲区由以下四个字段定义:dma_area
、dma_addr
、dma_bytes
和 dma_private
。dma_area
保存缓冲区指针(逻辑地址)。你可以从/向该指针调用 memcpy()
。同时,dma_addr
保存缓冲区的物理地址。仅当缓冲区为线性缓冲区时才指定此字段。dma_bytes
保存以字节为单位的缓冲区大小。dma_private
用于 ALSA DMA 分配器。
如果你使用托管缓冲区分配模式或标准 API 函数 snd_pcm_lib_malloc_pages()
来分配缓冲区,则这些字段由 ALSA 中间层设置,你不应自己更改它们。你可以读取它们,但不能写入它们。另一方面,如果你想自己分配缓冲区,则需要在 hw_params 回调中管理它。至少 dma_bytes
是强制性的。当缓冲区被 mmap 时,dma_area
是必需的。如果你的驱动程序不支持 mmap,则此字段不是必需的。dma_addr
也是可选的。你也可以根据需要使用 dma_private。
运行状态¶
可以通过 runtime->status
引用运行状态。这是一个指向 struct snd_pcm_mmap_status 记录的指针。例如,你可以通过 runtime->status->hw_ptr
获取当前的 DMA 硬件指针。
可以通过 runtime->control
引用 DMA 应用程序指针,该指针指向 struct snd_pcm_mmap_control 记录。但是,不建议直接访问此值。
私有数据¶
你可以为子流分配一个记录并将其存储在 runtime->private_data
中。通常,这是在 PCM open callback 中完成的。不要将其与 pcm->private_data
混淆。pcm->private_data
通常指向在创建 PCM 设备时静态分配的芯片实例,而 runtime->private_data
指向在 PCM open callback 中创建的动态数据结构。
static int snd_xxx_open(struct snd_pcm_substream *substream)
{
struct my_pcm_data *data;
....
data = kmalloc(sizeof(*data), GFP_KERNEL);
substream->runtime->private_data = data;
....
}
分配的对象必须在 close callback 中释放。
运算符¶
好的,现在让我详细介绍每个 PCM 回调(ops
)。通常,如果成功,每个回调都必须返回 0;如果失败,则返回负错误号(例如 -EINVAL
)。要选择合适的错误号码,建议检查内核的其他部分在相同类型的请求失败时返回什么值。
每个回调函数至少采用一个参数,该参数包含 struct snd_pcm_substream 指针。要从给定的子流实例中检索芯片记录,可以使用以下宏:
int xxx(...) {
struct mychip *chip = snd_pcm_substream_chip(substream);
....
}
该宏读取 substream->private_data
,这是 pcm->private_data
的副本。如果需要为每个 PCM 子流分配不同的数据记录,则可以覆盖前者。例如,cmi8330 驱动程序为回放和捕获方向分配不同的 private_data
,因为它对不同的方向使用两个不同的编解码器(与 SB 和 AD 兼容)。
PCM open callback¶
static int snd_xxx_open(struct snd_pcm_substream *substream);
当 PCM 子流被打开时,会调用此函数。
至少,你必须在这里初始化 runtime->hw
记录。通常,这是这样完成的
static int snd_xxx_open(struct snd_pcm_substream *substream)
{
struct mychip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
runtime->hw = snd_mychip_playback_hw;
return 0;
}
其中 snd_mychip_playback_hw
是预定义的硬件描述。
你可以在此回调函数中分配私有数据,如 私有数据 部分所述。
如果硬件配置需要更多约束,也请在此处设置硬件约束。有关更多详细信息,请参阅 约束。
关闭回调¶
static int snd_xxx_close(struct snd_pcm_substream *substream);
显然,当 PCM 子流被关闭时,会调用此函数。
在 open
回调函数中为 PCM 子流分配的任何私有实例都将在此处释放
static int snd_xxx_close(struct snd_pcm_substream *substream)
{
....
kfree(substream->runtime->private_data);
....
}
ioctl 回调¶
这用于对 PCM ioctl 的任何特殊调用。但通常你可以将其保留为 NULL,然后 PCM 核心将调用通用 ioctl 回调函数 snd_pcm_lib_ioctl()
。如果你需要处理通道信息的唯一设置或重置过程,你可以在此处传递你自己的回调函数。
hw_params 回调¶
static int snd_xxx_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *hw_params);
当硬件参数 (hw_params
) 由应用程序设置时,即一旦为 PCM 子流定义了缓冲区大小、周期大小、格式等,就会调用此函数。
许多硬件设置应该在此回调函数中完成,包括缓冲区的分配。
要初始化的参数通过 params_xxx()
宏检索。
当你为子流选择托管缓冲区分配模式时,在此回调函数被调用之前,缓冲区已经被分配。或者,你可以调用下面的辅助函数来分配缓冲区
snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(hw_params));
snd_pcm_lib_malloc_pages()
仅当 DMA 缓冲区已被预先分配时才可用。有关更多详细信息,请参阅 缓冲区类型 部分。
请注意,这个回调函数和 prepare
回调函数可能每次初始化都会被多次调用。例如,OSS 仿真可能会在每次通过其 ioctl 进行更改时调用这些回调函数。
因此,你需要小心不要多次分配相同的缓冲区,这会导致内存泄漏!多次调用上面的辅助函数是可以的。当它已经被分配时,它会自动释放之前的缓冲区。
另一个需要注意的是,默认情况下,此回调函数是非原子性的(可调度的),即当没有设置 nonatomic
标志时。这很重要,因为 trigger
回调函数是原子性的(不可调度的)。也就是说,互斥锁或任何与调度相关的功能在 trigger
回调函数中不可用。有关详细信息,请参阅子节 原子性。
hw_free 回调¶
static int snd_xxx_hw_free(struct snd_pcm_substream *substream);
调用此函数是为了释放通过 hw_params
分配的资源。
此函数始终在调用 close 回调函数之前调用。此外,该回调函数也可能被多次调用。跟踪每个资源是否已被释放。
当你为 PCM 子流选择托管缓冲区分配模式时,分配的 PCM 缓冲区将在调用此回调函数后自动释放。否则,你必须手动释放缓冲区。通常,当缓冲区是从预先分配的池中分配的时,你可以使用标准的 API 函数 snd_pcm_lib_malloc_pages()
,如下所示
snd_pcm_lib_free_pages(substream);
prepare 回调¶
static int snd_xxx_prepare(struct snd_pcm_substream *substream);
当 PCM 被“准备”时,会调用此回调函数。你可以在此处设置格式类型、采样率等。prepare
与 hw_params
的区别在于,每次调用 snd_pcm_prepare()
时,都会调用 prepare
回调函数,即在欠载等情况下恢复后。
请注意,此回调函数是非原子性的。你可以在此回调函数中安全地使用与调度相关的功能。
在此和以下回调函数中,你可以通过 runtime 记录 substream->runtime
来引用这些值。例如,要获取当前的速率、格式或通道,请分别访问 runtime->rate
、runtime->format
或 runtime->channels
。分配的缓冲区的物理地址设置为 runtime->dma_area
。缓冲区和周期大小分别位于 runtime->buffer_size
和 runtime->period_size
中。
请注意,每次设置时,此回调函数也会被多次调用。
trigger 回调¶
static int snd_xxx_trigger(struct snd_pcm_substream *substream, int cmd);
当 PCM 启动、停止或暂停时,会调用此函数。
该操作在第二个参数 SNDRV_PCM_TRIGGER_XXX
中指定,该参数在 <sound/pcm.h>
中定义。至少,必须在此回调函数中定义 START
和 STOP
命令
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
/* do something to start the PCM engine */
break;
case SNDRV_PCM_TRIGGER_STOP:
/* do something to stop the PCM engine */
break;
default:
return -EINVAL;
}
当 PCM 支持暂停操作(在硬件表的信息字段中给出)时,也必须在此处处理 PAUSE_PUSH
和 PAUSE_RELEASE
命令。前者是暂停 PCM 的命令,后者是再次重启 PCM 的命令。
当 PCM 支持挂起/恢复操作时,无论是否完全或部分支持挂起/恢复,也必须处理 SUSPEND
和 RESUME
命令。当电源管理状态发生变化时,会发出这些命令。显然,SUSPEND
和 RESUME
命令会挂起和恢复 PCM 子流,通常,它们分别与 STOP
和 START
命令相同。有关详细信息,请参阅 电源管理 部分。
如前所述,默认情况下,此回调函数是原子性的,除非设置了 nonatomic
标志,并且你不能调用可能休眠的函数。trigger
回调函数应尽可能小,只需真正触发 DMA 即可。其他内容应在 hw_params
和 prepare
回调函数中事先正确初始化。
sync_stop 回调¶
static int snd_xxx_sync_stop(struct snd_pcm_substream *substream);
此回调是可选的,可以传递 NULL。它在 PCM 核心停止流之后调用,在它通过 prepare
、hw_params
或 hw_free
更改流状态之前调用。由于 IRQ 处理程序可能仍在挂起,我们需要等待挂起的任务完成后才能进入下一步;否则可能会由于资源冲突或访问释放的资源而导致崩溃。典型的行为是调用像 synchronize_irq()
这样的同步函数。
对于大多数只需要调用 synchronize_irq()
的驱动程序,也有一个更简单的设置。在保持 sync_stop
PCM 回调为 NULL 的同时,驱动程序可以将 card->sync_irq
字段设置为请求 IRQ 后返回的中断号。然后 PCM 核心将适当地使用给定的 IRQ 调用 synchronize_irq()
。
如果 IRQ 处理程序由卡析构函数释放,则无需清除 card->sync_irq
,因为卡本身正在被释放。因此,通常你只需要在驱动程序代码中添加一行来分配 card->sync_irq
,除非驱动程序重新获取 IRQ。当驱动程序动态释放和重新获取 IRQ 时(例如,用于挂起/恢复),它需要再次适当地清除和重新设置 card->sync_irq
。
pointer 回调¶
static snd_pcm_uframes_t snd_xxx_pointer(struct snd_pcm_substream *substream)
当 PCM 中间层查询缓冲区中当前的硬件位置时,会调用此回调函数。位置必须以帧为单位返回,范围从 0 到 buffer_size - 1
。
这通常是从 PCM 中间层中的缓冲区更新例程中调用的,该例程在中断例程调用 snd_pcm_period_elapsed()
时被调用。然后 PCM 中间层更新位置并计算可用空间,并唤醒休眠的 poll 线程等。
默认情况下,此回调函数也是原子性的。
copy 和 fill_silence 操作¶
这些回调函数不是强制性的,在大多数情况下可以省略。当硬件缓冲区不能位于正常的内存空间中时,会使用这些回调函数。有些芯片在硬件中拥有自己的缓冲区,这些缓冲区是不可映射的。在这种情况下,你必须手动将数据从内存缓冲区传输到硬件缓冲区。或者,如果缓冲区在物理和虚拟内存空间上都是非连续的,则也必须定义这些回调函数。
如果定义了这两个回调函数,则复制和设置静音操作将由它们完成。详细信息将在后面的 缓冲区和内存管理 部分中描述。
ack 回调¶
此回调函数也不是强制性的。当在读取或写入操作中更新 appl_ptr
时,会调用此回调函数。一些驱动程序(如 emu10k1-fx 和 cs46xx)需要跟踪内部缓冲区的当前 appl_ptr
,并且此回调函数仅对此类目的有用。
回调函数可能会返回 0 或负错误。当返回值是 -EPIPE
时,PCM 核心将其视为缓冲区 XRUN,并将状态自动更改为 SNDRV_PCM_STATE_XRUN
。
默认情况下,此回调函数是原子性的。
page 回调¶
此回调函数也是可选的。mmap 调用此回调函数来获取页面错误地址。
对于标准的 SG 缓冲区或 vmalloc 缓冲区,你不需要特殊的回调函数。因此,很少使用此回调函数。
mmap 回调¶
这是另一个用于控制 mmap 行为的可选回调函数。定义后,当页面被内存映射时,PCM 核心会调用此回调函数,而不是使用标准的辅助函数。如果你需要特殊处理(由于某些架构或设备特定的问题),请根据你的喜好在此处实现所有内容。
PCM 中断处理程序¶
PCM 内容的剩余部分是 PCM 中断处理程序。声音驱动程序中 PCM 中断处理程序的作用是更新缓冲区位置,并在缓冲区位置跨越指定的周期边界时通知 PCM 中间层。要告知此信息,请调用 snd_pcm_period_elapsed()
函数。
声音芯片可以通过多种方式生成中断。
周期(片段)边界处的中断¶
这是最常见的类型:硬件在每个周期边界生成中断。在这种情况下,你可以在每次中断时调用 snd_pcm_period_elapsed()
。
snd_pcm_period_elapsed()
将子流指针作为其参数。因此,你需要保持子流指针可以从芯片实例访问。例如,在芯片记录中定义 substream
字段以保存当前正在运行的子流指针,并在 open
回调函数中设置指针值(并在 close
回调函数中重置)。
如果在中断处理程序中获取自旋锁,并且该锁也在其他 PCM 回调函数中使用,则必须在调用 snd_pcm_period_elapsed()
之前释放该锁,因为 snd_pcm_period_elapsed()
在内部调用其他 PCM 回调函数。
典型的代码如下所示
static irqreturn_t snd_mychip_interrupt(int irq, void *dev_id)
{
struct mychip *chip = dev_id;
spin_lock(&chip->lock);
....
if (pcm_irq_invoked(chip)) {
/* call updater, unlock before it */
spin_unlock(&chip->lock);
snd_pcm_period_elapsed(chip->substream);
spin_lock(&chip->lock);
/* acknowledge the interrupt if necessary */
}
....
spin_unlock(&chip->lock);
return IRQ_HANDLED;
}
此外,当设备可以检测到缓冲区欠载/溢出时,驱动程序可以通过调用 snd_pcm_stop_xrun()
将 XRUN 状态通知给 PCM 核心。此函数停止流并将 PCM 状态设置为 SNDRV_PCM_STATE_XRUN
。请注意,它必须在 PCM 流锁之外调用,因此不能从原子回调函数中调用。
高频定时器中断¶
当硬件不在周期边界生成中断,而是以固定的定时器速率发出定时器中断时(例如 es1968 或 ymfpci 驱动程序),就会发生这种情况。在这种情况下,你需要检查当前的硬件位置,并在每次中断时累积已处理的样本长度。当累积大小超过周期大小时,调用 snd_pcm_period_elapsed()
并重置累加器。
典型的代码如下所示
static irqreturn_t snd_mychip_interrupt(int irq, void *dev_id)
{
struct mychip *chip = dev_id;
spin_lock(&chip->lock);
....
if (pcm_irq_invoked(chip)) {
unsigned int last_ptr, size;
/* get the current hardware pointer (in frames) */
last_ptr = get_hw_ptr(chip);
/* calculate the processed frames since the
* last update
*/
if (last_ptr < chip->last_ptr)
size = runtime->buffer_size + last_ptr
- chip->last_ptr;
else
size = last_ptr - chip->last_ptr;
/* remember the last updated point */
chip->last_ptr = last_ptr;
/* accumulate the size */
chip->size += size;
/* over the period boundary? */
if (chip->size >= runtime->period_size) {
/* reset the accumulator */
chip->size %= runtime->period_size;
/* call updater */
spin_unlock(&chip->lock);
snd_pcm_period_elapsed(substream);
spin_lock(&chip->lock);
}
/* acknowledge the interrupt if necessary */
}
....
spin_unlock(&chip->lock);
return IRQ_HANDLED;
}
在调用 snd_pcm_period_elapsed()
时¶
在这两种情况下,即使已经过去了不止一个周期,你也不必多次调用 snd_pcm_period_elapsed()
。只需调用一次即可。PCM 层将检查当前的硬件指针并更新到最新的状态。
原子性¶
内核编程中最重要(因此也最难调试)的问题之一是竞争条件。在 Linux 内核中,通常通过自旋锁、互斥锁或信号量来避免它们。一般来说,如果竞争条件可能发生在中断处理程序中,则必须以原子方式进行管理,并且你必须使用自旋锁来保护关键部分。如果关键部分不在中断处理程序代码中,并且可以接受相对较长的执行时间,则应使用互斥锁或信号量代替。
如前所述,一些 PCM 回调函数是原子性的,而另一些则不是。例如,hw_params
回调函数是非原子性的,而 trigger
回调函数是原子性的。这意味着,后者已经在 PCM 中间层持有的自旋锁(PCM 流锁)中调用。在回调函数中选择锁定方案时,请考虑此原子性。
在原子回调函数中,你不能使用可能调用 schedule()
或进入 sleep()
的函数。信号量和互斥锁可能会休眠,因此它们不能在原子回调函数(例如 trigger
回调函数)中使用。要在此类回调函数中实现某些延迟,请使用 udelay()
或 mdelay()
。
所有三个原子回调函数(trigger、pointer 和 ack)都是在禁用本地中断的情况下调用的。
但是,可以请求所有 PCM 操作都是非原子性的。这假设所有调用站点都在非原子上下文中。例如,函数 snd_pcm_period_elapsed()
通常从中断处理程序中调用。但是,如果将驱动程序设置为使用线程中断处理程序,则此调用也可以在非原子上下文中进行。在这种情况下,你可以在创建 struct snd_pcm 对象后设置其 nonatomic
字段。设置此标志后,PCM 核心内部将使用互斥锁和 rwsem 代替 spin 和 rwlocks,以便你可以在非原子上下文中安全地调用所有 PCM 函数。
此外,在某些情况下,你可能需要在原子上下文中调用 snd_pcm_period_elapsed()
(例如,在 ack
或其他回调期间经过了周期)。也有一个变体可以在 PCM 流锁内部调用 snd_pcm_period_elapsed_under_stream_lock()
用于此目的。
约束¶
由于物理限制,硬件不是无限可配置的。这些限制通过设置约束来表达。
例如,为了将采样率限制为一些支持的值,请使用 snd_pcm_hw_constraint_list()
。你需要在 open 回调函数中调用此函数
static unsigned int rates[] =
{4000, 10000, 22050, 44100};
static struct snd_pcm_hw_constraint_list constraints_rates = {
.count = ARRAY_SIZE(rates),
.list = rates,
.mask = 0,
};
static int snd_mychip_pcm_open(struct snd_pcm_substream *substream)
{
int err;
....
err = snd_pcm_hw_constraint_list(substream->runtime, 0,
SNDRV_PCM_HW_PARAM_RATE,
&constraints_rates);
if (err < 0)
return err;
....
}
有许多不同的约束。查看 sound/pcm.h
以获取完整列表。你甚至可以定义自己的约束规则。例如,假设 my_chip 可以管理一个 1 通道的子流,当且仅当格式为 S16_LE
,否则它支持 struct snd_pcm_hardware 中指定的任何格式(或任何其他 constraint_list)。你可以构建如下规则
static int hw_rule_channels_by_format(struct snd_pcm_hw_params *params,
struct snd_pcm_hw_rule *rule)
{
struct snd_interval *c = hw_param_interval(params,
SNDRV_PCM_HW_PARAM_CHANNELS);
struct snd_mask *f = hw_param_mask(params, SNDRV_PCM_HW_PARAM_FORMAT);
struct snd_interval ch;
snd_interval_any(&ch);
if (f->bits[0] == SNDRV_PCM_FMTBIT_S16_LE) {
ch.min = ch.max = 1;
ch.integer = 1;
return snd_interval_refine(c, &ch);
}
return 0;
}
然后你需要调用此函数来添加你的规则
snd_pcm_hw_rule_add(substream->runtime, 0, SNDRV_PCM_HW_PARAM_CHANNELS,
hw_rule_channels_by_format, NULL,
SNDRV_PCM_HW_PARAM_FORMAT, -1);
当应用程序设置 PCM 格式时,将调用规则函数,并且它会相应地优化通道数。但是应用程序可能会在设置格式之前设置通道数。因此,你还需要定义反向规则
static int hw_rule_format_by_channels(struct snd_pcm_hw_params *params,
struct snd_pcm_hw_rule *rule)
{
struct snd_interval *c = hw_param_interval(params,
SNDRV_PCM_HW_PARAM_CHANNELS);
struct snd_mask *f = hw_param_mask(params, SNDRV_PCM_HW_PARAM_FORMAT);
struct snd_mask fmt;
snd_mask_any(&fmt); /* Init the struct */
if (c->min < 2) {
fmt.bits[0] &= SNDRV_PCM_FMTBIT_S16_LE;
return snd_mask_refine(f, &fmt);
}
return 0;
}
... 并在 open 回调函数中
snd_pcm_hw_rule_add(substream->runtime, 0, SNDRV_PCM_HW_PARAM_FORMAT,
hw_rule_format_by_channels, NULL,
SNDRV_PCM_HW_PARAM_CHANNELS, -1);
hw 约束的一个典型用法是将缓冲区大小与周期大小对齐。默认情况下,ALSA PCM 核心不强制缓冲区大小与周期大小对齐。例如,可以有像 256 周期字节和 999 缓冲区字节这样的组合。
但是,许多设备芯片要求缓冲区是周期的倍数。在这种情况下,调用 snd_pcm_hw_constraint_integer()
用于 SNDRV_PCM_HW_PARAM_PERIODS
snd_pcm_hw_constraint_integer(substream->runtime,
SNDRV_PCM_HW_PARAM_PERIODS);
这确保了周期的数量是整数,因此缓冲区大小与周期大小对齐。
hw 约束是定义首选 PCM 配置的非常强大的机制,并且有相关的辅助函数。我不会在此处提供更多详细信息,而是想说“卢克,使用源代码。”
控制接口¶
常规¶
控制接口广泛用于许多从用户空间访问的开关、滑块等。其最重要的用途是混音器接口。换句话说,自 ALSA 0.9.x 以来,所有混音器内容都是在控制内核 API 上实现的。
ALSA 有一个定义良好的 AC97 控制模块。如果你的芯片仅支持 AC97,而没有其他内容,则可以跳过本节。
控制 API 在 <sound/control.h>
中定义。如果要添加你自己的控件,请包含此文件。
控制的定义¶
要创建新控件,你需要定义以下三个回调函数:info
、get
和 put
。然后,定义一个 struct snd_kcontrol_new 记录,例如
static struct snd_kcontrol_new my_control = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "PCM Playback Switch",
.index = 0,
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
.private_value = 0xffff,
.info = my_control_info,
.get = my_control_get,
.put = my_control_put
};
iface
字段指定控制类型 SNDRV_CTL_ELEM_IFACE_XXX
,通常是 MIXER
。对于逻辑上不是混音器一部分的全局控件,请使用 CARD
。如果控制与声卡上的某个特定设备密切相关,请使用 HWDEP
、PCM
、RAWMIDI
、TIMER
或 SEQUENCER
,并使用 device
和 subdevice
字段指定设备号。
name
是名称标识符字符串。自 ALSA 0.9.x 以来,控制名称非常重要,因为它的角色是从其名称分类的。有预定义的标准控制名称。详细信息在 控制名称 子节中描述。
index
字段保存此控件的索引号。如果存在具有相同名称的几个不同的控件,则可以通过索引号来区分它们。当卡上存在多个编解码器时,就是这种情况。如果索引为零,则可以省略上面的定义。
access
字段包含此控件的访问类型。在此处提供位掩码 SNDRV_CTL_ELEM_ACCESS_XXX
的组合。详细信息将在 访问标志 子节中说明。
private_value
字段包含此记录的任意长整数值。当使用通用的 info
、get
和 put
回调函数时,你可以通过此字段传递一个值。如果需要几个小数字,则可以在按位组合它们。或者,也可以在此字段中存储某个记录的指针(强制转换为 unsigned long)。
tlv
字段可用于提供有关控件的元数据;请参阅 元数据 小节。
另外三个是 控件回调。
控件名称¶
有一些标准用于定义控件名称。通常,控件由三个部分定义,即 “SOURCE DIRECTION FUNCTION”。
第一个 SOURCE
指定控件的来源,是一个字符串,例如 “Master”、“PCM”、“CD” 和 “Line”。有许多预定义的来源。
第二个 DIRECTION
是以下字符串之一,具体取决于控件的方向:“Playback”、“Capture”、“Bypass Playback” 和 “Bypass Capture”。或者,它可以省略,表示回放和捕获方向。
第三个 FUNCTION
是以下字符串之一,具体取决于控件的功能:“Switch”、“Volume” 和 “Route”。
因此,控件名称的示例是“Master Capture Switch”或“PCM Playback Volume”。
有一些例外情况
全局捕获和回放¶
“Capture Source”、“Capture Switch” 和 “Capture Volume” 用于全局捕获(输入)源、开关和音量。类似地,“Playback Switch” 和 “Playback Volume” 用于全局输出增益开关和音量。
音调控制¶
音调控制开关和音量指定为 “Tone Control - XXX”,例如 “Tone Control - Switch”、“Tone Control - Bass”、“Tone Control - Center”。
3D 控制¶
3D 控制开关和音量指定为 “3D Control - XXX”,例如 “3D Control - Switch”、“3D Control - Center”、“3D Control - Space”。
麦克风增强¶
麦克风增强开关设置为 “Mic Boost” 或 “Mic Boost (6dB)”。
更精确的信息可以在 Documentation/sound/designs/control-names.rst
中找到。
访问标志¶
访问标志是一个位掩码,用于指定给定控件的访问类型。默认访问类型为 SNDRV_CTL_ELEM_ACCESS_READWRITE
,这意味着允许对此控件进行读写操作。如果省略访问标志(即 = 0),则默认将其视为 READWRITE
访问。
如果控件是只读的,请传递 SNDRV_CTL_ELEM_ACCESS_READ
。在这种情况下,您不必定义 put
回调。类似地,当控件是只写的(虽然这种情况很少见),您可以使用 WRITE
标志,并且您不需要 get
回调。
如果控件值频繁更改(例如 VU 表),则应提供 VOLATILE
标志。这意味着控件可能会在没有 更改通知 的情况下更改。应用程序应不断轮询此类控件。
当控件可以更新,但目前对任何事物都没有影响时,设置 INACTIVE
标志可能是合适的。例如,当没有打开 PCM 设备时,PCM 控件应处于非活动状态。
有 LOCK
和 OWNER
标志来更改写入权限。
控件回调¶
info 回调¶
info
回调用于获取有关此控件的详细信息。这必须存储给定的 struct snd_ctl_elem_info 对象的值。例如,对于具有单个元素的布尔控件
static int snd_myctl_mono_info(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_info *uinfo)
{
uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
uinfo->count = 1;
uinfo->value.integer.min = 0;
uinfo->value.integer.max = 1;
return 0;
}
type
字段指定控件的类型。有 BOOLEAN
、INTEGER
、ENUMERATED
、BYTES
、IEC958
和 INTEGER64
。count
字段指定此控件中的元素数。例如,立体声音量将具有 count = 2。value
字段是一个联合,并且存储的值取决于类型。布尔类型和整数类型相同。
枚举类型与其他类型略有不同。您需要为选定的项目索引设置字符串
static int snd_myctl_enum_info(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_info *uinfo)
{
static char *texts[4] = {
"First", "Second", "Third", "Fourth"
};
uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED;
uinfo->count = 1;
uinfo->value.enumerated.items = 4;
if (uinfo->value.enumerated.item > 3)
uinfo->value.enumerated.item = 3;
strcpy(uinfo->value.enumerated.name,
texts[uinfo->value.enumerated.item]);
return 0;
}
上面的回调可以使用辅助函数 snd_ctl_enum_info()
简化。最终代码如下所示。(您可以传递 ARRAY_SIZE(texts)
而不是第三个参数中的 4;这是一个品味问题。)
static int snd_myctl_enum_info(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_info *uinfo)
{
static char *texts[4] = {
"First", "Second", "Third", "Fourth"
};
return snd_ctl_enum_info(uinfo, 1, 4, texts);
}
一些常见的 info 回调可供您方便地使用:snd_ctl_boolean_mono_info()
和 snd_ctl_boolean_stereo_info()
。显然,前者是单声道布尔项的 info 回调,就像上面的 snd_myctl_mono_info()
一样,后者是立体声通道布尔项的 info 回调。
get 回调¶
此回调用于读取控件的当前值,以便可以将其返回到用户空间。
例如:
static int snd_myctl_get(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct mychip *chip = snd_kcontrol_chip(kcontrol);
ucontrol->value.integer.value[0] = get_some_value(chip);
return 0;
}
value
字段取决于控件的类型以及 info 回调。例如,sb 驱动程序使用此字段来存储寄存器偏移、位移和位掩码。private_value
字段设置如下
.private_value = reg | (shift << 16) | (mask << 24)
并在如下回调中检索
static int snd_sbmixer_get_single(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
int reg = kcontrol->private_value & 0xff;
int shift = (kcontrol->private_value >> 16) & 0xff;
int mask = (kcontrol->private_value >> 24) & 0xff;
....
}
在 get
回调中,如果控件有多个元素,即 count > 1
,则必须填充所有元素。在上面的示例中,我们只填充了一个元素 (value.integer.value[0]
),因为假设 count = 1
。
put 回调¶
此回调用于写入来自用户空间的值。
例如:
static int snd_myctl_put(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct mychip *chip = snd_kcontrol_chip(kcontrol);
int changed = 0;
if (chip->current_value !=
ucontrol->value.integer.value[0]) {
change_current_value(chip,
ucontrol->value.integer.value[0]);
changed = 1;
}
return changed;
}
如上所示,如果值已更改,则必须返回 1。如果值未更改,则返回 0。如果发生任何致命错误,请像往常一样返回负错误代码。
与 get
回调一样,当控件有多个元素时,也必须在此回调中评估所有元素。
回调不是原子的¶
这三个回调都不是原子的。
控件构造函数¶
当一切准备就绪时,我们终于可以创建一个新的控件。要创建控件,需要调用两个函数,snd_ctl_new1()
和 snd_ctl_add()
。
以最简单的方式,您可以这样做
err = snd_ctl_add(card, snd_ctl_new1(&my_control, chip));
if (err < 0)
return err;
其中 my_control
是上面定义的 struct snd_kcontrol_new 对象,而 chip 是要传递给 kcontrol->private_data 的对象指针,可以在回调中引用它。
snd_ctl_new1()
分配一个新的 struct snd_kcontrol 实例,而 snd_ctl_add()
将给定的控件组件分配给卡。
更改通知¶
如果您需要在中断例程中更改和更新控件,则可以调用 snd_ctl_notify()
。例如
snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, id_pointer);
此函数采用卡指针、事件掩码和控件 ID 指针进行通知。事件掩码指定通知的类型,例如,在上面的示例中,通知控件值的更改。id 指针是要通知的 struct snd_ctl_elem_id 的指针。您可以在 es1938.c
或 es1968.c
中找到一些硬件音量中断的示例。
元数据¶
要提供有关混音器控件的 dB 值的信息,请使用来自 <sound/tlv.h>
的 DECLARE_TLV_xxx
宏之一来定义包含此信息的变量,设置 tlv.p
字段以指向此变量,并在 access
字段中包含 SNDRV_CTL_ELEM_ACCESS_TLV_READ
标志;如下所示
static DECLARE_TLV_DB_SCALE(db_scale_my_control, -4050, 150, 0);
static struct snd_kcontrol_new my_control = {
...
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
SNDRV_CTL_ELEM_ACCESS_TLV_READ,
...
.tlv.p = db_scale_my_control,
};
DECLARE_TLV_DB_SCALE()
宏定义有关混音器控件的信息,其中控件值的每个步骤都将 dB 值更改一个恒定的 dB 量。第一个参数是要定义的变量的名称。第二个参数是最小值,以 0.01 dB 为单位。第三个参数是步长,以 0.01 dB 为单位。如果最小值实际上会使控件静音,则将第四个参数设置为 1。
DECLARE_TLV_DB_LINEAR()
宏定义有关混音器控件的信息,其中控件的值线性影响输出。第一个参数是要定义的变量的名称。第二个参数是最小值,以 0.01 dB 为单位。第三个参数是最大值,以 0.01 dB 为单位。如果最小值会使控件静音,则将第二个参数设置为 TLV_DB_GAIN_MUTE
。
AC97 编解码器的 API¶
常规¶
ALSA AC97 编解码器层是一个定义明确的层,您不必编写太多代码来控制它。只需要低级控制例程。AC97 编解码器 API 在 <sound/ac97_codec.h>
中定义。
完整代码示例¶
struct mychip {
....
struct snd_ac97 *ac97;
....
};
static unsigned short snd_mychip_ac97_read(struct snd_ac97 *ac97,
unsigned short reg)
{
struct mychip *chip = ac97->private_data;
....
/* read a register value here from the codec */
return the_register_value;
}
static void snd_mychip_ac97_write(struct snd_ac97 *ac97,
unsigned short reg, unsigned short val)
{
struct mychip *chip = ac97->private_data;
....
/* write the given register value to the codec */
}
static int snd_mychip_ac97(struct mychip *chip)
{
struct snd_ac97_bus *bus;
struct snd_ac97_template ac97;
int err;
static struct snd_ac97_bus_ops ops = {
.write = snd_mychip_ac97_write,
.read = snd_mychip_ac97_read,
};
err = snd_ac97_bus(chip->card, 0, &ops, NULL, &bus);
if (err < 0)
return err;
memset(&ac97, 0, sizeof(ac97));
ac97.private_data = chip;
return snd_ac97_mixer(bus, &ac97, &chip->ac97);
}
AC97 构造函数¶
要创建 ac97 实例,首先使用带有回调函数的 ac97_bus_ops_t
记录调用 snd_ac97_bus()
struct snd_ac97_bus *bus;
static struct snd_ac97_bus_ops ops = {
.write = snd_mychip_ac97_write,
.read = snd_mychip_ac97_read,
};
snd_ac97_bus(card, 0, &ops, NULL, &pbus);
总线记录在所有属于它的 ac97 实例之间共享。
然后,使用 struct snd_ac97_template 记录以及上面创建的总线指针调用 snd_ac97_mixer()
struct snd_ac97_template ac97;
int err;
memset(&ac97, 0, sizeof(ac97));
ac97.private_data = chip;
snd_ac97_mixer(bus, &ac97, &chip->ac97);
其中 chip->ac97 是指向新创建的 ac97_t
实例的指针。在这种情况下,芯片指针设置为私有数据,以便读/写回调函数可以引用此芯片实例。此实例不一定存储在芯片记录中。如果需要从驱动程序更改寄存器值,或者需要暂停/恢复 ac97 编解码器,请保留此指针以传递给相应的函数。
AC97 回调¶
标准回调是 read
和 write
。显然,它们对应于对硬件低级代码进行读写访问的函数。
read
回调返回参数中指定的寄存器值
static unsigned short snd_mychip_ac97_read(struct snd_ac97 *ac97,
unsigned short reg)
{
struct mychip *chip = ac97->private_data;
....
return the_register_value;
}
在这里,可以从 ac97->private_data
强制转换芯片。
同时,write
回调用于设置寄存器值
static void snd_mychip_ac97_write(struct snd_ac97 *ac97,
unsigned short reg, unsigned short val)
这些回调与控件 API 回调一样是非原子的。
还有其他回调:reset
、wait
和 init
。
reset
回调用于重置编解码器。如果芯片需要特殊类型的重置,则可以定义此回调。
wait
回调用于在编解码器的标准初始化中添加一些等待时间。如果芯片需要额外的等待时间,请定义此回调。
init
回调用于编解码器的其他初始化。
在驱动程序中更新寄存器¶
如果需要从驱动程序访问编解码器,可以调用以下函数:snd_ac97_write()
、snd_ac97_read()
、snd_ac97_update()
和 snd_ac97_update_bits()
。
snd_ac97_write()
和 snd_ac97_update()
函数都用于将值设置为给定的寄存器 (AC97_XXX
)。它们之间的区别在于,如果给定的值已设置,则 snd_ac97_update()
不会写入值,而 snd_ac97_write()
始终会重写该值
snd_ac97_write(ac97, AC97_MASTER, 0x8080);
snd_ac97_update(ac97, AC97_MASTER, 0x8080);
snd_ac97_read()
用于读取给定寄存器的值。例如
value = snd_ac97_read(ac97, AC97_MASTER);
snd_ac97_update_bits()
用于更新给定寄存器中的某些位
snd_ac97_update_bits(ac97, reg, mask, value);
此外,还有一个函数可以在编解码器支持 VRA 或 DRA 时更改采样率(给定寄存器的采样率,例如 AC97_PCM_FRONT_DAC_RATE
):snd_ac97_set_rate()
snd_ac97_set_rate(ac97, AC97_PCM_FRONT_DAC_RATE, 44100);
以下寄存器可用于设置速率:AC97_PCM_MIC_ADC_RATE
、AC97_PCM_FRONT_DAC_RATE
、AC97_PCM_LR_ADC_RATE
、AC97_SPDIF
。指定 AC97_SPDIF
时,实际上不会更改寄存器,但会更新相应的 IEC958 状态位。
时钟调整¶
在某些芯片中,编解码器的时钟不是 48000,而是使用 PCI 时钟(为了节省石英!)。在这种情况下,将字段 bus->clock
更改为相应的值。例如,intel8x0 和 es1968 驱动程序有自己的函数来从时钟读取。
Proc 文件¶
ALSA AC97 接口将创建一个 proc 文件,例如 /proc/asound/card0/codec97#0/ac97#0-0
和 ac97#0-0+regs
。您可以参考这些文件来查看编解码器的当前状态和寄存器。
多个编解码器¶
当同一张卡上有多个编解码器时,您需要使用 ac97.num=1
或更大的值多次调用 snd_ac97_mixer()
。num
字段指定编解码器编号。
如果设置了多个编解码器,则需要为每个编解码器编写不同的回调,或者在回调例程中检查 ac97->num
。
MIDI (MPU401-UART) 接口¶
常规¶
许多声卡都有内置的 MIDI (MPU401-UART) 接口。当声卡支持标准的 MPU401-UART 接口时,很可能可以使用 ALSA MPU401-UART API。MPU401-UART API 在 <sound/mpu401.h>
中定义。
一些声卡芯片具有类似但略有不同的 mpu401 实现。例如,emu10k1 有自己的 mpu401 例程。
MIDI 构造函数¶
要创建 rawmidi 对象,请调用 snd_mpu401_uart_new()
struct snd_rawmidi *rmidi;
snd_mpu401_uart_new(card, 0, MPU401_HW_MPU401, port, info_flags,
irq, &rmidi);
第一个参数是卡指针,第二个参数是此组件的索引。最多可以创建 8 个 rawmidi 设备。
第三个参数是硬件类型,MPU401_HW_XXX
。如果不是特殊的类型,可以使用 MPU401_HW_MPU401
。
第四个参数是 I/O 端口地址。许多向后兼容的 MPU401 都有一个 I/O 端口,例如 0x330。或者,它可能是其自身的 PCI I/O 区域的一部分。这取决于芯片设计。
第五个参数是附加信息的位标志。当上面的 I/O 端口地址是 PCI I/O 区域的一部分时,MPU401 I/O 端口可能已被驱动程序本身分配(保留)。在这种情况下,传递一个位标志 MPU401_INFO_INTEGRATED
,并且 mpu401-uart 层将自行分配 I/O 端口。
当控制器仅支持输入或输出 MIDI 流时,分别传递 MPU401_INFO_INPUT
或 MPU401_INFO_OUTPUT
位标志。然后,rawmidi 实例将创建为单个流。
MPU401_INFO_MMIO
位标志用于将访问方法更改为 MMIO(通过 readb 和 writeb),而不是 iob 和 outb。在这种情况下,您必须将 iomapped 地址传递给 snd_mpu401_uart_new()
。
设置 MPU401_INFO_TX_IRQ
时,不会在默认中断处理程序中检查输出流。驱动程序需要自行调用 snd_mpu401_uart_interrupt_tx()
以开始处理 irq 处理程序中的输出流。
如果 MPU-401 接口与卡上的其他逻辑设备共享其中断,请设置 MPU401_INFO_IRQ_HOOK
(请参阅 下方)。
通常,端口地址对应于命令端口,而端口 + 1 对应于数据端口。如果不是,您可以稍后手动更改 struct snd_mpu401 的 cport
字段。但是,struct snd_mpu401 指针不会由 snd_mpu401_uart_new()
显式返回。您需要将 rmidi->private_data
显式转换为 struct snd_mpu401
struct snd_mpu401 *mpu;
mpu = rmidi->private_data;
并根据您的喜好重置 cport
mpu->cport = my_own_control_port;
第六个参数指定要分配的 ISA irq 编号。如果没有要分配的中断(因为您的代码已分配共享中断,或者因为设备未使用中断),请传递 -1。对于没有中断的 MPU-401 设备,将改用轮询计时器。
MIDI 中断处理程序¶
如果在 snd_mpu401_uart_new()
中分配了中断,则会自动使用独占的 ISA 中断处理程序,因此除了创建 mpu401 相关内容之外,您无需执行其他任何操作。否则,您必须设置 MPU401_INFO_IRQ_HOOK
,并在确定发生了 UART 中断时,从您自己的中断处理程序中显式调用 snd_mpu401_uart_interrupt()
。
在这种情况下,您需要将从 snd_mpu401_uart_new()
返回的 rawmidi 对象的 private_data 作为第二个参数传递给 snd_mpu401_uart_interrupt()
snd_mpu401_uart_interrupt(irq, rmidi->private_data, regs);
RawMIDI 接口¶
概述¶
raw MIDI 接口用于可以作为字节流访问的硬件 MIDI 端口。它不适用于不直接理解 MIDI 的合成器芯片。
ALSA 处理文件和缓冲区管理。您所要做的就是编写一些代码来在缓冲区和硬件之间移动数据。
rawmidi API 在 <sound/rawmidi.h>
中定义。
RawMIDI 构造函数¶
要创建 rawmidi 设备,请调用 snd_rawmidi_new()
函数
struct snd_rawmidi *rmidi;
err = snd_rawmidi_new(chip->card, "MyMIDI", 0, outs, ins, &rmidi);
if (err < 0)
return err;
rmidi->private_data = chip;
strcpy(rmidi->name, "My MIDI");
rmidi->info_flags = SNDRV_RAWMIDI_INFO_OUTPUT |
SNDRV_RAWMIDI_INFO_INPUT |
SNDRV_RAWMIDI_INFO_DUPLEX;
第一个参数是 card 指针,第二个参数是 ID 字符串。
第三个参数是此组件的索引。您最多可以创建 8 个 rawmidi 设备。
第四个和第五个参数分别是该设备的输出和输入子流的数量(子流相当于 MIDI 端口)。
设置 info_flags
字段以指定设备的功能。如果至少有一个输出端口,则设置 SNDRV_RAWMIDI_INFO_OUTPUT
;如果至少有一个输入端口,则设置 SNDRV_RAWMIDI_INFO_INPUT
;如果设备可以同时处理输出和输入,则设置 SNDRV_RAWMIDI_INFO_DUPLEX
。
创建 rawmidi 设备后,您需要为每个子流设置运算符(回调)。有一些辅助函数可以为设备的所有子流设置运算符
snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_OUTPUT, &snd_mymidi_output_ops);
snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_INPUT, &snd_mymidi_input_ops);
这些运算符通常这样定义
static struct snd_rawmidi_ops snd_mymidi_output_ops = {
.open = snd_mymidi_output_open,
.close = snd_mymidi_output_close,
.trigger = snd_mymidi_output_trigger,
};
这些回调在 RawMIDI 回调 部分中进行了解释。
如果存在多个子流,则应为每个子流指定唯一的名称
struct snd_rawmidi_substream *substream;
list_for_each_entry(substream,
&rmidi->streams[SNDRV_RAWMIDI_STREAM_OUTPUT].substreams,
list {
sprintf(substream->name, "My MIDI Port %d", substream->number + 1);
}
/* same for SNDRV_RAWMIDI_STREAM_INPUT */
RawMIDI 回调¶
在所有回调中,您可以将为 rawmidi 设备设置的私有数据作为 substream->rmidi->private_data
访问。
如果存在多个端口,您的回调可以从传递给每个回调的 struct snd_rawmidi_substream 数据中确定端口索引
struct snd_rawmidi_substream *substream;
int index = substream->number;
RawMIDI open 回调¶
static int snd_xxx_open(struct snd_rawmidi_substream *substream);
当打开子流时,将调用此函数。您可以在此处初始化硬件,但不应开始传输/接收数据。
RawMIDI close 回调¶
static int snd_xxx_close(struct snd_rawmidi_substream *substream);
猜猜看。
rawmidi 设备的 open
和 close
回调与互斥锁序列化,并且可以休眠。
输出子流的 Rawmidi 触发回调¶
static void snd_xxx_output_trigger(struct snd_rawmidi_substream *substream, int up);
当子流缓冲区中存在必须传输的一些数据时,将使用非零 up
参数调用此函数。
要从缓冲区读取数据,请调用 snd_rawmidi_transmit_peek()
。它将返回已读取的字节数;当缓冲区中没有更多数据时,此值将小于请求的字节数。成功传输数据后,调用 snd_rawmidi_transmit_ack()
以从子流缓冲区中删除数据
unsigned char data;
while (snd_rawmidi_transmit_peek(substream, &data, 1) == 1) {
if (snd_mychip_try_to_transmit(data))
snd_rawmidi_transmit_ack(substream, 1);
else
break; /* hardware FIFO full */
}
如果您事先知道硬件将接受数据,则可以使用 snd_rawmidi_transmit()
函数,该函数读取一些数据并立即将其从缓冲区中删除
while (snd_mychip_transmit_possible()) {
unsigned char data;
if (snd_rawmidi_transmit(substream, &data, 1) != 1)
break; /* no more data */
snd_mychip_transmit(data);
}
如果您事先知道可以接受多少字节,则可以将缓冲区大小设置为大于 1,并使用 snd_rawmidi_transmit*()
函数。
trigger
回调不得休眠。如果在子流缓冲区为空之前硬件 FIFO 已满,则必须稍后在中断处理程序中继续传输数据,或者如果硬件没有 MIDI 传输中断,则使用计时器。
当数据传输应中止时,将使用零 up
参数调用 trigger
回调。
输入子流的 RawMIDI 触发回调¶
static void snd_xxx_input_trigger(struct snd_rawmidi_substream *substream, int up);
使用非零 up
参数调用此函数以启用接收数据,或使用零 up
参数调用此函数以禁用接收数据。
trigger
回调不得休眠;从设备读取数据的实际操作通常在中断处理程序中完成。
启用数据接收后,您的中断处理程序应为所有接收到的数据调用 snd_rawmidi_receive()
void snd_mychip_midi_interrupt(...)
{
while (mychip_midi_available()) {
unsigned char data;
data = mychip_midi_read();
snd_rawmidi_receive(substream, &data, 1);
}
}
drain 回调¶
static void snd_xxx_drain(struct snd_rawmidi_substream *substream);
此函数仅用于输出子流。此函数应等待直到从子流缓冲区读取的所有数据都已传输。这样可以确保设备可以关闭并且驱动程序可以卸载而不会丢失数据。
此回调是可选的。如果您未在 struct snd_rawmidi_ops 结构中设置 drain
,则 ALSA 只会等待 50 毫秒。
其他设备¶
FM OPL3¶
FM OPL3 仍然在许多芯片中使用(主要用于向后兼容)。ALSA 也有一个不错的 OPL3 FM 控制层。OPL3 API 在 <sound/opl3.h>
中定义。
可以通过 direct-FM API 直接访问 FM 寄存器,该 API 在 <sound/asound_fm.h>
中定义。在 ALSA 本机模式下,FM 寄存器通过硬件相关设备 direct-FM 扩展 API 访问,而在 OSS 兼容模式下,FM 寄存器可以通过 /dev/dmfmX
设备中使用 OSS direct-FM 兼容 API 访问。
要创建 OPL3 组件,您需要调用两个函数。第一个是 opl3_t
实例的构造函数
struct snd_opl3 *opl3;
snd_opl3_create(card, lport, rport, OPL3_HW_OPL3_XXX,
integrated, &opl3);
第一个参数是 card 指针,第二个参数是左端口地址,第三个参数是右端口地址。在大多数情况下,右端口位于左端口 + 2 的位置。
第四个参数是硬件类型。
如果卡驱动程序已分配了左右端口,则将非零值传递给第五个参数 (integrated
)。否则,opl3 模块将自行分配指定的端口。
当访问硬件需要特殊方法而不是标准 I/O 访问时,您可以使用 snd_opl3_new()
单独创建 opl3 实例
struct snd_opl3 *opl3;
snd_opl3_new(card, OPL3_HW_OPL3_XXX, &opl3);
然后为私有访问函数、私有数据和析构函数设置 command
、private_data
和 private_free
。l_port
和 r_port
不一定需要设置。只有 command 必须正确设置。您可以从 opl3->private_data
字段检索数据。
通过 snd_opl3_new()
创建 opl3 实例后,调用 snd_opl3_init()
以将芯片初始化为正确的状态。请注意,snd_opl3_create()
始终在内部调用它。
如果成功创建了 opl3 实例,则为此 opl3 创建一个 hwdep 设备
struct snd_hwdep *opl3hwdep;
snd_opl3_hwdep_new(opl3, 0, 1, &opl3hwdep);
第一个参数是您创建的 opl3_t
实例,第二个参数是索引号,通常为 0。
第三个参数是分配给 OPL3 端口的音序器客户端的索引偏移量。当存在 MPU401-UART 时,此处应为 1(UART 始终占用 0)。
硬件相关设备¶
某些芯片需要用户空间访问才能进行特殊控制或加载微代码。在这种情况下,您可以创建一个 hwdep(硬件相关)设备。hwdep API 在 <sound/hwdep.h>
中定义。您可以在 opl3 驱动程序或 isa/sb/sb16_csp.c
中找到示例。
hwdep
实例的创建通过 snd_hwdep_new()
完成
struct snd_hwdep *hw;
snd_hwdep_new(card, "My HWDEP", 0, &hw);
其中第三个参数是索引号。
然后,您可以将任何指针值传递给 private_data
。如果您分配了私有数据,则还应定义一个析构函数。析构函数在 private_free
字段中设置
struct mydata *p = kmalloc(sizeof(*p), GFP_KERNEL);
hw->private_data = p;
hw->private_free = mydata_free;
析构函数的实现将是
static void mydata_free(struct snd_hwdep *hw)
{
struct mydata *p = hw->private_data;
kfree(p);
}
可以为此实例定义任意文件操作。文件运算符在 ops
表中定义。例如,假设此芯片需要一个 ioctl
hw->ops.open = mydata_open;
hw->ops.ioctl = mydata_ioctl;
hw->ops.release = mydata_release;
并根据您的喜好实现回调函数。
IEC958 (S/PDIF)¶
通常,IEC958 设备的控制是通过控制接口实现的。有一个宏可以为 IEC958 控制组合名称字符串,SNDRV_CTL_NAME_IEC958()
在 <include/asound.h>
中定义。
有一些用于 IEC958 状态位的标准控制。这些控制使用类型 SNDRV_CTL_ELEM_TYPE_IEC958
,并且元素的大小固定为 4 字节数组 (value.iec958.status[x])。对于 info
回调,您不为此类型指定 value 字段(但必须设置 count 字段)。
“IEC958 Playback Con Mask” 用于返回消费者模式的 IEC958 状态位的位掩码。类似地,“IEC958 Playback Pro Mask” 返回专业模式的位掩码。它们是只读控制。
同时,定义了 “IEC958 Playback Default” 控制以获取和设置当前默认的 IEC958 位。
由于历史原因,Playback Mask 和 Playback Default 控制的两种变体都可以在 SNDRV_CTL_ELEM_IFACE_PCM
或 SNDRV_CTL_ELEM_IFACE_MIXER
接口上实现。但是,驱动程序应在同一接口上公开掩码和默认值。
此外,您可以定义控制开关以启用/禁用或设置原始位模式。该实现将取决于芯片,但控制应命名为 “IEC958 xxx”,最好使用 SNDRV_CTL_NAME_IEC958()
宏。
您可以找到几个案例,例如 pci/emu10k1
、pci/ice1712
或 pci/cmipci.c
。
缓冲区和内存管理¶
缓冲区类型¶
ALSA 提供了几种不同的缓冲区分配函数,具体取决于总线和架构。所有这些都具有一致的 API。物理连续页面的分配通过 snd_malloc_xxx_pages()
函数完成,其中 xxx 是总线类型。
具有回退的页面分配通过 snd_dma_alloc_pages_fallback()
完成。此函数尝试分配指定数量的页面,但如果没有足够的页面可用,它会尝试减小请求大小,直到找到足够的空间为止,最多为一个页面。
要释放页面,请调用 snd_dma_free_pages()
函数。
通常,ALSA 驱动程序会尝试在加载模块时分配和保留较大的连续物理空间以供以后使用。这称为“预分配”。如前所述,您可以在 PCM 实例构造时(对于 PCI 总线)调用以下函数
snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_DEV,
&pci->dev, size, max);
其中 size
是要预分配的字节大小,max
是可以通过 prealloc
proc 文件设置的最大大小。分配器将尝试在给定大小内获取尽可能大的区域。
第二个参数(类型)和第三个参数(设备指针)取决于总线。对于普通设备,使用 SNDRV_DMA_TYPE_DEV
类型将设备指针(通常与 card->dev
相同)传递给第三个参数。
可以使用 SNDRV_DMA_TYPE_CONTINUOUS
类型预分配与总线无关的连续缓冲区。在这种情况下,您可以将 NULL 传递给设备指针,这是默认模式,意味着使用 GFP_KERNEL
标志进行分配。如果您需要受限制(较低)的地址,请为设备设置一致的 DMA 掩码位,并传递设备指针,就像正常的设备内存分配一样。对于此类型,如果不需要地址限制,仍然允许将 NULL 传递给设备指针。
对于散布/收集缓冲区,请将 SNDRV_DMA_TYPE_DEV_SG
与设备指针一起使用(请参阅 非连续缓冲区 部分)。
预先分配缓冲区后,您可以在 hw_params
回调中使用分配器
snd_pcm_lib_malloc_pages(substream, size);
请注意,您必须预先分配才能使用此函数。
但是,大多数驱动程序使用“托管缓冲区分配模式”而不是手动分配和释放。这是通过调用 snd_pcm_set_managed_buffer_all()
而不是 snd_pcm_lib_preallocate_pages_for_all()
完成的
snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV,
&pci->dev, size, max);
其中传递的参数与两个函数相同。托管模式的区别在于,PCM 核心将在调用 PCM hw_params
回调之前在内部调用 snd_pcm_lib_malloc_pages()
,并在 PCM hw_free
回调之后自动调用 snd_pcm_lib_free_pages()
。因此,驱动程序不再需要在其回调中显式调用这些函数。这允许许多驱动程序具有 NULL hw_params
和 hw_free
条目。
外部硬件缓冲区¶
某些芯片具有自己的硬件缓冲区,并且无法从主机内存进行 DMA 传输。在这种情况下,您需要 1) 直接将音频数据复制/设置到外部硬件缓冲区,或者 2) 创建一个中间缓冲区并在中断中(或最好在 tasklet 中)将数据从该缓冲区复制/设置到外部硬件缓冲区。
如果外部硬件缓冲区足够大,则第一种情况效果很好。此方法不需要任何额外的缓冲区,因此效率更高。除了播放的 fill_silence
回调之外,您还需要为数据传输定义 copy
回调。但是,有一个缺点:它无法进行 mmap。示例包括 GUS 的 GF1 PCM 或 emu8000 的波表 PCM。
第二种情况允许在缓冲区上进行 mmap,尽管您必须处理中断或 tasklet 才能将数据从中间缓冲区传输到硬件缓冲区。您可以在 vxpocket 驱动程序中找到示例。
另一种情况是,芯片使用 PCI 内存映射区域作为缓冲区,而不是主机内存。在这种情况下,mmap 仅在某些架构(如 Intel)上可用。在非 mmap 模式下,数据无法像正常方式那样传输。因此,您还需要定义 copy
和 fill_silence
回调,就像上述情况一样。示例可以在 rme32.c
和 rme96.c
中找到。
copy
和 silence
回调的实现取决于硬件是否支持交错或非交错采样。根据方向是播放还是捕获,copy
回调的定义如下,略有不同
static int playback_copy(struct snd_pcm_substream *substream,
int channel, unsigned long pos,
struct iov_iter *src, unsigned long count);
static int capture_copy(struct snd_pcm_substream *substream,
int channel, unsigned long pos,
struct iov_iter *dst, unsigned long count);
在交错采样的情况下,不使用第二个参数 (channel
)。第三个参数 (pos
) 指定以字节为单位的位置。
第四个参数的含义在播放和捕获之间有所不同。对于播放,它保存源数据指针,对于捕获,它是目标数据指针。
最后一个参数是要复制的字节数。
您在此回调中所要做的操作在播放和捕获方向之间再次不同。在播放情况下,您将指定指针 (src
) 处的给定数据量 (count
) 复制到硬件缓冲区中的指定偏移量 (pos
) 处。当以类似 memcpy 的方式编码时,复制将如下所示
my_memcpy_from_iter(my_buffer + pos, src, count);
对于捕获方向,您将硬件缓冲区中指定偏移量 (pos
) 处的给定数据量 (count
) 复制到指定指针 (dst
)
my_memcpy_to_iter(dst, my_buffer + pos, count);
给定的 src
或 dst
是一个 struct iov_iter 指针,其中包含指针和大小。使用 linux/uio.h
中定义的现有辅助函数复制或访问数据。
细心的读者可能会注意到,这些回调接收的参数是以字节为单位的,而不是像其他回调那样以帧为单位。这是因为这使得编码更容易(如上面的示例所示),并且它也使得统一交错和非交错情况更容易,如下所述。
在非交错采样的情况下,实现将更加复杂。为每个通道调用回调,在第二个参数中传递,因此总共每次传输调用 N 次。
其他参数的含义与交错情况几乎相同。回调应该从/向给定的用户空间缓冲区复制数据,但仅针对给定的通道。有关详细信息,请查看 isa/gus/gus_pcm.c
或 pci/rme9652/rme9652.c
作为示例。
通常,对于播放,还定义了另一个回调 fill_silence
。它的实现方式与上面的 copy 回调类似
static int silence(struct snd_pcm_substream *substream, int channel,
unsigned long pos, unsigned long count);
参数的含义与 copy
回调中的含义相同,尽管没有缓冲区指针参数。在交错采样的情况下,channel 参数没有意义,就像 copy
回调一样。
fill_silence
回调的作用是在硬件缓冲区中的指定偏移量 (pos
) 处设置给定量 (count
) 的静音数据。假设数据格式已签名(即,静音数据为 0),并且使用类似 memset 函数的实现将如下所示
my_memset(my_buffer + pos, 0, count);
在非交错采样的情况下,实现再次变得更加复杂,因为它每次传输都为每个通道调用 N 次。例如,请参阅 isa/gus/gus_pcm.c
。
非连续缓冲区¶
如果您的硬件支持页面表(如 emu10k1 中)或缓冲区描述符(如 via82xx 中),则可以使用散布/收集 (SG) DMA。ALSA 提供了用于处理 SG 缓冲区的接口。该 API 在 <sound/pcm.h>
中提供。
为了创建 SG 缓冲区处理程序,请在 PCM 构造函数中使用 SNDRV_DMA_TYPE_DEV_SG
调用 snd_pcm_set_managed_buffer()
或 snd_pcm_set_managed_buffer_all()
,就像其他 PCI 预分配一样。您还需要传递 &pci->dev
,其中 pci 是芯片的 struct pci_dev 指针
snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV_SG,
&pci->dev, size, max);
然后,struct snd_sg_buf
实例创建为 substream->dma_private
。您可以像这样转换指针
struct snd_sg_buf *sgbuf = (struct snd_sg_buf *)substream->dma_private;
然后在 snd_pcm_lib_malloc_pages()
调用中,通用的 SG-buffer 处理程序将分配给定大小的非连续内核页面,并将它们映射为虚拟连续内存。虚拟指针通过 runtime->dma_area 进行寻址。物理地址 (runtime->dma_addr
) 设置为零,因为缓冲区在物理上是非连续的。物理地址表在 sgbuf->table
中设置。您可以通过 snd_pcm_sgbuf_get_addr()
获取特定偏移量的物理地址。
如果您需要显式释放 SG-buffer 数据,请像往常一样调用标准 API 函数 snd_pcm_lib_free_pages()
。
Vmalloc 分配的缓冲区¶
可以使用通过 vmalloc()
分配的缓冲区,例如,作为中间缓冲区。在设置了 SNDRV_DMA_TYPE_VMALLOC
类型的缓冲区预分配后,您可以简单地通过标准的 snd_pcm_lib_malloc_pages()
等函数来分配它。
snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_VMALLOC,
NULL, 0, 0);
NULL 作为设备指针参数传递,这表示将分配默认页面(GFP_KERNEL 和 GFP_HIGHMEM)。
另外,请注意,此处将零作为大小和最大大小参数传递。由于每个 vmalloc 调用都应该随时成功,因此我们不需要像其他连续页面那样预先分配缓冲区。
Proc 接口¶
ALSA 为 procfs 提供了一个简单的接口。proc 文件对于调试非常有用。我建议您设置 proc 文件,如果您编写驱动程序并希望获得运行状态或寄存器转储。该 API 位于 <sound/info.h>
中。
要创建 proc 文件,请调用 snd_card_proc_new()
struct snd_info_entry *entry;
int err = snd_card_proc_new(card, "my-file", &entry);
其中第二个参数指定要创建的 proc 文件的名称。上面的例子将在卡目录(例如 /proc/asound/card0/my-file
)下创建一个文件 my-file
。
与其他组件一样,通过 snd_card_proc_new()
创建的 proc 条目将在卡注册和释放函数中自动注册和释放。
创建成功后,该函数将新实例存储在第三个参数给出的指针中。它被初始化为只读文本 proc 文件。要按原样将此 proc 文件用作只读文本文件,请通过 snd_info_set_text_ops()
设置带有私有数据的读取回调。
snd_info_set_text_ops(entry, chip, my_proc_read);
其中第二个参数 (chip
) 是回调中要使用的私有数据。第三个参数指定读取缓冲区大小,第四个参数 (my_proc_read
) 是回调函数,其定义如下:
static void my_proc_read(struct snd_info_entry *entry,
struct snd_info_buffer *buffer);
在读取回调中,使用 snd_iprintf()
输出字符串,它的工作方式与普通的 printf()
一样。 例如:
static void my_proc_read(struct snd_info_entry *entry,
struct snd_info_buffer *buffer)
{
struct my_chip *chip = entry->private_data;
snd_iprintf(buffer, "This is my chip!\n");
snd_iprintf(buffer, "Port = %ld\n", chip->port);
}
文件权限可以在之后更改。默认情况下,它们对所有用户都是只读的。如果您想为用户(默认情况下是 root)添加写入权限,请执行以下操作:
entry->mode = S_IFREG | S_IRUGO | S_IWUSR;
并设置写入缓冲区大小和回调:
entry->c.text.write = my_proc_write;
在写入回调中,您可以使用 snd_info_get_line()
获取文本行,并使用 snd_info_get_str()
从该行检索字符串。 一些例子可以在 core/oss/mixer_oss.c
, core/oss/ 和 pcm_oss.c
中找到。
对于原始数据 proc 文件,请按如下方式设置属性:
static const struct snd_info_entry_ops my_file_io_ops = {
.read = my_file_io_read,
};
entry->content = SNDRV_INFO_CONTENT_DATA;
entry->private_data = chip;
entry->c.ops = &my_file_io_ops;
entry->size = 4096;
entry->mode = S_IFREG | S_IRUGO;
对于原始数据,必须正确设置 size
字段。这指定了 proc 文件访问的最大大小。
原始模式的读/写回调比文本模式更直接。您需要使用低级 I/O 函数,例如 copy_from_user()
和 copy_to_user()
来传输数据。
static ssize_t my_file_io_read(struct snd_info_entry *entry,
void *file_private_data,
struct file *file,
char *buf,
size_t count,
loff_t pos)
{
if (copy_to_user(buf, local_data + pos, count))
return -EFAULT;
return count;
}
如果信息条目的大小已正确设置,则保证 count
和 pos
适合 0 和给定大小之间。除非需要任何其他条件,否则您不必在回调中检查范围。
电源管理¶
如果芯片应该与挂起/恢复功能一起使用,您需要将电源管理代码添加到驱动程序中。用于电源管理的附加代码应该使用 CONFIG_PM
进行 ifdef,或者使用 __maybe_unused 属性进行注释;否则编译器会报错。
如果驱动程序完全支持挂起/恢复,即设备可以正确恢复到调用挂起时的状态,则可以在 PCM 信息字段中设置 SNDRV_PCM_INFO_RESUME
标志。通常,只有当芯片的寄存器可以安全地保存并恢复到 RAM 时,这才有可能。如果设置了此选项,则在恢复回调完成后,将使用 SNDRV_PCM_TRIGGER_RESUME
调用触发回调。
即使驱动程序不支持完全的 PM,但仍然可以进行部分挂起/恢复,仍然值得实现挂起/恢复回调。在这种情况下,应用程序将通过调用 snd_pcm_prepare()
来重置状态并适当地重新启动流。因此,您可以在下面定义挂起/恢复回调,但不要将 SNDRV_PCM_INFO_RESUME
信息标志设置为 PCM。
请注意,无论 SNDRV_PCM_INFO_RESUME
标志如何,当调用 snd_pcm_suspend_all()
时,始终可以调用带有 SUSPEND 的触发器。RESUME
标志仅影响 snd_pcm_resume()
的行为。(因此,理论上,当未设置 SNDRV_PCM_INFO_RESUME
标志时,无需在触发回调中处理 SNDRV_PCM_TRIGGER_RESUME
。但是,为了兼容性,最好保留它。)
驱动程序需要根据设备连接到的总线定义挂起/恢复钩子。在 PCI 驱动程序的情况下,回调如下所示:
static int __maybe_unused snd_my_suspend(struct device *dev)
{
.... /* do things for suspend */
return 0;
}
static int __maybe_unused snd_my_resume(struct device *dev)
{
.... /* do things for suspend */
return 0;
}
实际挂起作业的方案如下:
检索卡和芯片数据。
使用
SNDRV_CTL_POWER_D3hot
调用snd_power_change_state()
来更改电源状态。如果使用 AC97 编解码器,请为每个编解码器调用
snd_ac97_suspend()
。如果需要,保存寄存器值。
如果需要,停止硬件。
典型的代码如下所示
static int __maybe_unused mychip_suspend(struct device *dev)
{
/* (1) */
struct snd_card *card = dev_get_drvdata(dev);
struct mychip *chip = card->private_data;
/* (2) */
snd_power_change_state(card, SNDRV_CTL_POWER_D3hot);
/* (3) */
snd_ac97_suspend(chip->ac97);
/* (4) */
snd_mychip_save_registers(chip);
/* (5) */
snd_mychip_stop_hardware(chip);
return 0;
}
实际恢复作业的方案如下:
检索卡和芯片数据。
重新初始化芯片。
如果需要,恢复保存的寄存器。
恢复混音器,例如通过调用
snd_ac97_resume()
。重新启动硬件(如果有)。
使用
SNDRV_CTL_POWER_D0
调用snd_power_change_state()
以通知进程。
典型的代码如下所示
static int __maybe_unused mychip_resume(struct pci_dev *pci)
{
/* (1) */
struct snd_card *card = dev_get_drvdata(dev);
struct mychip *chip = card->private_data;
/* (2) */
snd_mychip_reinit_chip(chip);
/* (3) */
snd_mychip_restore_registers(chip);
/* (4) */
snd_ac97_resume(chip->ac97);
/* (5) */
snd_mychip_restart_chip(chip);
/* (6) */
snd_power_change_state(card, SNDRV_CTL_POWER_D0);
return 0;
}
请注意,在调用此回调时,PCM 流已通过其自身的 PM 操作在内部调用 snd_pcm_suspend_all()
挂起。
好的,我们现在有了所有回调。让我们设置它们。在卡的初始化中,确保您可以从卡实例中获取芯片数据,通常通过 private_data
字段,以防您单独创建了芯片数据。
static int snd_mychip_probe(struct pci_dev *pci,
const struct pci_device_id *pci_id)
{
....
struct snd_card *card;
struct mychip *chip;
int err;
....
err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,
0, &card);
....
chip = kzalloc(sizeof(*chip), GFP_KERNEL);
....
card->private_data = chip;
....
}
当您使用 snd_card_new()
创建芯片数据时,无论如何都可以通过 private_data
字段访问它。
static int snd_mychip_probe(struct pci_dev *pci,
const struct pci_device_id *pci_id)
{
....
struct snd_card *card;
struct mychip *chip;
int err;
....
err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,
sizeof(struct mychip), &card);
....
chip = card->private_data;
....
}
如果您需要空间来保存寄存器,请在此处也分配缓冲区,因为如果您无法在挂起阶段分配内存,那将是致命的。分配的缓冲区应在相应的析构函数中释放。
接下来,将挂起/恢复回调设置为 pci_driver:
static DEFINE_SIMPLE_DEV_PM_OPS(snd_my_pm_ops, mychip_suspend, mychip_resume);
static struct pci_driver driver = {
.name = KBUILD_MODNAME,
.id_table = snd_my_ids,
.probe = snd_my_probe,
.remove = snd_my_remove,
.driver = {
.pm = &snd_my_pm_ops,
},
};
模块参数¶
ALSA 有标准的模块选项。至少,每个模块都应该有 index
、id
和 enable
选项。
如果模块支持多个卡(通常最多 8 个 = SNDRV_CARDS
卡),它们应该是数组。默认的初始值已经定义为常量,以便于编程。
static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
static int enable[SNDRV_CARDS] = SNDRV_DEFAULT_ENABLE_PNP;
如果模块仅支持单张卡,它们可以是单个变量。在这种情况下,enable
选项并非总是必需的,但最好有一个虚拟选项以实现兼容性。
模块参数必须使用标准的 module_param()
, module_param_array()
和 MODULE_PARM_DESC()
宏声明。
典型的代码如下所示:
#define CARD_NAME "My Chip"
module_param_array(index, int, NULL, 0444);
MODULE_PARM_DESC(index, "Index value for " CARD_NAME " soundcard.");
module_param_array(id, charp, NULL, 0444);
MODULE_PARM_DESC(id, "ID string for " CARD_NAME " soundcard.");
module_param_array(enable, bool, NULL, 0444);
MODULE_PARM_DESC(enable, "Enable " CARD_NAME " soundcard.");
另外,不要忘记定义模块描述和许可证。特别是,最近的 modprobe 要求将模块许可证定义为 GPL 等,否则系统会显示为“已污染”。
MODULE_DESCRIPTION("Sound driver for My Chip");
MODULE_LICENSE("GPL");
设备管理的资源¶
在上面的例子中,所有资源都是手动分配和释放的。但是人的天性是懒惰的,尤其是开发人员更懒惰。因此,有一些方法可以自动化释放部分;它是(设备)管理的资源,又名 devres 或 devm 系列。例如,通过 devm_kmalloc()
分配的对象将在取消绑定设备时自动释放。
ALSA 核心还提供了设备管理的助手,即 snd_devm_card_new()
用于创建卡对象。调用此函数而不是普通的 snd_card_new()
,您可以忘记显式的 snd_card_free()
调用,因为它会在错误和删除路径中自动调用。
一个需要注意的是,只有在您调用 snd_card_register()
之后,才会将 snd_card_free()
的调用放在调用链的开头。
此外,private_free
回调总是在卡释放时调用,因此请注意将硬件清理过程放在 private_free
回调中。即使在您在较早的错误路径中实际设置之前,也可能会调用它。为了避免这种无效的初始化,您可以在 snd_card_register()
调用成功后设置 private_free
回调。
另一个需要注意的是,一旦您以这种方式管理卡,您应该尽可能多地为每个组件使用设备管理的助手。将正常资源和托管资源混合使用可能会搞砸释放顺序。
如何将您的驱动程序放入 ALSA 树中¶
常规¶
到目前为止,您已经学习了如何编写驱动程序代码。您现在可能有一个问题:如何将我自己的驱动程序放入 ALSA 驱动程序树中?此处(最后 :) 简要介绍了标准程序。
假设您为卡“xyz”创建一个新的 PCI 驱动程序。卡模块名称将为 snd-xyz。新的驱动程序通常放在 alsa-driver 树中,在 PCI 卡的情况下,放在 sound/pci
目录中。
在以下各节中,驱动程序代码应该放入 Linux 内核树中。涵盖了两种情况:由单个源文件组成的驱动程序和由多个源文件组成的驱动程序。
具有单个源文件的驱动程序¶
修改 sound/pci/Makefile
假设您有一个文件 xyz.c。添加以下两行:
snd-xyz-y := xyz.o obj-$(CONFIG_SND_XYZ) += snd-xyz.o
创建 Kconfig 条目
为您的 xyz 驱动程序添加 Kconfig 的新条目:
config SND_XYZ tristate "Foobar XYZ" depends on SND select SND_PCM help Say Y here to include support for Foobar XYZ soundcard. To compile this driver as a module, choose M here: the module will be called snd-xyz.
行 select SND_PCM
指定驱动程序 xyz 支持 PCM。除了 SND_PCM 之外,以下组件还支持 select 命令:SND_RAWMIDI, SND_TIMER, SND_HWDEP, SND_MPU401_UART, SND_OPL3_LIB, SND_OPL4_LIB, SND_VX_LIB, SND_AC97_CODEC。为每个受支持的组件添加 select 命令。
请注意,某些选择暗示着低级选择。例如,PCM 包括 TIMER,MPU401_UART 包括 RAWMIDI,AC97_CODEC 包括 PCM,OPL3_LIB 包括 HWDEP。您无需再次进行低级选择。
有关 Kconfig 脚本的详细信息,请参阅 kbuild 文档。
具有多个源文件的驱动程序¶
假设驱动程序 snd-xyz 有多个源文件。它们位于新的子目录 sound/pci/xyz 中。
在
sound/pci/Makefile
中添加一个新目录 (sound/pci/xyz
),如下所示:obj-$(CONFIG_SND) += sound/pci/xyz/
在目录
sound/pci/xyz
下,创建一个 Makefile:snd-xyz-y := xyz.o abc.o def.o obj-$(CONFIG_SND_XYZ) += snd-xyz.o
创建 Kconfig 条目
此过程与上一节相同。
有用的功能¶
snd_BUG()
¶
它显示 BUG?
消息和堆栈跟踪,以及 snd_BUG_ON()
在该点。 它对于表明发生致命错误很有用。
当未设置调试标志时,将忽略此宏。
snd_BUG_ON()
¶
snd_BUG_ON()
宏与 WARN_ON()
宏类似。 例如, snd_BUG_ON(!pointer); 或者它可以用作条件, 如果 (snd_BUG_ON(non_zero_is_bug)) return -EINVAL;
该宏接受一个条件表达式进行评估。 当设置 CONFIG_SND_DEBUG
时,如果表达式为非零,它将显示警告消息,例如 BUG? (xxx)
,通常后跟堆栈跟踪。 在这两种情况下,它都会返回评估的值。
致谢¶
我要感谢 Phil Kerr 对改进和更正本文档的帮助。
Kevin Conder 将原始纯文本重新格式化为 DocBook 格式。
Giuliano Pochini 更正了拼写错误,并在硬件约束部分贡献了示例代码。