编写 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 和 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;

在以上代码中,存储了卡记录。此指针也用于删除回调和电源管理回调。

析构函数

析构函数,即删除回调,只是释放卡实例。然后,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() 分配。

如上所述,您可以将额外数据长度传递给 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() 释放卡后退出探测函数。

要释放卡实例,您可以简单地调用 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 */

一些需要注意的事项

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_portrequest_region() 通过 kmalloc() 分配。该指针必须通过 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 回调部分。

现在让我们为上面的资源编写相应的析构函数。析构函数的作用很简单:禁用硬件(如果已激活)并释放资源。到目前为止,我们还没有硬件部分,因此此处不编写禁用代码。

要释放资源,“检查并释放”方法是一种更安全的方法。对于中断,请这样做:

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);

我们上面没有实现硬件禁用部分。如果需要这样做,请注意,即使在芯片初始化完成之前,也可能会调用析构函数。如果硬件尚未初始化,最好使用一个标志来跳过硬件禁用。

当使用 snd_device_new()SNDRV_DEV_LOWLELVEL 将芯片数据分配给卡时,其析构函数最后调用。也就是说,可以保证所有其他组件(如 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,
};

proberemove 函数已在前面的章节中定义。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 传递给相应的参数。

如果芯片支持多次回放或捕获,则可以指定更多数字,但必须在打开/关闭等回调中正确处理它们。当您需要知道您正在引用哪个子流时,可以从传递给每个回调的 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 打开回调中定义它。请注意,运行时实例保存的是描述符的副本,而不是指向现有描述符的指针。也就是说,在打开回调中,您可以根据需要修改复制的描述符 (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_INTERLEAVEDSNDRV_PCM_INFO_NONINTERLEAVED 标志。如果两者都支持,您也可以同时设置两者。

    在上面的示例中,为 OSS mmap 模式指定了 MMAP_VALIDBLOCK_TRANSFER。通常两者都会设置。当然,只有在真正支持 mmap 时才设置 MMAP_VALID

    其他可能的标志是 SNDRV_PCM_INFO_PAUSESNDRV_PCM_INFO_RESUMEPAUSE 位表示 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_minrate_max 定义最小和最大采样率。这应该以某种方式与 rates 位对应。

  • channels_minchannels_max 定义了最小和最大通道数,正如您可能已经预料到的那样。

  • buffer_bytes_max 定义了最大缓冲区大小(以字节为单位)。没有 buffer_bytes_min 字段,因为它可以从最小周期大小和最小周期数计算得出。同时,period_bytes_minperiod_bytes_max 定义了以字节为单位的最小和最大周期大小。periods_maxperiods_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 = 通道数 * 采样大小。对于帧和字节之间的转换,您可以使用 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_areadma_addrdma_bytesdma_privatedma_area 保存缓冲区指针(逻辑地址)。您可以从/向此指针调用 memcpy()。同时,dma_addr 保存缓冲区的物理地址。仅当缓冲区是线性缓冲区时才指定此字段。dma_bytes 保存缓冲区的大小(以字节为单位)。dma_private 用于 ALSA DMA 分配器。

如果您使用托管缓冲区分配模式或标准 API 函数 snd_pcm_lib_malloc_pages() 来分配缓冲区,则这些字段由 ALSA 中间层设置,您不应该自己更改它们。您可以读取它们,但不能写入它们。另一方面,如果您想自己分配缓冲区,则需要在 hw_params 回调中管理它。至少,dma_bytes 是强制性的。当缓冲区被 mmapped 时,dma_area 是必要的。如果您的驱动程序不支持 mmap,则此字段不是必需的。dma_addr 也是可选的。您也可以随意使用 dma_private。

运行状态

可以通过 runtime->status 访问运行状态。这是指向 struct snd_pcm_mmap_status 记录的指针。例如,您可以通过 runtime->status->hw_ptr 获取当前的 DMA 硬件指针。

DMA 应用程序指针可以通过 runtime->control 访问,它指向 struct snd_pcm_mmap_control 记录。但是,不建议直接访问此值。

私有数据

您可以为子流分配一个记录,并将其存储在 runtime->private_data 中。通常,这在 PCM 打开回调 中完成。不要将其与 pcm->private_data 混淆。pcm->private_data 通常指向在 PCM 设备创建时静态分配的芯片实例,而 runtime->private_data 指向在 PCM 打开回调中创建的动态数据结构。

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;
        ....
}

分配的对象必须在关闭回调中释放。

