☰
Petalinux中单独编译设备树:原理、方法与避坑指南
2026/10/5 16:12:21 网站建设 项目流程

做嵌入式Linux的兄弟,应该都有过这种经历:Petalinux工程里改了一行设备树,比如给某个SPI设备换个片选脚、把某路UART的时钟频率调整一下,然后顺手跑一遍petalinux-build,结果一等等了大半天,最后发现就生成了一个几百KB的dtb文件,内核、FSBL、rootfs根本没动。这事搁谁身上都挺窝火的。

设备树(Device Tree)说白了就是一张描述“板子上有哪些硬件、它们挂在哪个地址、用什么中断”的清单。Linux内核启动时要拿它做硬件对接。Petalinux作为Xilinx/AMD官方嵌入式Linux工具,把内核、U-Boot、根文件系统、设备树这一整套都管理起来了,但它的全量构建链路确实长。如果只想调试设备树,完全没必要每次都全量编译。

这篇文章我就从设备树的基本概念讲起,结合Petalinux工程里的实际文件组织,把“单独编译设备树”这件事彻底讲透:怎么改、怎么编译、怎么验证、踩过哪些坑。适合正在做Zynq/ZynqMP/MPSoC开发,或者刚接触Petalinux但对全量编译效率不满意的同学参考。这套思路不仅适用于Xilinx系,换成其他SoC平台,比如瑞芯微RK3568这类同样用设备树描述硬件的方案,单独抽离设备树来编译调试的逻辑也基本一致,一通百通。

1. 设备树:Linux内核怎么“认识”你的板子

1.1 从“写死驱动”到“配置文件”

在没有设备树的时代,Linux内核每支持一块板子,都要在源码里写死一堆板级信息,比如内存基地址、外设寄存器地址、中断号。代码里到处都是#ifdef CONFIG_BOARD_XXX这种宏开关,换一块板子,哪怕只是改个GPIO极性,都要改内核源码重新编译。久而久之,内核源码里板级文件堆积如山,代码复用性差,维护成本极高。我记得早年间给一块开发板适配BSP,光改arch目录下的board文件就要折腾一整天,而且一不小心就会影响其他平台。

设备树把“硬件长什么样”从“内核对硬件的操作方法”里彻底剥离开来。硬件信息写在一个独立的数据结构里,也就是dts/dtsi源文件,经过编译变成dtb二进制;内核启动时把dtb解析成设备模型,然后驱动通过设备模型去匹配、访问硬件。驱动不再关心板子细节,只要硬件符合某个compatible字符串,就能绑定对应驱动。这个设计很像装修时用的“户型图”。水电工、木工、油漆工不需要知道你家电表装在哪、水管怎么走,他们只需要按户型图干活就行。设备树就是那张户型图,内核对各种硬件的驱动就是那些工种。换户型,只需要换图,不用换人。

1.2 dts、dtsi、dtb、dtc,名字相近别搞混

这四个名词是新手最容易搞混的,我在这里逐个拆开说清楚。

  • dts(Device Tree Source):设备树源文件,描述某一款具体板子的完整硬件树,通常以/ { ... };为根节点。板级厂商拿到的往往就是这张“最终图纸”。
  • dtsi(Device Tree Source Include):公共片段,类似于C语言的头文件。SoC厂家一般把CPU、中断控制器、UART、SPI、I2C等通用控制器写在dtsi里,板级厂商再在dts里引用并加入具体外设、引脚复用配置。
  • dtb(Device Tree Blob):dts经过编译后的二进制文件,大小通常只有几十KB到几百KB。U-Boot启动时把它加载到内存,再传给内核解析。
  • dtc(Device Tree Compiler):编译设备树的工具。Linux内核源码里自带一份,系统里也经常能装上独立的device-tree-compiler包。

编译链路并不复杂:dtc把.dts和.dtsi按#include的方式组合起来,经过C预处理器完成宏展开,得到完整树,再生成.dtb。不过Petalinux里并不只靠裸的dtc,它还会经过一个开源设备树生成器,把XSA硬件描述文件里的PL侧IP、PS端配置转换成对应的dtsi片段,然后再和用户自定义片段合并。所以单独编译时,我们要明白Petalinux拿到的是一套由XSA和用户片段拼接出的完整源文件,而不是你手写的一个dts。

