編寫 ALSA 驅動程式

作者:

Takashi Iwai <tiwai@suse.de>

前言

本文件描述瞭如何編寫 ALSA (高階 Linux 聲音架構) 驅動程式。該文件主要關注 PCI 音效卡。對於其他裝置型別,API 可能也會有所不同。但是,至少 ALSA 核心 API 是一致的,因此它仍然對編寫它們有所幫助。

本文件面向已經具有足夠的 C 語言技能並且具有基本的 Linux 核心程式設計知識的人。本文件不解釋 Linux 核心編碼的一般主題,也不涵蓋低階驅動程式實現的細節。它僅描述了在 ALSA 上編寫 PCI 聲音驅動程式的標準方法。

檔案樹結構

概述

ALSA 驅動程式的檔案樹結構如下所示

sound
        /core
                /oss
                /seq
                        /oss
        /include
        /drivers
                /mpu401
                /opl3
        /i2c
        /synth
                /emux
        /pci
                /(cards)
        /isa
                /(cards)
        /arm
        /ppc
        /sparc
        /usb
        /pcmcia /(cards)
        /soc
        /oss

core 目錄

此目錄包含中間層,它是 ALSA 驅動程式的核心。在此目錄中,儲存了原生 ALSA 模組。子目錄包含不同的模組,並且依賴於核心配置。

core/oss

OSS PCM 和混音器模擬模組的程式碼儲存在此目錄中。OSS rawmidi 模擬包含在 ALSA rawmidi 程式碼中,因為它非常小。音序器程式碼儲存在 core/seq/oss 目錄中(請參閱 下方)。

core/seq

此目錄及其子目錄用於 ALSA 音序器。此目錄包含音序器核心和主要音序器模組,例如 snd-seq-midi, snd-seq-virmidi 等。僅當在核心配置中設定了 CONFIG_SND_SEQUENCER 時才編譯它們。

core/seq/oss

這包含 OSS 音序器模擬程式碼。

include 目錄

這是 ALSA 驅動程式的公共標頭檔案的地方,這些標頭檔案要匯出到使用者空間,或者由不同目錄中的多個檔案包含。基本上,私有標頭檔案不應放在此目錄中,但由於歷史原因,您仍然可能會在那裡找到檔案 :)。

drivers 目錄

此目錄包含在不同架構上的不同驅動程式之間共享的程式碼。因此,它們不應該是特定於架構的。例如,虛擬 PCM 驅動程式和序列 MIDI 驅動程式在此目錄中找到。在子目錄中,存在獨立於匯流排和 CPU 架構的元件的程式碼。

drivers/mpu401

MPU401 和 MPU401-UART 模組儲存在此處。

drivers/opl3 and opl4

OPL3 和 OPL4 FM 合成器內容在此處找到。

i2c 目錄

這包含 ALSA i2c 元件。

儘管 Linux 上有一個標準的 i2c 層,但 ALSA 為某些卡提供了自己的 i2c 程式碼,因為音效卡只需要一個簡單的操作,並且標準的 i2c API 對於這樣的目的來說太複雜了。

synth 目錄

這包含合成器中間級模組。

到目前為止,在 synth/emux 子目錄下只有 Emu8000/Emu10k1 合成器驅動程式。

pci 目錄

此目錄及其子目錄儲存 PCI 音效卡的頂級卡模組和特定於 PCI 匯流排的程式碼。

從單個檔案編譯的驅動程式直接儲存在 pci 目錄中,而具有多個原始檔的驅動程式儲存在它們自己的子目錄中 (例如 emu10k1, ice1712)。

isa 目錄

此目錄及其子目錄儲存 ISA 音效卡的頂級卡模組。

arm, ppc 和 sparc 目錄

它們用於特定於這些架構之一的頂級卡模組。

usb 目錄

此目錄包含 USB 音訊驅動程式。USB MIDI 驅動程式已整合到 usb 音訊驅動程式中。

pcmcia 目錄

PCMCIA,特別是 PCCard 驅動程式將進入此處。CardBus 驅動程式將位於 pci 目錄中,因為它們的 API 與標準 PCI 卡的 API 相同。

soc 目錄

此目錄包含 ASoC(片上 ALSA 系統)層的程式碼,包括 ASoC 核心、編解碼器和機器驅動程式。

oss 目錄

這包含 OSS/Lite 程式碼。在編寫時,除了 m68k 上的 dmasound 之外,所有程式碼都已被刪除。

PCI 驅動程式的基本流程

概述

PCI 音效卡的最小流程如下

  • 定義 PCI ID 表(請參閱 PCI 條目 部分)。

  • 建立 probe 回撥。

  • 建立 remove 回撥。

  • 建立一個 struct pci_driver 結構,其中包含上述三個指標。

  • 建立一個 init 函式,該函式僅呼叫 pci_register_driver() 來註冊上面定義的 pci_driver 表。

  • 建立一個 exit 函式來呼叫 pci_unregister_driver() 函式。

完整程式碼示例

程式碼示例如下所示。某些部分目前尚未實現,但將在下一節中填充。 snd_mychip_probe() 函式註釋行中的數字是指以下部分中解釋的詳細資訊。

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

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

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

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

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

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

        *rchip = NULL;

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

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

        chip->card = card;

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

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

        *rchip = chip;
        return 0;
}

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

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

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

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

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

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

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

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

error:
        snd_card_free(card);
        return err;
}

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

驅動程式建構函式

PCI 驅動程式的真正建構函式是 probe 回撥。 probe 回撥和從 probe 回撥呼叫的其他元件建構函式不能與 __init 字首一起使用,因為任何 PCI 裝置都可能是熱插拔裝置。

probe 回撥中,經常使用以下方案。

1) 檢查並遞增裝置索引。

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

其中 enable[dev] 是模組選項。

每次呼叫 probe 回撥時,檢查裝置的可用性。如果不可用,只需遞增裝置索引並返回。dev 稍後也會遞增 (步驟 7)。

2) 建立卡例項

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

詳細資訊將在 卡和元件的管理 部分中解釋。

3) 建立主元件

在此部分中,分配 PCI 資源

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

詳細資訊將在 PCI 資源管理 部分中解釋。

當出現問題時,probe 函式需要處理錯誤。在本例中,我們在函式末尾放置了一個錯誤處理路徑

error:
        snd_card_free(card);
        return err;

由於每個元件都可以正確釋放,因此在大多數情況下,單個 snd_card_free() 呼叫就足夠了。

4) 設定驅動程式 ID 和名稱字串。

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

driver 欄位儲存晶片的最小 ID 字串。alsa-lib 的配置器使用此字串,因此請保持簡單但唯一。即使是同一個驅動程式也可以具有不同的驅動程式 ID,以區分每種晶片型別的功能。

shortname 欄位是一個字串,顯示為更詳細的名稱。longname 欄位包含 /proc/asound/cards 中顯示的資訊。

5) 建立其他元件,例如混音器、MIDI 等。

在這裡,您定義基本元件,例如 PCM、混音器(例如 AC97)、MIDI(例如 MPU-401)和其他介面。此外,如果您想要一個 proc 檔案,也請在此處定義它。

6) 註冊卡例項。

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

也將在 卡和元件的管理 部分中解釋。

7) 設定 PCI 驅動程式資料並返回零。

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

在上面,儲存了卡記錄。此指標也用於 remove 回撥和電源管理回撥中。

解構函式

解構函式,remove 回撥,只是釋放卡例項。然後,ALSA 中間層將自動釋放所有附加的元件。

通常只是呼叫 snd_card_free()

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

上面的程式碼假設卡指標已設定為 PCI 驅動程式資料。

標頭檔案

對於上面的示例,至少需要以下包含檔案

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

其中最後一個僅在原始檔中定義了模組選項時才需要。如果程式碼拆分為多個檔案,則沒有模組選項的檔案不需要它們。

除了這些標頭檔案,您還需要 <linux/interrupt.h> 用於中斷處理,以及 <linux/io.h> 用於 I/O 訪問。如果您使用 mdelay()udelay() 函式,您還需要包含 <linux/delay.h>

ALSA 介面(如 PCM 和控制 API)在其他 <sound/xxx.h> 標頭檔案中定義。它們必須在 <sound/core.h> 之後包含。

卡和元件的管理

卡例項

對於每張音效卡,必須分配一個“卡”記錄。

卡記錄是音效卡的總部。它管理音效卡上裝置(元件)的整個列表,例如 PCM、混音器、MIDI、合成器等。此外,卡記錄還儲存卡的 ID 和名稱字串,管理 proc 檔案的根,並控制電源管理狀態和熱插拔斷開連線。卡記錄上的元件列表用於管理銷燬時資源的正確釋放。

如上所述,要建立卡例項,請呼叫 snd_card_new()

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

該函式採用六個引數:父裝置指標、卡索引號、id 字串、模組指標(通常為 THIS_MODULE)、額外資料空間的大小以及返回卡例項的指標。 extra_size 引數用於為晶片特定資料分配 card->private_data。請注意,這些資料由 snd_card_new() 分配。

第一個引數,struct device 的指標,指定父裝置。對於 PCI 裝置,通常在此處傳遞 &pci->

元件

建立卡後,您可以將元件(裝置)附加到卡例項。在 ALSA 驅動程式中,元件表示為 struct snd_device 物件。元件可以是 PCM 例項、控制介面、原始 MIDI 介面等。每個此類例項都有一個元件條目。

可以透過 snd_device_new() 函式建立元件

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

這需要卡指標、裝置級別 (SNDRV_DEV_XXX)、資料指標和回撥指標 (&ops)。裝置級別定義了元件的型別以及註冊和取消註冊的順序。對於大多陣列件,已經定義了裝置級別。對於使用者定義的元件,可以使用 SNDRV_DEV_LOWLEVEL

此函式本身不分配資料空間。資料必須事先手動分配,並且其指標作為引數傳遞。此指標(在上面的示例中為 chip)用作例項的識別符號。