操作符

好的,现在让我详细介绍每个 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 打开回调

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));

只有在预先分配了 DMA 缓冲区时,才能使用snd_pcm_lib_malloc_pages()。有关详细信息,请参阅 缓冲区类型 部分。

请注意,此回调和 prepare 回调可能会在每次初始化时多次调用。例如,OSS 模拟可能会在每次通过其 ioctl 进行更改时调用这些回调。

因此,您需要小心不要多次分配相同的缓冲区,这会导致内存泄漏!多次调用上面的辅助函数是可以的。当它已经被分配时,它将自动释放之前的缓冲区。

另一个需要注意的是,默认情况下,此回调是非原子的(可调度的),即当没有设置 nonatomic 标志时。这很重要,因为 trigger 回调是原子的(不可调度的)。也就是说,互斥锁或任何与调度相关的函数在 trigger 回调中不可用。有关详细信息,请参阅子部分 原子性

hw_free 回调

static int snd_xxx_hw_free(struct snd_pcm_substream *substream);

此函数用于释放通过 hw_params 分配的资源。

此函数始终在调用关闭回调之前调用。此外,该回调也可能会被多次调用。请跟踪每个资源是否已被释放。

当您为 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 “准备就绪”时调用此回调。您可以在此处设置格式类型、采样率等。与 hw_params 的区别在于,每次调用 snd_pcm_prepare() 时都会调用 prepare 回调,例如,在欠载后恢复时。

请注意,此回调是非原子的。您可以安全地在此回调中使用与调度相关的函数。

在此回调和以下回调中,您可以通过运行时记录 substream->runtime 引用值。例如,要获取当前速率、格式或通道,请分别访问 runtime->rateruntime->formatruntime->channels。分配缓冲区的物理地址设置为 runtime->dma_area。缓冲区和周期大小分别位于 runtime->buffer_sizeruntime->period_size 中。

请注意,此回调也会在每次设置时多次调用。

trigger 回调

static int snd_xxx_trigger(struct snd_pcm_substream *substream, int cmd);

当 PCM 启动、停止或暂停时调用此函数。

该操作在第二个参数 SNDRV_PCM_TRIGGER_XXX 中指定,该参数在 <sound/pcm.h> 中定义。至少,必须在此回调中定义 STARTSTOP 命令

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_PUSHPAUSE_RELEASE 命令。前者是暂停 PCM 的命令,后者是重新启动 PCM 的命令。

当 PCM 支持挂起/恢复操作时,无论是否完全或部分支持挂起/恢复,都必须处理 SUSPENDRESUME 命令。当电源管理状态更改时会发出这些命令。显然,SUSPENDRESUME 命令会挂起和恢复 PCM 子流,通常,它们分别与 STOPSTART 命令相同。有关详细信息,请参阅电源管理部分。

如前所述,默认情况下此回调是原子的,除非设置了 nonatomic 标志,并且您不能调用可能会休眠的函数。trigger 回调应尽可能小,只需真正触发 DMA 即可。其他内容应事先在 hw_paramsprepare 回调中正确初始化。

sync_stop 回调

static int snd_xxx_sync_stop(struct snd_pcm_substream *substream);

此回调是可选的,可以传递 NULL。它在 PCM 核心停止流之后,在通过 preparehw_paramshw_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

指针回调

static snd_pcm_uframes_t snd_xxx_pointer(struct snd_pcm_substream *substream)

当 PCM 中间层查询缓冲区中的当前硬件位置时,会调用此回调。位置必须以帧为单位返回,范围从 0 到 buffer_size - 1

这通常是从 PCM 中间层中的缓冲区更新例程调用的,该例程在中断例程调用 snd_pcm_period_elapsed() 时被调用。然后,PCM 中间层更新位置并计算可用空间,并唤醒睡眠的轮询线程等。

此回调默认也是原子的。

复制和填充静音操作

这些回调不是强制性的,在大多数情况下可以省略。当硬件缓冲区不能位于普通内存空间中时,会使用这些回调。一些芯片在硬件中拥有自己的缓冲区,该缓冲区是不可映射的。在这种情况下,你必须手动将数据从内存缓冲区传输到硬件缓冲区。或者,如果缓冲区在物理和虚拟内存空间上都是不连续的,也必须定义这些回调。

如果定义了这两个回调,则复制和设置静音操作将由它们完成。详细信息将在后面的 缓冲区和内存管理部分中描述。

ack 回调

