☰
手写dts设备树文件:从最小示例到GPIO串口调试实战
2026/10/3 8:08:13 网站建设 项目流程

写设备树文件这事,不少做Linux BSP的人第一次接触都是一脸懵:明明C代码写得很顺,U-Boot也能跑,一打开.dts文件就不知道这东西到底由谁来解析、节点名能不能随便写、reg里那串数字又是怎么算出来的。我头一回给一块ARM板子补dts的时候,连着调了两天LED不亮,后来发现根本不是LED节点写错了,而是对应的GPIO控制器节点被裁掉了。从那以后我算是明白了——设备树不是普通的配置文件,它是一份“给内核看的硬件说明书”,写错了不会报编译错误,只会让驱动悄悄不工作。

这篇文章就围绕dts设备树文件怎么写来展开。我会从最基本的概念讲起,把手写最小dts、GPIO和串口节点的添加、RK3568这类常见SoC的差异、U-Boot和PetaLinux场景里的注意事项、编译反编译调试方法,以及我实际踩过的坑都过一遍。不管你是刚入门想把一块板子跑起来,还是被某个节点覆盖问题折磨了好几天,这篇应该都能给你一个能照着做的思路。

1. 先搞懂设备树在系统里的位置:它到底替谁干活

1.1 “硬件描述翻译官”到底翻译给谁听

设备树全称Device Tree,作用很好理解:把“硬件长什么样”从“驱动代码”里剥出来。在ARM Linux出来之前,很多平台的做法是把板子上的外设信息写死在arch/arm/mach-xxx/board-xxx.c里,换一块板子就要改一堆C文件重新编内核。后来大家把这块“硬件描述”抽成独立的数据文件,内核启动时通过统一的解析接口读进来,驱动自己去匹配节点。这样同一个内核,只要换不同的.dtb,就能适配不同硬件。

dts设备树文件最终要编译成dtb(Device Tree Blob),U-Boot会把它加载到内存,再把地址传给内核。内核在启动早期就解析dtb,然后构建出平台设备、把中断、GPIO、时钟这些资源告诉对应驱动。换句话说,设备树不是给程序员看的,也不是给U-Boot看的,它最终是给Linux内核驱动的probe函数看的。

很多初学者会问:我明明在dts里定了一个节点,为什么驱动没跑?大概率是compatible对不上,或者父节点没使能,又或者这个设备根本不归platform驱动管。所以理解设备树的第一个关键点,就是搞清楚内核里存在一套“节点名 + compatible字符串”的匹配机制。驱动通过compatible找节点,节点里的属性再变成驱动需要的资源。

1.2 dts、dtsi、dtb、dtbo分别是什么

这几种文件的区别,看着像后缀不同,其实是三个阶段:

文件全称/性质作用
dtsDevice Tree Source一块具体板子的完整设备树源码,通常是顶层入口
dtsiDevice Tree Source Include被dts include进去的公共片段,比如SoC级通用配置
dtbDevice Tree Blobdts编译后的二进制,最终由U-Boot加载并传给内核
dtboDevice Tree Overlay Bloboverlay机制用的二进制,可以在运行时叠加修改设备树

实际工程里,dtsi一般由芯片原厂维护,描述的是SoC内部有多少个UART、I2C、SPI、时钟、中断控制器;dts则描述这块板子上外接了什么,比如LED接在哪个GPIO,网卡芯片挂在哪个I2C上,内存大小是多少。很多情况下你自己要动的只是这块板级的dts,以及少量需要覆盖的SoC级dtsi内容。

1.3 为什么不能把所有硬件信息直接写进C文件

有这个疑问很正常,毕竟裸机开发习惯就是写寄存器地址。但在Linux体系里,驱动是跨平台复用的,一个看门狗驱动可能同时跑在五六家芯片上。如果硬件地址写死在驱动里,每换一颗芯片就要改驱动。设备树把“地址、中断号、时钟索引、引脚复用”这些可变信息抽出来,驱动代码只负责解析,从而做到一套驱动适配多个平台。