1.3 设备树文件里的关键四要素

快速看懂一个设备树节点,重点看四个东西,这也是排查设备树问题时最先要核对的四个属性。

  • compatible:驱动的“身份证”。Linux驱动通过这个字符串匹配硬件节点,格式通常是"厂商名,设备型号"。若名称和驱动里不匹配,驱动压根不会probe,设备在用户空间压根不可见。
  • reg:寄存器或内存区域地址。具体含义取决于父节点的#address-cells和#size-cells——前者决定地址占几个32位单元,后者决定长度占几个32位单元。ARM64平台上经常是<address length>两段结构,地址和长度各占2个cell。
  • interrupts:中断号和触发方式。这个必须和中断控制器(如GIC)的配置对得上,不然中断来了内核不知道路由给谁,外设就会一直卡在等待中断的状态。
  • status:节点是否启用。SoC厂商默认把大量用不到的外设节点写成disabled,板级厂商用到哪个外设就在哪个节点上显式改成okay。

我见过不少开发者在设备树里把外设地址写错、中断号跟原理图对不上,导致设备工作不正常的案例。排查时第一件事就是反编译dtb,确认最终加载的这颗dtb里节点内容确实是自己改的。

2. Petalinux工程里,设备树到底藏在哪

2.1 Petalinux目录结构速览

Petalinux创建工程后,通常会有这样几个核心目录。先把它捋顺了,后面操作才不会迷路。

  • project-spec/:工程配置、用户自定义层、补丁等都在这。你日常90%的定制工作都在这个目录下完成。
  • components/:由配置阶段生成的各种子系统源码目录、编译配置和中间产物。
  • images/linux/:最终产物输出目录。system.dtb、image.ub、BOOT.BIN基本都在这里,也是做烧录启动时最常看的目录。
  • build/:实际执行构建的工作目录。BitBake会把各个recipe拉到这里展开源码、编译,中间文件非常多,一般不建议手动去翻里面的文件。

对大多数开发者来说,日常打交道最多的是project-spec/meta-user/,这是Petalinux专门给用户预留的自定义层。你要做的绝大多数修改——不管是设备树、U-Boot环境变量还是内核配置——都会被收集到这里,再通过BitBake的追加机制覆盖默认内容。这个设计有点像Git里的覆盖分支,底层的官方配置是主分支,你的个性化修改是上面叠加的commit,干净又隔离。

2.2 设备树源文件的“三段式”组织

Petalinux里,最终编译用的设备树源文件并不是一个单一dts,而是多个文件按层级拼接出来的。以ZynqMP为例,典型结构如下:

  • system-top.dts:顶层文件,是所有片段的汇总入口。它通过#include把SoC描述、板级配置、用户片段都拉进来。
  • zynqmp.dtsi:SoC本身的硬件描述,来自上游内核,一般不需要动。这里定义了CPU、GIC、各种控制器基础节点。
  • system-conf.dtsi:由XSA自动生成,包含PS端配置、DDR地址、MIO映射等。这个文件会随硬件描述更新而重新生成。
  • system-user.dtsi:用户自定义片段,默认存在且内容基本为空。我们改设备树的地方就是这里。
  • pl.dtsi:由PL逻辑的XSA生成,描述你在Vivado里搭的IP,比如AXI GPIO、自定义IP的寄存器映射。如果PL设计变了,需要重新生成XSA、重新配置Petalinux,否则设备树和实际硬件就对不上。

这套三段式分层组织,核心价值在于不同来源的信息互不污染:芯片原厂维护SoC基础描述,硬件工程自动生成PS/PL配置,用户手动补板级细节。出问题的时候,你可以快速定位是哪一层的信息出了偏差。

2.3 修改设备树应该动哪个文件

正确且唯一的答案是:优先改project-spec/meta-user/recipes-bsp/device-tree/files/下的system-user.dtsi。

我先说一下为什么不能瞎改其他文件。Petalinux在构建时会把工程重新生成一份设备树工作副本,像system-top.dts、system-conf.dtsi这种由XSA生成的中间文件,可能在一次petalinux-config或重新生成硬件描述后就被覆盖。你的修改如果写在这些文件里,等于白改,甚至会让工程出现“改了又变回去”的诡异现象。而system-user.dtsi位于meta-user层,配置阶段不会覆盖它,追加和覆盖的逻辑是稳定的。

