基於 ACPI 的裝置列舉

ACPI 5 引入了一組新資源(UartTSerialBus、I2cSerialBus、SpiSerialBus、GpioIo 和 GpioInt),可用於列舉序列匯流排控制器後面的從屬裝置。

此外,我們開始看到整合在 SoC/晶片組中的外設僅出現在 ACPI 名稱空間中。這些通常是透過記憶體對映暫存器訪問的裝置。

為了支援這一點並儘可能重用現有驅動程式,我們決定採取以下措施:

  • 沒有匯流排聯結器資源的裝置表示為平臺裝置。

  • 具有聯結器資源的真實匯流排後面的裝置表示為 struct spi_devicestruct 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 伴隨裝置將繫結到生成的子平臺裝置。

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 匯流排拓撲中的位置建立的。

參考文獻