还有一个现实原因:同一款高端芯片经常被做成几十种开发板或产品,这些板子CPU一样,但内存大小、外设连接、LED定义可能完全不同。厂商不可能为每块板子都编译一个专属内核,更合理的做法是统一内核+不同dtb,工厂烧录的时候把对应的.dtb刷进去就行。理解了这一点,你再看dts文件里的节点覆盖和overlay机制,就会觉得顺理成章。

2. dts文件的骨架:写之前必须先会看这几块

2.1 头部的/dts-v1/和根节点

先看一个最小dts的样子:

/dts-v1/; / { model = "MyBoard"; compatible = "myvendor,myboard", "myvendor,soc"; chosen { bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait"; }; memory@0 { device_type = "memory"; reg = <0x00000000 0x20000000>; }; };

/dts-v1/;是版本标记,告诉dtc用新版语法,现在基本所有dts都会带这一行。/ { };是根节点,整棵设备树只有一个根。model是板子名称,给人看的;compatible是给内核做平台匹配的,格式推荐“厂商名,型号”,后面一般再追加一个兼容的SoC名。root节点的compatible会被内核用来和machine_desc匹配,写错的话可能连板子都认不出来。

chosen节点是内核启动参数和stdout路径的入口,这里可以放bootargs。memory@0描述内存块,注意@0表示这个节点对应的寄存器起始地址是0,它必须和后面reg里的第一个地址对齐。这是个很容易忽略的规矩:节点名里的地址后缀和reg属性应保持一致,虽然有些工具不强制,但内核的schema校验会当成问题。

2.2 节点、属性、字符串、数组:最小单位

设备树里最基本的概念就两个:节点和属性。节点以node-name { };形式存在,属性写在节点内部。属性的值有三种主要形式:

compatible = "myvendor,mydevice"; // 字符串 reg = <0x1000 0x100>; // 32位整数数组 enable-gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>; // 混合引用

每个节点天然拥有一个路径,比如/soc/spi@10000。节点之间可以是父子关系,比如I2C控制器节点下面挂I2C设备节点。属性就是键值对,键名不能随意起,驱动依赖特定属性名。compatible、reg、interrupts、clocks这类都有标准含义,另外厂商还经常自定义前缀,比如rockchip,grf之类。

初次写dts的人最容易把节点名随性取,比如led { };、button { };。不是不行,但如果同一层有多个LED,最好写成led@0和led@1,并且用label属性区分。命名规范主要是给工具和人看的,真正决定驱动能不能匹配的,永远是compatible。

2.3 地址编码:reg和#address-cells/#size-cells

地址这块是新手重灾区。dts中reg里的数字不是直接写成物理地址就完事,它由父节点的#address-cells和#size-cells决定语义。

soc { #address-cells = <1>; #size-cells = <1>; uart0: serial@10000000 { reg = <0x10000000 0x1000>; }; };

#address-cells = <1>表示地址用1个32位整数表示;#size-cells = <1>表示长度用1个32位整数。所以reg = <0x10000000 0x1000>代表起始地址0x10000000、长度0x1000。有些64位平台会写#address-cells = <2>,这时一个地址要拆成两个数字,比如reg = <0x0 0x10000000 0x0 0x1000>,第一次遇到时很容易看懵。

还有个容易混的地方:不同父节点可以有不同的address-cells和size-cells。比如根节点为了给memory节点表示64位地址可能用2,但SoC内部总线可能还是1。写某个节点的reg之前,先确认它挂在哪个父节点下,按那个父节点的cells来解释。这件事比背寄存器地址更重要。

2.4 引用已有节点:&符号、phandle和aliases

设备树里经常要引用别的节点。比如SoC的dtsi里已经定义了一个UART节点,但属于disabled状态,板级dts要把它打开:

&uart0 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&uart0_xfer>; };

这个&uart0就是引用。dtc编译时会把引用的节点生成一个整数标识叫phandle,被引用的节点里会自动多出一个phandle = <数字>属性。所以你在某些反编译的dts里看到phandle = <0x5>,不要以为是原厂写死的,那是引用机制产出的产物。

aliases节点则是一个给节点路径起别名的快捷方式。比如:

aliases { serial0 = &uart0; };

内核里很多驱动会通过of_alias_get()拿serial0这个别名,来避免写死节点路径。如果你新增了一个端口,却忘记在aliases里注册,驱动可能找不到设备。

3. 手写一个最小可用dts:LED、GPIO和串口一个不少

3.1 先把板子的“灵魂”写进根节点

我不建议一上来就照着原厂大文件改,最好是先写一个能启动的最小dts,再一点点加功能。一个最小系统至少包含:/dts-v1/、根节点、model、compatible、chosen里的bootargs,以及内存节点。串口驱动一般不需要你从头建整个控制器,因为SoC级dtsi已经把所有外设控制器节点写好了。你需要做的通常是找到对应的UART节点,打开status,并配置引脚复用。

如果你完全没有原厂dtsi,那就要从裸SoC开始画,工作量大很多。实际工程里这种情况极少,绝大多数时候是“基于原厂SDK改”,而不是“从零写”。可越是这样,反而越要理解最小系统的结构,否则你不知道SDK里哪些节点能删、哪些不能删。比如cpus节点里的CPU频率和cluster信息,删错了启动阶段就可能卡住。

3.2 添加GPIO和LED节点

以最常见的gpio-leds为例:

/ { leds { compatible = "gpio-leds"; led0 { label = "user0"; gpios = <&gpio0 RK_PA7 GPIO_ACTIVE_HIGH>; default-state = "off"; }; led1 { label = "user1"; gpios = <&gpio1 RK_PB2 GPIO_ACTIVE_LOW>; default-state = "keep"; }; }; };

gpios = <&gpio0 ...>这个写法里,&gpio0是GPIO控制器的phandle,后面的RK_PA7是Rockchip平台定义的引脚宏,GPIO_ACTIVE_HIGH来自dt-bindings/gpio/gpio.h。LED节点本身没有reg,因为它挂在根节点下,不需要地址。判断一个节点是否需要reg,看父节点给它分配的是什么资源:挂在CPU总线上需要地址;挂在GPIO这类控制器下面,通常用gpios之类的属性就够了。

我自己的经验是:写LED前先确认GPIO控制器是否已经启用。有些SoC的GPIO0默认就是okay,但某些低功耗平台默认disabled。LED不亮时,第一步永远不是怀疑dts语法,而是看/sys/class/leds/下面有没有节点、出错的返工点在哪儿。

3.3 串口节点:启用已有的外设比新建节点更常见

现在很多SoC的UART控制器已经定义在dtsi里。板级dts要做的无非三件事:把status改成okay,配上pinctrl,设置时钟和别名。例如:

&uart0 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&uart0_xfer>; };

其中uart0_xfer一定是在某个dtsi里用pinctrl节点定义的pin function。如果你在别人的板子上看到pinctrl-0 = <&uart0_xfer &uart0_cts &uart0_rts>,说明这个串口开了流控。实际项目里流控经常不用,但引脚复用配置里多出来的cts/rts如果没有实际信号,驱动也不会出错。

什么时候需要自己新建UART节点?只有当SoC dtsi里没写这个接口时。比如有些廉价SoC只把一部分串口做成通用异步收发器,其他当成普通GPIO。这时候你要新建:

apuart2: serial@10024000 { compatible = "myvendor,my-uart"; reg = <0x10024000 0x1000>; interrupts = <GIC_SPI 64 IRQ_TYPE_LEVEL_HIGH>; clocks = <&uart2_clk>; status = "disabled"; };

但建议少干这事,因为如果内核里没有对应驱动,写了也白写。设备树驱动的核心是“内核里先有driver,dts才能被match”。

3.4 自己写节点的时机界定

在自己动手新增节点之前,先在内核源码里搜三个东西:一是compatible字符串有没有对应驱动,二是dtsi里有没有同名节点,三是原厂参考板dts里有没有类似写法。搜compatible最直接:在kernel/drivers下执行grep -r "myvendor,my-uart",如果搜不到,这个节点大概率没人认领。

反过来,驱动存在但节点缺失,那就是dts的活。比如内核里有gpio-leds驱动,任何板子只要新建一个compatible为gpio-leds的节点,系统就会注册对应LED设备。我的建议是:能用标准compatible解决的事,绝不自造厂商专属属性。

4. 编写时的“江湖规矩”:compatible、status和pinctrl是命门

4.1 compatible的顺序不能随便排

compatible属性可以有多个字符串,内核匹配时按顺序挨个试。比如:

compatible = "myvendor,myboard", "myvendor,soc-v1", "simple-bus";

内核会先找有没有驱动声明匹配myvendor,myboard,找不到再找myvendor,soc-v1。这给了兼容设计空间:新板子可以声明具体型号,也能fallback到通用SoC版本。很多人抄原厂dts时喜欢把第一个字符串保留成原厂型号,结果自己板子的驱动永远匹配不上,因为第一个字符串没对应驱动,后面的也没被尝试到。正确做法是把自己的板子型号放最前面,通用SoC兼容串放后面。

4.2 status = "okay"/"disabled"到底管什么用

外设控制器在SoC级dtsi里通常默认status = "disabled",目的是避免一个芯片所有接口全部打开导致功耗和引脚冲突。板级dts通过&uart0 { status = "okay"; };打开对应接口。这个机制是“引用+覆盖”,理论上一个节点只能有一个status,最终生效值是最后一次覆盖后的结果。

有个坑:有时候你打开了&i2c2 { status = "okay"; };,但I2C下面的子设备还是probe不到。原因可能是I2C控制器依赖的父节点(比如SYS电源域、时钟控制器)仍为disabled。这时候要去查时钟和电源domain的status,不能只盯着外设节点看。缺少这种全局意识,是很多BSP新手调试外设不工作的真正原因。

4.3 pinctrl-0里的函数从哪来、怎么找

pinctrl是设备树里最绕的一块。以Rockchip平台为例,dts里常见:

&pinctrl { uart0 { uart0_xfer: uart0-xfer { rockchip,pins = <0 RK_PB0 1 &pcfg_pull_up>, <0 RK_PB1 1 &pcfg_pull_up>; }; }; };

然后UART节点引用pinctrl-0 = <&uart0_xfer>。这里<0 RK_PB0 1>的意思是:bank 0、引脚PB0、功能mux为1。不同厂商解析方式略有不同,但思路一致:pinctrl节点先定义一组“引脚功能”,外设节点引用它,设备probe时内核pinctrl子系统会把引脚切到对应功能。

如果你拿到一块新板子不知道引脚该选哪个function,最简单的方法是看原厂参考板的dts和原理图,或者在/sys/kernel/debug/pinctrl/下查看当前引脚复用状态。不建议凭空创造pinctrl节点,容易把引脚的GPIO功能也冲掉。

4.4 中断和时钟属性速查

中断在dts里的写法是:

interrupt-parent = <&gic>; interrupts = <GIC_SPI 105 IRQ_TYPE_LEVEL_HIGH>;

interrupt-parent指定中断控制器,interrupts里的第一个值表示中断类型,GIC_SPI是共享外设中断,GIC_PPI是私有外设中断。第二个值是中断号,第三个是触发类型。如果不写interrupt-parent,内核会向上找父节点或根节点里声明的interrupt-parent。

时钟属性也比较固定:

clocks = <&cru CLK_UART0>, <&cru PCLK_UART0>; clock-names = "baudclk", "apb_pclk";

驱动里使用devm_clk_get(&pdev->dev, "apb_pclk")按名字取时钟。所以clock-names不是装饰,顺序可以调整,但name必须和驱动代码对应。写时钟属性最靠谱的方法,还是去原厂dtsi里找同系列外设的模板,别自己猜时钟索引,少了前置依赖会让设备直接卡死或不工作。

5. 不同SoC场景下的差异:Rockchip RK3568、U-Boot 2018与PetaLinux

5.1 RK3568设备树:官方SDK里别另起炉灶

瑞芯微RK3568在嵌入式里用得非常多,很多板子是基于Rockchip SDK开发的。rk3568的SoC级dtsi一般叫rk3568.dtsi,板级文件是rk3568-evb.dts或者你们自己命名的板级dts。常见的管理方式是在SDK的arch/arm64/boot/dts/rockchip/目录下,Makefile里把板级dtb编译进去。

对RK3568这种双Cortex-A55加一堆外设的SoC,不建议从零写dts。原因很简单:很多关键配置分散在多个dtsi片段里,包括电源域、CPU频率、DDR调优、GMAC、NFC、PCIe等。你从零写一个,漏掉电源域或时钟,轻则外设不识别,严重时内核都起不来。正确做法是复制原厂EVB板dts,改成自己的板名,再逐项修改。

RK3568上经常看到的写法之一:

&cpu0 { cpu-supply = <&vdd_cpu_reg>; };

这不是随便加的,它让cpufreq驱动知道怎么给CPU调压。如果原理图上CPU供电不是这个regulator,你直接抄EVB可能让CPU频率不稳。写RK3568板级dts时,我的习惯是:先跑起来,再逐个删用不到的外设,然后按原理图重新认领引脚。

5.2 U-Boot 2018的dts:内核复用还是独立source

U-Boot 2018那段时间,很多BSP把同一份dts同时用于U-Boot和内核。U-Boot源码树下有自己的arch/arm/dts/,里面可能是从内核同步过来的副本。U-Boot启动时如果需要设备树来初始化DDR或外设,就会编译并使用这份dts。内核启动时使用的又是另一份dtb。两者可以相同,也可以不同。

以U-Boot 2018为例,常见的做法是在arch/arm/dts/Makefile里加一行,把你自己的dts编译进dtb列表,然后在头文件里配置CONFIG_DEFAULT_DEVICE_TREE="myboard"。U-Boot代码中通过fdt接口读取节点,比如fdt_getprop(),所以dts里节点是否被U-Boot使用,取决于U-Boot代码里有没有主动查。

一个常见的迷惑:你在内核dts里改了一个GPIO,U-Boot启动阶段没有任何反应,U-Boot自己用的是它副本里的dts,两者没同步。所以每次改设备树,先确认你改的是内核那份还是U-Boot那份,同时同步,否则就会出现“内核起来是新的,U-Boot阶段行为却是老的”这类问题。

5.3 PetaLinux下设备树生成和手动修改

PetaLinux常用于Xilinx/AMD平台,它有一个自动生成设备树的机制:从硬件描述文件XSA导出BSP,自动生成pl.dtsi、psu.dtsi等文件。很多刚开始接触的人有个误解:PetaLinux生成的东西不能手改。其实可以,至少有两个层面:一是用petalinux-config里的Device Tree相关选项,二是在项目目录的meta-user/recipes-kernel/linux/里添加dts补丁或直接用文件覆盖。

在PetaLinux里,手动修改设备树最常见的方式是编辑项目里的设备树源文件,然后调用petalinux-build -c device-tree重新编译。改完之后要确认实际生成到镜像里的dtb路径是不是你期望的那份。因为PetaLinux可能从多个layer收集dts片段,同名节点会被拼接/覆盖,最终结果用反编译工具看一眼最保险。

PetaLinux平台一个常见问题是:PL侧的IP通过overlay动态加载,设备树里会生成fragment@0这类overlay片段。如果你在手动改dts时破坏了fragment结构,可能导致FPGA加载后设备没有生成。遇到这种情况,别先怀疑驱动,反编译dtb看overlay片段还在不在。

5.4 网上到处找“dts下载”到底靠不靠谱

网上确实有人搜“dts下载”就是为了拿到一份能直接编译的例程。我的建议是:不要用来源不明的dts直接替换官方文件。你没法确定它对应的内核版本、引脚复用和regulator关系。对照参考板文件来改,远比自己下载一份“看起来能用”的dts更高效。设备树不像独立驱动程序,它和内核版本、SoC版本绑定得很紧,跨版本拷贝往往是灾难的开始。

6. 编译、反编译和调试三板斧

6.1 dtc编译设备和常见报错

Linux内核里自带dtc编译器,一般在scripts/dtc/dtc,你也可以用系统自带的device-tree-compiler软件包。最常用的编译命令:

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

如果dts里有include其他文件,直接用dtc编译会报#include错误,因为dtc默认不展开C预处理器。实际项目中更好的做法是像内核一样用cpp -nostdinc -I...预处理,或者直接借助内核的编译系统:

make dtbs

只编译某个板级dtb时,可以指定:

make ARCH=arm64 myboard.dtb

常见的编译报错包括:节点重复定义、属性类型不匹配、引用不存在的标签(undefined label)、/dts-v1/缺失。看到Error: myboard.dts:1.1-2 syntax error这类信息,先检查文件头有没有/dts-v1/;和/ {结尾的闭合括号,很多时候是漏了配对的};。

6.2 反编译dtb确认实际生效内容

调试设备树时,一定要养成反编译核实的好习惯。因为我们往往在多个dtsi文件里加内容,最终dtb长什么样并不一定和你的直觉一致。反编译命令:

dtc -I dtb -O dts -o dump.dts myboard.dtb

或者使用fdtdump myboard.dtb。这两者输出风格略有不同:dtc反编译出来的更像源码,fdtdump更接近二进制原始结构的转储。我一般先fdtdump快速看节点列表,再dtc反编译查看具体属性。

举个例子:如果板级dts里写了status = "okay",但SoC dtsi在更后编译单元里又把它改回disabled,最终生效值是什么?反编译一下就知道。很多人在原厂dtsi上做修改,以为改了节点就会生效,结果发现自己的文件根本没被编译进最终dtb,原因就是Makefile里的dtb列表没加全。

6.3 用内核log验证节点匹配情况

编译没问题、反编译也有节点,但设备还是没出现,这时候要看内核log。启动阶段相关驱动会打印of_node匹配信息,很多内核开了CONFIG_OF_*调试选项后,会输出类似“OF: fdt: Machine model: ...”或“driver probed with compatible ...”。用dmesg | grep -i of能看到不少信息。

另外,/sys/firmware/devicetree/base是内核把设备树展开后的运行时视图。你可以在板子上执行:

ls /sys/firmware/devicetree/base/ cat /sys/firmware/devicetree/base/model

如果这里没有你写的节点,说明dtb根本没传对;如果节点在这里但驱动没绑上,问题就在驱动侧。这个目录对排错价值极大,可以说是设备树调试里最直接的“证据现场”。

6.4 启动参数和U-Boot环境里的fdt相关项

在不同平台上,U-Boot把dtb传给内核的方式略有不同,通常是在bootcmd里先加载dtb到某个内存地址,然后通过环境变量fdt_addr、fdt_addr_r传入bootm/booti。调试设备树时,建议检查U-Boot环境变量:

printenv fdt_addr printenv fdt_addr_r printenv bootcmd

有时候你发现改了设备树编译出了新dtb,烧进去后却毫无变化,原因就是U-Boot实际加载的不是你烧写的分区,或者fdt地址写的是旧分区。我遇到过不止一次:明明改对了文件,结果U-Boot环境里fdt_addr指向的是一个过期的dtb加载地址,内核拿到的是旧包。清掉环境变量重新设置,比反复烧写有效得多。

7. 那些坑了我很久的细节:overlay、覆盖与phandle

7.1 为什么改了某个dtsi文件却不生效

这是个非常经典的问题。很多人习惯直接在dtsi里把一个节点状态改成okay,编译烧录后却发现不起作用。原因分两类。第一类:你的dts根本没用cpp展开这个dtsi,或者Makefile没有把对应的dts编进内核。检查方法就是我前面说的反编译dtb。第二类:你的dts内容顺序导致节点覆盖逻辑重置。多个文件里同名节点会被“合并”,但同一种属性后面的赋值覆盖前面还是前面的覆盖后面,取决于编译顺序和结点书写形式。

有时候你看到dtsi里定义了一个&gpio0的子节点,又在另一个文件中用&gpio0 { ... }新增属性。dtc对同一节点多次打补丁是允许的,属性覆盖遵循“后解析的覆盖先解析的”。但如果你直接用/ { ... };把整棵根节点重写一遍,那不会把原先节点覆盖,而是合并。所以避免“写成另一个完整根节点”这种操作,尽量用&引用的方式补充。

7.2 overlay和__overlay__:运行时动态修改的原理

设备树overlay常用于FPGA、可插拔板卡这类场景。一个overlay片段长这样:

/dts-v1/; /plugin/; / { fragment@0 { target = <&i2c0>; __overlay__ { status = "okay"; newdevice@48 { compatible = "myvendor,mydevice"; reg = <0x48>; }; }; }; };

编译生成dtbo后,可以在运行时用configfs接口加载:

mkdir /config/device-tree/overlays/myoverlay cat myoverlay.dtbo > /config/device-tree/overlays/myoverlay/dtbo

overlay的坑也很明显:如果target节点路径不存在,加载会报错;如果属性名有拼写问题,它不会像编译错误那样明显提醒你。我调试overlay时,习惯先用dtc -I dts -O dtb编译出dtbo,再用fdtdump看它解析后的target和__overlay__内容是不是我预期结构。很多所谓overlay不生效,其实是节点target路径写错了,或者目标节点在基础树里是disabled而overlay没有把status改成okay。

7.3 phandle重复和外设引用错乱的血泪教训

设备树里所有带&的引用都会生成phandle,这是内核对节点的唯一标识。正常编译时会自动分配,但如果你手动从别的板子拷贝节点,就可能出现重复phandle。重复phandle不会马上报错,但内核遍历设备树时可能找到错误的节点,比如本来想给UART0配时钟,结果时钟属性指向了UART1的时钟,导致波特率异常。

经验教训:不要从网上随便找一份dts里的phandle = <0x3>就直接抄到你自己的文件里。phandle应该让dtc自动生成,除非你在做一些特殊的fixup。拷贝节点时,把里面的phandle、interrupt-parent这类引用属性整体理解一遍,比死记数字重要得多。

7.4 最小改动原则和注释习惯

最后说一个工作习惯:设备树文件最好保持最小改动,每处修改都加注释,注释里写清楚“改了什么、为什么改、对应原理图哪一页”。因为设备树是BSP里最容易出现“历史遗留拼接”的文件,可能三个人在不同时期改过同一个节点。没有注释的话,半年后你自己都记不清为什么status要反复改。

我一般会在每个板级dts开头维护一个改动记录:

/* * myboard.dts * 2023-05-10: 增加eth1的phy-rst gpio,适配新硬件R2版本 * 2023-08-02: 打开uart3,console从uart0切到uart3 */

这不是多余的事情。遇到内核升级时,这些注释能告诉你哪些改动是新板子必需的,哪些是临时代调。设备树不像C代码有编译器帮你查类型错误,它的错误往往是运行时才暴露的,而清晰的注释和严格的config管理,就是把这种运行时报错风险降到最低的笨但有效的方法。

我自己每一次写dts都是这套流程:先用原厂EVB板跑通,再对照原理图一点一点改,每次只改一个功能模块,验证通过再动下一个。这样即使出了问题,回滚范围也很小。很多所谓“dts太玄学”的问题,最后追下来都出在“一次改太多”或者“直接照抄别的板子却没对照原理图”上。设备树本身并不复杂,复杂的是它背后那张硬件网络,而写dts的能力,说到底就是读原理图、找原厂资料、验证内核log这三件事的反复循环。

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

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

立即咨询