此回调也不是强制性的。当在读取或写入操作中更新 appl_ptr 时,会调用此回调。一些驱动程序(如 emu10k1-fx 和 cs46xx)需要跟踪内部缓冲区的当前 appl_ptr,此回调仅为此目的有用。

回调函数可以返回 0 或负错误。当返回值是 -EPIPE 时,PCM 核心将其视为缓冲区 XRUN,并自动将状态更改为 SNDRV_PCM_STATE_XRUN

此回调默认是原子的。

页面回调

此回调也是可选的。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()

所有三个原子回调(触发、指针和确认)都会在禁用本地中断的情况下被调用。

但是,可以请求所有 PCM 操作都是非原子的。这假设所有调用点都在非原子上下文中。例如,函数 snd_pcm_period_elapsed() 通常从中断处理程序中调用。但是,如果您将驱动程序设置为使用线程中断处理程序,则此调用也可以在非原子上下文中。在这种情况下,您可以在创建 struct snd_pcm 对象后设置其 nonatomic 字段。当设置此标志时,PCM 核心内部将使用互斥锁和读写信号量而不是自旋锁和读写锁,以便您可以在非原子上下文中安全地调用所有 PCM 函数。

此外,在某些情况下,您可能需要在原子上下文中调用 snd_pcm_period_elapsed()(例如,在 ack 或其他回调期间经过了一个周期)。有一个变体可以在 PCM 流锁内调用 snd_pcm_period_elapsed_under_stream_lock(),用于此目的。

约束

由于物理限制,硬件并非无限可配置。这些限制通过设置约束来表达。

例如,为了将采样率限制为某些支持的值,请使用 snd_pcm_hw_constraint_list()。您需要在打开回调中调用此函数

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 只能在格式为 S16_LE 时管理 1 通道的子流,否则它支持 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;
}

... 以及在打开回调中

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);

硬件约束的一个典型用法是将缓冲区大小与周期大小对齐。默认情况下,ALSA PCM 核心不强制缓冲区大小与周期大小对齐。例如,可能会出现 256 字节周期和 999 字节缓冲区的组合。

然而,许多设备芯片要求缓冲区是周期的倍数。在这种情况下,对 SNDRV_PCM_HW_PARAM_PERIODS 调用 snd_pcm_hw_constraint_integer()

snd_pcm_hw_constraint_integer(substream->runtime,
                              SNDRV_PCM_HW_PARAM_PERIODS);

这确保了周期数是整数,因此缓冲区大小与周期大小对齐。

硬件约束是一种非常强大的机制,用于定义首选的 PCM 配置,并且有相关的助手函数。我在这里不再赘述,只想说一句,“卢克,请看源代码。”

控制接口

概述

控制接口被广泛用于许多从用户空间访问的开关、滑块等。其最重要的用途是混音器接口。换句话说,自 ALSA 0.9.x 以来,所有混音器功能都在控制内核 API 上实现。

ALSA 有一个定义完善的 AC97 控制模块。如果您的芯片仅支持 AC97 而不支持其他任何功能,您可以跳过本节。

控制 API 在 <sound/control.h> 中定义。如果要添加自己的控件,请包含此文件。

控制的定义

要创建新控件,您需要定义以下三个回调:infogetput。然后,定义一个 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。如果控件与声卡上的某个特定设备密切相关,请使用 HWDEPPCMRAWMIDITIMERSEQUENCER,并使用 devicesubdevice 字段指定设备号。

name 是名称标识符字符串。自 ALSA 0.9.x 以来,控制名称非常重要,因为其角色是从其名称中分类的。有一些预定义的标准控制名称。详细信息在 控制名称 小节中描述。

index 字段保存此控件的索引号。如果有多个具有相同名称的不同控件,则可以通过索引号来区分它们。当卡上有多个编解码器时,就会出现这种情况。如果索引为零,则可以省略上面的定义。

access 字段包含此控件的访问类型。在此处给出位掩码 SNDRV_CTL_ELEM_ACCESS_XXX 的组合。详细信息将在 访问标志 小节中解释。

private_value 字段包含此记录的任意长整数值。当使用通用的 infogetput 回调时,您可以通过此字段传递值。如果需要几个小的数字,您可以按位组合它们。或者,也可以在此字段中存储某个记录的指针(转换为 unsigned long)。

tlv 字段可用于提供有关控件的元数据;请参阅 元数据 小节。

其他三个是 控制回调

控制名称

有一些标准来定义控制名称。控件通常由三个部分定义:“源 方向 功能”。

