从零写一个ALSA声卡驱动学习(1)

前言:

本文档描述了如何编写 ALSA(高级 Linux 音频架构)驱动程序。文档主要聚焦于 PCI 声卡的实现。对于其他类型的设备,可能会使用不同的 API。不过,至少 ALSA 的内核 API 是一致的,因此本文档在编写这些驱动时仍然具有一定的参考价值。 本指南面向那些已经具备足够 C 语言技能并且掌握基本 Linux 内核编程知识的开发者。本文档不会讲解 Linux 内核编程的一般性内容,也不会涉及底层驱动实现的细节。它仅介绍在 ALSA 框架下编写 PCI 声卡驱动的标准方法。

内容参考学习链接文档:

https://kernel.org/doc/html/latest/sound/kernel-api/writing-an-alsa-driver.html#header-files

File Tree Structure:

下面的文件结构是Linux内核关于sound目录下的介绍:

sound
        /core
                /oss
                /seq
                        /oss
        /include
        /drivers
                /mpu401
                /opl3
        /i2c
        /synth
                /emux
        /pci
                /(cards)
        /isa
                /(cards)
        /arm
        /ppc
        /sparc
        /usb
        /pcmcia /(cards)
        /soc
        /oss
  • core 目录 : 该目录包含 ALSA 驱动的中间层,是 ALSA 驱动的核心。此目录中存放的是 ALSA 的原生模块。其子目录包含了不同的模块,并依赖于内核配置选项。

  • core/oss  : 该目录存放 OSS PCM 和混音器(mixer)仿真模块的代码。由于 OSS 的 rawmidi 仿真部分非常小,因此被直接包含在 ALSA 的 rawmidi 代码中。序列器(sequencer)相关的 OSS 仿真代码则位于 core/seq/oss 目录(见下文)。

  • core/seq  : 该目录及其子目录存放 ALSA 序列器(sequencer)相关代码。它包含了序列器核心以及主要模块,如 snd-seq-midi、snd-seq-virmidi 等。这些模块仅在内核配置中启用了 CONFIG_SND_SEQUENCER 时才会被编译。

  • core/seq/oss  : 此目录包含 OSS 序列器仿真代码。

  • include 目录  : 该目录用于存放 ALSA 驱动对用户空间公开的头文件,或供多个不同目录中的文件共享使用。一般来说,私有头文件不应放在这个目录中,但你仍可能会在其中看到一些私有头文件,这是历史遗留问题所致。:)

  • drivers 目录  :该目录包含在不同平台间共享的驱动代码,因此它们不应具有架构相关性。例如,dummy PCM 驱动和串口 MIDI 驱动都位于此目录。其子目录中存放的是与总线或 CPU 架构无关的组件代码。 drivers/mpu401 包含 MPU401 和 MPU401-UART 模块。 drivers/opl3 与 opl4 包含 OPL3 和 OPL4 FM 合成器相关代码。

  • i2c 目录  :该目录包含 ALSA 的 i2c 相关组件。虽然 Linux 系统本身有标准的 i2c 层,但某些声卡只需要简单的操作,而标准的 i2c API 过于复杂,因此 ALSA 对某些声卡实现了自己的 i2c 代码。

  • synth 目录  :此目录包含中间层的合成器模块。目前,只有 Emu8000/Emu10k1 合成器驱动存放在 synth/emux 子目录中。

  • pci 目录  :此目录及其子目录存放 PCI 声卡的顶层驱动模块,以及与 PCI 总线相关的专用代码。如果驱动只包含单个源文件,则直接放在 pci 目录下;如果驱动包含多个源文件,则会单独创建子目录(例如 emu10k1、ice1712)。

  • isa 目录  : 此目录及其子目录存放 ISA 声卡的顶层驱动模块。

  • arm、ppc 和 sparc 目录  : 这些目录用于存放特定于某一架构的顶层声卡驱动模块。

  • usb 目录  : 该目录包含 USB 音频驱动。USB MIDI 驱动已经整合进了 USB 音频驱动中。

  • pcmcia 目录  : 该目录用于存放 PCMCIA(尤其是 PCCard)驱动。由于 CardBus 的 API 与标准 PCI 卡相同,因此其驱动位于 pci 目录中。

  • soc 目录  : 该目录包含 ASoC(ALSA SoC,系统级芯片)层的代码,包括 ASoC 核心、编解码器(codec)和 machine 驱动等。

  • oss 目录  : 该目录包含 OSS/Lite 代码。截止目前,除了 m68k 架构下的 dmasound 之外,其他代码均已被移除。

