基于ACPI的设备枚举
ACPI 5引入了一组新的资源(UartTSerialBus, I2cSerialBus, SpiSerialBus, GpioIo and GpioInt),可用于枚举串行总线控制器后面的从设备。
此外,我们开始看到集成在SoC/芯片组中的外设只出现在ACPI命名空间中。这些通常是通过内存映射寄存器访问的设备。
为了支持这一点,并尽可能重用现有的驱动程序,我们决定做以下事情:
- 没有总线连接器资源的设备表示为平台设备。
- 存在连接器资源的实际总线后面的设备表示为struct spi_device或struct i2c_client。请注意,标准UARTs不是总线,因此没有struct uart_device,尽管其中一些可以用struct serdev_device表示。
由于ACPI和设备树都表示设备树(及其资源),因此此实现尽可能地遵循设备树的方式。
ACPI实现枚举总线(platform、SPI、I2C和某些情况下的UART)后面的设备,创建物理设备并将它们绑定到ACPI命名空间中的ACPI句柄。
这意味着当ACPI_HANDLE(dev)返回非null时,设备从ACPI命名空间枚举。此句柄可用于提取其他特定于设备的配置。下面是一个例子。
Platform总线支持
由于我们使用平台设备来表示没有连接到任何物理总线的设备,我们只需要为设备实现一个平台驱动程序并添加支持的ACPI ID。如果在其他非ACPI平台上使用相同的IP-block,则驱动程序可能可以开箱即用或需要进行一些小更改。
为现有驱动程序添加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 */
并根据需要实现自定义 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 提供的控制器属性来选择适当的通道。
Clients调用 dma_request_chan()必须传入与特定 FixedDMA 资源对应的字符串参数。默认情况下,“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”。
在稳健情况下,不幸的是,client需要直接调用 acpi_dma_request_slave_chan_by_index(),因此根据其索引选择特定的 FixedDMA 资源。
Named Interrupts
通过 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 核心会自动提取该资源,一旦总线驱动程序调用 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。下面是我们向 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 配套设备绑定到生成的子平台设备。
Device Tree 命名空间链接 device ID
设备树协议使用基于“compatible”属性的设备标识,该属性的值是一个字符串或一个字符串数组,驱动程序和驱动程序核心将其识别为设备标识符。所有这些字符串的集合可以被视为类似于 ACPI/PNP device ID 命名空间的设备标识命名空间。因此,原则上,对于在设备树 (DT) 命名空间中具有现有标识字符串的设备,无需分配新的(可以说是冗余的)ACPI/PNP device ID,尤其是如果该 ID 仅用于指示给定设备与另一个设备兼容,并且可能在内核中已经有一个匹配的驱动程序。
在 ACPI 中,称为 _CID(Compatible ID)的设备标识对象用于列出与给定设备兼容的设备的 ID,但这些 ID 必须属于 ACPI 规范规定的命名空间之一(有关详细信息,请参阅 ACPI 6.0 第 6.1.2 节),而 DT 命名空间不是其中之一。此外,规范要求所有代表设备的 ACPI 对象都必须存在 _HID 或 _ADR 标识对象(ACPI 6.0 第 6.1 节)。对于不可枚举的总线类型,该对象必须是 _HID,并且其值也必须是来自规范规定的命名空间之一的 device ID。
特殊的 DT 命名空间链接设备 ID PRP0001 提供了一种使用 ACPI 中现有的 DT-compatible 设备标识的方法,同时满足 ACPI 规范中的上述要求。也就是说,如果 _HID 返回 PRP0001,ACPI 子系统将在设备对象的 _DSD 中查找“compatible”属性,并使用该属性的值来识别相应的设备,类似于原始 DT 设备识别算法。如果“compatible”属性不存在或其值无效,则 ACPI 子系统不会枚举该设备。否则,它将被自动枚举为platform设备(除非存在从设备到其父设备的 I2C 或 SPI 链接,在这种情况下 ACPI 核心会将设备枚举留给父设备的驱动程序),并且“compatible”属性值中的标识字符串将用于查找设备的驱动程序以及 _CID 列出的 device ID(如果存在)。
类似地,如果 PRP0001 存在于 _CID 返回的 device ID 列表中,则将使用“compatible”属性值(如果存在且有效)列出的标识字符串来查找与设备匹配的驱动程序,但在这种情况下,它们相对于 _HID 和 _CID 列出的其他 device ID 的相对优先级取决于 PRP0001 在 _CID 返回包中的位置。具体而言,将首先检查 _HID 返回的 device ID 和 _CID 返回包中之前的 PRP0001。同样,在这种情况下,设备将被枚举到的总线类型取决于 _HID 返回的 device 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) } }
只要其祖先之一提供具有有效“compatible”属性的 _DSD,则定义具有返回 PRP0001 的 _HID 且 _DSD 或 _CID 中没有“compatible”属性的设备对象是有效的。然后,此类设备对象将简单地被视为向复合祖先设备的驱动程序提供分层配置信息的附加“块”。
但是,只有当与设备对象关联的 _DSD(设备对象本身的 _DSD 或上述“复合设备”情况下其祖先的 _DSD)所返回的所有属性都可以在 ACPI 环境中使用时,PRP0001 才能从设备对象的 _HID 或 _CID 返回。否则,_DSD 本身将被视为无效,因此其返回的“compatible”属性毫无意义。
有关更多信息,请参阅 _DSD 设备属性使用规则。
PCI 层次表示
有时枚举 PCI 设备并了解其在 PCI 总线上的位置可能会很有用。
例如,某些系统使用直接焊接在主板上的固定位置的 PCI 设备(以太网、Wi-Fi、串行端口等)。在这种情况下,可以参考这些 PCI 设备,了解它们在 PCI 总线拓扑上的位置。
要识别 PCI 设备,需要完整的层次描述,从芯片组root port到最终设备,通过主板的所有中间桥/交换机。
例如,假设我们有一个带有 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 设备,我们必须从芯片组桥(也称为“root port”)的 ACPI 名称开始,地址为:
Bus: 0 - Device: 14 - Function: 1
为了找到这些信息,需要反汇编 BIOS ACPI 表,特别是 DSDT:
mkdir ~/tables/ cd ~/tables/ acpidump > acpidump acpixtract -a acpidump iasl -e ssdt?.* -d dsdt.dat
现在,在 dsdt.dsl 中,我们必须搜索地址与 0x14(device)和 0x01(function)相关的设备。在本例中,我们可以找到以下设备:
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 方法恰好返回我们正在寻找的设备/功能对。有了这些信息并分析上述 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 总线拓扑中的位置而创建的。
参考资料
[3]
ACPI 规范,版本 6.3 - 第 6.1.1 段 _ADR 地址)https://uefi.org/sites/default/files/resources/ACPI_6_3_May16.pdf,引用于 2020-11-18
本文来自博客园,作者:闹闹爸爸,转载请注明原文链接:https://www.cnblogs.com/wanglouxiaozi/p/18722623
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)