基于 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() 访问从设备的驱动程序必须在 probe 函数的末尾注册它自身,如下所示:

err = devm_acpi_dma_controller_register(dev, xlate_func, dw);
/* Handle the error if it's not a case of !CONFIG_ACPI */

并在需要时实现自定义的 xlate 函数(通常 acpi_dma_simple_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 和中断名称作为参数调用函数 fwnode_irq_get_byname() 来获取相应的 IRQ 编号。

SPI 串行总线支持

SPI 总线后面的从设备附加了 SpiSerialBus 资源。一旦总线驱动程序调用 spi_register_master(),SPI 核心会自动提取此资源,并且枚举从设备。

以下是 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。下面是一个示例,我们向 at25 SPI eeprom 驱动程序添加 ACPI 支持(这是针对上述 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 总线控制器后面的从设备只需要像平台和 SPI 驱动程序一样添加 ACPI ID。一旦注册了适配器,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 绑定的更多信息,请参阅与 GPIO 相关的 _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 设备可能会很有用。

例如,某些系统使用直接焊接在主板上的固定位置的 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 设备,我们必须从地址为

Bus: 0 - Device: 14 - Function: 1

的芯片组桥接器(也称为 “根端口”)的 ACPI 名称开始。要查找此信息,需要反汇编 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 总线拓扑中的位置创建的。

参考