basic Flow for PCI Drivers:

PCI 声卡驱动的最小实现流程如下:

  1. 定义 PCI ID 表(参见“PCI 条目”部分)。

  2. 创建 probe 回调函数(用于设备检测和初始化)。

  3. 创建 remove 回调函数(用于设备移除时的清理操作)。

  4. 创建一个 struct pci_driver 结构体,包含上述三个函数指针(即:PCI ID 表、probe 和 remove 函数)。

  5. 创建初始化函数,该函数调用 pci_register_driver() 来注册上面定义的 pci_driver 表。

  6. 创建退出函数,该函数调用 pci_unregister_driver() 来注销该驱动。

Full Code Example:

下面展示了一个代码示例。目前有些部分尚未实现,但会在接下来的章节中补充完成。 在 snd_mychip_probe() 函数中的注释行里标注的数字,对应的是下一节中将详细解释的内容。

#include <linux/init.h>
#include <linux/pci.h>
#include <linux/slab.h>
#include <sound/core.h>
#include <sound/initval.h>

/* module parameters (see "Module Parameters") */
/* SNDRV_CARDS: maximum number of cards supported by this module */
static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
static bool enable[SNDRV_CARDS] = SNDRV_DEFAULT_ENABLE_PNP;

/* definition of the chip-specific record */
struct mychip {
        struct snd_card *card;
        /* the rest of the implementation will be in section
         * "PCI Resource Management"
         */
};

/* chip-specific destructor
 * (see "PCI Resource Management")
 */
static int snd_mychip_free(struct mychip *chip)
{
        .... /* will be implemented later... */
}

/* component-destructor
 * (see "Management of Cards and Components")
 */
static int snd_mychip_dev_free(struct snd_device *device)
{
        return snd_mychip_free(device->device_data);
}

/* chip-specific constructor
 * (see "Management of Cards and Components")
 */
static int snd_mychip_create(struct snd_card *card,
                             struct pci_dev *pci,
                             struct mychip **rchip)
{
        struct mychip *chip;
        int err;
        static const struct snd_device_ops ops = {
               .dev_free = snd_mychip_dev_free,
        };

        *rchip = NULL;

        /* check PCI availability here
         * (see "PCI Resource Management")
         */
        ....

        /* allocate a chip-specific data with zero filled */
        chip = kzalloc(sizeof(*chip), GFP_KERNEL);
        if (chip == NULL)
                return -ENOMEM;

        chip->card = card;

        /* rest of initialization here; will be implemented
         * later, see "PCI Resource Management"
         */
        ....

        err = snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);
        if (err < 0) {
                snd_mychip_free(chip);
                return err;
        }

        *rchip = chip;
        return 0;
}

/* constructor -- see "Driver Constructor" sub-section */
static int snd_mychip_probe(struct pci_dev *pci,
                            const struct pci_device_id *pci_id)
{
        static int dev;
        struct snd_card *card;
        struct mychip *chip;
        int err;

        /* (1) */
        if (dev >= SNDRV_CARDS)
                return -ENODEV;
        if (!enable[dev]) {
                dev++;
                return -ENOENT;
        }

        /* (2) */
        err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,
                           0, &card);
        if (err < 0)
                return err;

        /* (3) */
        err = snd_mychip_create(card, pci, &chip);
        if (err < 0)
                goto error;

        /* (4) */
        strcpy(card->driver, "My Chip");
        strcpy(card->shortname, "My Own Chip 123");
        sprintf(card->longname, "%s at 0x%lx irq %i",
                card->shortname, chip->port, chip->irq);

        /* (5) */
        .... /* implemented later */

        /* (6) */
        err = snd_card_register(card);
        if (err < 0)
                goto error;

        /* (7) */
        pci_set_drvdata(pci, card);
        dev++;
        return 0;

error:
        snd_card_free(card);
        return err;
}

/* destructor -- see the "Destructor" sub-section */
static void snd_mychip_remove(struct pci_dev *pci)
{
        snd_card_free(pci_get_drvdata(pci));
}

驱动构造函数:

PCI 驱动的真正构造函数是 probe 回调函数。由于 PCI 设备可能是热插拔设备,因此 probe 回调函数及其调用的其他组件构造函数不能使用 __init 前缀。 在 probe 回调函数中,通常会使用以下流程:

  • 1)检查并递增设备索引。

static int dev;
....
if (dev >= SNDRV_CARDS)
        return -ENODEV;