第一个 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 控件应处于非活动状态。

LOCKOWNER 标志来更改写入权限。

控制回调

信息回调

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 字段指定控件的类型。有 BOOLEANINTEGERENUMERATEDBYTESIEC958INTEGER64count 字段指定此控件中的元素数量。例如,一个立体声音量控件的 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);
}

为了方便起见,提供了一些常见的信息回调:snd_ctl_boolean_mono_info()snd_ctl_boolean_stereo_info()。显然,前者是单声道布尔项的信息回调,就像上面的 snd_myctl_mono_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 字段取决于控件的类型以及信息回调。例如,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.ces1968.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 实例的指针。在这种情况下,chip 指针设置为私有数据,以便读取/写入回调函数可以引用此 chip 实例。此实例不一定存储在 chip 记录中。如果需要从驱动程序更改寄存器值,或者需要暂停/恢复 ac97 编解码器,请保留此指针以传递给相应的函数。

AC97 回调

标准回调是 readwrite。显然,它们对应于对硬件低级代码进行读写访问的函数。

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 回调一样。

还有其他回调函数:resetwaitinit

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_RATEAC97_PCM_FRONT_DAC_RATEAC97_PCM_LR_ADC_RATEAC97_SPDIF。当指定 AC97_SPDIF 时,寄存器不会真正更改,但会更新相应的 IEC958 状态位。

时钟调整

在某些芯片中,编解码器的时钟不是 48000,而是使用 PCI 时钟(为了节省晶振!)。在这种情况下,请将字段 bus->clock 更改为相应的值。例如,intel8x0 和 es1968 驱动程序有自己的函数来读取时钟。

Proc 文件

ALSA AC97 接口将创建一个 proc 文件,例如 /proc/asound/card0/codec97#0/ac97#0-0ac97#0-0+regs。您可以参考这些文件来查看编解码器的当前状态和寄存器。

多个编解码器

当同一张卡上有多个编解码器时,您需要多次调用 snd_ac97_mixer(),并使用 ac97.num=1 或更大值。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

第 4 个参数是 I/O 端口地址。许多向后兼容的 MPU401 都有一个 I/O 端口,例如 0x330。或者,它可能是其自己的 PCI I/O 区域的一部分。这取决于芯片设计。

第 5 个参数是用于附加信息的位标志。当上面的 I/O 端口地址是 PCI I/O 区域的一部分时,MPU401 I/O 端口可能已被驱动程序本身分配(保留)。在这种情况下,请传递位标志 MPU401_INFO_INTEGRATED,mpu401-uart 层将自行分配 I/O 端口。

当控制器仅支持输入或输出 MIDI 流时,请分别传递 MPU401_INFO_INPUTMPU401_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 字段。但是,snd_mpu401_uart_new() 不会显式返回 struct snd_mpu401 指针。您需要将 rmidi->private_data 显式转换为 struct snd_mpu401。

struct snd_mpu401 *mpu;
mpu = rmidi->private_data;

并根据需要重置 cport

mpu->cport = my_own_control_port;

第 6 个参数指定要分配的 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 接口

概述

原始 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;

第一个参数是指向卡的指针,第二个参数是 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 访问。

如果存在多个端口,您的回调可以从传递给每个回调的结构体 `snd_rawmidi_substream` 数据中确定端口索引。

struct snd_rawmidi_substream *substream;
int index = substream->number;

RawMIDI 打开回调

static int snd_xxx_open(struct snd_rawmidi_substream *substream);

当子流打开时,会调用此回调。您可以在此处初始化硬件,但不应立即开始传输/接收数据。

RawMIDI 关闭回调

static int snd_xxx_close(struct snd_rawmidi_substream *substream);

猜猜看。

rawmidi 设备的 openclose 回调通过互斥锁进行序列化,并且可以休眠。

用于输出子流的 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);

第一个参数是卡指针,第二个参数是左端口地址,第三个参数是右端口地址。在大多数情况下,右端口位于左端口 + 2 的位置。

第四个参数是硬件类型。

当左右端口已由卡驱动程序分配时,请将非零值传递给第五个参数 (integrated)。否则,opl3 模块将自行分配指定的端口。

当访问硬件需要特殊方法而不是标准 I/O 访问时,您可以单独使用 snd_opl3_new() 创建 opl3 实例。

struct snd_opl3 *opl3;
snd_opl3_new(card, OPL3_HW_OPL3_XXX, &opl3);

