1. 先理清背景为什么会有“ACPI模拟DTS”这种奇怪需求在ARM64平台上摸爬滚打久了你早晚会遇到这种需求某套固件是按UEFIACPI标准做的但你要在上面跑的一个驱动、一个中间件、甚至整个BSP只认设备树DTS。两边给的硬件描述完全不同系统起不来驱动不认识外设全靠手工去查寄存器抄地址效率低到崩溃。这种局面并不冷门。ARM64服务器领域ACPI是标准化启动路径而很多SoC外设IP原本都在嵌入式设备树生态里开发Linux内核里的驱动大量用of_match_table、of_property_read_xx这类接口读取硬件信息。到了ACPI环境这些驱动要么跑不起来要么只能看到残缺资源。于是“ACPI来模拟DTS”就成了一个绕不过去的话题。先明确一个概念模拟DTS不是要把DTS文件直接塞给内核。真正要做的是让ACPI表给出的硬件信息在内核里以类似设备树的方式被解析、被匹配、被绑定让那些只写了DT支持的驱动能正常工作。内核里已经有一套官方铺垫好的机制关键是你要会用它。我在实际项目里踩过一遍完整的坑走了不少弯路这里把整个思路、映射逻辑、可落地的操作流程都整理出来。无论你是在固件侧工作还是在跑OS适配这篇文章应该都能帮你省下几天排查时间。1.1 ACPI和DTS到底在描述什么要理解“模拟”先得看这两套东西干了什么活。设备树DTS本质是一个描述硬件拓扑的树形结构文本经过dtc编译成二进制DTB由引导程序传给内核。内核解析后建立device_node树驱动通过of_xxx接口去读节点属性。DTS的表达非常自由节点名、属性名、属性结构都可以根据硬件实际情况灵活定义配合各种binding文档社区生态非常成熟。ACPI则是另一套逻辑。它以固定格式的表FADT、DSDT、SSDT、MADT等存放在固件里OS在启动时解析ACPI命名空间得到的是被定义好的设备对象、资源描述、电源管理方法。ACPI更强调规范和平台无关尤其在中断、电源管理这些领域有非常严格的资源模型。两套东西的差异说白了就是设备树像一份手写的硬件说明文档格式灵活谁都能加页ACPI像一份标准化录入的数据库字段结构固定校验严格。你要把一份手写文档翻译成数据库条目自然不能照抄文本得对着各自的语义逐个对应。1.2 典型触发场景迁移服务器平台时遇到的两套世界观最常见的场景之一某公司要做ARM64服务器固件按照行业标准从DTS切换到ACPI。内核里绝大多数驱动都是社区贡献的很多外设驱动作者在开发时就只适配了设备树。固件切换之后网卡、DMA控制器、I2C控制器一个个在ACPI路径下消失甚至有时候连设备名都找不到。另一个场景在虚拟化和模拟器方向也常见。某开发者在QEMU里验证一个新的ACPI固件特性但手头的客户机内核模块是从设备树时代一直维护下来的只认DT节点。你不想为测试重写驱动更不想整个平台切回DT启动方式那就只能让ACPI环境尽量“伪装”成DTS环境。还有一类更隐蔽的需求部分BSP开发工具链、诊断脚本、自动化测试框架默认读取/proc/device-tree或者/sys/firmware/devicetree目录。ACPI启动的内核根本没有这些目录。要让老工具继续工作也需要在ACPI命名空间里提供等价的设备信息并让内核生成对应的of_node。我自己的实际体会是这类需求本质上不是“把ACPI改成DTS”而是“让内核在ACPI启动路径下仍然能创建设备树风格的节点并完成驱动匹配”。顺着这个思路去找解决方案路其实很清晰。1.3 这件事的本质与边界ACPI模拟DTS核心落在“桥接”两个字。Linux内核里早就提供了PRP0001机制这是官方钦定的用于把设备树兼容性引入ACPI的方案。当你把一个ACPI设备的_HID设为“PRP0001”并在_DSD里面提供compatible属性内核就会为这个设备创建of_node让设备树驱动的匹配逻辑能够生效。所以我们真正要做的翻译工作主要有三块设备身份_HID _DSD里的compatible 对应DTS里的compatible资源描述_CRS 对应 DTS里的reg、interrupts自定义属性_DSD键值对 对应 DTS里各种自定义属性把这三块处理好绝大多数设备树外设驱动都能在ACPI环境下重新活过来。这个方案也是我在项目里最终落地选用的后面会讲完整实操。2. 原理先行ACPI要模拟DTS核心靠哪三板斧实操之前一定要先搞懂映射原理不然写出来的ASL全是在碰运气。这一类问题排错太痛苦了因为错误不一定在编译时报出来更多是运行期静默失效你连到底是表结构坏了还是驱动匹配失败都分不清。2.1 设备识别PRP0001与compatible之间的桥设备树里驱动靠compatible属性来找设备。比如“vendor,xdma”这个字符串在一个有of_match_table的驱动里只要匹配上probe就会执行。ACPI体系里设备身份靠_HID来定。但_HID要求注册过的PNP ID或者ACPI ID一般的厂商自定义设备并没有这类ID。PRP0001就是专门解决这个问题的通用HID它告诉内核这个ACPI设备的真实身份要看_DSD里的compatible属性。内核扫描到_HID为“PRP0001”的设备时会在_DSD里查找“compatible”这个键。如果存在就以该字符串创建一个虚拟的of_node并把所有_DSD里的属性挂到这个of_node上。后续设备驱动用of_device_is_compatible、of_property_read_xx这些接口时实际上读的就是这个虚拟of_node的信息。这里有个容易踩的坑compatible字符串必须要符合设备树bindng里的大小写要求并且格式一般是“厂商名,设备型号”全部小写。写成“XDMA”之类大写的内核比对时区分大小写直接匹配不上。2.2 资源映射_CRS如何翻译reg和interruptsDTS里描述外设资源比ACPI自由得多。reg描述地址范围interrupts描述中断dmas描述DMA通道clocks描述时钟。ACPI里标准的资源描述都在_CRS方法返回值里主要是Memory32Fixed和Interrupt这些资源宏。地址翻译最简单。DTS里的reg如果只有一个地址段两边几乎能逐一对应。你要注意_DMA和cacheable这些标志位如果硬件是正常内存映射设备用Memory32Fixed (ReadWrite, start, length)基本没啥问题。中断翻译是坑最多的地方。ACPI里的Interrupt (ResourceConsumer, Level, ActiveHigh, Exclusive) 这些标志和DTS里的interrupts属性并不是完全一致的映射。DTS通常是三个字段中断域、中断号、触发类型其中触发类型用IRQ_TYPE_LEVEL_HIGH这种宏。ACPI资源模板里的Level/Edge、ActiveHigh/ActiveLow是分开的两个字段顺序和含义要格外小心。比如你知道设备树里写的IRQ_TYPE_LEVEL_HIGH是电平触发、高有效那到了ACPI里就是Level, ActiveHigh。如果写反成ActiveLow驱动虽然能注册但中断触发时内核不认表现就是设备“看起来正常但一干活就卡死”。2.3 DT特有属性_DSD的完整用法设备树驱动的很多行为依赖自定义属性比如时钟名、DMA通道数、读写模式、内部寄存器配置。这类信息在ACPI里没有标准资源描述方式只能靠_DSD来承载。_DSD的全称是Device Specific Data格式要求很严格。外层必须包含特定UUID然后跟一个包里面是若干键值对子包。一个标准示例Name (_DSD, Package () { ToUUID (daffd814-6eba-4d8c-8a91-bc9bbf4aa301), Package () { Package () { compatible, vendor,xdma }, Package () { dma-channels, 8 }, Package () { status, okay }, } })注意下面几点都是我自己踩过的坑UUID是固定的“daffd814-6eba-4d8c-8a91-bc9bbf4aa301”错一个字符IASL不一定报错但运行时内核不会解析出任何属性。键名与DTS的属性名要保持完全一致包括大小写。驱动里读的是dma-channels你写成DMA-Channels读出来就是空。字符串值外面要加双引号整数直接写。布尔属性在DTS里只需要节点存在在_DSD里对应整数值1或0。同一个_DSD包里可以同时放compatible和普通属性内核会一并处理。如果驱动的自定义属性特别多_DSD包会非常大但别偷懒省略因为很多驱动在probe里会逐个检查属性缺一个可能整个设备初始化失败。2.4 三种实现路线对比与选型“ACPI模拟DTS”严格说不止一种实现路线。我在前期调研时列出过三个方案最终选了第三个实现方案做法优势劣势引导阶段生成DTB在bootloader里扫描ACPI表动态构建DTB传给内核物理层面完全等价于DTS环境偏离ACPI标准启动路径在服务器平台基本不可行用户空间转换工具用脚本解析ACPI表输出DTS文本适合离线分析、文档归档不能解决运行期驱动绑定问题ACPI PRP0001机制在ACPI表中用_DSD描述compatible和自定义属性让内核创建of_node符合ACPI规范内核原生支持改动集中在表侧需要深入理解ACPI ASL和内核匹配机制门槛稍高从工程落地角度第三种方案是最可靠也最可持续的这也是本文后面实操部分采用的方式。3. 实操全流程把一个DTS外设“翻译”成ACPI表这一节我用一个虚构的DMA控制器外设来演示完整过程。硬件描述全部是模拟的但流程和我实际项目里跑通的完全一致你只要把设备名、寄存器地址、中断号替换成自己的就可以直接参考。3.1 准备环境与工具首先准备一套基础环境。最方便的是在QEMU的ARM64 virt机器上搭建既可以用UEFI固件模拟ACPI启动也能快速验证表加载。工具方面必须装好ACPICA工具集核心是iasl编译器用来把ASL源文件编译成AML二进制表。在常见发行版里可以直接装# Debian/Ubuntu apt install acpica-tools # 验证 iasl -v acpidump -s如果能正常打印版本号和ACPI表状态摘要环境就OK了。还要确认内核编译时打开了ACPI和OF相关配置至少需要CONFIG_ACPI、CONFIG_OF、CONFIG_ACPI_CONFIGFS这几个。内核开发者可以直接在内核配置菜单里搜PRP0001相关的选项确认。3.2 从原始DTS提取设备信息假设原来设备树里有这样一个节点xdma0: dma40400000 { compatible vendor,xdma; reg 0x0 0x40400000 0x0 0x10000; interrupts GIC_SPI 44 IRQ_TYPE_LEVEL_HIGH; dma-channels 8; };这个节点描述的信息很明确设备在0x40400000长度0x10000使用SPI中断44号高电平触发DMA通道数为8。把这些信息提取出来之后要决定它们各自映射到ACPI的哪个位置DTS信息值ACPI映射目标compatiblevendor,xdma_HIDPRP0001 _DSD里的compatiblereg0x40400000, 0x10000_CRS里的Memory32FixedinterruptsSPI 44 LEVEL_HIGH_CRS里的Interrupt资源dma-channels8_DSD键值对这里有一个重要的预处理确认中断号。GIC_SPI在ACPI里通常直接用MADT提供的全局中断号表示很多平台SPI编号和ACPI全局中断号是一致的但还是要对照MADT表确认不要想当然。N私有外设中断的情况更特殊通常不建议直接用PRP0001模拟尤其在多核场景下容易出问题。3.3 编写并编译SSDT表下面就是核心文件一个完整的SSDT表把刚才的DMA设备搬进ACPI命名空间。为了演示方便我把这个设备挂在根总线_SB下面。DefinitionBlock (ssdt-xdma.aml, SSDT, 2, MYOEM, XDMA, 0x00000001) { Scope (\_SB) { Device (XDMA) { Name (_HID, PRP0001) Name (_UID, 0x0) Name (_CRS, ResourceTemplate () { Memory32Fixed (ReadWrite, 0x40400000, 0x10000) Interrupt (ResourceConsumer, Level, ActiveHigh, Exclusive) { 44 } }) Name (_DSD, Package () { ToUUID (daffd814-6eba-4d8c-8a91-bc9bbf4aa301), Package () { Package () { compatible, vendor,xdma }, Package () { dma-channels, 8 }, } }) } } }编译过程非常简单iasl -sa ssdt-xdma.asl如果ASL语法有错误iasl会明确报错并给出行号。我建议先解决所有编译错误再往系统里加载。有一次我因为Device节点名XDMA与系统里已有的某个设备对象重名编译没报错但加载后被忽略排查了很久所以节点名尽量选独特一些。3.4 把SSDT放进固件/加载到系统如果环境支持固件内置SSDT直接把这个aml文件加到固件的ACPI表区域即可。但开发调试阶段频繁重编固件显然不现实用Linux内核的configfs接口动态加载表更高效。前提是内核开启CONFIG_ACPI_CONFIGFS加载步骤如下mount -t configfs configfs /sys/kernel/config mkdir /sys/kernel/config/acpi/table/xdma cat ssdt-xdma.aml /sys/kernel/config/acpi/table/xdma/aml这个目录创建并写入aml文件后内核会立即把表挂到ACPI命名空间触发设备扫描。你可以用dmesg观察有没有“ACPI: Added SSDT”之类的日志。如果configfs不可用还有另一个办法直接把aml放到initramfs里通过内核启动参数acpi_override_table或自定义开机脚本加载。但该方法需要对应机制支持不如configfs通用。3.5 内核驱动适配与绑定验证表加载之后内核里应该会出现一个新的ACPI设备。首先做一轮快速验证# 查看ACPI设备 ls /sys/bus/acpi/devices/ dmesg | grep -i PRP0001 # 查看生成节点 ls /sys/bus/platform/devices/ cat /sys/bus/platform/devices/*xdma*/modalias正常情况下能看到类似“PRP0001:00”的ACPI设备并且可能注册成platform设备。关键是确认设备绑定了哪个驱动看dmesg里有没有驱动probe成功的输出。如果驱动没有自动绑定先手动验证匹配关系。大多数DT驱动的of_match_table是直接可见的在/sys/bus/platform/devices/对应设备目录下看driver_override或者用modprobe强制加载但这只是排查手段不建议作为长期方案。一个比较典型的成功日志如下ACPI: Added ACPI_DEVICE XDMA ACPI: bus type PRP0001 registered platform PRP0001:00: Firmware registered XDMA xdma 40400000.dma: new DMA controller registered这里第一行是ACPI扫描设备中间的platform节点绑定最后是驱动probe完成后打印的日志。走到最后一步说明“ACPI模拟DTS”这条路已经通畅了。4. 常见问题与排查技巧实录这个方案说起来原理不复杂但实际运行时出问题的点非常多。我根据自己排障经历把遇到过的典型问题整理成速查表并附上排查思路。4.1 表加载了设备却不probe这是最容易碰见的头号问题。表加载成功ACPI设备也出现了但驱动死活不probe。首先要确认一件事设备到底有没有被识别为platform设备以及驱动有没有机会匹配。依次检查dmesg里有没有“ACPI: Added ACPI_DEVICE”日志没有则说明表虽然加载但命名空间没被穷举检查当前内核ACPI扫描是否默认跳过该设备。/sys/bus/acpi/devices/下是否存在PRP0001:xx如果存在看uevent里的MODALIAS。查看设备目录下的driver_override如果之前手动设置过会永久覆盖匹配删除它。确认驱动模块加载了并且CONFIG_ANDROID之类不是这里重点是确认驱动的of_match_table导出时module alias里包含vendor,xdma。另外一个非常坑的点驱动用of_device_is_compatible去检查设备而of_node虽然创建了但ACPI生成的of_node的compatible属性不是_DSD里的compatible字符串而是“PRP0001:00”这种。这种情况少见但一旦发生光是读of_node属性就会失败。遇到这种问题我建议用perf或者ftrace直接去看驱动probe里调用了哪些of_xxx接口理性分析哪一个字节导致匹配失败不要盲目改ASL。检查项命令/方法预期结果表加载dmesgACPI: Added SSDT设备枚举ls /sys/bus/acpi/devices/PRP0001:xx设备存在性ls /sys/bus/platform/devices/xdma设备节点驱动绑定dmesgprobe成功of_nodecat /sys/kernel/debug/of_tree有xdma节点4.2 中断、DMA资源对不上设备probe成功但运行不稳定、中断不触发大概率是_CRS里的资源描述语义不对。DTS里中断号是相对GIC中断控制器的逻辑编号而ACPI里的中断号要与固件MADT表保持一致。虽然很多平台SPI编号一致但在涉及到中断重映射、虚拟化透传的情况下两者可能偏差很大。所以排查时一定先把MADT表dump出来用中断控制器的全局中断号做借口多一步验证。触发方式同样容易错。DTS里IRQ_TYPE_LEVEL_HIGH对应ACPI的Level, ActiveHighIRQ_TYPE_EDGE_RISING对应Edge, ActiveHigh。这里要特别小心字段的顺序ACPI的Interrupt宏里先是Level/Edge然后是ActiveHigh/ActiveLow和我见过的一些错误示例是反过来的。如果你用设备树方式查过irq可以对比/sys/irq相关目录或者用/proc/interrupts验证实际申请的中断号。如果实际申请的中断号和预期相差一个固定数很可能是MADT里GIC重映射做了偏移。4.3 时钟与GPIO层面掉链子很多外设不只是地址和中断还有时钟、GPIO、复位等依赖。ACPI对这些资源的标准描述远没有设备树那么灵活尤其时钟框架ACPI里没有标准的clk provider描述接口。现实中依赖简单的时钟通常在驱动初始化时通过clk_get_sys查一个固定的clkdev条目来获取这种情况下只要在板级初始化里注册clkdev就能解决。如果驱动用of_clk_get从设备树节点里获取多个时钟那在ACPI虚拟of_node下就完全失效了。这种情况下我通常建议在驱动里加一个ACPI时钟获取的fallback分支或者用clkdev别名绕过去当然了这要协调驱动维护方的接受度。GPIO同样是重灾区。设备树用gpio-controller、gpio-cells等描述ACPI这边标准API是_CRS里的GpioIo资源。如果驱动还是调of_xxx接口拿GPIO建议同样在驱动里加ACPI分支或者用一个小的补丁层把of请求转换为ACPI GPIO接口。4.4 几个容易踩的隐藏坑下面几个问题不一定是每个人都会遇到但一旦遇到特别浪费时间我在这里统一记录一下。第一_DSD里的compatible一定不能带大括号、空格这些特殊字符。ASL字符串里可以写但内核解析出来后会去做字符串比较一个多余空格都不行。建议直接照抄DTS里的原始compatible字符串。第二设备树里status属性如果写“disabled”ACPI_DSD里也要对应处理否则设备树驱动会照常probe这个本应禁用的硬件。在PRP0001机制下status属性会映射到of_node的status驱动如果检查这个属性需要保持一致。第三One跟1的区别。ASL里很多整数常量在Table体里是字节、字、双字区分但_DSD包里如果写成整数值内核统一按整数解析IASL有时候会把某些值优化成紧凑格式不会影响功能。反而真正危险的是用Package包嵌套时括号漏掉导致_DSD结构解析失败。第四加载SSDT时OEM ID不要留空。留空时有些固件/内核会自动填默认值但跨平台兼容性很差尤其在双固件环境下经常出现表被忽略的情况。给自己起一个简短的OEM ID和表名例如“MYOEM”“XDMA”能省不少麻烦。5. 写在最后几条实在的心得做完这个“ACPI模拟DTS”的适配我最大的感触是这类问题不是纯技术问题而是一个沟通问题。固件侧、内核侧、驱动侧、平台验证侧各自有各自的语义模型你要做的不是强迫某一方改变而是把差异翻译清楚在合适的层面做桥接。如果以后再遇到这种“描述风格迁移”的活我一般会先画一张映射表明确每个DTS属性跑到ACPI哪里去而不是急着写ASL。映射表画清楚了后面所有工作都只是翻译。流程跑起来后验证一定不能只看设备枚举出来必须看到驱动probe成功、中断正常、数据流畅通才算真正闭环。最后分享一个实用小工具习惯调试阶段务必保留所有版本的SSDT源文件加注释标明每次改了什么、为什么改。ASL表排错最怕的就是怀疑自己写错了但想不起改过什么版本管理能帮你快速定位。这个方法陪我熬过了不少个定位到深夜的问题现在也推荐给你。