if (!enable[dev]) {
        dev++;
        return -ENOENT;
}

其中 enable[dev] 是模块参数选项。 每次调用 probe 回调时,都要检查该设备是否可用。如果不可用,则只需递增设备索引并返回。dev 变量稍后(在第 7 步)也会继续递增。

  • 2)创建一个 sound card 实例

struct snd_card *card;
int err;
....
err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,
                   0, &card);

详细内容将在“声卡与组件的管理”章节中进行说明。

  • 3)创建主组件 在这一部分,将分配 PCI 资源

struct mychip *chip;
....
err = snd_mychip_create(card, pci, &chip);
if (err < 0)
        goto error;

详细内容将在“PCI 资源管理”章节中进行说明。 当出现错误时,probe 函数需要进行错误处理。在本示例中,我们采用统一的错误处理路径,该路径被放置在函数的末尾:

error:
        snd_card_free(card);
        return err;

于每个组件都可以被正确释放,因此在大多数情况下,只需调用一次 snd_card_free() 就足够了。

  • 4)设置驱动的 ID 和名称字符串。

strcpy(card->driver, "My Chip");
strcpy(card->shortname, "My Own Chip 123");
sprintf(card->longname, "%s at 0x%lx irq %i",
        card->shortname, chip->port, chip->irq);

driver 字段保存芯片的最小 ID 字符串。这个 ID 会被 alsa-lib 的配置器使用,因此应保持简洁但唯一。即使是同一个驱动,也可以使用不同的 driver ID 来区分不同类型芯片的功能。 shortname 字段是一个简短但更具描述性的名称字符串,用于展示。 longname 字段包含的是将在 /proc/asound/cards 中显示的信息。

  • 5)创建其他组件,如混音器、MIDI 等 在此步骤中定义基本组件,例如 PCM、混音器(如 AC97)、MIDI(如 MPU-401)以及其他接口。如果需要定义 proc 文件,也应在这里进行。

  • 6)注册该声卡实例

err = snd_card_register(card);
if (err < 0)
        goto error;

这一部分也将在“声卡与组件的管理”章节中进行说明。

  • 7) 设置 PCI 驱动的数据,并返回 0

pci_set_drvdata(pci, card);
dev++;
return 0;

在上述步骤中,声卡记录被保存了下来。该指针同样会在 remove 回调函数 和 电源管理回调函数 中使用。

Destructor(析构函数):

析构函数,即 remove 回调函数,仅需释放声卡实例即可。随后,ALSA 中间层会自动释放所有附加的组件。 通常只需调用一行代码即可:

static void snd_mychip_remove(struct pci_dev *pci)
{
        snd_card_free(pci_get_drvdata(pci));
}

上述代码的前提是:声卡指针已通过 PCI 驱动的数据接口(即 pci_set_drvdata())保存。

Header Files:

对于上述示例,至少需要包含以下头文件:

#include <linux/init.h>
#include <linux/pci.h>
#include <linux/slab.h>
#include <sound/core.h>
#include <sound/initval.h>

最后一个头文件(指 <linux/moduleparam.h>)仅在源码中定义了模块参数时才是必须的。如果代码被拆分为多个文件,那么没有模块参数定义的文件就不需要包含该头文件。 除了这些头文件之外:

  • 如果要处理中断,需要包含 <linux/interrupt.h>;

  • 如果需要进行 I/O 操作访问,需要包含 <linux/io.h>;

  • 如果使用了 mdelay() 或 udelay() 延时函数,还需要包含 <linux/delay.h>。

ALSA 接口,例如 PCM 和控制接口(control API),则定义在其他的 <sound/xxx.h> 头文件中。 这些头文件必须在 <sound/core.h> 之后包含。

声卡与组件的管理(Management of Cards and Components):

  • Card Instance

对于每一块声卡,必须分配一个 “card” 记录。 “card” 记录是声卡的核心管理结构,它负责管理该声卡上的所有设备(组件),例如 PCM、混音器(Mixer)、MIDI、合成器等。同时,该记录还保存声卡的 ID 和名称字符串,管理与之相关的 proc 文件入口,控制电源管理状态以及处理热插拔断开等情况。卡记录中的组件列表用于在驱动销毁时正确释放所有资源。 如前所述,要创建一个声卡实例,可以调用 snd_card_new() 函数:

struct snd_card *card;
int err;
err = snd_card_new(&pci->dev, index, id, module, extra_size, &card);