每個預定義的 ALSA 元件(如 AC97 和 PCM)都在其建構函式內部呼叫 snd_device_new()。每個元件的解構函式都在回撥指標中定義。因此,您無需注意為此類元件呼叫解構函式。

如果您希望建立自己的元件,則需要在 ops 中將解構函式設定為 dev_free 回撥,以便可以透過 snd_card_free() 自動釋放它。下一個示例將顯示特定於晶片的資料的實現。

特定於晶片的資料

特定於晶片的資訊(例如,I/O 埠地址、其資源指標或 irq 編號)儲存在特定於晶片的記錄中

struct mychip {
        ....
};

一般來說,有兩種分配晶片記錄的方法。

1. 透過 snd_card_new() 分配。

如上所述,您可以將 extra-data-length 傳遞給 snd_card_new() 的第 5 個引數,例如

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

struct mychip 是晶片記錄的型別。

作為回報,可以按如下方式訪問已分配的記錄

struct mychip *chip = card->private_data;

使用此方法,您不必分配兩次。該記錄與卡例項一起釋放。

2. 分配額外的裝置。

在透過 snd_card_new() (在第 4 個引數上使用 0) 分配卡例項後,呼叫 kzalloc()

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

晶片記錄應至少具有用於儲存卡指標的欄位,

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

然後,在返回的晶片例項中設定卡指標

chip->card = card;

接下來,初始化欄位,並將此晶片記錄註冊為具有指定 ops 的低階裝置

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

snd_mychip_dev_free() 是裝置解構函式,它將呼叫真正的解構函式

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

其中 snd_mychip_free() 是真正的解構函式。

此方法的缺點是程式碼量明顯更大。然而,優點是,您可以透過 snd_device_ops 中的設定,在註冊和斷開連線卡時觸發您自己的回撥。有關注冊和斷開連線卡的資訊,請參閱下面的小節。

註冊和釋放

分配所有元件後,透過呼叫 snd_card_register() 註冊卡例項。此時啟用到裝置檔案的訪問。也就是說,在呼叫 snd_card_register() 之前,元件從外部是安全不可訪問的。如果此呼叫失敗,請在透過 snd_card_free() 釋放卡後退出 probe 函式。

對於釋放卡例項,您可以簡單地呼叫 snd_card_free()。如前所述,所有元件都由此呼叫自動釋放。

對於允許熱插拔的裝置,可以使用 snd_card_free_when_closed()。這將推遲銷燬,直到所有裝置都關閉。

PCI 資源管理

完整程式碼示例

在本節中,我們將完成特定於晶片的建構函式、解構函式和 PCI 條目。首先顯示示例程式碼,如下所示

struct mychip {
        struct snd_card *card;
        struct pci_dev *pci;

        unsigned long port;
        int irq;
};

static int snd_mychip_free(struct mychip *chip)
{
        /* disable hardware here if any */
        .... /* (not implemented in this document) */

        /* release the irq */
        if (chip->irq >= 0)
                free_irq(chip->irq, chip);
        /* release the I/O ports & memory */
        pci_release_regions(chip->pci);
        /* disable the PCI entry */
        pci_disable_device(chip->pci);
        /* release the data */
        kfree(chip);
        return 0;
}

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

        *rchip = NULL;

        /* initialize the PCI entry */
        err = pci_enable_device(pci);
        if (err < 0)
                return err;
        /* check PCI availability (28bit DMA) */
        if (pci_set_dma_mask(pci, DMA_BIT_MASK(28)) < 0 ||
            pci_set_consistent_dma_mask(pci, DMA_BIT_MASK(28)) < 0) {
                printk(KERN_ERR "error to set 28bit mask DMA\n");
                pci_disable_device(pci);
                return -ENXIO;
        }

        chip = kzalloc(sizeof(*chip), GFP_KERNEL);
        if (chip == NULL) {
                pci_disable_device(pci);
                return -ENOMEM;
        }

        /* initialize the stuff */
        chip->card = card;
        chip->pci = pci;
        chip->irq = -1;

        /* (1) PCI resource allocation */
        err = pci_request_regions(pci, "My Chip");
        if (err < 0) {
                kfree(chip);
                pci_disable_device(pci);
                return err;
        }
        chip->port = pci_resource_start(pci, 0);
        if (request_irq(pci->irq, snd_mychip_interrupt,
                        IRQF_SHARED, KBUILD_MODNAME, chip)) {
                printk(KERN_ERR "cannot grab irq %d\n", pci->irq);
                snd_mychip_free(chip);
                return -EBUSY;
        }
        chip->irq = pci->irq;
        card->sync_irq = chip->irq;

        /* (2) initialization of the chip hardware */
        .... /*   (not implemented in this document) */

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

        *rchip = chip;
        return 0;
}

/* PCI IDs */
static struct pci_device_id snd_mychip_ids[] = {
        { PCI_VENDOR_ID_FOO, PCI_DEVICE_ID_BAR,
          PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0, },
        ....
        { 0, }
};
MODULE_DEVICE_TABLE(pci, snd_mychip_ids);

/* pci_driver definition */
static struct pci_driver driver = {
        .name = KBUILD_MODNAME,
        .id_table = snd_mychip_ids,
        .probe = snd_mychip_probe,
        .remove = snd_mychip_remove,
};

/* module initialization */
static int __init alsa_card_mychip_init(void)
{
        return pci_register_driver(&driver);
}

/* module clean up */
static void __exit alsa_card_mychip_exit(void)
{
        pci_unregister_driver(&driver);
}

module_init(alsa_card_mychip_init)
module_exit(alsa_card_mychip_exit)

EXPORT_NO_SYMBOLS; /* for old kernels only */

一些 Hafta's

PCI 資源的分配在 probe 函式中完成,通常為此目的編寫一個額外的 xxx_create() 函式。

對於 PCI 裝置,您首先必須在分配資源之前呼叫 pci_enable_device() 函式。此外,您需要設定正確的 PCI DMA 掩碼以限制訪問的 I/O 範圍。在某些情況下,您可能還需要呼叫 pci_set_master() 函式。

假設一個 28 位掩碼,要新增的程式碼如下所示

err = pci_enable_device(pci);
if (err < 0)
        return err;
if (pci_set_dma_mask(pci, DMA_BIT_MASK(28)) < 0 ||
    pci_set_consistent_dma_mask(pci, DMA_BIT_MASK(28)) < 0) {
        printk(KERN_ERR "error to set 28bit mask DMA\n");
        pci_disable_device(pci);
        return -ENXIO;
}

資源分配

I/O 埠和 irq 的分配透過標準的核心函式完成。這些資源必須在解構函式中釋放(見下文)。

現在假設 PCI 裝置有一個 8 位元組的 I/O 埠和一箇中斷。然後 struct mychip 將具有以下欄位

struct mychip {
        struct snd_card *card;

        unsigned long port;
        int irq;
};

對於 I/O 埠(以及記憶體區域),你需要擁有用於標準資源管理的資源指標。對於 irq,你只需要保留 irq 號碼(整數)。但是你需要在實際分配之前將該號碼初始化為 -1,因為 irq 0 是有效的。埠地址及其資源指標可以透過 kzalloc() 自動初始化為 null,因此你無需擔心重置它們。

I/O 埠的分配方式如下:

err = pci_request_regions(pci, "My Chip");
if (err < 0) {
        kfree(chip);
        pci_disable_device(pci);
        return err;
}
chip->port = pci_resource_start(pci, 0);

它將保留給定 PCI 裝置的 8 位元組 I/O 埠區域。返回值 chip->res_port 透過 kmalloc()request_region() 分配。該指標必須透過 kfree() 釋放,但這裡存在一個問題。這個問題稍後會解釋。

中斷源的分配方式如下:

if (request_irq(pci->irq, snd_mychip_interrupt,
                IRQF_SHARED, KBUILD_MODNAME, chip)) {
        printk(KERN_ERR "cannot grab irq %d\n", pci->irq);
        snd_mychip_free(chip);
        return -EBUSY;
}
chip->irq = pci->irq;

其中 snd_mychip_interrupt()稍後定義的中斷處理程式。請注意,只有當 request_irq() 成功時,才應該定義 chip->irq

在 PCI 總線上,中斷可以共享。因此,IRQF_SHARED 被用作 request_irq() 的中斷標誌。

request_irq() 的最後一個引數是傳遞給中斷處理程式的資料指標。通常,晶片特定的記錄用於此目的,但你也可以使用你喜歡的任何東西。

我不會在此處提供有關中斷處理程式的詳細資訊,但至少現在可以解釋它的外觀。中斷處理程式通常如下所示:

static irqreturn_t snd_mychip_interrupt(int irq, void *dev_id)
{
        struct mychip *chip = dev_id;
        ....
        return IRQ_HANDLED;
}

在請求 IRQ 之後,你可以將其傳遞給 card->sync_irq 欄位。

card->irq = chip->irq;

這允許 PCM 核心在正確的時間自動呼叫 synchronize_irq(),例如在 hw_free 之前。有關詳細資訊,請參見後面的章節sync_stop callback

現在,讓我們為上面的資源編寫相應的解構函式。解構函式的作用很簡單:停用硬體(如果已經啟用)並釋放資源。到目前為止,我們沒有硬體部分,因此此處未編寫停用程式碼。

要釋放資源,“檢查和釋放”方法是一種更安全的方法。對於中斷,執行以下操作:

if (chip->irq >= 0)
        free_irq(chip->irq, chip);

由於 irq 號碼可以從 0 開始,因此應使用負值(例如 -1)初始化 chip->irq,以便可以檢查上述 irq 號碼的有效性。

當你透過 pci_request_region()pci_request_regions() 請求 I/O 埠或記憶體區域時(如本示例中所示),請使用相應的函式 pci_release_region()pci_release_regions() 釋放資源。

pci_release_regions(chip->pci);

當你透過 request_region()request_mem_region() 手動請求時,可以透過 release_resource() 釋放它。假設你將從 request_region() 返回的資源指標儲存在 chip->res_port 中,則釋放過程如下所示:

release_and_free_resource(chip->res_port);

在結束之前,請不要忘記呼叫 pci_disable_device()

最後,釋放晶片特定的記錄:

kfree(chip);

我們沒有實現上面的硬體停用部分。如果需要這樣做,請注意,即使在完成晶片的初始化之前,也可以呼叫解構函式。最好有一個標誌來跳過硬體停用(如果尚未初始化硬體)。

當使用 SNDRV_DEV_LOWLELVEL 透過 snd_device_new() 將晶片資料分配給卡時,其解構函式最後被呼叫。也就是說,可以確保所有其他元件(例如 PCM 和控制元件)都已釋放。你不必顯式停止 PCM 等,只需呼叫低階硬體停止即可。

記憶體對映區域的管理幾乎與 I/O 埠的管理相同。你需要以下兩個欄位:

struct mychip {
        ....
        unsigned long iobase_phys;
        void __iomem *iobase_virt;
};

分配方式如下:

err = pci_request_regions(pci, "My Chip");
if (err < 0) {
        kfree(chip);
        return err;
}
chip->iobase_phys = pci_resource_start(pci, 0);
chip->iobase_virt = ioremap(chip->iobase_phys,
                                    pci_resource_len(pci, 0));

相應的解構函式將是:

static int snd_mychip_free(struct mychip *chip)
{
        ....
        if (chip->iobase_virt)
                iounmap(chip->iobase_virt);
        ....
        pci_release_regions(chip->pci);
        ....
}

當然,使用 pci_iomap() 的現代方法也會使事情變得更容易:

err = pci_request_regions(pci, "My Chip");
if (err < 0) {
        kfree(chip);
        return err;
}
chip->iobase_virt = pci_iomap(pci, 0, 0);

它與解構函式中的 pci_iounmap() 配對。

PCI 條目

到目前為止,一切都很好。讓我們完成缺少的 PCI 內容。首先,我們需要此晶片組的 struct pci_device_id 表。它是 PCI 供應商/裝置 ID 號碼的表,以及一些掩碼。

例如:

static struct pci_device_id snd_mychip_ids[] = {
        { PCI_VENDOR_ID_FOO, PCI_DEVICE_ID_BAR,
          PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0, },
        ....
        { 0, }
};
MODULE_DEVICE_TABLE(pci, snd_mychip_ids);

struct pci_device_id 的第一個和第二個欄位是供應商和裝置 ID。如果沒有理由過濾匹配的裝置,則可以按上述方式保留其餘欄位。struct pci_device_id 的最後一個欄位包含此條目的私有資料。你可以在此處指定任何值,例如,為受支援的裝置 ID 定義特定操作。在 intel8x0 驅動程式中可以找到這樣的示例。

此列表的最後一個條目是終止符。你必須指定此全零條目。

然後,準備 struct pci_driver 記錄:

static struct pci_driver driver = {
        .name = KBUILD_MODNAME,
        .id_table = snd_mychip_ids,
        .probe = snd_mychip_probe,
        .remove = snd_mychip_remove,
};

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 傳遞給相應的引數。

如果晶片支援多個回放或捕獲,則可以指定更多數字,但必須在 open/close 等回撥中正確處理它們。當需要知道你正在引用哪個子流時,可以從傳遞給每個回撥的 struct snd_pcm_substream 資料中獲取它,如下所示:

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

建立 PCM 後,你需要為每個 PCM 流設定運算子:

snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK,
                &snd_mychip_playback_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE,
                &snd_mychip_capture_ops);

運算子通常定義如下:

static struct snd_pcm_ops snd_mychip_playback_ops = {
        .open =        snd_mychip_pcm_open,
        .close =       snd_mychip_pcm_close,
        .hw_params =   snd_mychip_pcm_hw_params,
        .hw_free =     snd_mychip_pcm_hw_free,
        .prepare =     snd_mychip_pcm_prepare,
        .trigger =     snd_mychip_pcm_trigger,
        .pointer =     snd_mychip_pcm_pointer,
};

所有回撥都在 運算子 小節中描述。

設定運算子後,你可能想要預先分配緩衝區並設定託管分配模式。為此,只需呼叫以下內容:

snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV,
                               &chip->pci->dev,
                               64*1024, 64*1024);

預設情況下,它將分配高達 64kB 的緩衝區。緩衝區管理詳細資訊將在後面的章節 緩衝區和記憶體管理 中描述。

此外,你可以在 pcm->info_flags 中為此 PCM 設定一些額外的資訊。可用值在 <sound/asound.h> 中定義為 SNDRV_PCM_INFO_XXX,它用於硬體定義(稍後描述)。當你的音效卡晶片僅支援半雙工時,請像這樣指定它:

pcm->info_flags = SNDRV_PCM_INFO_HALF_DUPLEX;

... 還有解構函式?

PCM 例項的解構函式並非總是必要的。由於 PCM 裝置將由中間層程式碼自動釋放,因此你不必顯式呼叫解構函式。

如果你在內部建立了特殊記錄並且需要釋放它們,則解構函式是必要的。在這種情況下,將解構函式設定為 pcm->private_free

static void mychip_pcm_free(struct snd_pcm *pcm)
{
        struct mychip *chip = snd_pcm_chip(pcm);
        /* free your own data */
        kfree(chip->my_private_pcm_data);
        /* do what you like else */
        ....
}

static int snd_mychip_new_pcm(struct mychip *chip)
{
        struct snd_pcm *pcm;
        ....
        /* allocate your own data */
        chip->my_private_pcm_data = kmalloc(...);
        /* set the destructor */
        pcm->private_data = chip;
        pcm->private_free = mychip_pcm_free;
        ....
}

執行時指標 - PCM 資訊的寶庫

開啟 PCM 子流時,將分配一個 PCM 執行時例項並將其分配給該子流。可以透過 substream->runtime 訪問此指標。此執行時指標儲存著控制 PCM 所需的大部分資訊:hw_params 和 sw_params 配置的副本、緩衝區指標、mmap 記錄、自旋鎖等。

執行時例項的定義位於 <sound/pcm.h> 中。以下是此檔案的相關部分:

struct _snd_pcm_runtime {
        /* -- Status -- */
        struct snd_pcm_substream *trigger_master;
        snd_timestamp_t trigger_tstamp;       /* trigger timestamp */
        int overrange;
        snd_pcm_uframes_t avail_max;
        snd_pcm_uframes_t hw_ptr_base;        /* Position at buffer restart */
        snd_pcm_uframes_t hw_ptr_interrupt; /* Position at interrupt time*/

        /* -- HW params -- */
        snd_pcm_access_t access;      /* access mode */
        snd_pcm_format_t format;      /* SNDRV_PCM_FORMAT_* */
        snd_pcm_subformat_t subformat;        /* subformat */
        unsigned int rate;            /* rate in Hz */
        unsigned int channels;                /* channels */
        snd_pcm_uframes_t period_size;        /* period size */
        unsigned int periods;         /* periods */
        snd_pcm_uframes_t buffer_size;        /* buffer size */
        unsigned int tick_time;               /* tick time */
        snd_pcm_uframes_t min_align;  /* Min alignment for the format */
        size_t byte_align;
        unsigned int frame_bits;
        unsigned int sample_bits;
        unsigned int info;
        unsigned int rate_num;
        unsigned int rate_den;

        /* -- SW params -- */
        struct timespec tstamp_mode;  /* mmap timestamp is updated */
        unsigned int period_step;
        unsigned int sleep_min;               /* min ticks to sleep */
        snd_pcm_uframes_t start_threshold;
        /*
         * The following two thresholds alleviate playback buffer underruns; when
         * hw_avail drops below the threshold, the respective action is triggered:
         */
        snd_pcm_uframes_t stop_threshold;     /* - stop playback */
        snd_pcm_uframes_t silence_threshold;  /* - pre-fill buffer with silence */
        snd_pcm_uframes_t silence_size;       /* max size of silence pre-fill; when >= boundary,
                                               * fill played area with silence immediately */
        snd_pcm_uframes_t boundary;   /* pointers wrap point */

        /* internal data of auto-silencer */
        snd_pcm_uframes_t silence_start; /* starting pointer to silence area */
        snd_pcm_uframes_t silence_filled; /* size filled with silence */

        snd_pcm_sync_id_t sync;               /* hardware synchronization ID */

        /* -- mmap -- */
        volatile struct snd_pcm_mmap_status *status;
        volatile struct snd_pcm_mmap_control *control;
        atomic_t mmap_count;

        /* -- locking / scheduling -- */
        spinlock_t lock;
        wait_queue_head_t sleep;
        struct timer_list tick_timer;
        struct fasync_struct *fasync;

        /* -- private section -- */
        void *private_data;
        void (*private_free)(struct snd_pcm_runtime *runtime);

        /* -- hardware description -- */
        struct snd_pcm_hardware hw;
        struct snd_pcm_hw_constraints hw_constraints;

        /* -- timer -- */
        unsigned int timer_resolution;        /* timer resolution */

        /* -- DMA -- */
        unsigned char *dma_area;      /* DMA area */
        dma_addr_t dma_addr;          /* physical bus address (not accessible from main CPU) */
        size_t dma_bytes;             /* size of DMA area */

        struct snd_dma_buffer *dma_buffer_p;  /* allocated buffer */

#if defined(CONFIG_SND_PCM_OSS) || defined(CONFIG_SND_PCM_OSS_MODULE)
        /* -- OSS things -- */
        struct snd_pcm_oss_runtime oss;
#endif
};

對於每個音效卡驅動程式的運算子(回撥),這些記錄中的大多數都應該是隻讀的。只有 PCM 中間層才能更改/更新它們。例外情況是硬體描述 (hw) DMA 緩衝區資訊和私有資料。此外,如果使用標準託管緩衝區分配模式,則無需自己設定 DMA 緩衝區資訊。

在下面的章節中,將解釋重要的記錄。

硬體描述