然后为私有访问函数、私有数据和析构函数设置 commandprivate_dataprivate_freel_portr_port 不一定需要设置。只有命令必须正确设置。您可以从 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 中找到示例。

通过 snd_hwdep_new() 完成 hwdep 实例的创建:

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 回调,您无需为此类型指定值字段(但必须设置 count 字段)。

“IEC958 Playback Con Mask” 用于返回消费者模式的 IEC958 状态位的位掩码。类似地,“IEC958 Playback Pro Mask” 返回专业模式的位掩码。它们是只读控件。

同时,定义了 “IEC958 Playback Default” 控制,用于获取和设置当前默认的 IEC958 位。

由于历史原因,播放掩码和播放默认控件的两个变体都可以在 SNDRV_CTL_ELEM_IFACE_PCMSNDRV_CTL_ELEM_IFACE_MIXER iface 上实现。驱动程序应在同一 iface 上公开掩码和默认值。

此外,您可以定义控制开关以启用/禁用或设置原始位模式。实现将取决于芯片,但控制应命名为 “IEC958 xxx”,最好使用 SNDRV_CTL_NAME_IEC958() 宏。

您可以找到几个示例,例如 pci/emu10k1pci/ice1712pci/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文件设置的最大大小。分配器将尝试在给定大小内获取尽可能大的区域。

第二个参数(type)和第三个参数(设备指针)取决于总线。对于普通设备,将设备指针(通常与card->dev相同)传递给第三个参数,并将类型设置为SNDRV_DMA_TYPE_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_paramshw_free条目。

外部硬件缓冲区

某些芯片有自己的硬件缓冲区,并且无法从主机内存进行DMA传输。在这种情况下,您需要 1) 直接将音频数据复制/设置到外部硬件缓冲区,或者 2) 创建一个中间缓冲区,并在中断(或最好在tasklet中)中将数据从该缓冲区复制/设置到外部硬件缓冲区。

如果外部硬件缓冲区足够大,第一种情况可以很好地工作。此方法不需要任何额外的缓冲区,因此效率更高。除了用于播放的fill_silence回调之外,您还需要为数据传输定义copy回调。但是,有一个缺点:它不能被mmap。示例包括GUS的GF1 PCM或emu8000的波表PCM。

第二种情况允许对缓冲区进行mmap,尽管您必须处理中断或tasklet才能将数据从中间缓冲区传输到硬件缓冲区。您可以在vxpocket驱动程序中找到示例。

另一种情况是,芯片使用PCI内存映射区域作为缓冲区,而不是主机内存。在这种情况下,mmap仅在某些架构(例如Intel)上可用。在非mmap模式下,数据无法像正常方式一样传输。因此,您还需要像上面的情况一样定义copyfill_silence回调。在rme32.crme96.c中可以找到示例。

copysilence回调的实现取决于硬件是否支持交错或非交错采样。 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);

给定的srcdst是一个包含指针和大小的struct iov_iter指针。使用linux/uio.h中定义的现有助手来复制或访问数据。

细心的读者可能会注意到,这些回调接收的参数是以字节为单位,而不是像其他回调那样以帧为单位。这是因为这使得编码更容易(如上面的示例所示),并且也使得统一交错和非交错情况更容易,如下所述。

在非交错采样的情况下,实现将稍微复杂一些。为每个通道调用回调,在第二个参数中传递,因此每次传输总共调用N次。

其他参数的含义与交错情况几乎相同。回调应该从/向给定的用户空间缓冲区复制数据,但仅针对给定的通道。有关详细信息,请检查isa/gus/gus_pcm.cpci/rme9652/rme9652.c作为示例。

通常,对于播放,会定义另一个回调fill_silence。它的实现方式与上面的复制回调类似

static int silence(struct snd_pcm_substream *substream, int channel,
                   unsigned long pos, unsigned long count);

参数的含义与copy回调中的含义相同,尽管没有缓冲区指针参数。在交错采样的情况下,通道参数没有意义,就像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 构造函数中像其他 PCI 预分配一样,使用 SNDRV_DMA_TYPE_DEV_SG 调用 snd_pcm_set_managed_buffer()snd_pcm_set_managed_buffer_all()。你需要传递 &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 缓冲区处理程序将分配给定大小的不连续内核页,并将它们映射为虚拟连续内存。虚拟指针通过 runtime->dma_area 寻址。物理地址(runtime->dma_addr)设置为零,因为缓冲区在物理上是不连续的。物理地址表在 sgbuf->table 中设置。你可以通过 snd_pcm_sgbuf_get_addr() 获取特定偏移量的物理地址。

