☰
PetaLinux设备树单独编译实战:从DTG机制到dtc命令
2026/10/5 12:00:48 网站建设 项目流程

先聊一个很实际的场景:很多刚开始用 PetaLinux 的开发者,会遇到一个特别难受的问题——我只是改了一个 GPIO 的引脚定义,或者给某个 I2C 外设加了个节点,结果执行petalinux-build要把整个工程的 Linux 镜像、根文件系统全部重新编一遍。运气好等十几分钟,运气不好预编译缓存没命中,直接给你编译半个钟头起步,最后出来一个image.ub,复制到 SD 卡里往板子上一插,发现除了设备树改了,其他啥也没变。

所以后来我基本养成了一个习惯:改设备树,绝不轻易走完整编译流程,单独编译设备树才是正确的打开方式。

这篇文章我就围绕 PetaLinux 工程里的设备树来聊,从工程里的文件体系、配置逻辑、单独编译的实操命令,到常见坑点的排查思路,尽量把这块一次性讲透。适合手里有 Zynq、Versal、MPSoC 这类赛灵思/AMD 平台开发板、正准备用 PetaLinux 做驱动适配和系统裁剪的开发者。不管你是在做 AD9361 这类射频子卡的移植,还是搭 SPI 显示屏、I2C 传感器之类的普通外设,流程是通用的。

1. 内容整体设计与思路拆解

1.1 PetaLinux 工程里的设备树到底从哪来

很多人第一次打开 PetaLinux 工程,会先在工程目录下搜.dts文件,搜来搜去只找到一堆零散的.dtsi,就是找不到一个完整的、可以拿去编译的 dts。这是因为 PetaLinux 的设备树生成机制和传统嵌入式 Linux 工程不太一样。传统做法是内核源码的arch/arm/boot/dts或arch/arm64/boot/dts目录下放一份完整的 dts,编译内核时顺带把 dtb 编出来。PetaLinux 则是在构建时通过一套叫 device-tree-generator(简称 DTG)的机制动态生成的,从一个叫 hardware description 的输入文件开始,结合一系列 dtsi 片段,最终拼接成一个完整的 dts。

那个输入文件就是硬件平台描述,传统环境是 hdf 文件,在新版本工具链中是 xsa 文件,由 Vivado 导出的硬件配置信息。DTG 会从 xsa 里读出 CPU 类型、内存大小、外设地址、中断号、引脚配置等硬件信息,然后自动生成基础设备树,比如pl.dtsi就是描述可编程逻辑部分的外设节点,pcw.dtsi描述 PS 端被配置的外设。再加上 SoC 本身的公共 dtsi,例如 zynqmp.dtsi、versal.dtsi 这类,最终拼出一个可以实际启动 Linux 的 dts。

这套机制的好处是硬件改动后设备树能够跟着自动更新,比如你在 Vivado 里给 PL 加了一个 AXI UART 或者 DMA,PetaLinux rebuild 时不需要手写设备树节点,DTG 会自动把相关节点生成出来。缺点是它让很多原来可以直接改 dts 的人找不到下手的入口,于是容易出现一个情况:在工程某个目录下找到一个与板级相关的 dts,辛辛苦苦改了半天,一编译发现改动被覆盖掉了,因为真正的生成流程把你改的文件忽略了。

1.2 为什么要单独编译设备树,而不是重新走完整构建

单独编译设备树绝不仅仅是为了省时间,更重要的是为了调试效率。设备树在嵌入式 Linux 启动流程里属于最早期生效的东西,U-Boot 在启动内核前会先加载它,外设驱动在 probe 阶段就要去节点里读取寄存器地址、中断号、GPIO 编号等参数。如果设备树有毛病,后续的内核日志压根不会出现这个设备,或者 probe 直接失败,看起来就像驱动没编进去一样,很容易让人误判方向。

开发阶段的做法是反复修改设备树、烧写到板子上、看启动日志验证,这个循环越快越好。走完整petalinux-build的问题是不仅慢,而且会因为其他编译错误阻塞设备树的验证。单独编译设备树,可以做到两分钟内完成一次“改配置-编译 dtb-打包-验证”的循环。另外一个场景是换用一个不同的 DTB 来适配不同硬件版本,比如同一个 PetaLinux 工程要支持两套电路板,只改了 FPGA 引脚和外设选型,那直接构建出针对不同板的 dtb 分别加载就行,根本不需要维护两个完整的镜像。

