前言:
本文檔描述了如何編寫 ALSA(高級 Linux 音頻架構)驅動程序。文檔主要聚焦于 PCI 聲卡的實現。對于其他類型的設備,可能會使用不同的 API。不過,至少 ALSA 的內核 API 是一致的,因此本文檔在編寫這些驅動時仍然具有一定的參考價值。 本指南面向那些已經具備足夠 C 語言技能并且掌握基本 Linux 內核編程知識的開發者。本文檔不會講解 Linux 內核編程的一般性內容,也不會涉及底層驅動實現的細節。它僅介紹在 ALSA 框架下編寫 PCI 聲卡驅動的標準方法。
內容參考學習鏈接文檔:
https://kernel.org/doc/html/latest/sound/kernel-api/writing-an-alsa-driver.html#header-files
File Tree Structure:
下面的文件結構是Linux內核關于sound目錄下的介紹:
sound/core/oss/seq/oss/include/drivers/mpu401/opl3/i2c/synth/emux/pci/(cards)/isa/(cards)/arm/ppc/sparc/usb/pcmcia /(cards)/soc/oss
-
core 目錄 : 該目錄包含 ALSA 驅動的中間層,是 ALSA 驅動的核心。此目錄中存放的是 ALSA 的原生模塊。其子目錄包含了不同的模塊,并依賴于內核配置選項。
-
core/oss ?: 該目錄存放 OSS PCM 和混音器(mixer)仿真模塊的代碼。由于 OSS 的 rawmidi 仿真部分非常小,因此被直接包含在 ALSA 的 rawmidi 代碼中。序列器(sequencer)相關的 OSS 仿真代碼則位于 core/seq/oss 目錄(見下文)。
-
core/seq ?: 該目錄及其子目錄存放 ALSA 序列器(sequencer)相關代碼。它包含了序列器核心以及主要模塊,如 snd-seq-midi、snd-seq-virmidi 等。這些模塊僅在內核配置中啟用了 CONFIG_SND_SEQUENCER 時才會被編譯。
-
core/seq/oss ?: 此目錄包含 OSS 序列器仿真代碼。
-
include 目錄 ?: 該目錄用于存放 ALSA 驅動對用戶空間公開的頭文件,或供多個不同目錄中的文件共享使用。一般來說,私有頭文件不應放在這個目錄中,但你仍可能會在其中看到一些私有頭文件,這是歷史遺留問題所致。:)
-
drivers 目錄 ?:該目錄包含在不同平臺間共享的驅動代碼,因此它們不應具有架構相關性。例如,dummy PCM 驅動和串口 MIDI 驅動都位于此目錄。其子目錄中存放的是與總線或 CPU 架構無關的組件代碼。 drivers/mpu401 包含 MPU401 和 MPU401-UART 模塊。 drivers/opl3 與 opl4 包含 OPL3 和 OPL4 FM 合成器相關代碼。
-
i2c 目錄 ?:該目錄包含 ALSA 的 i2c 相關組件。雖然 Linux 系統本身有標準的 i2c 層,但某些聲卡只需要簡單的操作,而標準的 i2c API 過于復雜,因此 ALSA 對某些聲卡實現了自己的 i2c 代碼。
-
synth 目錄 ?:此目錄包含中間層的合成器模塊。目前,只有 Emu8000/Emu10k1 合成器驅動存放在 synth/emux 子目錄中。
-
pci 目錄 ?:此目錄及其子目錄存放 PCI 聲卡的頂層驅動模塊,以及與 PCI 總線相關的專用代碼。如果驅動只包含單個源文件,則直接放在 pci 目錄下;如果驅動包含多個源文件,則會單獨創建子目錄(例如 emu10k1、ice1712)。
-
isa 目錄 ?: 此目錄及其子目錄存放 ISA 聲卡的頂層驅動模塊。
-
arm、ppc 和 sparc 目錄 ?: 這些目錄用于存放特定于某一架構的頂層聲卡驅動模塊。
-
usb 目錄 ?: 該目錄包含 USB 音頻驅動。USB MIDI 驅動已經整合進了 USB 音頻驅動中。
-
pcmcia 目錄 ?: 該目錄用于存放 PCMCIA(尤其是 PCCard)驅動。由于 CardBus 的 API 與標準 PCI 卡相同,因此其驅動位于 pci 目錄中。
-
soc 目錄 ?: 該目錄包含 ASoC(ALSA SoC,系統級芯片)層的代碼,包括 ASoC 核心、編解碼器(codec)和 machine 驅動等。
-
oss 目錄 ?: 該目錄包含 OSS/Lite 代碼。截止目前,除了 m68k 架構下的 dmasound 之外,其他代碼均已被移除。
basic Flow for PCI Drivers:
PCI 聲卡驅動的最小實現流程如下:
-
定義 PCI ID 表(參見“PCI 條目”部分)。
-
創建 probe 回調函數(用于設備檢測和初始化)。
-
創建 remove 回調函數(用于設備移除時的清理操作)。
-
創建一個 struct pci_driver 結構體,包含上述三個函數指針(即:PCI ID 表、probe 和 remove 函數)。
-
創建初始化函數,該函數調用 pci_register_driver() 來注冊上面定義的 pci_driver 表。
-
創建退出函數,該函數調用 pci_unregister_driver() 來注銷該驅動。
Full Code Example:
下面展示了一個代碼示例。目前有些部分尚未實現,但會在接下來的章節中補充完成。 在 snd_mychip_probe() 函數中的注釋行里標注的數字,對應的是下一節中將詳細解釋的內容。
#include <linux/init.h>
#include <linux/pci.h>
#include <linux/slab.h>
#include <sound/core.h>
#include <sound/initval.h>/* module parameters (see?"Module Parameters") */
/* SNDRV_CARDS: maximum number of cards supported by this module */
static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
static bool?enable[SNDRV_CARDS] = SNDRV_DEFAULT_ENABLE_PNP;/* definition of the chip-specific record */
struct mychip {struct snd_card *card;/* the rest of the implementation will be?in?section*?"PCI Resource Management"*/
};/* chip-specific destructor* (see?"PCI Resource Management")*/
static int snd_mychip_free(struct mychip *chip)
{.... /* will be implemented later... */
}/* component-destructor* (see?"Management of Cards and Components")*/
static int snd_mychip_dev_free(struct snd_device *device)
{return?snd_mychip_free(device->device_data);
}/* chip-specific constructor* (see?"Management of Cards and Components")*/
static int snd_mychip_create(struct snd_card *card,struct pci_dev *pci,struct mychip **rchip)
{struct mychip *chip;int err;static const struct snd_device_ops ops = {.dev_free = snd_mychip_dev_free,};*rchip = NULL;/* check PCI availability here* (see?"PCI Resource Management")*/..../* allocate a chip-specific data with zero filled */chip = kzalloc(sizeof(*chip), GFP_KERNEL);if?(chip == NULL)return?-ENOMEM;chip->card = card;/* rest of initialization here; will be implemented* later, see?"PCI Resource Management"*/....err = snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);if?(err < 0) {snd_mychip_free(chip);return?err;}*rchip = chip;return?0;
}/* constructor -- see?"Driver Constructor"?sub-section */
static int snd_mychip_probe(struct pci_dev *pci,const struct pci_device_id *pci_id)
{static int dev;struct snd_card *card;struct mychip *chip;int err;/* (1) */if?(dev >= SNDRV_CARDS)return?-ENODEV;if?(!enable[dev]) {dev++;return?-ENOENT;}/* (2) */err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,0, &card);if?(err < 0)return?err;/* (3) */err = snd_mychip_create(card, pci, &chip);if?(err < 0)goto error;/* (4) */strcpy(card->driver,?"My Chip");strcpy(card->shortname,?"My Own Chip 123");sprintf(card->longname,?"%s at 0x%lx irq %i",card->shortname, chip->port, chip->irq);/* (5) */.... /* implemented later *//* (6) */err = snd_card_register(card);if?(err < 0)goto error;/* (7) */pci_set_drvdata(pci, card);dev++;return?0;error:snd_card_free(card);return?err;
}/* destructor -- see the?"Destructor"?sub-section */
static void snd_mychip_remove(struct pci_dev *pci)
{snd_card_free(pci_get_drvdata(pci));
}
驅動構造函數:
PCI 驅動的真正構造函數是 probe 回調函數。由于 PCI 設備可能是熱插拔設備,因此 probe 回調函數及其調用的其他組件構造函數不能使用 __init 前綴。 在 probe 回調函數中,通常會使用以下流程:
-
1)檢查并遞增設備索引。
static int dev;
....
if?(dev >= SNDRV_CARDS)return?-ENODEV;
if?(!enable[dev]) {dev++;return?-ENOENT;
}
其中 enable[dev] 是模塊參數選項。 每次調用 probe 回調時,都要檢查該設備是否可用。如果不可用,則只需遞增設備索引并返回。dev 變量稍后(在第 7 步)也會繼續遞增。
-
2)創建一個 sound card 實例
struct snd_card *card;
int err;
....
err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,0, &card);
詳細內容將在“聲卡與組件的管理”章節中進行說明。
-
3)創建主組件 在這一部分,將分配 PCI 資源
struct mychip *chip;
....
err = snd_mychip_create(card, pci, &chip);
if?(err < 0)goto error;
詳細內容將在“PCI 資源管理”章節中進行說明。 當出現錯誤時,probe 函數需要進行錯誤處理。在本示例中,我們采用統一的錯誤處理路徑,該路徑被放置在函數的末尾:
error:snd_card_free(card);return?err;
于每個組件都可以被正確釋放,因此在大多數情況下,只需調用一次 snd_card_free() 就足夠了。
-
4)設置驅動的 ID 和名稱字符串。
strcpy(card->driver,?"My Chip");
strcpy(card->shortname,?"My Own Chip 123");
sprintf(card->longname,?"%s at 0x%lx irq %i",card->shortname, chip->port, chip->irq);
driver 字段保存芯片的最小 ID 字符串。這個 ID 會被 alsa-lib 的配置器使用,因此應保持簡潔但唯一。即使是同一個驅動,也可以使用不同的 driver ID 來區分不同類型芯片的功能。 shortname 字段是一個簡短但更具描述性的名稱字符串,用于展示。 longname 字段包含的是將在 /proc/asound/cards 中顯示的信息。
-
5)創建其他組件,如混音器、MIDI 等 在此步驟中定義基本組件,例如 PCM、混音器(如 AC97)、MIDI(如 MPU-401)以及其他接口。如果需要定義 proc 文件,也應在這里進行。
-
6)注冊該聲卡實例
err = snd_card_register(card);
if?(err < 0)goto error;
這一部分也將在“聲卡與組件的管理”章節中進行說明。
-
7) 設置 PCI 驅動的數據,并返回 0
pci_set_drvdata(pci, card);
dev++;
return?0;
在上述步驟中,聲卡記錄被保存了下來。該指針同樣會在 remove 回調函數 和 電源管理回調函數 中使用。
Destructor(析構函數):
析構函數,即 remove 回調函數,僅需釋放聲卡實例即可。隨后,ALSA 中間層會自動釋放所有附加的組件。 通常只需調用一行代碼即可:
static void snd_mychip_remove(struct pci_dev *pci)
{snd_card_free(pci_get_drvdata(pci));
}
上述代碼的前提是:聲卡指針已通過 PCI 驅動的數據接口(即 pci_set_drvdata())保存。
Header Files:
對于上述示例,至少需要包含以下頭文件:
#include <linux/init.h>
#include <linux/pci.h>
#include <linux/slab.h>
#include <sound/core.h>
#include <sound/initval.h>
最后一個頭文件(指 <linux/moduleparam.h>)僅在源碼中定義了模塊參數時才是必須的。如果代碼被拆分為多個文件,那么沒有模塊參數定義的文件就不需要包含該頭文件。 除了這些頭文件之外:
-
如果要處理中斷,需要包含 <linux/interrupt.h>;
-
如果需要進行 I/O 操作訪問,需要包含 <linux/io.h>;
-
如果使用了 mdelay() 或 udelay() 延時函數,還需要包含 <linux/delay.h>。
ALSA 接口,例如 PCM 和控制接口(control API),則定義在其他的 <sound/xxx.h> 頭文件中。 這些頭文件必須在 <sound/core.h> 之后包含。
聲卡與組件的管理(Management of Cards and Components):
-
Card Instance
對于每一塊聲卡,必須分配一個 “card” 記錄。 “card” 記錄是聲卡的核心管理結構,它負責管理該聲卡上的所有設備(組件),例如 PCM、混音器(Mixer)、MIDI、合成器等。同時,該記錄還保存聲卡的 ID 和名稱字符串,管理與之相關的 proc 文件入口,控制電源管理狀態以及處理熱插拔斷開等情況。卡記錄中的組件列表用于在驅動銷毀時正確釋放所有資源。 如前所述,要創建一個聲卡實例,可以調用 snd_card_new() 函數:
struct snd_card *card;
int err;
err = snd_card_new(&pci->dev, index, id, module, extra_size, &card);
該函數接受六個參數:父設備指針、聲卡索引號、ID 字符串、模塊指針(通常為 THIS_MODULE)、額外數據空間的大小,以及用于返回聲卡實例的指針。 其中,extra_size 參數用于為芯片相關的私有數據分配 card->private_data 空間。需要注意的是,這塊私有數據是由 snd_card_new() 函數自動分配的。 第一個參數是 struct device 的指針,用于指定父設備。對于 PCI 設備,通常傳入 &pci->dev
-
Components
在創建好聲卡之后,你可以將各個組件(設備)附加到該聲卡實例上。在 ALSA 驅動中,每個組件由一個 struct snd_device 對象表示。組件可以是一個 PCM 實例、控制接口、Raw MIDI 接口等。每個這樣的實例對應一個組件條目。 組件可以通過 snd_device_new() 函數來創建:
snd_device_new(card, SNDRV_DEV_XXX, chip, &ops);
該函數接收以下參數:聲卡指針、設備級別(SNDRV_DEV_XXX)、數據指針以及回調函數指針(&ops)。
其中,設備級別用于定義組件的類型,并決定其注冊和注銷的順序。對于大多數組件,設備級別已在 ALSA 中預定義。對于用戶自定義的組件,可以使用 SNDRV_DEV_LOWLEVEL。
此函數本身不會分配數據空間,數據需要在調用之前手動分配,并將其指針作為參數傳入。在上述示例中,該指針(如 chip)會作為該實例的標識符使用。
對于 ALSA 中預定義的組件(如 AC97、PCM 等),它們在各自的構造函數內部會調用 snd_device_new()。組件的析構函數由回調函數中的指針指定,因此開發者無需手動調用組件的析構函數。
如果你希望創建自定義組件,需要在 ops 結構中設置 dev_free 回調指針為對應的析構函數,這樣在調用 snd_card_free() 時該組件就能自動被釋放。 接下來的示例將展示如何實現與芯片相關的私有數據結構。
-
Chip-Specific Data
與芯片相關的特定信息,例如 I/O 端口地址、資源指針或中斷號(irq number),會被存儲在芯片專用的數據結構(record)中:
struct mychip {....
};
一般來說,分配芯片記錄有兩種方式。
-
通過 snd_card_new() 分配。
如前所述,你可以將額外數據的長度作為 snd_card_new() 的第 5 個參數傳入,例如:
err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,sizeof(struct mychip), &card);
struct mychip 是芯片記錄的結構體類型。 作為返回值,分配好的記錄可以通過以下方式訪問:
struct mychip *chip = card->private_data;
使用這種方法,你無需進行兩次內存分配。該記錄會隨著聲卡實例一起被釋放。
-
Allocating an extra device.
在通過 snd_card_new() 分配聲卡實例(第 4 個參數設為 0)之后,調用 kzalloc():
struct snd_card *card;
struct mychip *chip;
err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,0, &card);
.....
chip = kzalloc(sizeof(*chip), GFP_KERNEL);
芯片記錄結構體中至少應包含一個字段用于保存聲卡指針:
struct mychip {struct snd_card *card;....
};
然后,在返回的芯片實例中設置該聲卡指針:
chip->card = card;
接下來,初始化各個字段,并使用指定的 ops 將該芯片記錄作為一個低層設備(low-level device)進行注冊:
static const struct snd_device_ops ops = {.dev_free = ? ? ? ?snd_mychip_dev_free,
};
....
snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);
snd_mychip_dev_free() 是設備的析構函數,它將調用真正的析構操作:
static int snd_mychip_dev_free(struct snd_device *device)
{return?snd_mychip_free(device->device_data);
}
其中,snd_mychip_free() 才是真正的析構函數。 這種方法的缺點是:代碼量明顯更大。 但它的優點在于:你可以通過在 snd_device_ops 中設置相關回調函數,在聲卡注冊和斷開連接時觸發自定義的回調操作。 關于聲卡的注冊與斷開連接,請參見下文的子章節。
注冊與釋放
在所有組件都分配完成后,通過調用 snd_card_register() 來注冊聲卡實例。從這一刻起,設備文件的訪問才會被啟用。
也就是說,在調用 snd_card_register() 之前,外部無法訪問這些組件,因此是安全的。
如果該函數調用失敗,應在調用 snd_card_free() 釋放聲卡資源后退出 probe 函數。
要釋放聲卡實例,只需調用 snd_card_free()。
如前所述,該調用會自動釋放所有附加的組件。
對于支持熱插拔的設備,可以使用 snd_card_free_when_closed()。 該函數將在所有設備文件被關閉后,再延遲銷毀聲卡實例。