改法也很直观。比如你要给某个SPI控制器加一个spidev子节点,就在该文件里追加:

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

这里用了&spi1的方式引用已有节点,比重新写整个节点更安全、更清晰。因为spi1的定义在SoC dtsi里已经存在,覆盖引用只会修改我们关心的属性,不会破坏其他配置。如果是新加的板级节点,比如一颗外部ADC挂在I2C总线上,则在根节点之外新增独立节点,再由&i2c0追加子节点即可。

2.4 XSA变了,设备树要怎么同步

还有个高频场景:你改了Vivado里的PL设计,导出了新的XSA,然后执行petalinux-config --get-hw-description重新导入硬件描述。此时Petalinux会基于新XSA重新生成system-conf.dtsi和pl.dtsi,system-user.dtsi不会被覆盖。但你在里面引用的某些节点名、reg地址如果和新的XSA不再对得上,就会出现编译警告或运行异常。所以每次更新硬件后,建议顺手打开system-user.dtsi,把里面引用的标签、地址重新核对一遍,别等到上板了才发现寄存器地址已经变了。

3. 单独编译设备树的几种姿势,实测对比

3.1 姿势一:Petalinux自带target,只编设备树

我已经反复确认过,最稳妥也最被官方支持的方式就是:

petalinux-build -c device-tree

这条命令只针对设备树这个recipe执行BitBake构建。它会把system-top.dts以及所有include进来的dtsi重新预编译、转换、生成新的system.dtb。构建结束后去images/linux/下面看,system.dtb的时间戳就是刚才的生成时间。我实测过,一个中等复杂度的ZynqMP工程,冷启动执行这条命令大概一到两分钟,热缓存时能快到十几秒。相比整包petalinux-build动不动一两个小时,效率完全不是一个数量级。

如果改完设备树还要更新启动镜像,记得手动执行打包:

petalinux-package --boot --dtb images/linux/system.dtb

这会生成新的BOOT.BIN,并把新dtb一并打包进去,省得后面单独拷贝dtb文件。

3.2 姿势二:直接从内核源码目录make dtbs

如果你平时喜欢直接操作内核源码树,不想依赖Petalinux的完整构建体系,或者你使用的是纯Linux主线内核而不是Petalinux自带的xlnx内核,也可以直接在源码目录单独编译设备树:

export ARCH=arm64 export CROSS_COMPILE=aarch64-linux-gnu- make dtbs

这条命令会编译所有在arch/arm64/boot/dts/下的目标dtb。如果你的板子配置在arch/arm64/boot/dts/xilinx/下,也可以只编单个文件:

make xilinx/zynqmp-zcu104-revC.dtb

编完的dtb在arch/arm64/boot/dts/对应路径下。这种方式更适合想绕开Petalinux、自己driven整个构建流程的开发者。注意依赖关系:设备树里很多头文件宏来自内核源码,比如include/dt-bindings/下的宏定义,所以dtbs编译需要内核源码已经完成基本的配置准备,比如运行过make defconfig或相关配置。不同平台的ARCH值不一样,32位ARM用make ARCH=arm,RISC-V用make ARCH=riscv,交叉编译前缀也要换成对应的工具链。

3.3 姿势三:用dtc工具手动编译单个dts

如果你只是想把一个孤立dts快速变成dtb做测试,不牵扯整个工程,直接用系统里安装的dtc是最快的:

sudo apt install device-tree-compiler dtc -I dts -O dtb -o test.dtb test.dts

也支持反向操作,把dtb还原成可读的dts,这在排查问题上非常有用:

dtc -I dtb -O dts -o decompiled.dts system.dtb

需要注意,手动dtc编译时通常不带预处理器,像#include和宏展开这些需要你先用cpp跑一遍,或者干脆用内核提供的scripts/dtc/dtc并配合cpp。更省事儿的做法是让Petalinux自己处理,别自己给自己找麻烦。这种方法适用于快速验证一段孤立设备树语法是否正确,但并不适合完整的Petalinux工程场景。

3.4 快速验证:QEMU里先跑一遍

每次改完设备树就烧板卡,来回插拔SD卡确实烦。Petalinux提供了QEMU支持,可以先把新dtb在QEMU里验证一下:

petalinux-boot --qemu --kernel images/linux/Image --dtb images/linux/system.dtb

如果设备树里的内存配置、外设地址有硬伤,QEMU启动阶段通常就会暴露出来,不用等板子回来。这个方法特别适合远程开发、手头暂时没有板子的场景。不过QEMU对PL侧自定义IP的模拟能力有限,涉及复杂PL逻辑的设备树改动,最终还是得上板验证。QEMU能帮你过滤掉45%以上的低级错误,尤其是内存大小、UART地址这类PS侧基础配置错误,上板前跑一遍能省很多时间。

3.5 打包进启动介质:SD卡和image.ub

设备树编出来不是终点,关键是让它真正在启动时被用到。Petalinux默认会生成image.ub,里面融合了内核和设备树。如果你改完设备树直接拿去烧录,但不动image.ub,板子启动时用的还是旧的设备树。

要把单独编译的dtb更新到启动介质里,常见做法分两种。一种是SD卡启动:把SD卡分成BOOT分区和rootfs分区,BOOT分区是FAT格式,里面放着BOOT.BIN、image.ub或者单独的system.dtb。你只要把新的system.dtb覆盖到BOOT分区对应文件,再重启即可,U-Boot会按预设环境变量从BOOT分区读取dtb。另一种是重新打包image.ub:

petalinux-package --image --dtb images/linux/system.dtb --format uImage

打包完再替换SD卡上的image.ub,这样内核和dtb是打包在一起的,可靠性更高,但更新也没那么灵活。

这里我补充一下:如果你走的是U-Boot + TFTP + NFS这种网络启动调试模式,单独编译设备树简直太方便了——只需把dtb文件扔到TFTP服务器目录,U-Boot里重新下载对应文件即可,不需要重新打包任何镜像。整个调试循环从“改文件-交叉编译-烧卡-上电”变成“改文件-编译-传服务器-重启”,节省的时间是按小时计的。

4. 我踩过的设备树编译与启动坑

4.1 编译期异常:syntax error和引用无效

设备树语法错误是第一类高频问题。最常见的是忘了分号或者大括号不匹配,编译器会提示:

Error: system-top.dts: 12.13-14 syntax error FATAL ERROR: Unable to parse input tree

这种问题通常发生在你手动编辑dts、复制网上的片段时。我的经验是先缩进对齐再数括号,能用dtc的-@或-O dts做语法校验就赶紧校验,别两眼一抹黑直接上板。

另一个高频错误是引用了不存在的节点,比如:

&uart99 { ... };

如果SoC dtsi里根本没有uart99这个标签,编译直接报ERROR (phandle_references): Reference to non-existent node or label。这个原因多半是你从别的芯片平台照搬了片段,没有改成自己平台上的实际标签。尤其是从旧工程迁移设备树时,比如把AD9361相关节点从另一套Petalinux工程搬过来,最容易出现这种问题——标签不同、层级结构不同、引用的时钟节点对不上,直接编译肯定炸。反编译当前工作副本的dtb,用dtc查一遍有效标签,再回填到自己的dtsi里,比盲猜靠谱得多。

4.2 改完没生效,这种坑最扎心

改完设备树也编译了,上板却发现行为没变。我总结下来,按概率排序有这么几个原因。

  • 第一,你改的文件根本没参与构建。常见于直接改了build/下的生成dts,而不是project-spec/meta-user/下的system-user.dtsi。下次构建时生成文件被覆盖,白改。
  • 第二,启动脚本没加载新dtb。比如SD卡里还有旧的image.ub,U-Boot优先读了它,你单独更新的system.dtb根本没人用。
  • 第三,内核里没有对应驱动。设备树节点写得再对,内核没编译进驱动,设备节点就不会出现在/dev/下,或者driver probe直接失败。
  • 第四,你改的是dtb但U-Boot在启动时传了覆盖参数,或者用了fdt overlay,运行时的树和你编出来的不一致。

排查顺序建议是:先确认生成的system.dtb时间戳,再看启动日志里U-Boot打印的设备树地址,最后进内核看/proc/device-tree的实际内容。三层对上了,基本不会错。这个排查逻辑我用了很多年,每次都管用。

4.3 运行时设备树故障的排查方法

启动阶段设备树问题最容易表现为内核panic或外设失灵。我提供几个亲测有效的排查手段。