我实际测试过,一个规模中等的 ZynqMP 工程,完整构建一次在机器配置一般的情况下大概要 25 到 40 分钟,而单独编译设备树通常不超过 30 秒,编译完再手动把新的 dtb 复制到 SD 卡或者通过 tftp 加载,整个迭代时间缩短到了原来的十分之一还不到。

1.3 单独编译方案选型:PetaLinux 内置工具链还是纯 DTC

做单独编译设备树,有两种路线可选。第一种是继续使用 PetaLinux 工程的环境,只是不执行完整构建,而是只构建 device-tree 这个组件;第二种是完全脱离 PetaLinux 环境,直接从临时目录中提取出最终的 dts 源文件,在本地安装的 dtc 工具下直接编译成 dtb。

两条路线各有适用场景。如果只是在现有工程内做增量修改,比如改引脚复用、添加外设节点,推荐第一种,因为 PetaLinux 工程内部会维护好各 dtsi 片段之间的 include 关系,你不用操心依赖文件缺失;如果你需要把设备树文件分享给别人,或者在一个没有安装 PetaLinux 的编译服务器上单独构建 dtb,那就用第二种,因为 dtc 是独立的小工具,安装部署非常轻量,只是需要先把源文件提取完整,否则容易出现 include 文件找不到的报错。在后面的实操部分,我会把两条路线的具体命令和操作注意事项分别讲清楚。

2. 核心细节解析与实操要点

2.1 设备树在 PetaLinux 构建体系中的位置

在 PetaLinux 工程中,设备树材料的存储位置其实分散在多个目录里。常规情况下可以关注这几个位置:

  • 工程级的project-spec/meta-user/recipes-bsp/device-tree/files/,这里有用户自定义的 system-user.dtsi,是修改设备树最主要的入口之一。
  • 构建输出目录build/或者./build/下由 DTG 生成完整 dts 和 dtb 的临时位置。
  • components/目录里会有来自 BSP 的公共 dtsi 文件。

理解这套目录结构对后续的单独编译很重要。system-user.dtsi 从名字上就能看出定位,它是给用户留出的覆盖层,PetaLinux 在组装最终 dts 时会把它的内容作为默认最后引入的部分,所以你在其中写的节点覆盖规则会对前面的通用定义生效。

比如你要修改某个 UART 的时钟频率、给某个 SPI 控制器增加子节点,或者禁用一个默认使能的节点,最合理的做法就是打开 system-user.dtsi,在里面写覆盖逻辑。而不是直接去改那些从 BSP 继承来的 dtsi,否则下次重新配置工程或者升级 BSP 时改动就丢了。

2.2 设备树节点修改的核心机制:覆盖与追加

对习惯直接写 dts 的开发者来说,PetaLinux 这种组装式的构建方式意味着需要习惯一种思维:不要试图去篡改源头文件,而是通过追加片段来改变最终结果。

覆盖机制基于设备树的标签引用。假如公共 dtsi 里定义了一个节点uart1: serial@ff010000,你想给它追加属性,传统的做法是进到uart1节点内部去增加内容,但在 dtsi 覆盖场景里,你只需要在 system-user.dtsi 里通过引用&uart1来追加或修改:

&uart1 { clock-frequency = <100000000>; status = "okay"; };

这样写既不破坏源文件,又能保证 DTG 生成最终 dts 时合并逻辑正确。合并规则是:同一个节点,后解析到的属性值覆盖先解析到的,后追加的子节点保留下来。所以 system-user.dtsi 因为解析顺序靠后,天然就有最高的覆写优先级。

还有一种场景是禁用板级默认打开的外设,例如某些 SoC 的默认 dtsi 里把多个 UART 都设置了status = "okay",但实际板子上只引出了其中一路,另外一路的引脚被复用作 GPIO 了。这时候就可以在 system-user.dtsi 里把冗余的外设状态改为disabled:

&uart2 { status = "disabled"; };

很多在调试过程中遇到的现象,比如外设中断风暴、引脚冲突、GPIO 申请失败,追到根上往往就是设备树里没有正确禁用未使用的外设。