如果你需要显式释放 SG 缓冲区数据,请像往常一样调用标准 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)。

另请注意,此处传递的 size 和 max size 参数均为零。由于每次 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;
}

如果已正确设置 info 条目的大小,则保证 countpos 在 0 和给定大小之间。除非需要任何其他条件,否则你不必在回调中检查范围。

电源管理

如果芯片应该使用挂起/恢复功能,则需要在驱动程序中添加电源管理代码。用于电源管理的附加代码应使用 CONFIG_PM 进行 ifdef 定义,或使用 __maybe_unused 属性进行注释;否则,编译器会报错。

如果驱动程序完全支持挂起/恢复,也就是说,设备可以在调用挂起时正确恢复到其状态,则可以在 PCM info 字段中设置 SNDRV_PCM_INFO_RESUME 标志。通常,当芯片的寄存器可以安全地保存和恢复到 RAM 时,这是可能的。如果设置了此标志,则在恢复回调完成后,将使用 SNDRV_PCM_TRIGGER_RESUME 调用触发回调。

即使驱动程序不完全支持 PM,但仍然可以进行部分挂起/恢复,仍然值得实现挂起/恢复回调。在这种情况下,应用程序将通过调用 snd_pcm_prepare() 重置状态并适当地重新启动流。因此,你可以在下面定义挂起/恢复回调,但不要将 SNDRV_PCM_INFO_RESUME info 标志设置为 PCM。

请注意,当调用 snd_pcm_suspend_all() 时,无论 SNDRV_PCM_INFO_RESUME 标志如何,都可以始终调用带有 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;
}

实际挂起作业的方案如下:

  1. 检索卡和芯片数据。

  2. 使用 SNDRV_CTL_POWER_D3hot 调用 snd_power_change_state() 以更改电源状态。

  3. 如果使用 AC97 编解码器,请为每个编解码器调用 snd_ac97_suspend()

  4. 如有必要,保存寄存器值。

  5. 如有必要,停止硬件。

典型的代码如下所示

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;
}

实际恢复作业的方案如下:

  1. 检索卡和芯片数据。

  2. 重新初始化芯片。

  3. 如有必要,恢复保存的寄存器。

  4. 恢复混音器,例如,通过调用 snd_ac97_resume()

  5. 重新启动硬件(如果有)。

  6. 使用 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 ops 在内部调用 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 有标准的模块选项。至少,每个模块都应该具有 indexidenable 选项。

如果模块支持多个卡(通常最多 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 内核树中。涵盖了两种情况:一个驱动程序由单个源文件组成,另一个驱动程序由多个源文件组成。

具有单个源文件的驱动程序

  1. 修改 sound/pci/Makefile

    假设您有一个文件 xyz.c。添加以下两行

    snd-xyz-y := xyz.o
    obj-$(CONFIG_SND_XYZ) += snd-xyz.o
    
  2. 创建 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 中。

  1. sound/pci/Makefile 中添加一个新目录 (sound/pci/xyz),如下所示

    obj-$(CONFIG_SND) += sound/pci/xyz/
    
  2. 在目录 sound/pci/xyz 下,创建一个 Makefile

    snd-xyz-y := xyz.o abc.o def.o
    obj-$(CONFIG_SND_XYZ) += snd-xyz.o
    
  3. 创建 Kconfig 条目

    此过程与上一节中的相同。

有用的函数

snd_BUG()

它会显示 BUG? 消息和堆栈跟踪,以及 snd_BUG_ON() 在该点。它有助于表明那里发生了致命错误。

当未设置调试标志时,此宏将被忽略。

snd_BUG_ON()

snd_BUG_ON() 宏类似于 WARN_ON() 宏。例如,snd_BUG_ON(!pointer); 或者它可以作为条件使用,例如 if (snd_BUG_ON(non_zero_is_bug)) return -EINVAL;

该宏接受一个条件表达式进行求值。当设置了 CONFIG_SND_DEBUG 时,如果表达式为非零,它会显示警告消息,例如 BUG? (xxx),通常后跟堆栈跟踪。在这两种情况下,它都会返回计算后的值。

致谢

我要感谢 Phil Kerr 帮助改进和更正本文档。

Kevin Conder 将原始纯文本重新格式化为 DocBook 格式。

Giuliano Pochini 更正了错别字,并在硬件约束部分贡献了示例代码。