硬體描述符(struct snd_pcm_hardware)包含基本硬體配置的定義。首先,你需要 PCM open callback 中定義它。請注意,執行時例項儲存描述符的副本,而不是指向現有描述符的指標。也就是說,在 open 回撥中,你可以根據需要修改複製的描述符(runtime->hw)。例如,如果只有在某些晶片型號上通道的最大數量為 1,你仍然可以使用相同的硬體描述符並稍後更改 channels_max:

struct snd_pcm_runtime *runtime = substream->runtime;
...
runtime->hw = snd_mychip_playback_hw; /* common definition */
if (chip->model == VERY_OLD_ONE)
        runtime->hw.channels_max = 1;

通常,你將具有如下所示的硬體描述符:

static struct snd_pcm_hardware snd_mychip_playback_hw = {
        .info = (SNDRV_PCM_INFO_MMAP |
                 SNDRV_PCM_INFO_INTERLEAVED |
                 SNDRV_PCM_INFO_BLOCK_TRANSFER |
                 SNDRV_PCM_INFO_MMAP_VALID),
        .formats =          SNDRV_PCM_FMTBIT_S16_LE,
        .rates =            SNDRV_PCM_RATE_8000_48000,
        .rate_min =         8000,
        .rate_max =         48000,
        .channels_min =     2,
        .channels_max =     2,
        .buffer_bytes_max = 32768,
        .period_bytes_min = 4096,
        .period_bytes_max = 32768,
        .periods_min =      1,
        .periods_max =      1024,
};
  • info 欄位包含此 PCM 的型別和功能。位標誌在 <sound/asound.h> 中定義為 SNDRV_PCM_INFO_XXX。在此處,至少你必須指定是否支援 mmap 以及支援哪些交錯格式。當硬體支援 mmap 時,在此處新增 SNDRV_PCM_INFO_MMAP 標誌。當硬體支援交錯格式或非交錯格式時,必須分別設定 SNDRV_PCM_INFO_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 frame = channels * samples-size。要在幀和位元組之間進行轉換,可以使用 frames_to_bytes()bytes_to_frames() 輔助函式:

period_bytes = frames_to_bytes(runtime, runtime->period_size);

此外,許多軟體引數 (sw_params) 也以幀儲存。請檢查欄位的型別。snd_pcm_uframes_t 用於作為無符號整數的幀,而 snd_pcm_sframes_t 用於作為有符號整數的幀。

DMA 緩衝區資訊

DMA 緩衝區由以下四個欄位定義:dma_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 是強制性的。當緩衝區被 mmap 時,dma_area 是必需的。如果你的驅動程式不支援 mmap,則此欄位不是必需的。dma_addr 也是可選的。你也可以根據需要使用 dma_private。

執行狀態

可以透過 runtime->status 引用執行狀態。這是一個指向 struct snd_pcm_mmap_status 記錄的指標。例如,你可以透過 runtime->status->hw_ptr 獲取當前的 DMA 硬體指標。

可以透過 runtime->control 引用 DMA 應用程式指標,該指標指向 struct snd_pcm_mmap_control 記錄。但是,不建議直接訪問此值。

私有資料

你可以為子流分配一個記錄並將其儲存在 runtime->private_data 中。通常,這是在 PCM open callback 中完成的。不要將其與 pcm->private_data 混淆。pcm->private_data 通常指向在建立 PCM 裝置時靜態分配的晶片例項,而 runtime->private_data 指向在 PCM open callback 中建立的動態資料結構。

static int snd_xxx_open(struct snd_pcm_substream *substream)
{
        struct my_pcm_data *data;
        ....
        data = kmalloc(sizeof(*data), GFP_KERNEL);
        substream->runtime->private_data = data;
        ....
}

分配的物件必須在 close callback 中釋放。

運算子

好的,現在讓我詳細介紹每個 PCM 回撥(ops)。通常,如果成功,每個回撥都必須返回 0;如果失敗,則返回負錯誤號(例如 -EINVAL)。要選擇合適的錯誤號碼,建議檢查核心的其他部分在相同型別的請求失敗時返回什麼值。

每個回撥函式至少採用一個引數,該引數包含 struct snd_pcm_substream 指標。要從給定的子流例項中檢索晶片記錄,可以使用以下宏:

int xxx(...) {
        struct mychip *chip = snd_pcm_substream_chip(substream);
        ....
}

該宏讀取 substream->private_data,這是 pcm->private_data 的副本。如果需要為每個 PCM 子流分配不同的資料記錄,則可以覆蓋前者。例如,cmi8330 驅動程式為回放和捕獲方向分配不同的 private_data,因為它對不同的方向使用兩個不同的編解碼器(與 SB 和 AD 相容)。

PCM open callback

static int snd_xxx_open(struct snd_pcm_substream *substream);

當 PCM 子流被開啟時,會呼叫此函式。

至少,你必須在這裡初始化 runtime->hw 記錄。通常,這是這樣完成的

static int snd_xxx_open(struct snd_pcm_substream *substream)
{
        struct mychip *chip = snd_pcm_substream_chip(substream);
        struct snd_pcm_runtime *runtime = substream->runtime;

        runtime->hw = snd_mychip_playback_hw;
        return 0;
}

其中 snd_mychip_playback_hw 是預定義的硬體描述。

你可以在此回撥函式中分配私有資料,如 私有資料 部分所述。

如果硬體配置需要更多約束,也請在此處設定硬體約束。有關更多詳細資訊,請參閱 約束

關閉回撥

static int snd_xxx_close(struct snd_pcm_substream *substream);

顯然,當 PCM 子流被關閉時,會呼叫此函式。

open 回撥函式中為 PCM 子流分配的任何私有例項都將在此處釋放

static int snd_xxx_close(struct snd_pcm_substream *substream)
{
        ....
        kfree(substream->runtime->private_data);
        ....
}

ioctl 回撥

這用於對 PCM ioctl 的任何特殊呼叫。但通常你可以將其保留為 NULL,然後 PCM 核心將呼叫通用 ioctl 回撥函式 snd_pcm_lib_ioctl()。如果你需要處理通道資訊的唯一設定或重置過程,你可以在此處傳遞你自己的回撥函式。

hw_params 回撥

static int snd_xxx_hw_params(struct snd_pcm_substream *substream,
                             struct snd_pcm_hw_params *hw_params);

當硬體引數 (hw_params) 由應用程式設定時,即一旦為 PCM 子流定義了緩衝區大小、週期大小、格式等,就會呼叫此函式。

許多硬體設定應該在此回撥函式中完成,包括緩衝區的分配。

要初始化的引數透過 params_xxx() 宏檢索。

當你為子流選擇託管緩衝區分配模式時,在此回撥函式被呼叫之前,緩衝區已經被分配。或者,你可以呼叫下面的輔助函式來分配緩衝區

snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(hw_params));

snd_pcm_lib_malloc_pages() 僅當 DMA 緩衝區已被預先分配時才可用。有關更多詳細資訊,請參閱 緩衝區型別 部分。

請注意,這個回撥函式和 prepare 回撥函式可能每次初始化都會被多次呼叫。例如,OSS 模擬可能會在每次透過其 ioctl 進行更改時呼叫這些回撥函式。

因此,你需要小心不要多次分配相同的緩衝區,這會導致記憶體洩漏!多次呼叫上面的輔助函式是可以的。當它已經被分配時,它會自動釋放之前的緩衝區。

另一個需要注意的是,預設情況下,此回撥函式是非原子性的(可排程的),即當沒有設定 nonatomic 標誌時。這很重要,因為 trigger 回撥函式是原子性的(不可排程的)。也就是說,互斥鎖或任何與排程相關的功能在 trigger 回撥函式中不可用。有關詳細資訊,請參閱子節 原子性

hw_free 回撥

static int snd_xxx_hw_free(struct snd_pcm_substream *substream);

呼叫此函式是為了釋放透過 hw_params 分配的資源。

此函式始終在呼叫 close 回撥函式之前呼叫。此外,該回調函式也可能被多次呼叫。跟蹤每個資源是否已被釋放。

當你為 PCM 子流選擇託管緩衝區分配模式時,分配的 PCM 緩衝區將在呼叫此回撥函式後自動釋放。否則,你必須手動釋放緩衝區。通常,當緩衝區是從預先分配的池中分配的時,你可以使用標準的 API 函式 snd_pcm_lib_malloc_pages(),如下所示

snd_pcm_lib_free_pages(substream);

prepare 回撥

static int snd_xxx_prepare(struct snd_pcm_substream *substream);

當 PCM 被“準備”時,會呼叫此回撥函式。你可以在此處設定格式型別、取樣率等。preparehw_params 的區別在於,每次呼叫 snd_pcm_prepare() 時,都會呼叫 prepare 回撥函式,即在欠載等情況下恢復後。

請注意,此回撥函式是非原子性的。你可以在此回撥函式中安全地使用與排程相關的功能。

在此和以下回調函式中,你可以透過 runtime 記錄 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

pointer 回撥

static snd_pcm_uframes_t snd_xxx_pointer(struct snd_pcm_substream *substream)

當 PCM 中間層查詢緩衝區中當前的硬體位置時,會呼叫此回撥函式。位置必須以幀為單位返回,範圍從 0 到 buffer_size - 1

這通常是從 PCM 中間層中的緩衝區更新例程中呼叫的,該例程在中斷例程呼叫 snd_pcm_period_elapsed() 時被呼叫。然後 PCM 中間層更新位置並計算可用空間,並喚醒休眠的 poll 執行緒等。

預設情況下,此回撥函式也是原子性的。

copy 和 fill_silence 操作

這些回撥函式不是強制性的,在大多數情況下可以省略。當硬體緩衝區不能位於正常的記憶體空間中時,會使用這些回撥函式。有些晶片在硬體中擁有自己的緩衝區,這些緩衝區是不可對映的。在這種情況下,你必須手動將資料從記憶體緩衝區傳輸到硬體緩衝區。或者,如果緩衝區在物理和虛擬記憶體空間上都是非連續的,則也必須定義這些回撥函式。

