资讯动态

从设备树到ACPI:_DSD与PRP0001实现硬件信息模拟

发布时间:2026/10/15 17:57:17 来源:尧图企业网站定制
1. “ACPI模拟DTS”到底是在解决什么问题我最早听到“用ACPI来模拟DTS”这个说法时第一反应是觉得方向反了。毕竟在很多嵌入式工程师的印象里设备树Device TreeDTS才是描述硬件的主流方式ACPI是PC和服务器上的那一套。但在实际项目中我确实遇到过必须把DTS里的信息“翻译”成ACPI规范体系的情况硬件平台固件只能输出设备树结构或者外围设备的验证资料全是DTS风格描述的可目标操作系统或者某个大型框架偏偏只认ACPI启动。这时候你能做的不是在操作系统里强行塞一个DTB而是让ACPI“看起来”像是描述了一份等效的DTS——这就是标题里“模拟”两字的核心含义。需要先说明一点ACPI和DTS不是两种可以随意互换的语言它们是两套面向不同使用模型的硬件描述方案。设备树更像一张完整的硬件拓扑清单从CPU、内存到每个I2C外设、GPIO引脚、中断触发方式全都写在dts/dtsi文件里内核起来后照着清单去“注册设备”。而ACPI除了静态描述设备还包含一套运行时方法论操作系统通过AML字节码动态查询设备状态、管理电源和热事件。所以在ACPI体系下“模拟DTS”本质上不是做文本翻译而是要把DTS里的静态信息重新组织成ACPI表结构并通过特殊机制让内核驱动感知到这些信息。1.1 什么场景下会逼着你走这条路我实际踩过这类需求起因是一个边缘计算设备底层硬件是ARM架构调试用的固件只负责把DTB传给操作系统但用户方指定的某个检测系统只接受ACPI描述。我当时的思路很简单既然操作系统那边不可能为了一个外设去改内核配置那我就在固件和操作系统之间做适配层手工为这个外设写一份ACPI表把所有原本在DTS里描述的设备信息都放到ACPI表的设备节点里。这样操作系统起来之后它能通过ACPI枚举到设备。类似的场景还有几类。比如在新平台验证阶段某个外设模块在x86参考平台上用ACPI工作正常但同样的模块到了ARM平台上只有DTS描述而你想快速复用原来的ACPI验证脚本又比如开发板跑的是某个只支持ACPI启动的嵌入式系统而外设厂商给的驱动模板却是基于设备树的需要让驱动在ACPI环境下也能识别到外设再比如虚拟机环境中宿主机只提供虚拟ACPI固件但你要把一个原本为设备树编写的虚拟设备“塞”进虚拟机里让客户机看到。这些需求都可以归纳为让ACPI体系下的设备枚举结果等价于一份预定义的DTS描述。1.2 ACPI与DTS的能力分工差异设备树描述硬件有一个很大的特点——“属性即事实”。节点下写一个compatible demo,light-sensor内核的驱动匹配就直接拿字符串去比对reg 0x38就能确定I2C从机地址gpios gpio0 3 GPIO_ACTIVE_HIGH就能确定引脚和有效电平。这套机制简单、透明适合启动阶段快速完成设备注册。ACPI不同它被设计成一种“系统固件与操作系统之间约定好的协议”。操作系统启动时通过RSDP找到DSDTDSDT里有设备对象、资源模板、控制方法比如_CRS返回设备占用的IO/IRQ资源_PS0控制设备上电_PRW描述唤醒能力。ACPI表不仅能描述设备在哪个总线地址上还能描述设备在不同电源状态下的行为。用ACPI模拟DTS最难的不是把设备名写进表里而是把DTS中那些“扁平化”的属性正确地放入ACPI的各种方法和对象里让内核的通用设备模型能消费它们。想要在ACPI和DTS之间搭桥关键是找到一个已经被内核承认的“翻译层”。幸运的是Linux内核确实提供了这样一条路径——_DSDDevice Specific Data方法配合PRP0001兼容ID可以让ACPI设备在不同的角度“伪装”成一个设备树节点。这个机制我在后面的章节里会逐步拆开讲。2. _DSD机制让ACPI节点携带设备树风格属性的关键通道ACPI规范里有个概念叫“设备特定数据”通过_DSD方法返回一段键值对集合。它最早用来给设备附加一些厂商自定义信息后来的ACPI修订版本为它定义了标准的UUIDdaffd814-6eba-4d8c-8a91-bc9bbf4aa301这套UUID也被Linux内核的ACPI属性子系统直接识别。也就是说只要ACPI设备节点的_DSD方法里以这个UUID开头返回一个包包里的每一项是“键-值”对Linux内核就会把这组键值对解析成设备属性和读设备树属性走同一个接口。这里有个有意思的设计_DSD里专门认可了名为compatible的键。设备树驱动的匹配逻辑是基于compatible字符串的而ACPI设备默认是用_HID里的PNP ID匹配驱动。PRP0001这个东西就是内核定义的一个特殊PNP ID意思大约等价于“我没法用HID告诉你我是谁请去查我的_DSD里的compatible”。因此当你把一个ACPI设备的_HID写成PRP0001又在该设备节点的_DSD里给出compatible属性后Linux内核的设备匹配层会把它当成一个兼容设备树描述的设备来处理。也就是说“用ACPI模拟DTS”在Linux内核里的最小落地形式不是要你去写一套完整的设备树解析器而是建立一条传输管线ACPI表里的_DSD承担了“设备树属性包”的角色PRP0001承担了“虚拟设备树节点兼容ID”的角色而内核里统一的property API则让驱动无感读取这些属性。2.1 哪些设备树属性可以装进_DSD_DSD能装的属性类型有明确限制它的值只能是整数、字符串、字节数组、引用的包。这覆盖了设备树属性里的compatible、reg、label、status、gpio-*、clock-frequency等大多数“静态描述类”信息。常见做法是把设备树节点里的纯数据属性原封不动地搬到_DSD里的键值包中比如Package(){compatible, demo,light-sensor}, Package(){reg, 0x38}, Package(){label, board-front-light}, Package(){poll-interval-ms, 500},对于GPIO属性_DSD里还可以放置形如Package(){reset-gpios, Package(){GPIO控制器引用, 引脚号, 有效电平, 其他标志}}这样的结构内核的ACPI GPIO子系统会专门解析这一类键。中断信息则通常不走_DSD而是放在设备的_CRS资源模板里用Interrupt或GpioInt描述。这一点和设备树不太一样设备树里interrupts 17 IRQ_TYPE_EDGE_RISING是直接写在节点属性里的ACPI则倾向于把中断当作一个总线资源交给_CRS来描述。2.2 多节点层级关系要用Hierarchical _DSD单层_DSD能解决的问题比较有限。如果一个DTS节点下挂了子节点比如一个I2C总线控制器下面挂了多个外设你会需要在ACPI表里把这些外设分别定义成_SB.I2C0下的子设备对象。这是普通ACPI作用域就能描述的层级不需要特殊扩展。但如果你的场景是某个设备节点下面挂着抽象子设备比如GPIO扩展器、子功能模块就需要ACPI 6.0引入的Hierarchical Data Extension。它使用另一个UUID支持在_DSD内部构建带引用关系的子节点树但这一部分在Linux内核里的支持仍不如基础_DSD成熟实战中我个人更倾向于把子设备直接展开成独立的ACPI设备对象而不是强行套用层级树结构。因此我想给一个最实用的建议模拟DTS时优先把每个设备树子节点映射为ACPI作用域下的独立Device对象节点间的父子关系用作用域嵌套表达而不是尝试用一个巨大的_DSD包把所有信息压缩进一棵树里。这样既符合ACPI的习惯也让后续调试时看到lspci、/sys/bus/platform/devices下的设备更直观。3. 手把手把一份DTS节点改写成ACPI SSDT纸上谈兵没有意义我直接用一个具体例子来演示完整流程。假设有这么一份DTS描述外部I2C总线上挂了一个环境光传感器同时有一条GPIO复位线、一条上升沿中断线i2c0 { light-sensor38 { compatible demo,light-sensor; reg 0x38; reset-gpios gpio0 3 GPIO_ACTIVE_HIGH; interrupt-parent gpio0; interrupts 17 IRQ_TYPE_EDGE_RISING; }; };现在需要在一个纯ACPI启动的系统里让内核能枚举到同样的设备、匹配同样的驱动、正确申请GPIO中断和复位引脚。3.1 先做信息映射表改写的第一步不是直接写ASL而是列一张映射表。DTS节点对应一个ACPI Device对象寄存器地址对应ACPI的_CRS里的I2C串行总线描述compatible和reset-gpios放到_DSD中断信息放在_CRS的资源模板里。关于作用域i2c0对应ACPI namespace里的某个I2C控制器例如_SB.I2C0这部分通常已经由固件里的DSDT定义了。我们只需要在同一个作用域下新增子设备。3.2 写出对应的SSDT源码下面是一份可以编译的ASL源码。它使用外部引用的方式把新设备挂到已有的_SB.I2C0总线上DefinitionBlock(SSDT-DEMO.aml, SSDT, 2, DEMO, LIGHT-SNS, 0x00000001) { External(_SB.I2C0, DeviceObj) Scope(_SB.I2C0) { Device(DEMO) { Name(_HID, PRP0001) Name(_UID, 0x01) Name(_CRS, ResourceTemplate() { I2cSerialBus( 0x38, // 对应DTS里的 reg 0x38 ControllerInitiated, 100000, // 100kHz 总线速率 AddressingMode7Bit, \\_SB.I2C0, 0, ResourceConsumer, ) Interrupt(ResourceConsumer, Edge, ActiveHigh, Exclusive) { 17 // 对应 DTS 里的 interrupts 17 IRQ_TYPE_EDGE_RISING } }) Name(_DSD, Package() { ToUUID(daffd814-6eba-4d8c-8a91-bc9bbf4aa301), Package() { Package() { compatible, demo,light-sensor }, Package() { reg, 0x38 }, Package() { reset-gpios, Package() { \_SB.GP00, 3, 0, 0 } }, } }) } } }这段ASL里值得解释几个关键点。_HID写成PRP0001是促使内核进入“兼容设备树匹配”模式的核心_CRS里I2cSerialBus的第一个参数是从机地址ControllerInitiated指控制器发起通信这是I2C从设备的典型写法后面的Interrupt描述一定要和设备树里IRQ_TYPE_EDGE_RISING对应我用Edge加ActiveHigh表达上升沿触发。最后_DSD里的reset-gpios引用了namespace中的\_SB.GP00这个GPIO控制器引脚号是3后面的0,0分别表示有效电平和GPIO标志这里对应DTS中的GPIO_ACTIVE_HIGH。3.3 用iasl编译并加载写好的ASL需要用iasl编译成AML字节码。标准流程是iasl -tc SSDT-DEMO.asl如果语法正确会生成SSDT-DEMO.aml。-tc的作用是检查并生成AML文件表格类型写在DefinitionBlock第二个参数里类型是SSDT签名会自动写在表头。编译出错时iasl给的提示一般已经很明确常见的错误是括号配对、字符串没引号、引用的外部对象没有先声明External。把AML加载进系统的路径有几种。调试阶段最简单的方式是把AML放进能够被固件加载的目录比如通过某种initrd机制在启动时覆盖加载SSDT正式环境则一般会把SSDT编译进固件镜像里。无论哪种方式我建议先用iasl -d反编译一下最终加载的ACPI表确认表内容和设计一致。4. 内核侧如何读取这份“被模拟”的DTS信息写好了SSDT只是第一步更重要的是理解驱动侧是怎么无感知拿到这些数据的。Linux内核在设备模型之上抽象了一层叫fwnode的接口ACPI节点和设备树节点都能转换成fwnode。传统设备树驱动里大量使用的of_property_read_u32、of_get_named_gpio在较新的内核里已经可以替换成device_property_read_u32、devm_gpiod_get这套新接口同时服务设备树和ACPI这也正是“ACPI模拟DTS”能够成立的大前提。4.1 同一个属性API两套数据来源举个例子设备树平台下驱动可以这样读取环境光传感器的轮询间隔int interval; ret device_property_read_u32(dev, poll-interval-ms, interval);如果设备是ACPI描述的只要_DSD里有对应的poll-interval-ms键这行代码依然能读出一个值。内核的device_property_*系列函数会判断这个设备的fwnode类型底层分别走到of_device_property或者acpi_device_property驱动代码不用关心自己的设备到底是挂在DTB还是ACPI表里。我在一个模拟项目中正是利用这一点把原本为设备树编写的驱动直接放到ACPI平台下运行改动量比想象中小很多。但有一个陷阱需要提前说设备属性API能替代的是“设备树属性的读取方式”替代不了“设备树的IO资源管理”。比如of_iomap这个函数在ACPI平台下面对应的是从_CRS中解析内存资源并映射of_irq_get在ACPI平台下面对应解析_CRS中的Interrupt资源。如果驱动代码里大量直接调用of_*系列函数那么即使属性读出来了之后的IO获取依然会失败。所以认真改造驱动时尽量全面切换到device_property_*和devm_platform_get_resource这一类统一API。4.2 GPIO和中断在ACPI下的注意点GPIO部分设备树用reset-gpios属性描述复位引脚驱动侧通常调用devm_gpiod_get(dev, reset, GPIOD_OUT_LOW)。内核在ACPI平台下会去查找_DSD里的reset-gpios键拿到GPIO控制器和引脚号再调用对应的GPIO控制器驱动。从驱动代码角度看两者完全一致但GPIO的有效电平处理有细微区别设备树属性里第三个数字若是1表示低电平有效ACPI的_DSD里第三个数字若是1表示有效电平是低。我上面例子中写了0对应高电平有效所以和DTS里GPIO_ACTIVE_HIGH是吻合的。中断部分的差异更明显。设备树的interrupts属性直接写在节点里而且常见的是直接引用中断控制器ACPI则是把Interrupt或者GpioInt放进_CRS资源模板。内核在枚举设备时会从_CRS中提取中断号并建立对应的irq domain映射。因此如果外设信息里既有I2C从机地址又有中断_CRS里必须同时描述两种资源顺序通常不影响解析但类型一定要写对。4.3 注册顺序相关的一点观察在设备树里子设备的注册顺序很大程度上依赖节点在dts文件里的排列顺序以及status属性。ACPI体系下设备对象的注册顺序由namespace枚举顺序决定如果你有多个挂在同一总线下的传感器设备它们的_UID值会影响枚举顺序。建议在_DSD里显式给出reg属性同时让_CRS的I2C地址与它保持一致这样即使注册顺序不同驱动最终也能通过地址正确区分设备。5. 调试过程中的真实踩坑记录这部分我想分享几个实际遇到过的问题由浅入深展开希望能帮你少走弯路。毕竟是“模拟”机制一旦某处没有对上现象往往很隐蔽。5.1 表加载失败SSDT根本没进namespace第一次试验时我把编译好的SSDT放进了启动加载路径结果dmesg里没有看到任何设备注册的日志查看/sys/firmware/acpi/tables也没找到对应的表。后来排查发现是表的header签名和文件名不一致导致的加载不识别。标准做法是确认DefinitionBlock第二个参数为SSDT生成的文件后缀用.aml。另外如果固件里已经存在同名SSDT表新的表可能因为版本号或OEM ID冲突被跳过。我最后把OEM ID改成一个独特的字符串并且把表版本号递增问题就消失了。5.2 设备枚举了但驱动匹配不上表成功加载后/sys/bus/platform/devices下能看到新设备但驱动没有被绑定。通常原因是_DSD里的compatible字符串和驱动模块里的of_match_table不一致。很多驱动用的是MODULE_DEVICE_TABLE(of, ...)这意味着匹配要走到of_match_table才能成功。这里有个技巧如果驱动还没有支持OF匹配光靠PRP0001compatible也未必能绑定需要驱动在platform_driver.driver.of_match_table里添加对应条目。如果驱动没有这个字段就只能在设备属性之外通过acpi_device_id增加一个匹配ID但那样驱动代码就要改比纯compatible方案重。5.3 GPIO属性解析失败引用路径写错我最开始写的reset-gpios是这样引用的Package() { reset-gpios, Package() { GP00, 3, 0, 0 } }没有加\_SB.GP00的完整路径限定结果驱动一直拿不到GPIO。后来反编译固件DSDT发现GPIO控制器实际在_SB.GP00作用域下必须在_DSD里用完整路径引用并且先加一行External(_SB.GP00, DeviceObj)。这个问题在项目里排查了很久核心原因是依赖层级关系不清晰。建议每次写GPIO引用前先用iasl -d确认控制器节点在namespace中的完整路径。5.4 中断触发类型不一致导致设备无法工作环境光传感器在DTS里定义的是上升沿中断我在_CRS里写的也是Edge ActiveHigh但实际运行中驱动依然收到的是低电平不断触发。后来发现是我修改ASL时把Shared和Exclusive选错了很多人不注意这个字段。中断共享模式下驱动和固件对触发类型的解释会受到另一个设备影响。建议排查时用/proc/interrupts看中断号对应的触发方式如果显示Level而预期是Edge那就是_CRS没改对或者驱动在中断申请时强制改了类型。5.5 一个实用建议善用acpidump和反编译链遇到现象不符合预期时我最常用的做法是先通过系统的ACPI导出工具把完整表导出来比如acpidump得到的二进制文件再用acpixtract按表拆分最后用iasl -d反编译。你会发现最终看到的内容未必和你当初写的一样因为固件的重新编译过程可能改动了一些命名、连接方式。反编译结果才是内核真正看到的内容以此为基准去对照问题远比自己盲猜AST省时间。6. 哪些场景不该硬用ACPI模拟DTS写到这里我必须泼一点冷水。ACPI模拟DTS不是万能的有些场景下用它是给自己挖坑。首先设备树里的电源管理描述比如regulator、power-domain、clock这些关联关系在ACPI下并没有一一对应的简单表达方式。比如设备树可以通过clocks属性把设备挂到某个时钟树上而ACPI的时钟管理更倾向于通过_DSD配合特定厂商驱动或_PR0电源资源实现。复杂的时钟树改写到ACPI体系里工作量和验证成本会成倍上升。其次大量使用pinctrl的设备树节点也很难模拟。设备树里pinctrl-0 pinctrl_i2c0_default这种写法到了ACPI体系里要么依靠固件在_CRS里配置好引脚复用要么就只能通过专门驱动绕过。如果硬件外设非常依赖复杂的引脚复用关系我一般会建议改用真正的设备树加载而不是做ACPI模拟。再说一个方向相反的思路如果系统本身支持设备树那直接加载DTS/DTO肯定是最自然的方案不必舍近求远去写ACPI表。ACPI模拟DTS的价值主要体现在“启动协议必须使用ACPI”的固定环境比如某些仅支持ACPI的固件平台、虚拟机镜像、合规启动环境。在这些限制下用_DSDPRP0001做数据层翻译是性价比最高的选择。最后如果模拟的设备太多整个namespace会变得非常庞大维护起来也很累。我建议把经常出问题的外围设备单独拆成一张SSDT便于在不改动主DSDT的情况下独立更新和调试。这样每次只需要替换一个表文件风险范围最小回滚也容易。我自己实际用了这套方案之后最大的体会是ACPI模拟DTS本质上是一种“信息重构”而不是“代码模拟”。你不需要让操作系统觉得存在一份真正的设备树二进制文件只要让内核对每个设备节点拿到的属性、资源、中断信息发生期待后续的匹配和驱动加载自然就顺了。把_DSD的键值包当成设备树属性的“快递箱”把PRP0001当成“到货标签”整个思路就清晰了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