2.3 理解 DTC 编译过程:从 dts 到 dtb 的细节

单独编译设备树,用户最终得到的是一个二进制的 dtb,可中间转换过程值得了解,因为很多报错信息就是从工具链里带出来的。

DTC 编译的输入通常是扩展名为 dts 的源文件,它支持 C 语言风格的预处理指令,比如#include、#define。所以常见的 dts 开头会包含一个类似这样的部分:

/dts-v1/; #include "dt-bindings/gpio/gpio.h" #include "dt-bindings/interrupt-controller/irq.h" #include "system-top.dtsi"

这个文件经过预处理器展开之后,才真正进入 DTC 的语法解析阶段。预处理器会去解析所有 include 的路径,如果找不到头文件或者依赖的 dtsi,直接就是一个致命的预处理错误。所以手动用 dtc 去编译一个从 PetaLinux 里提取出来的 dts 时,最常遇到的问题就是头文件路径不完整,编译马上失败。

DTC 本身的编译过程主要做几件事:语法检查、节点树构建、属性值合并,以及生成二进制的扁平设备树结构。这个过程中如果出现多个节点定义了相同的 unit-address,或者标签重复,就会报语法错误或警告,严重时会拒绝生成 dtb。

在调试阶段,我建议编译后顺手做一次反解析验证,把编译好的 dtb 再拆回 dts 的格式,确认关键节点、关键属性都正确生效了,避免了“改了之后根本没有编进最终文件”这类问题。

3. 实操过程与核心环节实现

3.1 在 PetaLinux 工程中修改设备树的标准流程

完整流程大致分成四步:准备硬件描述、在用户层添加设备树修改、构建设备树组件、验证生成的 dtb。

硬件描述这一块,通常是在 Vivado 中完成的,这一步导出 xsa 文件后,PetaLinux 工程通过petalinux-config --get-hw-description=<xsa目录>把硬件信息导入。注意,这一步最好在做任何设备树修改之前执行,因为重新导入硬件描述会触发 DTG 重新生成与硬件相关的 dtsi,后续对 system-user.dtsi 的修改是基于新硬件信息的。

导入硬件描述之后,打开 system-user.dtsi 进行修改:

$ cd <petalinux-project> $ vi project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi

举个例子,在实际项目中常会用到通过 SPI 外接 ADC 或显示屏。假设 SoC 的 SPI0 控制器需要挂一个 SPI 从设备,那么就要在 system-user.dtsi 中为 SPI0 增加子节点。这个操作方式非常典型,可以用作入门模板:

/include/ "system-conf.dtsi" / { model = "My Custom Board"; }; &spi0 { status = "okay"; spidev@0 { compatible = "rohm,dh2228fv"; reg = <0>; spi-max-frequency = <1000000>; }; };

这里用compatible字段表示这个从设备的驱动匹配名称,reg = <0>表示挂在 SPI 控制器的第 0 个片选上,spi-max-frequency设置了通讯时钟频率上限。这个写法基本覆盖了大多数 SPI 外设的接入需求,也是热词里经常被搜索的 spidev 设备树配置的核心套路。

修改完成后,并不需要直接去构建完整镜像,而是先单独构建 device-tree 组件。这属于 PetaLinux 工程标准操作中最常用的简化和提速技巧之一:

$ petalinux-build -c device-tree

执行完成之后,可以在输出目录里找到生成的 dtb,通常位于工程的images/linux/目录下,名字类似system.dtb。如果后续还要打包 boot 镜像,再执行完整的petalinux-build也不迟,开发迭代过程中完全没必要每次改动都全量 rebuild。

3.2 单独编译设备树的两种实战方法

接下来的实战内容,是前面讲的两种方案之间更详细的落地操作。首先要明确一点:petalinux-build -c device-tree只是 PetaLinux 工程内部单独构建某一个组件,最终还是会通过 DTG 机制重新生成 dts 再编译 dtb。这是方案 A。