如果定義了這兩個回撥函式,則複製和設定靜音操作將由它們完成。詳細資訊將在後面的 緩衝區和記憶體管理 部分中描述。

ack 回撥

此回撥函式也不是強制性的。當在讀取或寫入操作中更新 appl_ptr 時,會呼叫此回撥函式。一些驅動程式(如 emu10k1-fx 和 cs46xx)需要跟蹤內部緩衝區的當前 appl_ptr,並且此回撥函式僅對此類目的有用。

回撥函式可能會返回 0 或負錯誤。當返回值是 -EPIPE 時,PCM 核心將其視為緩衝區 XRUN,並將狀態自動更改為 SNDRV_PCM_STATE_XRUN

預設情況下,此回撥函式是原子性的。

page 回撥

此回撥函式也是可選的。mmap 呼叫此回撥函式來獲取頁面錯誤地址。

對於標準的 SG 緩衝區或 vmalloc 緩衝區,你不需要特殊的回撥函式。因此,很少使用此回撥函式。

mmap 回撥

這是另一個用於控制 mmap 行為的可選回撥函式。定義後,當頁面被記憶體對映時,PCM 核心會呼叫此回撥函式,而不是使用標準的輔助函式。如果你需要特殊處理(由於某些架構或裝置特定的問題),請根據你的喜好在此處實現所有內容。

PCM 中斷處理程式

PCM 內容的剩餘部分是 PCM 中斷處理程式。聲音驅動程式中 PCM 中斷處理程式的作用是更新緩衝區位置,並在緩衝區位置跨越指定的週期邊界時通知 PCM 中間層。要告知此資訊,請呼叫 snd_pcm_period_elapsed() 函式。

聲音晶片可以透過多種方式生成中斷。

週期(片段)邊界處的中斷

這是最常見的型別:硬體在每個週期邊界生成中斷。在這種情況下,你可以在每次中斷時呼叫 snd_pcm_period_elapsed()

snd_pcm_period_elapsed() 將子流指標作為其引數。因此,你需要保持子流指標可以從晶片例項訪問。例如,在晶片記錄中定義 substream 欄位以儲存當前正在執行的子流指標,並在 open 回撥函式中設定指標值(並在 close 回撥函式中重置)。

如果在中斷處理程式中獲取自旋鎖,並且該鎖也在其他 PCM 回撥函式中使用,則必須在呼叫 snd_pcm_period_elapsed() 之前釋放該鎖,因為 snd_pcm_period_elapsed() 在內部呼叫其他 PCM 回撥函式。

典型的程式碼如下所示

static irqreturn_t snd_mychip_interrupt(int irq, void *dev_id)
{
        struct mychip *chip = dev_id;
        spin_lock(&chip->lock);
        ....
        if (pcm_irq_invoked(chip)) {
                /* call updater, unlock before it */
                spin_unlock(&chip->lock);
                snd_pcm_period_elapsed(chip->substream);
                spin_lock(&chip->lock);
                /* acknowledge the interrupt if necessary */
        }
        ....
        spin_unlock(&chip->lock);
        return IRQ_HANDLED;
}

此外,當裝置可以檢測到緩衝區欠載/溢位時,驅動程式可以透過呼叫 snd_pcm_stop_xrun() 將 XRUN 狀態通知給 PCM 核心。此函式停止流並將 PCM 狀態設定為 SNDRV_PCM_STATE_XRUN。請注意,它必須在 PCM 流鎖之外呼叫,因此不能從原子回撥函式中呼叫。

高頻定時器中斷

當硬體不在週期邊界生成中斷,而是以固定的定時器速率發出定時器中斷時(例如 es1968 或 ymfpci 驅動程式),就會發生這種情況。在這種情況下,你需要檢查當前的硬體位置,並在每次中斷時累積已處理的樣本長度。當累積大小超過週期大小時,呼叫 snd_pcm_period_elapsed() 並重置累加器。

典型的程式碼如下所示

static irqreturn_t snd_mychip_interrupt(int irq, void *dev_id)
{
        struct mychip *chip = dev_id;
        spin_lock(&chip->lock);
        ....
        if (pcm_irq_invoked(chip)) {
                unsigned int last_ptr, size;
                /* get the current hardware pointer (in frames) */
                last_ptr = get_hw_ptr(chip);
                /* calculate the processed frames since the
                 * last update
                 */
                if (last_ptr < chip->last_ptr)
                        size = runtime->buffer_size + last_ptr
                                 - chip->last_ptr;
                else
                        size = last_ptr - chip->last_ptr;
                /* remember the last updated point */
                chip->last_ptr = last_ptr;
                /* accumulate the size */
                chip->size += size;
                /* over the period boundary? */
                if (chip->size >= runtime->period_size) {
                        /* reset the accumulator */
                        chip->size %= runtime->period_size;
                        /* call updater */
                        spin_unlock(&chip->lock);
                        snd_pcm_period_elapsed(substream);
                        spin_lock(&chip->lock);
                }
                /* acknowledge the interrupt if necessary */
        }
        ....
        spin_unlock(&chip->lock);
        return IRQ_HANDLED;
}

在呼叫 snd_pcm_period_elapsed()

在這兩種情況下,即使已經過去了不止一個週期,你也不必多次呼叫 snd_pcm_period_elapsed()。只需呼叫一次即可。PCM 層將檢查當前的硬體指標並更新到最新的狀態。

原子性

核心程式設計中最重要(因此也最難除錯)的問題之一是競爭條件。在 Linux 核心中,通常透過自旋鎖、互斥鎖或訊號量來避免它們。一般來說,如果競爭條件可能發生在中斷處理程式中,則必須以原子方式進行管理,並且你必須使用自旋鎖來保護關鍵部分。如果關鍵部分不在中斷處理程式程式碼中,並且可以接受相對較長的執行時間,則應使用互斥鎖或訊號量代替。

如前所述,一些 PCM 回撥函式是原子性的,而另一些則不是。例如,hw_params 回撥函式是非原子性的,而 trigger 回撥函式是原子性的。這意味著,後者已經在 PCM 中間層持有的自旋鎖(PCM 流鎖)中呼叫。在回撥函式中選擇鎖定方案時,請考慮此原子性。

在原子回撥函式中,你不能使用可能呼叫 schedule() 或進入 sleep() 的函式。訊號量和互斥鎖可能會休眠,因此它們不能在原子回撥函式(例如 trigger 回撥函式)中使用。要在此類回撥函式中實現某些延遲,請使用 udelay()mdelay()

所有三個原子回撥函式(trigger、pointer 和 ack)都是在停用本地中斷的情況下呼叫的。

但是,可以請求所有 PCM 操作都是非原子性的。這假設所有呼叫站點都在非原子上下文中。例如,函式 snd_pcm_period_elapsed() 通常從中斷處理程式中呼叫。但是,如果將驅動程式設定為使用執行緒中斷處理程式,則此呼叫也可以在非原子上下文中進行。在這種情況下,你可以在建立 struct snd_pcm 物件後設置其 nonatomic 欄位。設定此標誌後,PCM 核心內部將使用互斥鎖和 rwsem 代替 spin 和 rwlocks,以便你可以在非原子上下文中安全地呼叫所有 PCM 函式。

此外,在某些情況下,你可能需要在原子上下文中呼叫 snd_pcm_period_elapsed()(例如,在 ack 或其他回撥期間經過了週期)。也有一個變體可以在 PCM 流鎖內部呼叫 snd_pcm_period_elapsed_under_stream_lock() 用於此目的。

約束

由於物理限制,硬體不是無限可配置的。這些限制透過設定約束來表達。

例如,為了將取樣率限制為一些支援的值,請使用 snd_pcm_hw_constraint_list()。你需要在 open 回撥函式中呼叫此函式

static unsigned int rates[] =
        {4000, 10000, 22050, 44100};
static struct snd_pcm_hw_constraint_list constraints_rates = {
        .count = ARRAY_SIZE(rates),
        .list = rates,
        .mask = 0,
};

static int snd_mychip_pcm_open(struct snd_pcm_substream *substream)
{
        int err;
        ....
        err = snd_pcm_hw_constraint_list(substream->runtime, 0,
                                         SNDRV_PCM_HW_PARAM_RATE,
                                         &constraints_rates);
        if (err < 0)
                return err;
        ....
}

有許多不同的約束。檢視 sound/pcm.h 以獲取完整列表。你甚至可以定義自己的約束規則。例如,假設 my_chip 可以管理一個 1 通道的子流,當且僅當格式為 S16_LE,否則它支援 struct snd_pcm_hardware 中指定的任何格式(或任何其他 constraint_list)。你可以構建如下規則

static int hw_rule_channels_by_format(struct snd_pcm_hw_params *params,
                                      struct snd_pcm_hw_rule *rule)
{
        struct snd_interval *c = hw_param_interval(params,
                      SNDRV_PCM_HW_PARAM_CHANNELS);
        struct snd_mask *f = hw_param_mask(params, SNDRV_PCM_HW_PARAM_FORMAT);
        struct snd_interval ch;

        snd_interval_any(&ch);
        if (f->bits[0] == SNDRV_PCM_FMTBIT_S16_LE) {
                ch.min = ch.max = 1;
                ch.integer = 1;
                return snd_interval_refine(c, &ch);
        }
        return 0;
}

然後你需要呼叫此函式來新增你的規則

snd_pcm_hw_rule_add(substream->runtime, 0, SNDRV_PCM_HW_PARAM_CHANNELS,
                    hw_rule_channels_by_format, NULL,
                    SNDRV_PCM_HW_PARAM_FORMAT, -1);

當應用程式設定 PCM 格式時,將呼叫規則函式,並且它會相應地最佳化通道數。但是應用程式可能會在設定格式之前設定通道數。因此,你還需要定義反向規則

static int hw_rule_format_by_channels(struct snd_pcm_hw_params *params,
                                      struct snd_pcm_hw_rule *rule)
{
        struct snd_interval *c = hw_param_interval(params,
              SNDRV_PCM_HW_PARAM_CHANNELS);
        struct snd_mask *f = hw_param_mask(params, SNDRV_PCM_HW_PARAM_FORMAT);
        struct snd_mask fmt;

