基於 ACPI 的裝置列舉¶
ACPI 5 引入了一組新資源(UartTSerialBus、I2cSerialBus、SpiSerialBus、GpioIo 和 GpioInt),可用於列舉序列匯流排控制器後面的從屬裝置。
此外,我們開始看到整合在 SoC/晶片組中的外設僅出現在 ACPI 名稱空間中。這些通常是透過記憶體對映暫存器訪問的裝置。
為了支援這一點並儘可能重用現有驅動程式,我們決定採取以下措施:
沒有匯流排聯結器資源的裝置表示為平臺裝置。
具有聯結器資源的真實匯流排後面的裝置表示為
struct spi_device或struct i2c_client。請注意,標準 UART 不是匯流排,因此沒有 struct uart_device,儘管其中一些可能由 struct serdev_device 表示。
由於 ACPI 和裝置樹都表示裝置(及其資源)的樹,因此此實現儘可能遵循裝置樹的方式。
ACPI 實現列舉匯流排(平臺、SPI、I2C,在某些情況下是 UART)後面的裝置,建立物理裝置並將它們繫結到 ACPI 名稱空間中的 ACPI 控制代碼。
這意味著當 ACPI_HANDLE(dev) 返回非 NULL 時,裝置是從 ACPI 名稱空間列舉的。此控制代碼可用於提取其他特定於裝置的配置。下面有一個示例。
平臺匯流排支援¶
由於我們使用平臺裝置來表示未連線到任何物理匯流排的裝置,因此我們只需為裝置實現一個平臺驅動程式並新增受支援的 ACPI ID。如果同一 IP 塊用於其他非 ACPI 平臺,則驅動程式可能可以直接使用或需要一些細微更改。
為現有驅動程式新增 ACPI 支援應該非常簡單。這是最簡單的示例:
static const struct acpi_device_id mydrv_acpi_match[] = {
/* ACPI IDs here */
{ }
};
MODULE_DEVICE_TABLE(acpi, mydrv_acpi_match);
static struct platform_driver my_driver = {
...
.driver = {
.acpi_match_table = mydrv_acpi_match,
},
};
如果驅動程式需要執行更復雜的初始化,例如獲取和配置 GPIO,它可以獲取其 ACPI 控制代碼並從 ACPI 表中提取此資訊。
ACPI 裝置物件¶
通常,在使用 ACPI 作為平臺韌體和作業系統之間介面的系統中,裝置分為兩類:可以透過為特定匯流排定義的協議(例如,PCI 中的配置空間)原生髮現和列舉的裝置,而無需平臺韌體協助;以及需要平臺韌體描述才能被發現的裝置。儘管如此,對於平臺韌體已知的任何裝置,無論其屬於哪一類,都可以在 ACPI 名稱空間中存在相應的 ACPI 裝置物件,在這種情況下,Linux 核心將基於該物件為該裝置建立 struct acpi_device 物件。
這些 struct acpi_device 物件從不用於將驅動程式繫結到原生可發現裝置,因為它們由其他型別的裝置物件(例如,PCI 裝置的 struct pci_dev)表示,這些裝置物件與裝置驅動程式繫結(相應的 struct acpi_device 物件隨後用作給定裝置配置資訊的額外來源)。此外,核心 ACPI 裝置列舉程式碼為大多數借助平臺韌體發現和列舉的裝置建立 struct platform_device 物件,並且這些平臺裝置物件可以由平臺驅動程式繫結,與原生可列舉裝置的情況直接類比。因此,將驅動程式繫結到 struct acpi_device 物件在邏輯上是不一致的,因此通常是無效的,包括用於藉助平臺韌體發現的裝置的驅動程式。
歷史上,一些藉助平臺韌體列舉的裝置實現了直接繫結到 struct acpi_device 物件的 ACPI 驅動程式,但對於任何新驅動程式都不推薦這樣做。如上所述,通常會為這些裝置建立平臺裝置物件(此處不相關的少數例外),因此即使在這種情況下相應的 ACPI 裝置物件是裝置配置資訊的唯一來源,也應使用平臺驅動程式來處理它們。
對於每個具有相應 struct acpi_device 物件的裝置,ACPI_COMPANION() 宏會返回指向它的指標,因此總可以透過這種方式獲取儲存在 ACPI 裝置物件中的裝置配置資訊。因此,struct acpi_device 可以被視為核心和 ACPI 名稱空間之間介面的一部分,而其他型別的裝置物件(例如,struct pci_dev 或 struct platform_device)用於與系統其餘部分互動。
DMA 支援¶
透過 ACPI 列舉的 DMA 控制器應在系統中註冊,以提供對其資源的通用訪問。例如,希望透過通用 API 呼叫 dma_request_chan() 訪問從屬裝置的驅動程式必須在探測函式的末尾註冊自己,如下所示:
err = devm_acpi_dma_controller_register(dev, xlate_func, dw);
/* Handle the error if it's not a case of !CONFIG_ACPI */
如果需要(通常 acpi_dma_simple_xlate() 就足夠了),實現自定義 xlate 函式,將 struct acpi_dma_spec 提供的 FixedDMA 資源轉換為相應的 DMA 通道。這種情況下的程式碼片段可能如下所示:
#ifdef CONFIG_ACPI
struct filter_args {
/* Provide necessary information for the filter_func */
...
};
static bool filter_func(struct dma_chan *chan, void *param)
{
/* Choose the proper channel */
...
}
static struct dma_chan *xlate_func(struct acpi_dma_spec *dma_spec,
struct acpi_dma *adma)
{
dma_cap_mask_t cap;
struct filter_args args;
/* Prepare arguments for filter_func */
...
return dma_request_channel(cap, filter_func, &args);
}
#else
static struct dma_chan *xlate_func(struct acpi_dma_spec *dma_spec,
struct acpi_dma *adma)
{
return NULL;
}
#endif
dma_request_chan() 將為每個註冊的 DMA 控制器呼叫 xlate_func()。在 xlate 函式中,必須根據 struct acpi_dma_spec 中的資訊和 struct acpi_dma 提供的控制器屬性選擇正確的通道。
客戶端必須使用與特定 FixedDMA 資源對應的字串引數呼叫 dma_request_chan()。預設情況下,“tx”表示 FixedDMA 資源陣列的第一個條目,“rx”表示第二個條目。下表顯示了佈局:
Device (I2C0)
{
...
Method (_CRS, 0, NotSerialized)
{
Name (DBUF, ResourceTemplate ()
{
FixedDMA (0x0018, 0x0004, Width32bit, _Y48)
FixedDMA (0x0019, 0x0005, Width32bit, )
})
...
}
}
因此,在此示例中,請求行 0x0018 的 FixedDMA 是“tx”,下一個是“rx”。
在複雜情況下,客戶端不幸需要直接呼叫 acpi_dma_request_slave_chan_by_index(),因此透過其索引選擇特定的 FixedDMA 資源。
命名中斷¶
透過 ACPI 列舉的驅動程式可以在 ACPI 表中為中斷命名,這些名稱可用於在驅動程式中獲取 IRQ 號。
中斷名稱可以列在 _DSD 中作為“interrupt-names”。名稱應列為字串陣列,這些字串將對映到 ACPI 表中相應索引的 Interrupt() 資源。
下表顯示了其用法示例:
Device (DEV0) {
...
Name (_CRS, ResourceTemplate() {
...
Interrupt (ResourceConsumer, Level, ActiveHigh, Exclusive) {
0x20,
0x24
}
})
Name (_DSD, Package () {
ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"),
Package () {
Package () { "interrupt-names", Package () { "default", "alert" } },
}
...
})
}
中斷名稱“default”將對應於 Interrupt() 資源中的 0x20,而“alert”對應於 0x24。請注意,只有 Interrupt() 資源被對映,而不是 GpioInt() 或類似資源。
驅動程式可以呼叫函式 fwnode_irq_get_byname(),並以 fwnode 和中斷名稱作為引數,以獲取相應的 IRQ 號。
SPI 序列匯流排支援¶
SPI 匯流排後面的從屬裝置附帶 SpiSerialBus 資源。SPI 核心會自動提取此資源,一旦匯流排驅動程式呼叫 spi_register_master(),從屬裝置就會被列舉。
以下是 SPI 從屬裝置的 ACPI 名稱空間可能的樣子:
Device (EEP0)
{
Name (_ADR, 1)
Name (_CID, Package () {
"ATML0025",
"AT25",
})
...
Method (_CRS, 0, NotSerialized)
{
SPISerialBus(1, PolarityLow, FourWireMode, 8,
ControllerInitiated, 1000000, ClockPolarityLow,
ClockPhaseFirst, "\\_SB.PCI0.SPI1",)
}
...
SPI 裝置驅動程式只需要以與平臺裝置驅動程式類似的方式新增 ACPI ID。下面是一個將 ACPI 支援新增到 at25 SPI eeprom 驅動程式(這是針對上述 ACPI 片段的)的示例:
static const struct acpi_device_id at25_acpi_match[] = {
{ "AT25", 0 },
{ }
};
MODULE_DEVICE_TABLE(acpi, at25_acpi_match);
static struct spi_driver at25_driver = {
.driver = {
...
.acpi_match_table = at25_acpi_match,
},
};
請注意,此驅動程式實際上需要更多資訊,如 eeprom 的頁大小等。此資訊可以透過 _DSD 方法傳遞,例如:
Device (EEP0)
{
...
Name (_DSD, Package ()
{
ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"),
Package ()
{
Package () { "size", 1024 },
Package () { "pagesize", 32 },
Package () { "address-width", 16 },
}
})
}
然後 at25 SPI 驅動程式可以在 ->probe() 階段透過呼叫裝置屬性 API 來獲取此配置,例如:
err = device_property_read_u32(dev, "size", &size);
if (err)
...error handling...
err = device_property_read_u32(dev, "pagesize", &page_size);
if (err)
...error handling...
err = device_property_read_u32(dev, "address-width", &addr_width);
if (err)
...error handling...
I2C 序列匯流排支援¶
I2C 匯流排控制器後面的從屬裝置只需新增 ACPI ID,就像平臺和 SPI 驅動程式一樣。一旦介面卡註冊,I2C 核心會自動列舉控制器裝置後面的任何從屬裝置。
以下是為現有 mpu3050 輸入驅動程式新增 ACPI 支援的示例:
static const struct acpi_device_id mpu3050_acpi_match[] = {
{ "MPU3050", 0 },
{ }
};
MODULE_DEVICE_TABLE(acpi, mpu3050_acpi_match);
static struct i2c_driver mpu3050_i2c_driver = {
.driver = {
.name = "mpu3050",
.pm = &mpu3050_pm,
.of_match_table = mpu3050_of_match,
.acpi_match_table = mpu3050_acpi_match,
},
.probe = mpu3050_probe,
.remove = mpu3050_remove,
.id_table = mpu3050_ids,
};
module_i2c_driver(mpu3050_i2c_driver);
引用 PWM 裝置¶
有時裝置可能是 PWM 通道的消費者。顯然作業系統想知道是哪個。為了提供這種對映,引入了一個特殊屬性,即:
Device (DEV)
{
Name (_DSD, Package ()
{
ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"),
Package () {
Package () { "compatible", Package () { "pwm-leds" } },
Package () { "label", "alarm-led" },
Package () { "pwms",
Package () {
"\\_SB.PCI0.PWM", // <PWM device reference>
0, // <PWM index>
600000000, // <PWM period>
0, // <PWM flags>
}
}
}
})
...
}
在上述示例中,基於 PWM 的 LED 驅動程式引用了 _SB.PCI0.PWM 裝置的 PWM 通道 0,其初始週期設定為 600 毫秒(請注意,該值以納秒為單位)。
GPIO 支援¶
ACPI 5 引入了兩個新資源來描述 GPIO 連線:GpioIo 和 GpioInt。這些資源可用於將裝置使用的 GPIO 號傳遞給驅動程式。ACPI 5.1 透過 _DSD(裝置特定資料)對此進行了擴充套件,使得可以命名 GPIO 等。
例如:
Device (DEV)
{
Method (_CRS, 0, NotSerialized)
{
Name (SBUF, ResourceTemplate()
{
// Used to power on/off the device
GpioIo (Exclusive, PullNone, 0, 0, IoRestrictionOutputOnly,
"\\_SB.PCI0.GPI0", 0, ResourceConsumer) { 85 }
// Interrupt for the device
GpioInt (Edge, ActiveHigh, ExclusiveAndWake, PullNone, 0,
"\\_SB.PCI0.GPI0", 0, ResourceConsumer) { 88 }
}
Return (SBUF)
}
// ACPI 5.1 _DSD used for naming the GPIOs
Name (_DSD, Package ()
{
ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"),
Package ()
{
Package () { "power-gpios", Package () { ^DEV, 0, 0, 0 } },
Package () { "irq-gpios", Package () { ^DEV, 1, 0, 0 } },
}
})
...
}
這些 GPIO 號是控制器相關的,路徑“\_SB.PCI0.GPI0”指定了控制器的路徑。為了在 Linux 中使用這些 GPIO,我們需要將它們轉換為相應的 Linux GPIO 描述符。
有一個標準的 GPIO API 用於此目的,它記錄在 Documentation/admin-guide/gpio/ 中。
在上面的例子中,我們可以用這樣的程式碼獲取相應的兩個 GPIO 描述符:
#include <linux/gpio/consumer.h>
...
struct gpio_desc *irq_desc, *power_desc;
irq_desc = gpiod_get(dev, "irq");
if (IS_ERR(irq_desc))
/* handle error */
power_desc = gpiod_get(dev, "power");
if (IS_ERR(power_desc))
/* handle error */
/* Now we can use the GPIO descriptors */
還有這些函式的 devm_* 版本,它們在裝置釋放後會釋放描述符。
有關與 GPIO 相關的 _DSD 繫結的更多資訊,請參閱_DSD 裝置屬性使用規則。
RS-485 支援¶
ACPI _DSD(裝置特定資料)可用於描述 UART 的 RS-485 功能。
例如:
Device (DEV)
{
...
// ACPI 5.1 _DSD used for RS-485 capabilities
Name (_DSD, Package ()
{
ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"),
Package ()
{
Package () {"rs485-rts-active-low", Zero},
Package () {"rs485-rx-active-high", Zero},
Package () {"rs485-rx-during-tx", Zero},
}
})
...
MFD 裝置¶
MFD 裝置將其子設備註冊為平臺裝置。對於子裝置,需要一個 ACPI 控制代碼,它們可以使用該控制代碼來引用與它們相關的 ACPI 名稱空間部分。在 Linux MFD 子系統中,我們提供兩種方式:
子裝置共享父 ACPI 控制代碼。
MFD 單元可以指定裝置的 ACPI ID。
對於第一種情況,MFD 驅動程式不需要做任何事情。生成的子平臺裝置將將其 ACPI_COMPANION() 設定為指向父裝置。
如果 ACPI 名稱空間中有一個裝置我們可以使用 ACPI ID 或 ACPI adr 進行匹配,則單元應設定為:
static struct mfd_cell_acpi_match my_subdevice_cell_acpi_match = {
.pnpid = "XYZ0001",
.adr = 0,
};
static struct mfd_cell my_subdevice_cell = {
.name = "my_subdevice",
/* set the resources relative to the parent */
.acpi_match = &my_subdevice_cell_acpi_match,
};
然後使用 ACPI ID“XYZ0001”直接在 MFD 裝置下查詢 ACPI 裝置,如果找到,該 ACPI 伴隨裝置將繫結到生成的子平臺裝置。
裝置樹名稱空間連結裝置 ID¶
裝置樹協議使用基於“compatible”屬性的裝置標識,其值是驅動程式和驅動程式核心識別為裝置識別符號的字串或字串陣列。所有這些字串的集合可以被視為類似於 ACPI/PNP 裝置 ID 名稱空間的裝置標識名稱空間。因此,原則上,對於在裝置樹 (DT) 名稱空間中已存在標識字串的裝置,不應有必要分配新的(並且可以說冗餘的)ACPI/PNP 裝置 ID,特別是如果該 ID 僅用於指示給定裝置與另一個裝置相容,並且核心中可能已經有匹配的驅動程式。
在 ACPI 中,名為 _CID(相容 ID)的裝置識別物件用於列出給定裝置相容的裝置 ID,但這些 ID 必須屬於 ACPI 規範規定的名稱空間之一(詳情請參閱 ACPI 6.0 的第 6.1.2 節),而 DT 名稱空間不屬於其中。此外,規範強制規定,對於所有表示裝置的 ACPI 物件,必須存在 _HID 或 _ADR 標識物件(ACPI 6.0 的第 6.1 節)。對於不可列舉的匯流排型別,該物件必須是 _HID,其值也必須是規範規定的名稱空間之一的裝置 ID。
特殊的 DT 名稱空間連結裝置 ID,PRP0001,提供了一種在 ACPI 中使用現有 DT 相容裝置標識並同時滿足 ACPI 規範上述要求的方法。具體來說,如果 _HID 返回 PRP0001,ACPI 子系統將在裝置物件的 _DSD 中查詢“compatible”屬性,並使用該屬性的值來標識相應的裝置,這與原始 DT 裝置識別演算法類似。如果“compatible”屬性不存在或其值無效,ACPI 子系統將不會列舉該裝置。否則,它將自動作為平臺裝置列舉(除非裝置到其父裝置之間存在 I2C 或 SPI 連結,在這種情況下,ACPI 核心會將裝置列舉留給父裝置的驅動程式),並且“compatible”屬性值中的標識字串將與 _CID 列出的裝置 ID(如果存在)一起用於查詢裝置的驅動程式。
類似地,如果 PRP0001 存在於 _CID 返回的裝置 ID 列表中,“compatible”屬性值(如果存在且有效)列出的標識字串將用於查詢與裝置匹配的驅動程式,但在這種情況下,它們相對於 _HID 和 _CID 列出的其他裝置 ID 的相對優先順序取決於 PRP0001 在 _CID 返回包中的位置。具體來說,將首先檢查 _HID 返回的裝置 ID 以及 _CID 返回包中 PRP0001 之前的裝置 ID。在這種情況下,裝置將列舉到的匯流排型別取決於 _HID 返回的裝置 ID。
例如,以下 ACPI 示例可用於列舉 lm75 型別的 I2C 溫度感測器,並使用裝置樹名稱空間連結將其與驅動程式匹配:
Device (TMP0)
{
Name (_HID, "PRP0001")
Name (_DSD, Package () {
ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"),
Package () {
Package () { "compatible", "ti,tmp75" },
}
})
Method (_CRS, 0, Serialized)
{
Name (SBUF, ResourceTemplate ()
{
I2cSerialBusV2 (0x48, ControllerInitiated,
400000, AddressingMode7Bit,
"\\_SB.PCI0.I2C1", 0x00,
ResourceConsumer, , Exclusive,)
})
Return (SBUF)
}
}
定義 _HID 返回 PRP0001 且 _DSD 中沒有“compatible”屬性或 _CID 的裝置物件是有效的,只要其祖先之一提供了帶有有效“compatible”屬性的 _DSD。然後,此類裝置物件簡單地被視為額外的“塊”,向複合祖先裝置的驅動程式提供分層配置資訊。
然而,只有當與裝置物件關聯的 _DSD(無論是裝置物件本身的 _DSD 還是上述“複合裝置”情況中其祖先的 _DSD)返回的所有屬性都可以在 ACPI 環境中使用時,PRP0001 才能從裝置物件的 _HID 或 _CID 返回。否則,_DSD 本身被視為無效,因此其返回的“compatible”屬性是無意義的。
有關更多資訊,請參閱_DSD 裝置屬性使用規則。
PCI 層次結構表示¶
有時,瞭解 PCI 裝置在 PCI 總線上的位置,可以方便地列舉該裝置。
例如,一些系統使用直接焊在主機板上的 PCI 裝置,位於固定位置(乙太網、Wi-Fi、序列埠等)。在這種情況下,可以通過了解它們在 PCI 匯流排拓撲中的位置來引用這些 PCI 裝置。
要識別 PCI 裝置,需要完整的層次結構描述,從晶片組根埠到最終裝置,透過主機板上的所有中間橋接器/交換機。
例如,假設我們有一個帶有 PCIe 序列埠(Exar XR17V3521)的系統,該埠焊在主機板上。該 UART 晶片還包括 16 個 GPIO,我們想為這些引腳新增屬性 gpio-line-names [1]。在這種情況下,此元件的 lspci 輸出為:
07:00.0 Serial controller: Exar Corp. XR17V3521 Dual PCIe UART (rev 03)
完整的 lspci 輸出(手動縮短)為:
00:00.0 Host bridge: Intel Corp... Host Bridge (rev 0d)
...
00:13.0 PCI bridge: Intel Corp... PCI Express Port A #1 (rev fd)
00:13.1 PCI bridge: Intel Corp... PCI Express Port A #2 (rev fd)
00:13.2 PCI bridge: Intel Corp... PCI Express Port A #3 (rev fd)
00:14.0 PCI bridge: Intel Corp... PCI Express Port B #1 (rev fd)
00:14.1 PCI bridge: Intel Corp... PCI Express Port B #2 (rev fd)
...
05:00.0 PCI bridge: Pericom Semiconductor Device 2404 (rev 05)
06:01.0 PCI bridge: Pericom Semiconductor Device 2404 (rev 05)
06:02.0 PCI bridge: Pericom Semiconductor Device 2404 (rev 05)
06:03.0 PCI bridge: Pericom Semiconductor Device 2404 (rev 05)
07:00.0 Serial controller: Exar Corp. XR17V3521 Dual PCIe UART (rev 03) <-- Exar
...
匯流排拓撲為:
-[0000:00]-+-00.0
...
+-13.0-[01]----00.0
+-13.1-[02]----00.0
+-13.2-[03]--
+-14.0-[04]----00.0
+-14.1-[05-09]----00.0-[06-09]--+-01.0-[07]----00.0 <-- Exar
| +-02.0-[08]----00.0
| \-03.0-[09]--
...
\-1f.1
要描述 PCI 總線上的此 Exar 裝置,我們必須從地址為的晶片組橋接器(也稱為“根埠”)的 ACPI 名稱開始:
Bus: 0 - Device: 14 - Function: 1
要找到此資訊,需要反彙編 BIOS ACPI 表,特別是 DSDT(另請參閱[2])。
mkdir ~/tables/
cd ~/tables/
acpidump > acpidump
acpixtract -a acpidump
iasl -e ssdt?.* -d dsdt.dat
現在,在 dsdt.dsl 中,我們必須搜尋地址與 0x14(裝置)和 0x01(功能)相關的裝置。在這種情況下,我們可以找到以下裝置:
Scope (_SB.PCI0)
{
... other definitions follow ...
Device (RP02)
{
Method (_ADR, 0, NotSerialized) // _ADR: Address
{
If ((RPA2 != Zero))
{
Return (RPA2) /* \RPA2 */
}
Else
{
Return (0x00140001)
}
}
... other definitions follow ...
而 _ADR 方法 [3] 正好返回我們正在尋找的裝置/功能對。有了這些資訊,並分析上述 lspci 輸出(裝置列表和裝置樹),我們可以為 Exar PCIe UART 編寫以下 ACPI 描述,並新增其 GPIO 線名稱列表:
Scope (_SB.PCI0.RP02)
{
Device (BRG1) //Bridge
{
Name (_ADR, 0x0000)
Device (BRG2) //Bridge
{
Name (_ADR, 0x00010000)
Device (EXAR)
{
Name (_ADR, 0x0000)
Name (_DSD, Package ()
{
ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"),
Package ()
{
Package ()
{
"gpio-line-names",
Package ()
{
"mode_232",
"mode_422",
"mode_485",
"misc_1",
"misc_2",
"misc_3",
"",
"",
"aux_1",
"aux_2",
"aux_3",
}
}
}
})
}
}
}
}
位置“_SB.PCI0.RP02”是從上述 dsdt.dsl 表的調查中獲得的,而裝置名稱“BRG1”、“BRG2”和“EXAR”是透過分析 Exar UART 在 PCI 匯流排拓撲中的位置建立的。