第一个是内核启动日志。如果设备树内存地址配置错误,内核可能直接死在早期启动,加earlycon能在串口上看到更多信息:

bootargs=... earlycon

第二个是运行时的/proc/device-tree。这是当前内核解析后的实时设备树,和编译的dtb不一定完全一样,但能直观看出哪些节点被加载、哪些属性是最终值:

ls /proc/device-tree/ cat /proc/device-tree/model

第三个是U-Boot下的fdt命令:

fdt print /soc/spi@ff040000

如果U-Boot能正确解析dtb,而内核起不来,问题多半在设备树里的内存和时钟配置上。第四个是设备树覆盖(overlay)的排查。如果你启用了U-Boot的overlay机制,同时又有多个dtbo在加载,运行时设备树就和基础dtb不一样了。这时候必须把overlay dtbo也反编译出来,才能看清完整树的结构。说实话,overlay排查看起来比普通设备树麻烦一个量级,能用基础dtb解决的尽量别引入overlay。

4.4 专项场景:spidev、GPIO复位、显示设备

结合我实际处理过的案例,单独提三个高频场景,都是后台留言里经常被问到的。

  • spidev设备树:很多板子要挂SPI外设,比如LCD、Flash,想通过用户态spidev访问,必须在设备树上加子节点,且compatible要写成"rohm,dh2228fv"或者驱动可匹配到的spidev兼容字符串。如果写成外设真实型号,但内核里没有对应驱动,设备就不会probe,更不会有/dev/spidevX.Y。
  • GPIO复位信号时间:有的外设上电后需要复位脚拉低一段时间再释放,设备树里通过reset-gpios属性配置。常见错误是复位极性反了——GPIO_ACTIVE_LOW写成了GPIO_ACTIVE_HIGH,导致复位永远没生效。另外,有的驱动支持reset-delay-ms之类的属性来设定复位后的延时等待,这个时间要根据外设数据手册来配,配太短外设可能没准备好就访问了,表现为偶发读写失败。
  • 显示设备:比如给SSD1306这类OLED屏配设备树,需要配置compatible、reg(I2C地址)、width、height等属性。这里最容易错的是I2C地址没按7位格式写,导致驱动读不到设备。比如器件手册写0x3C,通常对应7位地址0x3C,但有时驱动内部会自动左移一位,填错之后要么识别不了,要么读出来的寄存器全是FF。这时候用i2c-tools里的i2cdetect -y 0扫一下总线,能少走很多弯路。

4.5 设备树编译相关命令速查表

最后整理一张速查表,方便你直接抄,也方便贴到团队内部文档里。

目的命令
Petalinux工程内仅编译设备树petalinux-build -c device-tree
重新打包BOOT.BIN并加入新dtbpetalinux-package --boot --dtb images/linux/system.dtb
源码树编译所有设备树make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs
源码树编译单个设备树make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- xilinx/zynqmp-zcu104-revC.dtb
系统dtc编译独立dtsdtc -I dts -O dtb -o test.dtb test.dts
反编译dtbdtc -I dtb -O dts -o out.dts system.dtb
QEMU启动验证petalinux-boot --qemu --kernel images/linux/Image --dtb images/linux/system.dtb
查看运行时设备树ls /proc/device-tree/
U-Boot查看设备树节点fdt print /soc/spi@ff040000

我个人做Petalinux开发这几年,最大的体会是:设备树单独编译并不难,难点在于建立“改-编-验-跑”的闭环思维。很多人习惯一上来就全量编译,等得久不说,出了问题还分不清是设备树的问题还是其他环节的问题。先把单独编译设备树的流程跑顺,整个调试效率能提升一大截。

最后再分享一个小技巧:在多核服务器上编译时,我会在petalinux-build -c device-tree之后马上用dtc -I dtb -O dts反编译一次新生成的system.dtb,和改动前的版本做个对拍,确认节点差异完全符合预期,再考虑打包和上板。这几乎不花时间,但能帮你筛掉八成低级错误。尤其是当你在system-user.dtsi里用&引用覆盖多个节点时,对拍能一眼看出你是否不小心覆盖错了对象。设备树这东西,看着简单,真到排查的时候,差一个逗号都能让你多熬一个通宵。把这些命令和流程记熟,能让你少熬很多个通宵。

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

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

立即咨询