如果要完全绕开 PetaLinux 构建系统来单独编译,那就需要先拿到一个完整的 dts 源文件。这一步可以这样操作:先执行一次petalinux-build -c device-tree,然后在 DTG 工作目录下找到实时生成的完整 dts,不同版本 PetaLinux 存放位置稍有差别,典型的是在build/目录下按配方路径搜索,例如find build -name "*.dts"就能定位。把需要的那份 dts 复制出来,同时把 dts 中用 include 引用的各级 dtsi 的相对路径也一并拷贝到同一个目录结构中去,最保险的方式是用cp -r把整个与设备树相关的目录结构一起保留。

之后在独立环境中用 dtc 编译即可:

$ dtc -@ -I dts -O dtb -o myboard.dtb myboard.dts

这里-@参数比较重要,它让生成的 dtb 保留符号表信息。如果这个 dtb 后续要在 U-Boot 里被修改或者被覆盖层加载,缺少符号表会带来很多麻烦。我自己在还没有编写独立编译环境时踩过这个坑,没有加-@生成的 dtb 在启动时提示了关于 symbols 的 mkimage 相关警告,后来加上就好了。

另外要提醒一个非常容易被忽略的细节:从 PetaLinux 里提取出来的 dts 可能引用了来自内核源码的头文件,比如dt-bindings/路径下的gpio.h、clk.h、pinctrl.h。这些头文件定义了大量宏,比如GPIO_ACTIVE_LOW、IRQ_TYPE_LEVEL_LOW等。单独环境编译时如果缺了它们,要么预处理器报错,要么宏展开失败。解决办法是确保 include 路径里包含完整的内核头文件目录,或者通过-I参数指定搜索目录:

$ dtc -@ -I dts -O dtb -o myboard.dtb -I src -i <kernel-src>/include myboard.dts

3.3 生成 boot.bin、boot.scr 与 image.ub 时如何处理单独编译好的 dtb

前面已经说过了,开发阶段可以直接把单独编译好的 dtb 喂给 U-Boot 或通过 SD 卡手动加载。但如果你的目标是要生成完整的启动文件 image.ub、boot.scr,那就需要稍微了解一点打包流程。

image.ub 本质上是由 FIT 打包器生成的镜像文件,其中可以容纳内核、设备树、根文件系统等多个组成部分。PetaLinux 工程在编译完整镜像时会把自动生成的设备树打包进去。如果你单独编译了一个自定义 dtb,想要替换掉镜像中的设备树,主要有两条路。

第一条路是修改 PetaLinux 的打包配置,让它使用你指定的 dtb。具体做法是在project-spec/meta-user/conf/petalinuxbsp.conf或相关配置文件中,通过变量指定设备树文件路径,覆盖 DTG 默认输出。配置完成后重新petalinux-build并打包 image.ub,这是比较正式的工程化做法。

第二条路是开发阶段图省事,把单独编译出的 dtb 文件手动复制到 SD 卡指定分区,然后修改 U-Boot 环境变量去加载这个自定义 dtb,比如 U-Boot 里常用的 fatload 命令:

fatload mmc 0:1 0x1000000 system.dtb

这种方法在调试阶段非常好用,因为不需要重新打包镜像、不需要制作整张 SD 卡,只需替换一个文件。等所有设备树改动稳定后,再正式走第一条路把 dtb 固化进 image.ub,避免开发和正式发布两套环境不一致。

3.4 实战记录:给一个 OLED 显示屏适配设备树

为了把前面的流程串起来,分享一个我最近给 SSD1306 OLED 屏适配设备树的真实操作记录。这种屏现在很常见,基本都是 I2C 接口,给 Linux 系统添加支持时需要 I2C 控制器节点下注册一个 display 子节点。

第一步,在 system-user.dtsi 中添加对应节点:

&i2c1 { status = "okay"; clock-frequency = <100000>; ssd1306: oled@3c { compatible = "solomon,ssd1306fb-i2c"; reg = <0x3c>; width = <128>; height = <64>; pages = <8>; }; };

这里reg = <0x3c>是设备在 I2C 总线上的地址,根据屏幕硬件实际地址填写;compatible要和内核驱动里的 of_match_table 对应。如果驱动还没有合入内核,那这一步做完驱动还是 probe 不了,需要在petalinux-config的内核配置中打开对应的驱动选项,再重新编译一次内核镜像,这个问题后面还会再提到。

第二步,单独编译设备树:

$ petalinux-build -c device-tree

第三步,把生成的 dtb 拷到 SD 卡启动分区,拿 U-Boot 手动加载。板子起来后,先看/sys/bus/i2c/devices/下有没有新的设备目录,再用dmesg看驱动 probe 日志:

# dmesg | grep ssd1306

如果一切正常,屏幕上会出现 Linux 启动 logo 或者自定义的 framebuffer 内容;如果没反应,按经验先查三个最可能的地方:I2C 地址是否填写正确、compatible 是否与驱动匹配、I2C 控制器本身是否已经被 PS 端引脚复用释放。在 DDR 调试场景下还可以用i2cdetect -y 1检查总线上是否有设备应答,这个命令是验证硬件链路最快的方式。

4. 常见问题与排查技巧实录

4.1 AD9361 设备树从一个 PetaLinux 工程移植到另一个工程

很多做无线电相关项目的开发者都遇到过把 AD9361 设备树从旧工程迁移到新 PetaLinux 工程的问题。这属于设备树移植场景中最容易出问题的案例之一。

AD9361 的设备树通常不是简单一个节点,而是一整套在&spi或&axi下的层次结构,还涉及 FPGA 侧的 AXI 接口映射、时钟配置、GPIO 复位控制、DMA、中断等十几个关联节点,直接从旧工程的 system-user.dtsi 复制粘贴到新工程里,大概率是不能直接跑的。

移植时最需要注意的点是:新工程的硬件描述要与原工程一致或至少兼容。比如原工程在 Vivado 中给 AD9361 分配的 AXI 地址空间是0x99000000,而新工程如果 PL 侧地址不同,那设备树里的reg属性必须同步修改。我曾经犯过一个错误,只复制了 dts 片段,忽略了reg地址映射,结果板子在加载驱动时访问了错误地址,导致系统锁定。

另外一个常见坑是设备树里的时钟名称不匹配。AD9361 驱动会通过时钟框架去获取ad9361_ext_refclk这类时钟,新工程如果换用了不同的时钟方案,原来写死的时钟节点引用就会解析失败。排查这种问题要看内核日志里有没有类似clk_get failed的记录,然后回头检查移植的设备树节点中的clocks和clock-names属性。

安全高效的移植步骤一般是:

  • 先在新工程内生成一份属于自身硬件的基础 dts,确认能够正常启动。
  • 再将旧工程中与 AD9361 相关的节点逐个拷贝到新工程的 system-user.dtsi。
  • 对比两份工程 Vivado 地址映射表,手动修正 reg 和 interrupts。
  • 编译后先不接 AD9361 模块,仅确认节点在/sys/firmware/devicetree/base/下可见。
  • 确认无误后再接上射频板卡验证功能。

4.2 在编译设备树时遇到“multiple nodes”或“label already exists”类错误

这一类报错属于 DTC 编译阶段最典型的错误,通常在启动日志或者构建日志中能看到类似这样的描述:

Error: /dts-v1/: multiple nodes have the same unit name and address

也就是说在组装 dts 时,有两个节点使用相同名称和相同地址,比如两个serial@ff010000,或者重复包含同一个 dtsi 片段导致节点定义重复出现。出现这个情况,往往是因为 system-user.dtsi 里硬编码定义了一个完整节点,同时 DTG 生成的 pl.dtsi 里也包含相同地址的节点。

这种问题的根源,一句话来说就是节点覆盖逻辑没走对。正确做法是只覆盖属性,而不要去把整个节点重新写一遍。想通过&device_node引用已有节点并只修改其中部分字段,而不是再写一个全新的同地址节点。如果你发现无论如何都会冲突,检查一下是不是在 system-user.dtsi 里重复 include 了/include/文件,或者 meta-user 层的 bbappend 文件里多次把相同文件列入了源码列表。

排查思路建议优先用grep在工程中搜索重复定义的位置:

$ grep -rn "serial@ff010000" project-spec/ build/

看完输出就会发现冲突的源头在哪一层,然后决定是删除自定义覆盖还是调整 DTG 生成逻辑。

4.3 修改了设备树之后系统没有变化的原因排查