        snd_mask_any(&fmt);    /* Init the struct */
        if (c->min < 2) {
                fmt.bits[0] &= SNDRV_PCM_FMTBIT_S16_LE;
                return snd_mask_refine(f, &fmt);
        }
        return 0;
}

... 並在 open 回撥函式中

snd_pcm_hw_rule_add(substream->runtime, 0, SNDRV_PCM_HW_PARAM_FORMAT,
                    hw_rule_format_by_channels, NULL,
                    SNDRV_PCM_HW_PARAM_CHANNELS, -1);

hw 約束的一個典型用法是將緩衝區大小與週期大小對齊。預設情況下,ALSA PCM 核心不強制緩衝區大小與週期大小對齊。例如,可以有像 256 週期位元組和 999 緩衝區位元組這樣的組合。

但是,許多裝置晶片要求緩衝區是週期的倍數。在這種情況下,呼叫 snd_pcm_hw_constraint_integer() 用於 SNDRV_PCM_HW_PARAM_PERIODS

snd_pcm_hw_constraint_integer(substream->runtime,
                              SNDRV_PCM_HW_PARAM_PERIODS);

這確保了週期的數量是整數,因此緩衝區大小與週期大小對齊。

hw 約束是定義首選 PCM 配置的非常強大的機制,並且有相關的輔助函式。我不會在此處提供更多詳細資訊,而是想說“盧克,使用原始碼。”

控制介面

常規

控制介面廣泛用於許多從使用者空間訪問的開關、滑塊等。其最重要的用途是混音器介面。換句話說,自 ALSA 0.9.x 以來,所有混音器內容都是在控制核心 API 上實現的。

ALSA 有一個定義良好的 AC97 控制模組。如果你的晶片僅支援 AC97,而沒有其他內容,則可以跳過本節。

控制 API 在 <sound/control.h> 中定義。如果要新增你自己的控制元件,請包含此檔案。

控制的定義

要建立新控制元件,你需要定義以下三個回撥函式: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 DIRECTION FUNCTION”。

第一個 SOURCE 指定控制元件的來源,是一個字串,例如 “Master”、“PCM”、“CD” 和 “Line”。有許多預定義的來源。

第二個 DIRECTION 是以下字串之一,具體取決於控制元件的方向:“Playback”、“Capture”、“Bypass Playback” 和 “Bypass Capture”。或者,它可以省略,表示回放和捕獲方向。

第三個 FUNCTION 是以下字串之一,具體取決於控制元件的功能:“Switch”、“Volume” 和 “Route”。

因此,控制元件名稱的示例是“Master Capture Switch”或“PCM Playback Volume”。

有一些例外情況

全域性捕獲和回放

“Capture Source”、“Capture Switch” 和 “Capture Volume” 用於全域性捕獲(輸入)源、開關和音量。類似地,“Playback Switch” 和 “Playback Volume” 用於全域性輸出增益開關和音量。

音調控制

音調控制開關和音量指定為 “Tone Control - XXX”,例如 “Tone Control - Switch”、“Tone Control - Bass”、“Tone Control - Center”。

3D 控制

3D 控制開關和音量指定為 “3D Control - XXX”,例如 “3D Control - Switch”、“3D Control - Center”、“3D Control - Space”。

麥克風增強

麥克風增強開關設定為 “Mic Boost” 或 “Mic Boost (6dB)”。

更精確的資訊可以在 Documentation/sound/designs/control-names.rst 中找到。

訪問標誌

訪問標誌是一個位掩碼,用於指定給定控制元件的訪問型別。預設訪問型別為 SNDRV_CTL_ELEM_ACCESS_READWRITE,這意味著允許對此控制元件進行讀寫操作。如果省略訪問標誌(即 = 0),則預設將其視為 READWRITE 訪問。

如果控制元件是隻讀的,請傳遞 SNDRV_CTL_ELEM_ACCESS_READ。在這種情況下,您不必定義 put 回撥。類似地,當控制元件是隻寫的(雖然這種情況很少見),您可以使用 WRITE 標誌,並且您不需要 get 回撥。

如果控制元件值頻繁更改(例如 VU 表),則應提供 VOLATILE 標誌。這意味著控制元件可能會在沒有 更改通知 的情況下更改。應用程式應不斷輪詢此類控制元件。

當控制元件可以更新,但目前對任何事物都沒有影響時,設定 INACTIVE 標誌可能是合適的。例如,當沒有開啟 PCM 裝置時,PCM 控制元件應處於非活動狀態。

LOCKOWNER 標誌來更改寫入許可權。

控制元件回撥

info 回撥

info 回撥用於獲取有關此控制元件的詳細資訊。這必須儲存給定的 struct snd_ctl_elem_info 物件的值。例如,對於具有單個元素的布林控制元件

static int snd_myctl_mono_info(struct snd_kcontrol *kcontrol,
                        struct snd_ctl_elem_info *uinfo)
{
        uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
        uinfo->count = 1;
        uinfo->value.integer.min = 0;
        uinfo->value.integer.max = 1;
        return 0;
}

type 欄位指定控制元件的型別。有 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);
}

一些常見的 info 回撥可供您方便地使用:snd_ctl_boolean_mono_info()snd_ctl_boolean_stereo_info()。顯然,前者是單聲道布林項的 info 回撥,就像上面的 snd_myctl_mono_info() 一樣,後者是立體聲通道布林項的 info 回撥。

get 回撥

此回撥用於讀取控制元件的當前值,以便可以將其返回到使用者空間。

例如:

static int snd_myctl_get(struct snd_kcontrol *kcontrol,
                         struct snd_ctl_elem_value *ucontrol)
{
        struct mychip *chip = snd_kcontrol_chip(kcontrol);
        ucontrol->value.integer.value[0] = get_some_value(chip);
        return 0;
}

value 欄位取決於控制元件的型別以及 info 回撥。例如,sb 驅動程式使用此欄位來儲存暫存器偏移、位移和位掩碼。private_value 欄位設定如下

.private_value = reg | (shift << 16) | (mask << 24)

並在如下回調中檢索

static int snd_sbmixer_get_single(struct snd_kcontrol *kcontrol,
                                  struct snd_ctl_elem_value *ucontrol)
{
        int reg = kcontrol->private_value & 0xff;
        int shift = (kcontrol->private_value >> 16) & 0xff;
        int mask = (kcontrol->private_value >> 24) & 0xff;
        ....
}

get 回撥中,如果控制元件有多個元素,即 count > 1,則必須填充所有元素。在上面的示例中,我們只填充了一個元素 (value.integer.value[0]),因為假設 count = 1

put 回撥

此回撥用於寫入來自使用者空間的值。

例如:

static int snd_myctl_put(struct snd_kcontrol *kcontrol,
                         struct snd_ctl_elem_value *ucontrol)
{
        struct mychip *chip = snd_kcontrol_chip(kcontrol);
        int changed = 0;
        if (chip->current_value !=
             ucontrol->value.integer.value[0]) {
                change_current_value(chip,
                            ucontrol->value.integer.value[0]);
                changed = 1;
        }
        return changed;
}

如上所示,如果值已更改,則必須返回 1。如果值未更改,則返回 0。如果發生任何致命錯誤,請像往常一樣返回負錯誤程式碼。

get 回撥一樣,當控制元件有多個元素時,也必須在此回撥中評估所有元素。

回撥不是原子的

這三個回撥都不是原子的。

控制元件建構函式

當一切準備就緒時,我們終於可以建立一個新的控制元件。要建立控制元件,需要呼叫兩個函式,snd_ctl_new1()snd_ctl_add()

以最簡單的方式,您可以這樣做

err = snd_ctl_add(card, snd_ctl_new1(&my_control, chip));
if (err < 0)
        return err;

其中 my_control 是上面定義的 struct snd_kcontrol_new 物件,而 chip 是要傳遞給 kcontrol->private_data 的物件指標,可以在回撥中引用它。

snd_ctl_new1() 分配一個新的 struct snd_kcontrol 例項,而 snd_ctl_add() 將給定的控制元件元件分配給卡。

更改通知

如果您需要在中斷例程中更改和更新控制元件,則可以呼叫 snd_ctl_notify()。例如

snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, id_pointer);

此函式採用卡指標、事件掩碼和控制元件 ID 指標進行通知。事件掩碼指定通知的型別,例如,在上面的示例中,通知控制元件值的更改。id 指標是要通知的 struct snd_ctl_elem_id 的指標。您可以在 es1938.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 例項的指標。在這種情況下,晶片指標設定為私有資料,以便讀/寫回調函式可以引用此晶片例項。此例項不一定儲存在晶片記錄中。如果需要從驅動程式更改暫存器值,或者需要暫停/恢復 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。您可以參考這些檔案來檢視編解碼器的當前狀態和暫存器。

多個編解碼器

當同一張卡上有多個編解碼器時,您需要使用 ac97.num=1 或更大的值多次呼叫 snd_ac97_mixer()num 欄位指定編解碼器編號。

如果設定了多個編解碼器,則需要為每個編解碼器編寫不同的回撥,或者在回撥例程中檢查 ac97->num

MIDI (MPU401-UART) 介面

常規

許多音效卡都有內建的 MIDI (MPU401-UART) 介面。當音效卡支援標準的 MPU401-UART 介面時,很可能可以使用 ALSA MPU401-UART API。MPU401-UART API 在 <sound/mpu401.h> 中定義。

一些音效卡晶片具有類似但略有不同的 mpu401 實現。例如,emu10k1 有自己的 mpu401 例程。

MIDI 建構函式

要建立 rawmidi 物件,請呼叫 snd_mpu401_uart_new()

struct snd_rawmidi *rmidi;
snd_mpu401_uart_new(card, 0, MPU401_HW_MPU401, port, info_flags,
                    irq, &rmidi);