该函数接受六个参数:父设备指针、声卡索引号、ID 字符串、模块指针(通常为 THIS_MODULE)、额外数据空间的大小,以及用于返回声卡实例的指针。 其中,extra_size 参数用于为芯片相关的私有数据分配 card->private_data 空间。需要注意的是,这块私有数据是由 snd_card_new() 函数自动分配的。 第一个参数是 struct device 的指针,用于指定父设备。对于 PCI 设备,通常传入 &pci->dev

  • Components

在创建好声卡之后,你可以将各个组件(设备)附加到该声卡实例上。在 ALSA 驱动中,每个组件由一个 struct snd_device 对象表示。组件可以是一个 PCM 实例、控制接口、Raw MIDI 接口等。每个这样的实例对应一个组件条目。 组件可以通过 snd_device_new() 函数来创建:

snd_device_new(card, SNDRV_DEV_XXX, chip, &ops);

该函数接收以下参数:声卡指针、设备级别(SNDRV_DEV_XXX)、数据指针以及回调函数指针(&ops)。

其中,设备级别用于定义组件的类型,并决定其注册和注销的顺序。对于大多数组件,设备级别已在 ALSA 中预定义。对于用户自定义的组件,可以使用 SNDRV_DEV_LOWLEVEL。

此函数本身不会分配数据空间,数据需要在调用之前手动分配,并将其指针作为参数传入。在上述示例中,该指针(如 chip)会作为该实例的标识符使用。

对于 ALSA 中预定义的组件(如 AC97、PCM 等),它们在各自的构造函数内部会调用 snd_device_new()。组件的析构函数由回调函数中的指针指定,因此开发者无需手动调用组件的析构函数。

如果你希望创建自定义组件,需要在 ops 结构中设置 dev_free 回调指针为对应的析构函数,这样在调用 snd_card_free() 时该组件就能自动被释放。 接下来的示例将展示如何实现与芯片相关的私有数据结构。

  • Chip-Specific Data

与芯片相关的特定信息,例如 I/O 端口地址、资源指针或中断号(irq number),会被存储在芯片专用的数据结构(record)中:

struct mychip {
        ....
};

一般来说,分配芯片记录有两种方式。

  • 通过 snd_card_new() 分配。

如前所述,你可以将额外数据的长度作为 snd_card_new() 的第 5 个参数传入,例如:

err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,
                   sizeof(struct mychip), &card);

struct mychip 是芯片记录的结构体类型。 作为返回值,分配好的记录可以通过以下方式访问:

struct mychip *chip = card->private_data;

使用这种方法,你无需进行两次内存分配。该记录会随着声卡实例一起被释放。

  • Allocating an extra device.

在通过 snd_card_new() 分配声卡实例(第 4 个参数设为 0)之后,调用 kzalloc():

struct snd_card *card;
struct mychip *chip;
err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,
                   0, &card);
.....
chip = kzalloc(sizeof(*chip), GFP_KERNEL);

芯片记录结构体中至少应包含一个字段用于保存声卡指针:

struct mychip {
        struct snd_card *card;
        ....
};

然后,在返回的芯片实例中设置该声卡指针:

chip->card = card;

接下来,初始化各个字段,并使用指定的 ops 将该芯片记录作为一个低层设备(low-level device)进行注册:

static const struct snd_device_ops ops = {
        .dev_free =        snd_mychip_dev_free,
};
....
snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);

snd_mychip_dev_free() 是设备的析构函数,它将调用真正的析构操作:

static int snd_mychip_dev_free(struct snd_device *device)
{
        return snd_mychip_free(device->device_data);
}

其中,snd_mychip_free() 才是真正的析构函数。 这种方法的缺点是:代码量明显更大。 但它的优点在于:你可以通过在 snd_device_ops 中设置相关回调函数,在声卡注册和断开连接时触发自定义的回调操作。 关于声卡的注册与断开连接,请参见下文的子章节。

注册与释放

在所有组件都分配完成后,通过调用 snd_card_register() 来注册声卡实例。从这一刻起,设备文件的访问才会被启用。

也就是说,在调用 snd_card_register() 之前,外部无法访问这些组件,因此是安全的。

如果该函数调用失败,应在调用 snd_card_free() 释放声卡资源后退出 probe 函数。

要释放声卡实例,只需调用 snd_card_free()。

如前所述,该调用会自动释放所有附加的组件。

对于支持热插拔的设备,可以使用 snd_card_free_when_closed()。 该函数将在所有设备文件被关闭后,再延迟销毁声卡实例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值