基于 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() 访问从设备的驱动程序必须在 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 伴随设备将绑定到生成的子平台设备。
设备树命名空间链接设备 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 规范的上述要求的同时使用现有 DT 兼容设备识别的方法。也就是说,如果 _HID 返回 PRP0001,则 ACPI 子系统将在设备对象的 _DSD 中查找 “compatible” 属性,并将该属性的值用于识别相应的设备,类似于原始 DT 设备识别算法。如果 “compatible” 属性不存在或其值无效,则 ACPI 子系统将不会枚举该设备。否则,它将自动枚举为平台设备(除非存在从设备到其父级的 I2C 或 SPI 链接,在这种情况下,ACPI 核心会将设备枚举留给父级的驱动程序),并且 “compatible” 属性值中的识别字符串将与 _CID 列出的设备 ID 一起用于查找设备的驱动程序(如果存在)。
类似地,如果 _CID 返回的设备 ID 列表中存在 PRP0001,则 “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 环境中使用时,才能从设备对象的 _HID 或 _CID 返回 PRP0001。否则,_DSD 本身被视为无效,因此它返回的 “compatible” 属性毫无意义。
有关更多信息,请参阅_DSD 设备属性使用规则。
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 总线拓扑中的位置创建的。