第一個引數是卡指標,第二個引數是此元件的索引。最多可以建立 8 個 rawmidi 裝置。

第三個引數是硬體型別,MPU401_HW_XXX。如果不是特殊的型別,可以使用 MPU401_HW_MPU401

第四個引數是 I/O 埠地址。許多向後相容的 MPU401 都有一個 I/O 埠,例如 0x330。或者,它可能是其自身的 PCI I/O 區域的一部分。這取決於晶片設計。

第五個引數是附加資訊的位標誌。當上面的 I/O 埠地址是 PCI I/O 區域的一部分時,MPU401 I/O 埠可能已被驅動程式本身分配(保留)。在這種情況下,傳遞一個位標誌 MPU401_INFO_INTEGRATED,並且 mpu401-uart 層將自行分配 I/O 埠。

當控制器僅支援輸入或輸出 MIDI 流時,分別傳遞 MPU401_INFO_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 欄位。但是,struct snd_mpu401 指標不會由 snd_mpu401_uart_new() 顯式返回。您需要將 rmidi->private_data 顯式轉換為 struct snd_mpu401

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

並根據您的喜好重置 cport

mpu->cport = my_own_control_port;

第六個引數指定要分配的 ISA irq 編號。如果沒有要分配的中斷(因為您的程式碼已分配共享中斷,或者因為裝置未使用中斷),請傳遞 -1。對於沒有中斷的 MPU-401 裝置,將改用輪詢計時器。

MIDI 中斷處理程式

如果在 snd_mpu401_uart_new() 中分配了中斷,則會自動使用獨佔的 ISA 中斷處理程式,因此除了建立 mpu401 相關內容之外,您無需執行其他任何操作。否則,您必須設定 MPU401_INFO_IRQ_HOOK,並在確定發生了 UART 中斷時,從您自己的中斷處理程式中顯式呼叫 snd_mpu401_uart_interrupt()

在這種情況下,您需要將從 snd_mpu401_uart_new() 返回的 rawmidi 物件的 private_data 作為第二個引數傳遞給 snd_mpu401_uart_interrupt()

snd_mpu401_uart_interrupt(irq, rmidi->private_data, regs);

RawMIDI 介面

概述

raw MIDI 介面用於可以作為位元組流訪問的硬體 MIDI 埠。它不適用於不直接理解 MIDI 的合成器晶片。

ALSA 處理檔案和緩衝區管理。您所要做的就是編寫一些程式碼來在緩衝區和硬體之間移動資料。

rawmidi API 在 <sound/rawmidi.h> 中定義。

RawMIDI 建構函式

要建立 rawmidi 裝置,請呼叫 snd_rawmidi_new() 函式

struct snd_rawmidi *rmidi;
err = snd_rawmidi_new(chip->card, "MyMIDI", 0, outs, ins, &rmidi);
if (err < 0)
        return err;
rmidi->private_data = chip;
strcpy(rmidi->name, "My MIDI");
rmidi->info_flags = SNDRV_RAWMIDI_INFO_OUTPUT |
                    SNDRV_RAWMIDI_INFO_INPUT |
                    SNDRV_RAWMIDI_INFO_DUPLEX;

第一個引數是 card 指標,第二個引數是 ID 字串。

第三個引數是此元件的索引。您最多可以建立 8 個 rawmidi 裝置。

第四個和第五個引數分別是該裝置的輸出和輸入子流的數量(子流相當於 MIDI 埠)。

設定 info_flags 欄位以指定裝置的功能。如果至少有一個輸出埠,則設定 SNDRV_RAWMIDI_INFO_OUTPUT;如果至少有一個輸入埠,則設定 SNDRV_RAWMIDI_INFO_INPUT;如果裝置可以同時處理輸出和輸入,則設定 SNDRV_RAWMIDI_INFO_DUPLEX

建立 rawmidi 裝置後,您需要為每個子流設定運算子(回撥)。有一些輔助函式可以為裝置的所有子流設定運算子

snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_OUTPUT, &snd_mymidi_output_ops);
snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_INPUT, &snd_mymidi_input_ops);

這些運算子通常這樣定義

static struct snd_rawmidi_ops snd_mymidi_output_ops = {
        .open =    snd_mymidi_output_open,
        .close =   snd_mymidi_output_close,
        .trigger = snd_mymidi_output_trigger,
};

這些回撥在 RawMIDI 回撥 部分中進行了解釋。

如果存在多個子流,則應為每個子流指定唯一的名稱

struct snd_rawmidi_substream *substream;
list_for_each_entry(substream,
                    &rmidi->streams[SNDRV_RAWMIDI_STREAM_OUTPUT].substreams,
                    list {
        sprintf(substream->name, "My MIDI Port %d", substream->number + 1);
}
/* same for SNDRV_RAWMIDI_STREAM_INPUT */

RawMIDI 回撥

在所有回撥中,您可以將為 rawmidi 裝置設定的私有資料作為 substream->rmidi->private_data 訪問。

如果存在多個埠,您的回撥可以從傳遞給每個回撥的 struct snd_rawmidi_substream 資料中確定埠索引

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

RawMIDI open 回撥

static int snd_xxx_open(struct snd_rawmidi_substream *substream);

當開啟子流時,將呼叫此函式。您可以在此處初始化硬體,但不應開始傳輸/接收資料。

RawMIDI close 回撥

static int snd_xxx_close(struct snd_rawmidi_substream *substream);

猜猜看。

rawmidi 裝置的 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);

第一個引數是 card 指標,第二個引數是左埠地址,第三個引數是右埠地址。在大多數情況下,右埠位於左埠 + 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 不一定需要設定。只有 command 必須正確設定。您可以從 opl3->private_data 欄位檢索資料。

透過 snd_opl3_new() 建立 opl3 例項後,呼叫 snd_opl3_init() 以將晶片初始化為正確的狀態。請注意,snd_opl3_create() 始終在內部呼叫它。

如果成功建立了 opl3 例項,則為此 opl3 建立一個 hwdep 裝置

struct snd_hwdep *opl3hwdep;
snd_opl3_hwdep_new(opl3, 0, 1, &opl3hwdep);

第一個引數是您建立的 opl3_t 例項,第二個引數是索引號,通常為 0。

第三個引數是分配給 OPL3 埠的音序器客戶端的索引偏移量。當存在 MPU401-UART 時,此處應為 1(UART 始終佔用 0)。

硬體相關裝置

某些晶片需要使用者空間訪問才能進行特殊控制或載入微程式碼。在這種情況下,您可以建立一個 hwdep(硬體相關)裝置。hwdep API 在 <sound/hwdep.h> 中定義。您可以在 opl3 驅動程式或 isa/sb/sb16_csp.c 中找到示例。

hwdep 例項的建立透過 snd_hwdep_new() 完成

struct snd_hwdep *hw;
snd_hwdep_new(card, "My HWDEP", 0, &hw);

其中第三個引數是索引號。

然後,您可以將任何指標值傳遞給 private_data。如果您分配了私有資料,則還應定義一個解構函式。解構函式在 private_free 欄位中設定

struct mydata *p = kmalloc(sizeof(*p), GFP_KERNEL);
hw->private_data = p;
hw->private_free = mydata_free;

解構函式的實現將是

static void mydata_free(struct snd_hwdep *hw)
{
        struct mydata *p = hw->private_data;
        kfree(p);
}

可以為此例項定義任意檔案操作。檔案運算子在 ops 表中定義。例如,假設此晶片需要一個 ioctl

hw->ops.open = mydata_open;
hw->ops.ioctl = mydata_ioctl;
hw->ops.release = mydata_release;

並根據您的喜好實現回撥函式。

IEC958 (S/PDIF)

通常,IEC958 裝置的控制是透過控制介面實現的。有一個宏可以為 IEC958 控制組合名稱字串,SNDRV_CTL_NAME_IEC958()<include/asound.h> 中定義。

有一些用於 IEC958 狀態位的標準控制。這些控制使用型別 SNDRV_CTL_ELEM_TYPE_IEC958,並且元素的大小固定為 4 位元組陣列 (value.iec958.status[x])。對於 info 回撥,您不為此型別指定 value 欄位(但必須設定 count 欄位)。

“IEC958 Playback Con Mask” 用於返回消費者模式的 IEC958 狀態位的位掩碼。類似地,“IEC958 Playback Pro Mask” 返回專業模式的位掩碼。它們是隻讀控制。

同時,定義了 “IEC958 Playback Default” 控制以獲取和設定當前預設的 IEC958 位。

由於歷史原因,Playback Mask 和 Playback Default 控制的兩種變體都可以在 SNDRV_CTL_ELEM_IFACE_PCMSNDRV_CTL_ELEM_IFACE_MIXER 介面上實現。但是,驅動程式應在同一介面上公開掩碼和預設值。

此外,您可以定義控制開關以啟用/停用或設定原始位模式。該實現將取決於晶片,但控制應命名為 “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 檔案設定的最大大小。分配器將嘗試在給定大小內獲取儘可能大的區域。

第二個引數(型別)和第三個引數(裝置指標)取決於匯流排。對於普通裝置,使用 SNDRV_DMA_TYPE_DEV 型別將裝置指標(通常與 card->dev 相同)傳遞給第三個引數。

可以使用 SNDRV_DMA_TYPE_CONTINUOUS 型別預分配與匯流排無關的連續緩衝區。在這種情況下,您可以將 NULL 傳遞給裝置指標,這是預設模式,意味著使用 GFP_KERNEL 標誌進行分配。如果您需要受限制(較低)的地址,請為裝置設定一致的 DMA 掩碼位,並傳遞裝置指標,就像正常的裝置記憶體分配一樣。對於此型別,如果不需要地址限制,仍然允許將 NULL 傳遞給裝置指標。

對於散佈/收集緩衝區,請將 SNDRV_DMA_TYPE_DEV_SG 與裝置指標一起使用(請參閱 非連續緩衝區 部分)。

預先分配緩衝區後,您可以在 hw_params 回撥中使用分配器

