ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

嵌入式Linux设备树实战指南:从dts语法到RK3568开发板定制

2026/10/3 12:46:38 拓冰建站 浏览量
嵌入式Linux设备树实战指南:从dts语法到RK3568开发板定制 做嵌入式Linux的这几年我翻过的资料里有一半是在跟设备树打交道。刚入行那会儿拿到一块新板子第一件事就是对着开机日志看半天然后满头大汗地找dts文件到底哪里写错了。现在回头看设备树这玩意儿说难也难说简单也简单难的是它牵扯了SoC、板级电路、内核驱动、bootloader好几层东西简单的是它本质上就是一套描述硬件的数据结构。这篇文章我把这些年在RK3568、ZynqMP这些平台上改设备树的经验整理出来从语法底子到实际动手再到遇到问题怎么查一次给你讲清楚。先回答一个最基础的问题dts设备树文件到底用来干什么简单说它就是一张硬件配置清单告诉内核你的板子上有什么外设、接在哪根总线上、用哪个中断、电压多少。以前ARM Linux是靠一大堆写死的board文件来干这个活的换一块板子就得重新编译内核后来社区受不了了就推出了设备树机制把硬件描述从内核代码里剥离开来。到了今天不管你用的是NXP的i.MX系列、Xilinx的ZynqMP还是瑞芯微的RK3568/RK3588改板子的第一步几乎都是动设备树文件。在动手之前还有几个基础概念必须捋清楚DTS是设备树源文件DTSI是可复用的设备树头文件DTB是编译后的二进制文件DTC是编译器。这几个缩写你会在后续所有开发流程中反复遇到。petalinux、uboot、kernel这些不同阶段都有各自的设备树处理方式后面我会逐个展开讲。1. 设备树的核心逻辑为什么内核非要这张配置清单1.1 从写死代码到数据驱动的转身如果你用过Linux 3.x时代的ARM内核源码一定见过arch/arm/mach-xxx目录下密密麻麻的board文件。每一块开发板都要有对应的C文件里面写满了结构体、回调函数、内存映射宏用来告诉内核我的UART在物理地址0x10000000我的网卡接在片选2上我的中断控制器是GIC版本x。这样做带来的问题很直接开发板厂商只要改一个GPIO的接法就得改C代码、重新编译整个kernel社区维护压力巨大。设备树把这个过程变成纯数据驱动。内核不再关心你用的是哪家板子它只负责在启动时解析一段二进制数据DTB按照里面的节点和属性去初始化驱动。硬件变了吗那就改数据编译成本低得多。你甚至可以在不改内核的情况下靠换一个DTB文件让同一份内核适配多块板卡这点在做产品序列的时候特别有用。1.2 设备树是树不是代码理解设备树最好的方式就是想象一颗家谱树。根节点是/下面挂着CPU节点、内存节点、总线节点、外设节点。每个节点用一对大括号包裹节点下可以继续挂子节点也可以直接写属性。属性就是key-value形式的配置项比如compatible兼容性字符串、reg寄存器地址、interrupts中断号等等。内核启动时的匹配过程很简单驱动通过compatible字符串去设备树里找跟我一样的节点。找到了就绑定找不到就跳过。这也是为什么新手犯的最多的错误之一就是把compatible拼错——一旦拼错驱动逻辑一个都不会执行设备自然就无法工作。1.3 三板斧准备好工具链先配齐写设备树不需要重型IDE有Linux环境就够了。核心工具是device-tree-compilerDebian/Ubuntu系统下执行sudo apt-get install device-tree-compiler就能装好。装完之后你就有dtc命令、fdtdump、fdtget、fdtput这一整套工具。如果做的是petalinux项目还得把petalinux SDK的环境变量source一下如果做瑞芯微平台一般SDK里自带交叉编译脚本直接用它来make dtbs就行。我个人的建议是不管用什么平台都先把内核源码树准备好因为设备树源文件永远以内核源码里arch/arm64/boot/dts/xx/目录下的文件为准。网上总有人搜什么kes dts下载之类的关键字其实最省心的办法就是从官方内核仓库或芯片厂商的SDK里直接拿版本对应关系比什么都重要。2. dts语法底子节点和属性是你看世界的两只眼2.1 最小dts长什么样拿一个最简单的dts文件开胃路径可以存成test.dts/dts-v1/; / { compatible vendor,board; #address-cells 1; #size-cells 1; chosen { bootargs consolettyS0,115200n8 root/dev/mmcblk0p2 rw; }; memory0 { device_type memory; reg 0x00000000 0x20000000; }; uart0: serialfe100000 { compatible arm,pl011; reg 0xfe100000 0x1000; interrupts 0 24 4; }; };你不需要马上理解每一行先记住几个硬性规则。文件第一行必须是/dts-v1/;这是告诉编译器用的是v1版本语法。紧跟着的/ { };是根节点一个dts文件必须有且仅有一个根节点。#address-cells和#size-cells是给父节点下的子设备用的用来定义reg属性里地址和大小各占几个u32单元。2.2 reg属性说清楚寄存器在哪、占多大reg是设备树里最常见的属性之一它的格式是reg address1 size1 [address2 size2 ...]。你要想让内核知道有一个外设它的寄存器基地址是0xfe100000长度为0x1000那就写reg 0xfe100000 0x1000。address和size各占几个格子cell由父节点的#address-cells和#size-cells决定。比如父节点写#address-cells 1; #size-cells 1;那么reg里每两个一组前一个数秒地址后一个数秒长度。有的设备父节点定义是#address-cells 2那reg里一个地址就要写两个u32就像RK3568这类64位平台常见的样子。新手最容易栽在这点子节点的reg到底写几个数必须看父节点定义别凭感觉拍脑袋。2.3 phandle与label设备的两种点名方式设备树里没有指针外设之间要实现引用靠的就是phandle和label。你可以给一个节点打上标签比如uart0: serialfe100000这里的uart0就是label。别的地方想引用它直接写uart0。编译器会把它翻译成一个整数phandle内核就能通过这个整数找到对应的设备节点。还有一种更隐晦的引用方式就是直接在属性里写uart0例如某个中断控制器节点定义interrupt-parent intc。这里的意思是把intc节点的phandle作为引用值传给父级。写GPIO引脚、中断引脚、时钟引用的时候全是这套逻辑。理解phandle你就理解了设备树百分之八十的连接关系。2.4 interrupt属性中断描述也没那么简单设备树里中断相关的常用属性有interrupt-parent、interrupts、interrupt-controller、#interrupt-cells这四兄弟。一个节点如果本身是中断控制器需要声明interrupt-controller并给出#interrupt-cells说明每个消费者节点用几个cell来描述中断。以ARM GIC为例#interrupt-cells 3三个cell分别代表中断类型0是SPI共享外设中断1是PPI私有外设中断、中断号、触发方式。我在实际项目里吃过这个亏RK3568的GIC定义#interrupt-cells 3结果我在一个触摸屏节点里只写了interrupts 0 67 IRQ_TYPE_LEVEL_LOW这种老式写法前半截没问题但后边的触发方式如果没引用头文件里的枚举定义编译会报错。更隐蔽的是写反SPI坐标导致中断号差32驱动根本收不到中断。2.5 头文件也能派上用场设备树源文件支持C语言预处理。最常见的用法是#include rk3568.dtsi、#include dt-bindings/interrupt-controller/irq.h、#include dt-bindings/gpio/gpio.h。这些头文件里定义了一些枚举比如GPIO_ACTIVE_HIGH、IRQ_TYPE_LEVEL_LOW。写完dts文件后编译流程本质上先过一遍gcc预处理把include展开再交给dtc去编译。所以我平时写dts的时候跟写C代码一样享受宏定义和头文件的便利。不过这里有个坑如果直接使用dtc去编译dts而不是走内核或者petalinux的make系统那么include头文件的展开需要靠dtc -H选项或者先手动cpp处理。直接用dtc test.dts -o test.dtb经常会被头文件路径问题卡住。我建议要么把编译动作交给平台的标准流程要么自己写一个简单的makefile先跑gcc -E再跑dtc。3. 项目实战从dtsi组织到RK3568平台定制3.1 一颗SoC多块板卡怎么组织才不乱刚接触设备树的人有个疑问一个芯片厂商的SDK里为什么有那么多长得差不多的dts文件还都喜欢include来include去答案是为了复用。一颗SoC芯片的硬件资源是固定的厂家会把这颗SoC所有的内部外设节点做成一个dtsi比如rk3568.dtsi里面写好了所有的UART、I2C、MMC、GPU、DPU这些控制器的地址、中断、时钟但status默认都写成disabled。板卡厂商拿到这颗芯片之后只需要新建一个板级dts比如rk3568-evb.dts然后#include rk3568.dtsi接着在板级dts里把自己用到的外设节点覆盖成status okay再补充板上额外接的设备比如某颗触摸屏芯片、某款以太网PHY、某颗eMMC就能完成一块板的适配。这样SoC厂家和板厂各管各的互不干扰。你如果要做多款板卡千万别复制一大份dts再改正确姿势是抽公共部分进dtsi每块板子只留差分片段。3.2 覆盖节点和追加子节点两种手段要分清在板级dts里你想把一个SoC的dtsi中已有的节点打开写法是uart2 { status okay; pinctrl-names default; pinctrl-0 uart2m0_xfer; };这里的uart2不是新创建一个节点而是对rk3568.dtsi中已有uart2节点做覆盖追加操作。方括号外面写的属性会合并到目标节点里如果属性名相同则以板级dts为准如果目标节点里原本没有这个属性就新增进去。这种机制非常灵活也是设备树强大之处。另外还有一种场景某个外设芯片挂在I2C0上但是这个外设节点并不存在于SoC的dtsi里那就要在板级dts里对它做完整的新增描述。写法是把新增节点直接挂在I2C总线节点下面然后指定compatible、regI2C从机地址、中断、复位脚等信息。这里要注意新增节点的reg是I2C从机地址不是寄存器地址千万别抄错。3.3 RK3568实战定制一块触摸屏的完整过程拿我近期调过的RK3568平台举例。硬件上我把一颗GT911电容触摸屏挂到了I2C0上复位脚接到GPIO3_C2中断脚接到GPIO0_B6。RK3568的SDK里已经有了基础板级dts我需要做的就是把I2C0的节点打开然后追加触摸屏子节点。大致改动如下#include rk3568-evb.dts i2c0 { status okay; clock-frequency 400000; gt911: gt9115d { compatible goodix,gt911; reg 0x5d; interrupt-parent gpio0; interrupts RK_PB6 IRQ_TYPE_LEVEL_LOW; reset-gpios gpio3 RK_PC2 GPIO_ACTIVE_LOW; irq-gpios gpio0 RK_PB6 GPIO_ACTIVE_HIGH; touchscreen-inverted-x 1; touchscreen-inverted-y 1; }; };这里面有几个细节值得展开。首先reg 0x5d是GT911的I2C地址这取决于芯片的ADR引脚电平实际如果地址是0x14之类的也要对应改。其次是interrupt-parent gpio0这表示中断线挂在GPIO0组上中断号和触发方式由interrupts RK_PB6 IRQ_TYPE_LEVEL_LOW给出。RK_PB6这类宏来自DT绑定头文件它实际上是GPIO bank的偏移计算方式。还要注意把irq-gpios属性一并写上因为goodix驱动会去申请这颗GPIO作为中断源如果不写驱动注册中断时可能因为拿不到GPIO而失败。写完dts后通过SDK的编译脚本生成dtb烧进boot分区。如果一切顺利内核启动时dmesg会输出类似input: Goodix Capacitive TouchScreen这样的信息。如果触摸无反应先查设备树有没有被正确加载再查pinctrl有没有被占用最后查reset gpio的电平时序这条排查路径我后面还会详细展开。3.4 pinctrl与GPIO说不完的爱恨纠葛设备树里配置引脚功能用的是pinctrl子系统。RK平台的dtsi里通常写了一大堆引脚组定义比如uart2m0_xfer、i2c0_xfer这些定义位于pinctrl节点下由引脚的IOMUX配置生成。你在板级dts里打开一个外设节点时通常还要设置pinctrl-0 uart2m0_xfer告诉内核这个节点用哪个复用配置。刚开始写的时候我经常忘记写pinctrl结果就是内核里设备明明注册了引脚却没被正确复用串口不出数据、GPIO输出高电平没反应。后来学乖了打开任何一个外设节点的第一步就是去查dtsi里有没有对应的pinctrl定义十有八九是有现成资源可以引用的。GPIO的定义则相对简单属性通常写成xxx-gpios gpioX 引脚号 GPIO_ACTIVE_HIGH。比如reset-gpios里引用的RK_PC2表示GPIO3组C2引脚这里的宏在dt-bindings/pinctrl/rockchip.h里定义。新手最容易把GPIO组号算错把GPIO3_C2当成GPIO2_C2写进去点灯怎么都不亮。4. 编译、反编译与bootloader的配合4.1 编译一个dtb你该走哪条路如果你用的是内核源码树编译设备树一般是make ARCHarm64 dtbs或者make ARCHarm64 rockchip/rk3568-evb.dtb这类命令。SDK打包流程里通常也集成了dtb编译比如直接执行./build.sh就会生成对应的dtb。petalinux工程项目则是通过petalinux-build -c device-tree来重新生 成设备树相关的内容。如果只是想快速验证一个单独的dts是否语法正确可以用这个命令dtc -I dts -O dtb -o test.dtb test.dts但如果dts里有#include引用了头文件这个命令大概率会报错因为dtc不做C预处理。正确的拆解流程是cpp -nostdinc -I include -undef -D__DTS__ -x assembler-with-cpp test.dts test.pre.dts dtc -I dts -O dtb -o test.dtb test.pre.dts你可以在内核源码里看到dtc的调用参数其实就是走了一套类似cpp预处理再调用dtc的流程。把这条链路想明白以后遇到找不到头文件之类的编译错误就能抄起命令行自己排查。4.2 反编译是调试神器我项目上最常用的调试操作是反编译。板子上跑着的DTB未必和源码一致你怀疑启动时加载的设备树不对就直接把boot分区里的dtb文件拉出来反编译dtc -I dtb -O dts -o debug.dts boot.dtb拿到反编译出来的dts先看有没有你改的节点再看status是不是okay最后看phandle有没有被冲掉。很多时候我改了dts但不生效的原因就是烧录压根没烧对反编译看一眼就能定性。还有一种不需要拉文件的方式板子在Linux运行时挂载了debugfs你可以直接查看运行中的设备树视图ls /sys/firmware/devicetree/base/ cat /sys/firmware/devicetree/base/model这个目录下能看到内核实际解析后的设备树结构每个节点就是一个目录每个属性就是一个文件内容全是原始二进制。想看树形结构可以直接用find /sys/firmware/devicetree/base/ -type f命令遍历一遍。4.3 uboot 2018与设备树的配合我们做RK3568平台时用的uboot版本大多是2017.09或2018.xx系列。从uboot 2016开始uboot自身也使用了设备树机制uboot的dts一般放在u-boot源码的arch/arm/dts/目录下板级文件叫rk3568-evb.dts。uboot在启动时会读取自己的DTB完成外设初始化然后把kernel的DTB地址传给内核。在uboot里你可以用fdt addr命令指定dtb内存地址用fdt print查看节点内容用fdt set修改属性。这些命令在做产品调试时非常有用。我们曾经遇到过一个问题kernel启动后串口控制台不出来排查后发现在uboot阶段fdt set修改了chosen节点的bootargs把console参数覆盖成了另一个tty串口。命令行的字符串一旦写错内核拿到了错误参数控制台自然就没有输出。如果你的平台是ZynqMP或者versal系列的Xilinx FPGA那么大概率会用到petalinux。petalinux设备树的管理方式和普通内核树不太一样它默认在project-spec/meta-user/recipes-bsp/device-tree/files/目录下放两个核心文件system-user.dtsi和pl.dtsi。system-user.dtsi就是留给用户的覆盖文件你可以在这里追加外设节点或者覆盖某些属性。petalinux-build的时候系统会用dtc把这些dtsi片段和内核里的基座设备树合并起来生成最终的DTB。所以你在petalinux工程里改设备树主战场就是system-user.dtsi尽量不要直接去动内核源码里的原始dts。4.4 设备树overlay机制聊两句除了直接把所有内容写进一个dtbLinux还支持设备树overlay插件机制。简单说就是把板级扩展设备的描述编译成一个单独的dtbo文件运行时通过configfs加载动态覆盖到基础设备树上。这个在树莓派、BeagleBone上用得非常多RK平台也支持类似能力。overlay的语法和普通dts基本一样但顶部声明稍有不同例如/dts-v1/; /plugin/; i2c1 { status okay; mydevice0x48 { compatible myvendor,mydevice; reg 0x48; }; };注意/plugin/;这个声明有了它dtc才会把外面的i2c1当成一个覆盖外部节点的片段而不是再去创建一颗独立的树。编译方式也类似dtc -I dts -O dtb - -o myoverlay.dtbo myoverlay.dts其中-表示生成带符号信息允许后续解析目标路径。在我做量产固件时overlay最大的价值是内核镜像不变只需要按需加载不同的dtbo就能让同一份系统支持多种硬件配置。不过这需要内核开启CONFIG_OF_OVERLAY和CONFIG_OF_CONFIGFS并在用户空间挂载configfs你先在开发板上把这条链路验证通了再往生产环境推广也不迟。5. 我在项目里踩过的设备树坑5.1 常见报错与排查思路做设备树开发不可避免会踩各种坑。我整理一张表把高频问题、现象和排查思路列出来方便你以后“按图索骥”现象可能原因排查方法内核找不到某外设dmesg无任何绑定日志compatible字符串拼写错误反编译dtb后grep字符串与驱动of_match_table逐一比对外设探测到了但功能异常如串口乱码pinctrl未配置或引脚复用冲突检查节点pinctrl-0引用确认引脚不被其他节点占用中断一直不触发interrupts类型或中断号算错对照SoC手册和dtsi里中断控制器定义确认SPI号启动阶段卡在memory相关报错reg或#address-cells/#size-cells定义不匹配在dts里打印memory节点确认大小未被截断触摸屏等设备ID重复busy提示多个外设reg地址冲突用i2cdetect扫描总线确认设备地址唯一uboot启动内核时flattened device tree报错dtb地址无效或dtb超限uboot下用fdt addr手动加载打印内存布局5.2 排查思路不能只会百度遇到设备树相关的问题我的固定套路是五步法第一步先从源码和反编译两个角度确认当前DTB里的真实内容。很多时候你以为自己改了某一行实际烧录的还是旧文件。第二步让内核先跑起来看dmesg和/proc/device-tree目录下的值。cat属性文件能看到原始二进制数值比如你想确认某个reg配置是否正确直接读对应文件再用十六进制工具看值就知道内核拿到的是什么。第三步利用sysfs验证驱动和设备的绑定关系。查看/sys/bus/platform/devices/或/sys/bus/i2c/devices/如果节点下的驱动是spi_master或者i2c_driver之类的名字说明绑定成功如果显示unknown说明compatible没对上或者驱动没编译进内核。第四步如果怀疑pinctrl问题去/sys/kernel/debug/pinctrl/下查看pin的状态。这里能看到当前每个引脚的复用功能和谁占用。两个外设抢同一根引脚在这里一眼就能看出冲突。第五步实在不行用printk或dev_info往驱动里加点调试打印看看of_match过程匹配到的到底是哪个compatible字符串。这一步会把问题范围缩小到是设备树描述错了还是驱动逻辑错了。5.3 我踩过的一个阴间bug说一个我印象特别深的装备树问题。在ZynqMP平台上调一款PHY芯片kernel启动时mdio总线能扫到这个PHY但网络死活link不上。我在dts里对比了phy-mode、phy-handle、reg地址都和硬件手册一致。最后用uboot的mii命令去读PHY寄存器发现PHY ID读出来全是0xff才意识到是PHY的复位GPIO时序问题。复位引脚在设备树里虽然有描述但reset-deassert-us参数配置过短芯片上电后还没完成内部初始化就被主控访问了。把延时从50us改到1000us后一切正常。这个坑其实不算纯dts语法问题但它揭示了设备树不止是地址和中断的集合很多外设的时序参数、电流参数、读写策略也藏在设备树里。你在写设备树的时候一定要养成查阅对应芯片官方设备树绑定文档的习惯别凭想当然写参数。5.4 版本对应关系千万不能乱设备树跟内核版本有非常强的耦合。你在Linux 5.10内核上写好的dts放到Linux 6.1内核上未必能编译通过同理uboot的dts和内核的dts也不是同一个文件两者可能语法上完全能用但节点表示方式差异很大。所以启动阶段uboot修改设备树、内核随后解析这中间如果格式不一致也会出现资源冲突或解析失败。我一般会记录一份每个BSP版本的三树对照表uboot使用的dts路径、kernel使用的dts路径、petalinux的system-user.dtsi覆盖路径。改代码之前先确认三棵树的对应关系可以省掉许多让人血压升高的排错时间。6. 最后给你几条实在的建议跟设备树打了这几年交道我最想说的是别把设备树当C代码写也别把它当文本文件乱改。它的本质是一套严谨的数据描述最好的学习路径就是亲手在一块板子上把某个外设从零调起来。实际操作中我的习惯是先在文档里画一个硬件连接草图标明每个外设的基地址、中断号、GPIO引脚、I2C地址、相关时钟、复位流程然后再去dts里逐个节点落地。这个草图其实比任何工具都好用它能逼着你把每个属性都对应到硬件本身。调试阶段优先用反编译工具确认当前系统解析的设备树和你设想的一致启动阶段多看uboot启动日志里的device tree字符串运行阶段多用sysfs和debugfs查询运行时状态而不是一遍遍重新编译烧录去赌运气。还有一个小技巧在板级dts里加一条自己独特的model字符串比如model my-evb-rk3568;这样以后从任何一份dmesg日志里都能快速确认当前烧的到底是不是你改的那份配置。这个习惯帮助我在一堆测试固件里准确找到目标版本。设备树文件看似平凡但它承载了整块板卡的“硬件基因”。把dts和dtsi的组织逻辑、节点引用方式、pinctrl与中断机制搞清楚你在嵌入式Linux开发里基本就打通了硬件和软件之间那道最关键的关卡。后面遇到再复杂的平台无非是这几招来回用。希望这篇能帮你少摔几跤少掉几根头发。