基于 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 总线拓扑中的位置创建的。