snd_pcm_lib_malloc_pages(substream, size);

請注意,您必須預先分配才能使用此函式。

但是,大多數驅動程式使用“託管緩衝區分配模式”而不是手動分配和釋放。這是透過呼叫 snd_pcm_set_managed_buffer_all() 而不是 snd_pcm_lib_preallocate_pages_for_all() 完成的

snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV,
                               &pci->dev, size, max);

其中傳遞的引數與兩個函式相同。託管模式的區別在於,PCM 核心將在呼叫 PCM hw_params 回撥之前在內部呼叫 snd_pcm_lib_malloc_pages(),並在 PCM hw_free 回撥之後自動呼叫 snd_pcm_lib_free_pages()。因此,驅動程式不再需要在其回撥中顯式呼叫這些函式。這允許許多驅動程式具有 NULL hw_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。它的實現方式與上面的 copy 回撥類似

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

引數的含義與 copy 回撥中的含義相同,儘管沒有緩衝區指標引數。在交錯取樣的情況下,channel 引數沒有意義,就像 copy 回撥一樣。

fill_silence 回撥的作用是在硬體緩衝區中的指定偏移量 (pos) 處設定給定量 (count) 的靜音資料。假設資料格式已簽名(即,靜音資料為 0),並且使用類似 memset 函式的實現將如下所示

my_memset(my_buffer + pos, 0, count);

在非交錯取樣的情況下,實現再次變得更加複雜,因為它每次傳輸都為每個通道呼叫 N 次。例如,請參閱 isa/gus/gus_pcm.c

非連續緩衝區

如果您的硬體支援頁面表(如 emu10k1 中)或緩衝區描述符(如 via82xx 中),則可以使用散佈/收集 (SG) DMA。ALSA 提供了用於處理 SG 緩衝區的介面。該 API 在 <sound/pcm.h> 中提供。

為了建立 SG 緩衝區處理程式,請在 PCM 建構函式中使用 SNDRV_DMA_TYPE_DEV_SG 呼叫 snd_pcm_set_managed_buffer()snd_pcm_set_managed_buffer_all(),就像其他 PCI 預分配一樣。您還需要傳遞 &pci->dev,其中 pci 是晶片的 struct pci_dev 指標

snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV_SG,
                               &pci->dev, size, max);

然後,struct snd_sg_buf 例項建立為 substream->dma_private。您可以像這樣轉換指標

struct snd_sg_buf *sgbuf = (struct snd_sg_buf *)substream->dma_private;

然後在 snd_pcm_lib_malloc_pages() 呼叫中,通用的 SG-buffer 處理程式將分配給定大小的非連續核心頁面,並將它們對映為虛擬連續記憶體。虛擬指標透過 runtime->dma_area 進行定址。物理地址 (runtime->dma_addr) 設定為零,因為緩衝區在物理上是非連續的。物理地址表在 sgbuf->table 中設定。您可以透過 snd_pcm_sgbuf_get_addr() 獲取特定偏移量的物理地址。

如果您需要顯式釋放 SG-buffer 資料,請像往常一樣呼叫標準 API 函式 snd_pcm_lib_free_pages()

Vmalloc 分配的緩衝區

可以使用透過 vmalloc() 分配的緩衝區,例如,作為中間緩衝區。在設定了 SNDRV_DMA_TYPE_VMALLOC 型別的緩衝區預分配後,您可以簡單地透過標準的 snd_pcm_lib_malloc_pages() 等函式來分配它。

snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_VMALLOC,
                               NULL, 0, 0);

NULL 作為裝置指標引數傳遞,這表示將分配預設頁面(GFP_KERNEL 和 GFP_HIGHMEM)。

另外,請注意,此處將零作為大小和最大大小引數傳遞。由於每個 vmalloc 呼叫都應該隨時成功,因此我們不需要像其他連續頁面那樣預先分配緩衝區。

Proc 介面

ALSA 為 procfs 提供了一個簡單的介面。proc 檔案對於除錯非常有用。我建議您設定 proc 檔案,如果您編寫驅動程式並希望獲得執行狀態或暫存器轉儲。該 API 位於 <sound/info.h> 中。

要建立 proc 檔案,請呼叫 snd_card_proc_new()

struct snd_info_entry *entry;
int err = snd_card_proc_new(card, "my-file", &entry);

其中第二個引數指定要建立的 proc 檔案的名稱。上面的例子將在卡目錄(例如 /proc/asound/card0/my-file)下建立一個檔案 my-file

與其他元件一樣,透過 snd_card_proc_new() 建立的 proc 條目將在卡註冊和釋放函式中自動註冊和釋放。

建立成功後,該函式將新例項儲存在第三個引數給出的指標中。它被初始化為只讀文字 proc 檔案。要按原樣將此 proc 檔案用作只讀文字檔案,請透過 snd_info_set_text_ops() 設定帶有私有資料的讀取回調。

snd_info_set_text_ops(entry, chip, my_proc_read);

其中第二個引數 (chip) 是回撥中要使用的私有資料。第三個引數指定讀取緩衝區大小,第四個引數 (my_proc_read) 是回撥函式,其定義如下:

static void my_proc_read(struct snd_info_entry *entry,
                         struct snd_info_buffer *buffer);

在讀取回調中,使用 snd_iprintf() 輸出字串,它的工作方式與普通的 printf() 一樣。 例如:

static void my_proc_read(struct snd_info_entry *entry,
                         struct snd_info_buffer *buffer)
{
        struct my_chip *chip = entry->private_data;

        snd_iprintf(buffer, "This is my chip!\n");
        snd_iprintf(buffer, "Port = %ld\n", chip->port);
}

檔案許可權可以在之後更改。預設情況下,它們對所有使用者都是隻讀的。如果您想為使用者(預設情況下是 root)新增寫入許可權,請執行以下操作:

entry->mode = S_IFREG | S_IRUGO | S_IWUSR;

並設定寫入緩衝區大小和回撥:

entry->c.text.write = my_proc_write;

在寫入回撥中,您可以使用 snd_info_get_line() 獲取文字行,並使用 snd_info_get_str() 從該行檢索字串。 一些例子可以在 core/oss/mixer_oss.c, core/oss/ 和 pcm_oss.c 中找到。

對於原始資料 proc 檔案,請按如下方式設定屬性:

static const struct snd_info_entry_ops my_file_io_ops = {
        .read = my_file_io_read,
};

entry->content = SNDRV_INFO_CONTENT_DATA;
entry->private_data = chip;
entry->c.ops = &my_file_io_ops;
entry->size = 4096;
entry->mode = S_IFREG | S_IRUGO;

對於原始資料,必須正確設定 size 欄位。這指定了 proc 檔案訪問的最大大小。

原始模式的讀/寫回調比文字模式更直接。您需要使用低階 I/O 函式,例如 copy_from_user()copy_to_user() 來傳輸資料。

static ssize_t my_file_io_read(struct snd_info_entry *entry,
                            void *file_private_data,
                            struct file *file,
                            char *buf,
                            size_t count,
                            loff_t pos)
{
        if (copy_to_user(buf, local_data + pos, count))
                return -EFAULT;
        return count;
}

如果資訊條目的大小已正確設定,則保證 countpos 適合 0 和給定大小之間。除非需要任何其他條件,否則您不必在回撥中檢查範圍。

電源管理

如果晶片應該與掛起/恢復功能一起使用,您需要將電源管理程式碼新增到驅動程式中。用於電源管理的附加程式碼應該使用 CONFIG_PM 進行 ifdef,或者使用 __maybe_unused 屬性進行註釋;否則編譯器會報錯。

如果驅動程式完全支援掛起/恢復,即裝置可以正確恢復到呼叫掛起時的狀態,則可以在 PCM 資訊欄位中設定 SNDRV_PCM_INFO_RESUME 標誌。通常,只有當晶片的暫存器可以安全地儲存並恢復到 RAM 時,這才有可能。如果設定了此選項,則在恢復回撥完成後,將使用 SNDRV_PCM_TRIGGER_RESUME 呼叫觸發回撥。

即使驅動程式不支援完全的 PM,但仍然可以進行部分掛起/恢復,仍然值得實現掛起/恢復回撥。在這種情況下,應用程式將透過呼叫 snd_pcm_prepare() 來重置狀態並適當地重新啟動流。因此,您可以在下面定義掛起/恢復回撥,但不要將 SNDRV_PCM_INFO_RESUME 資訊標誌設定為 PCM。

請注意,無論 SNDRV_PCM_INFO_RESUME 標誌如何,當呼叫 snd_pcm_suspend_all() 時,始終可以呼叫帶有 SUSPEND 的觸發器。RESUME 標誌僅影響 snd_pcm_resume() 的行為。(因此,理論上,當未設定 SNDRV_PCM_INFO_RESUME 標誌時,無需在觸發回撥中處理 SNDRV_PCM_TRIGGER_RESUME。但是,為了相容性,最好保留它。)

驅動程式需要根據裝置連線到的匯流排定義掛起/恢復鉤子。在 PCI 驅動程式的情況下,回撥如下所示:

static int __maybe_unused snd_my_suspend(struct device *dev)
{
        .... /* do things for suspend */
        return 0;
}
static int __maybe_unused snd_my_resume(struct device *dev)
{
        .... /* do things for suspend */
        return 0;
}

實際掛起作業的方案如下:

  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 操作在內部呼叫 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); 或者它可以用作條件, 如果 (snd_BUG_ON(non_zero_is_bug)) return -EINVAL;

該宏接受一個條件表示式進行評估。 當設定 CONFIG_SND_DEBUG 時,如果表示式為非零,它將顯示警告訊息,例如 BUG? (xxx),通常後跟堆疊跟蹤。 在這兩種情況下,它都會返回評估的值。

致謝

我要感謝 Phil Kerr 對改進和更正本文件的幫助。

Kevin Conder 將原始純文字重新格式化為 DocBook 格式。

Giuliano Pochini 更正了拼寫錯誤,並在硬體約束部分貢獻了示例程式碼。