有很多人遇到过:明明改了 system-user.dtsi,重新petalinux-build -c device-tree也成功了,但把新 dtb 烧进去之后发现系统行为一点没变,之前配的 GPIO 没生效,串口设置还是老样子,感觉像改了个寂寞。

这种“改而不生效”的情况,按经验排查顺序是这样的:

  • 确认加载的 dtb 确实是你刚编译出的那个 dtb。特别是使用 SD 卡启动时,可能 U-Boot 实际读取的是另一个分区的同名文件,或者文件名大小写不一致,导致加载到了旧文件。
  • 确认 PetaLinux 在打包时是否把这个 dtb 重新生成了。如果你走的是完整petalinux-build,DTG 会重新组装 dts,假如你是直接改了 DTG 输出目录下的原始 dts 而没有改 system-user.dtsi,那下一次构建时改动就被覆盖了。
  • 用反编译验证最终 dtb 的内容。执行dtc -I dtb -O dts -o /tmp/check.dts system.dtb,打开文件搜索你新增的节点或属性。只要这一步能搜到,说明 dtb 本身是对的,问题在加载链路;搜不到,说明修改没有被编进最终产物。
  • 检查 U-Boot 环境变量里是否有对 dtb 地址或者名称的硬编码,例如在 boot.scr 指定了从固定偏移加载 dtb,那么即使 SD 卡里复制了新文件也可能不会被使用。

这招针对设备树调试非常管用,几乎可以定位 90% 的“改了没反应”问题。

4.4 常见错误问题速查

问题现象可能原因检查方法
编译时提示找不到 dtsi 文件include 路径不完整检查 dts 头部 include 路径,确认-I参数覆盖了所有依赖
多个节点 unit-address 重复设备树节点重复定义在工程中搜索序号相同的节点地址,删除或改用标签覆盖
dtb 加载后设备无 probe 日志compatible 不匹配或驱动未编译用dtc反编译确认 compatible;检查内核配置
I2C/SPI 外设无应答引脚复用冲突或总线未使能i2cdetect或调试 GPIO 电平;检查 pinctrl 设置
启动时 U-Boot 无法加载 dtb文件系统格式/路径错误检查 FAT 分区文件,确认 mkimage 格式正确
dtb 编译有 warning 但能生成节点属性多余或缺失根据警告信息修复,特别关注 32 位地址截断

这张表是长期调试过程中的一个总结,里面每一个问题都对应着实际项目中真实的踩坑记录。尤其是“设备无 probe 日志”这个问题,十次里有七次是驱动没编译进内核,剩下三次才是设备树兼容性问题。所以在检查设备树前,先顺手查一下内核模块是否存在,能省掉不少无谓的时间。

5. 个人经验与调优建议

设备树调试做久了,会慢慢形成一套自己的检查顺序。我现在的做法是:先确认加载,再确认解析,最后确认驱动。先把启动过程中实际加载的 dtb 从 BOOT 介质里拿出来反编译,确认文件内容符合预期;接着在 U-Boot 里打印环境变量,确认加载地址和路径没问题;最后再看内核日志。这套顺序能避免很多低级的定位错误。特别是当你同时修改了设备树和内核配置的时候,如果驱动没编进去,日志里没有 probe 干扰判断,很容易让你怀疑设备树本身写错了,白白折腾几个小时。

另外一个小建议:system-user.dtsi 尽量保持干净和结构化,加注释说明每一块修改对应的是哪一次需求。时间一长,一个工程里可能积累了上百个节点改动,如果没有注释,下一个接手的人甚至几周后的你自己都会看不懂为什么会存在某些覆盖。比如我会在每一个移植外设的节点上方写一行注释,记录原始的地址映射表格来源,以及适配时修改了哪些关键属性,后续排查问题时能省不少力气。

单独编译设备树这个习惯,强烈建议每一个用 PetaLinux 做开发的人都养起来。刚开始可能不太适应,觉得多了一条命令,多了一个步骤,但用过几周之后就会觉得离不开了。它改变的不仅是编译时间,更是整个调试节奏。调试节奏快了,能试的配置组合就多了,Bug 的收敛速度自然也会快很多。设备树本身不复杂,真正考验人的是对这套构建流程的理解深度和碰到问题后的排查顺序。希望这篇文章能把你在设备树入口上的困惑一次性减少大半。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询