资讯动态

嵌入式Linux设备树语法与实战:从节点属性到RK3568外设配置

发布时间:2026/10/3 7:53:35 来源:尧图企业网站定制
干嵌入式Linux这一行绕不开设备树。跑个系统要先把.dtb编进镜像加个外设要改.dts节点驱动匹配不上第一个要查的就是compatible属性系统起不来最头疼的怀疑对象也是设备树。这些年我在RK3568、ZynqMP这些平台上折腾设备树积累了不少经验也踩过不少坑。今天这篇就把设备树语法从头到尾捋一遍从节点、属性、引用到编译、验证、问题排查再用一个RK3568上挂I2C温湿度传感器的实例带大家走完整个流程。适合刚入门Linux驱动开发的工程师也适合平时总在改设备树但说不清规则的老手。不管你用的是瑞芯微、全志还是ZynqMP平台只要你还是靠dtb启动系统这套知识就通用。1. 设备树到底是什么为什么嵌入式开发绕不开它1.1 从board文件到设备树硬件描述从代码里被拆了出来在早期Linux内核中ARM平台用一堆board-xxx.c文件来描述硬件板子上有哪些外设、中断引脚怎么接、I2C控制器地址在哪全写在C代码里。每个板子一个文件Linux内核因此积累了海量的平台代码不同厂商还反复重复定义同一颗SoC维护成本极高。后来PowerPC社区搞出了设备树用结构化的数据文件描述硬件内核启动时解析这份数据驱动再通过匹配字符串和节点去拿寄存器地址、中断号、引脚资源。ARM在2012年前后也全面切换到设备树。设备树存在的根本目的是把“硬件长什么样”和“驱动怎么写”这两件事分开。寄存器地址、中断号这些本来是驱动里的硬编码现在全部变成设备树节点里的属性数据。驱动不再直接关心某个I2C控制器具体在哪块地址只需要通过device_node提供的资源接口去获取。明白了这一点你就知道为什么设备树语法里最重要的就是节点层次关系、compatible匹配、reg地址和中断描述因为它们直接决定了驱动能不能找到硬件资源。这套数据描述方式不但大大减少了内核平台代码也让厂商BSP可以很方便地通过dts覆盖来适配不同硬件版本同一个SoC芯片可以衍生出几十款板卡而内核只需要一份通用的驱动。1.2 dts、dtsi、dtb、dtc一套文件体系和工具链设备树有几种后缀经常混在一起dts是设备树源文件dtsi是公共的include片段dtb是编译后的二进制dtc是编译器。实际项目中SoC厂商会发布一个包含所有硬件控制器默认状态的基础文件比如瑞芯微的rk3568.dtsi里面定义了串口、I2C、SPI、网卡、显示控制器等节点默认状态很多是disabled。板级dts再include这个dtsi并通过引用节点把需要打开的控制器状态改成okay、设置引脚复用、补充板载外设的子节点。所以你不是每次都要从头写一个完整设备树绝大多数工作是在板级dts里做“覆盖修改”。比如我最近调的一款RK3568板卡主控部分的串口、SD卡、以太网节点早就在厂商公开的rk3568-evb.dts里有现成配置我只需要在自定义dts里打开对应的一两个节点再加几颗板载传感器。dtc编译器通常跟随内核源码一起编译也可以在宿主系统上单独安装。还有像PetaLinux这类工具会通过硬件配置自动生成设备树但它生成出来的内容很大很乱语法不过关根本没法手工调整。我个人的体会是工具生成的设备树只能当参考最终你还是得自己看懂dts。1.3 动手前需要准备的环境这里补充一下实操环境这部分是通用做法不同平台略有差异。最省事的方式是直接准备一份内核源码里面自带dtc、所有dtsi和dts文件以及交叉编译链。以RK3568为例常见的内核路径是kernel/arch/arm64/boot/dts/rockchip/rk3568.dtsi和rk3568-evb.dts。也可以用独立安装的dtc包做单独实验命令是dtc常用参数包括-I dts -O dtb、-I dtb -O dts、-生成phandle和symbol支持。还需要能查看日志的串口工具方便启动后验证节点。工具链装好后用make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- xxx.dtb来编译单个设备树是最高效的不必编整个内核。实际我从入行到现在大约八成设备树问题都是先在编译阶段暴露的所以环境准备要多花一点时间。如果你只有dtc工具而没有内核源码也能做语法验证但一旦dts里出现#include和宏就需要先做预处理这个坑我会在第三章详细讲。2. 设备树语法核心节点、属性、值一个都不能漏2.1 节点是怎么定义的node-nameunit-address设备树就是一棵树根节点用“/”表示往下每一层都是子节点格式一般是node-nameunit-address。node-name用来表示设备类别比如serial、i2c、spi、ethernetunit-address是设备寄存器地址或总线地址要求有特定形式。例如RK3568的I2C0节点可以写成i2c0: i2cfeff0000其中i2c0是给这个节点起的标签后面真正给内核解析看到的是节点名i2cfeff0000。标签只是给人在dts文件中引用的别名编译后不会出现在设备树里除非启用了符号生成。节点名字并不是随便起的基本要求是1到31个字符只能包含小写字母、数字、逗号、点、下划线和连字符且必须以小写字母或数字开头。我见过有人把节点名写成“SHT30_Device”结果dtc直接报语法错误。如果节点没有地址比如根下的chosen、aliases就直接用纯名字。节点可以在一个dtsi里声明然后在另一个dts里通过标签继续追加子节点或修改属性这个后面细说。2.2 属性才是设备树的灵魂compatible、reg、status、interrupts节点本身只是树形目录真正让内核认识硬件的是属性。compatible是最要紧的匹配字符串格式是“厂商,芯片型号”如compatible rockchip,rk3568-gmac。驱动通过of_match_table匹配这个字符串一个节点可以写多个字符串比如my,sht30, sensirion,sht3x内核匹配时按顺序查找。reg属性用来描述设备的地址信息包括起始地址和区域长度但具体怎么解释完全取决于父节点的#address-cells和#size-cells属性。比如父节点写了#address-cells1、#size-cells1那么reg0xfeff0000 0x10000表示地址是0xfeff0000、长度为0x10000如果#size-cells0表示reg里只有地址没有长度很多I2C和SPI从设备就是这么写的reg0x44只表示从设备地址是0x44。status属性一般就两个值okay和disabledSoC默认关闭的控制器要靠板级dts改成okay才会被驱动probe。interrupts的写法取决于中断控制器比如ARM GIC的#interrupt-cells可能是3那么interruptsGIC_SPI 25 IRQ_TYPE_LEVEL_HIGH就表示共享外设中断号是25、高电平触发如果是2那第一段就是中断号第二段是触发方式。GIC_SPI这类宏来自头文件dts里直接写数字也行但不建议不同内核版本宏值可能变最好include相关头文件否则交叉编译后很容易出现中断配置错乱。2.3 引用的“语法糖”label、路径和节点覆盖这部分很关键。设备树源文件经常用到“引用加覆盖”形式是uart0 { status okay; };。这里的uart0并不是一个独立存在的节点而是找到标签为uart0的那个节点然后把花括号里的属性和子节点合并进去。这种引用语法在语义上很像编程语言里的语法糖实际效果就是把分散在不同地方的修改合并到同一个节点上。如果没有标签可以写完整路径覆盖例如{/soc/i2cfeff0000} { ... };但这种用法容易在dtsi路径改变后失效所以厂商代码普遍用标签。还有两个覆盖相关的特殊操作/delete-node/和/delete-property/可以把继承来的节点或属性删除。比如某dtsi默认有spi1节点但你的板子不想用它可以先spi1 { status disabled; };这不够彻底可以/delete-node/ spi1;把它整个删掉。此外aliases节点里可以用serial0 uart0;这种标签赋值来给设备起别名chosen节点里的stdout-path通常也引用别名用来告诉内核console设备是谁。理解了这些引用机制读dts就不会再一头雾水。2.4 其它高频属性pinctrl、clocks、gpios、regulator日常改设备树还会频繁碰到几类属性。pinctrl用于管脚复用例如uart0 { pinctrl-names default; pinctrl-0 uart0_xfer; };pinctrl-0引用SoC的pinctrl控制器里的一组引脚配置这组配置一般在dtsi中用节点定义包含引脚号和功能mux。clocks和clock-names配对描述设备使用的时钟源配合驱动里的clk_get或devm_clk_get例如clocks cru SCLK_UART0; clock-names baud;。gpios属性用于声明某颗GPIO例如gpios pio GPIO_A0 GPIO_ACTIVE_HIGH;这里pio是gpio控制器标签后面跟的是引脚bank、引脚index和极性标志。regulator电源管理IC也是设备树大户一般用一系列regulator节点描述各路供电驱动维护电压域时会用到。这类属性看起来多本质上都是同一套语法值可以是字符串、32位无符号整数数组、二进制数据也可以是对其它节点的引用。只要抓住属性名、属性值格式、关联控制器三个要素就能顺着dtsi一层层查下去。3. 实例解析在RK3568上添加一个I2C温湿度传感器3.1 需求与硬件接线为了不空谈语法我选一个很常见的场景RK3568主板上通过I2C0外接一个SHT30温湿度传感器芯片I2C地址是0x44内核里对应的驱动是sensirion,sht3x。我们在板级dts比如rk3568-myboard.dts中打开I2C0添加sht30子节点并验证Linux启动后能正确识别传感器。为什么选这个例子因为它覆盖了设备树语法里几个高频操作设置控制器状态okay、添加总线设备子节点、正确填写compatible和reg、编译dtb并在目标板上验证。很多人觉得设备树难其实像这样写一个最简单的从设备节点跑通流程之后后面再写SPI、GPIO、中断节点都是同一个套路。硬件接线这里我补充一下通用情况SHT30模块的SCL、SDA分别接到RK3568的I2C0_SCL和I2C0_SDA引脚GND和VDD接好上拉电阻一般模块自带。平台不同位置不同但设备树里不用管物理引脚因为I2C控制器节点和pinctrl已经把引脚复用关系写好了。你需要关心的只是控制器要选哪一个i2c0、从设备地址是多少0x44、驱动匹配字符串是什么sensirion,sht3x。3.2 编写完整DTS逐段看每个语法的意义以下是一个极简但可用的板级dts/dts-v1/; #include rk3568.dtsi / { model My RK3568 Board; compatible my,rk3568-board, rockchip,rk3568; aliases { serial0 uart0; }; chosen { stdout-path serial0:115200n8; }; }; i2c0 { status okay; clock-frequency 400000; sht3044 { compatible sensirion,sht3x; reg 0x44; status okay; }; };逐段解释一下。开头/dts-v1/是版本声明不能少。然后通过#include引入rk3568.dtsi这个头文件路径依赖内核编译时指定的include路径直接给dtc执行前一定记得先用cpp预处理。根节点下的model用来显示板卡型号compatible匹配机器级别内核启动时会拿它和MACHINE_START/DTS_MACHINE的匹配表对比。aliases和chosen给串口console用这里serial0 uart0的作用是给UART0起一个全局别名chosen的stdout-path让内核把日志输出到这个串口。之后是引用i2c0覆盖节点把原来rk3568.dtsi里i2c0的status从disabled改成okay设置总线时钟频率400kHz然后添加子节点sht3044。注意子节点reg0x44不需要写长度因为I2C子总线的#address-cells1、#size-cells0在父节点所在的总线定义里已经规定好了。此时覆盖合并后内核看到的i2c0节点属性里status是okayclock-frequency是400000子节点sht3044的compatible和reg都正确。这里有一个容易忽略的点dts中所有值都是32位无符号整数寄存器地址超过32位时要拆成两个cell写比如0x0 0xfeff0000这种具体看#address-cells。RK3568很多外设地址都在32位以内但ARM64全64位地址场景很常见看到reg里写两个数不要慌。3.3 编译、反编译与启动后验证编译这一步新手最容易卡住。直接执行dtc -I dts -O dtb -o rk3568-myboard.dtb rk3568-myboard.dts会报错因为dts里有#include宏、宏定义这些C预处理器语法必须先用cpp展开。更省心的方法是在内核源码目录里用内核的dtb编译规则它会自动帮你把include路径和C宏处理好。命令大概是make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- rockchip/rk3568-myboard.dtb这里rockchip是arch/arm64/boot/dts/rockchip目录下的子路径具体名字以你的平台为准。编译完成后会生成arch/arm64/boot/dts/rockchip/rk3568-myboard.dtb。如果你想脱离内核手工编译可以用类似下面的命令cpp -nostdinc -I kernel/include -I kernel/arch/arm64/boot/dts -undef -D__DTS__ -x assembler-with-cpp rk3568-myboard.dts | dtc -I dts -O dtb -o rk3568-myboard.dtb这行命令里cpp负责展开头文件和宏再把预处理结果喂给dtc注意include路径要按你实际的内核目录调整。每次编完设备树我建议顺手做一次反编译验证确认真正烧到板子上的dtb是不是你想要的。反编译命令是dtc -I dtb -O dts -o rk3568-myboard-decompiled.dts rk3568-myboard.dtb打开反编译结果重点看i2c0节点是否status变成okay子节点是否出现reg值对不对。这一步能拦截大量“改了dts却总不生效”的问题因为很多情况下你根本没把新dtb打进镜像。启动后在系统里查一下设备树是否解析成功/proc/device-tree目录下能看到一份与dtb内容一致的文件树你可以通过find /proc/device-tree -name sht30找节点。接着用i2cdetect -y 0扫描I2C总线能看到0x44地址有设备执行dmesg | grep sht3x或sensor读取工具确认驱动成功probe。实测中这一步跑通设备树基本就大功告成了。4. 设备树编译报错与运行排查常见问题实录4.1 高频问题排查速查表把多年经验浓缩成一张表问题现象可能原因排查思路dtc编译报unexpected character节点名含有大写字母或非法字符属性名写错引号未闭合按报错行号回看dts重点查节点名和分号编译Warning (unit_address_vs_reg)节点有reg属性但节点名地址部分不匹配让unit-address与reg第一个地址一致启动后没有生成设备节点compatible不匹配驱动status不是okay或节点路径错误确认驱动匹配表、查看dmesg、检查/proc/device-treei2cdetect扫描不到0x44I2C控制器没开、接线错误、地址不对、总线时钟过快确认statusokay检查硬件接线降低clock-frequency改了dts却不生效新dtb没编译进镜像、U-Boot没加载新dtb、设备树缓存反编译dtb确认内容查看boot log加载的dtb路径中断一直不触发interrupts格式不对interrupt-cells设置错误找到中断控制器节点确认#interrupt-cells和interrupts参数含义节点被覆盖但修改丢失标签写错、覆盖节点被放在错误位置、被delete-node删除搜索标签定义确认覆盖语法检查dtsi加载顺序这张表我建议收藏很多问题不是语法本身错了而是配置流程上没把新dtb用起来。4.2 dtc告警不是小事unit_address_vs_reg等很多人在编译时只关心有没有Error看到Warning就当成没看见。设备树的Warning往往埋着大雷。最常见的unit_address_vs_reg警告意思是节点有一个reg属性但节点名的unit-address与reg的地址值对不上比如节点名写成sht3044reg却写成0dtc直接提示。内核运行时虽然很多时候能容忍但影响设备树里查找节点、别名解析这些功能。还有simple_bus_reg、avoid_unnecessary_addr_size这些告警从名字就能看出问题总线节点上的#address-cells/#size-cells可能存在误导。遇到Warning我的习惯是逐条看能改就改。因为设备树是给内核用的数据不是你自己的笔记任何不一致最终都可能变成驱动probe失败或者系统启动异常。尤其是做产品化的工程师设备树里遍布Warning会让后面接手的人怀疑整体质量。4.3 运行时不生效怎么层层定位如果编译没问题、启动也正常但设备就是没出现这才是最耗时的。第一步在目标板上确认启动时加载的dtb是哪份很多板子会在FAT分区、UBoot环境变量或启动参数里指定dtb路径搞不清的话启动日志里会有设备树加载地址信息直接dmesg | grep -i dtb能看到线索。第二步用/proc/device-tree确认内核实际看到的节点比如cd /proc/device-tree ls soc/i2cfeff0000/如果没有sht3044子目录说明你改的设备树压根没进内核别急着查驱动。第三步看dmesg有没有of_platform、i2c相关日志以及驱动probe失败打印。第四步检查总线上设备树子节点和驱动匹配是否成功比如i2c设备驱动会通过i2c_match_id遍历compatible别名相关的宏定义要在头文件里对得上。这四步走下来大部分“不生效”问题都能定位到“设备树没被更新”或者“compatible匹配不上”。这一步我前前后后帮同事排查过几十次结论几乎都是设备树没错但没加载或者驱动不匹配。4.4 我踩过的几个坑标签重复、宏定义引用、多次覆盖最后分享几个我自己的实际操作经验。第一个坑是标签重复。不同dtsi里可能都定义了同一个标签比如uart0在多个soc变种文件里出现过板级dts再include时会发生重复定义覆盖如果你引用了其中一个标签来添加子节点最后到底合并到了哪份节点往往要反编译才能看清。解决方法是尽量沿用厂商BSP里的标签不要自己另起新标签。第二个坑是头文件里的宏。RK3568的pinctrl、clock、irq类型都定义在头文件里直接在dts里用宏名没问题因为内核编译设备树时会先过一遍C预处理器但如果你用独立dtc编译又忘了预处理就会看到一堆未定义的标识符报错。第三个坑是覆盖顺序。同一个节点的多次覆盖并不是后写的覆盖一定生效dtc把相同节点的属性合并如果子节点相同会叠加子节点但属性值后面的覆盖会替换前面的所以建议一个节点尽量只在一个地方覆盖完整别把i2c0的配置拆到两个dtsi里否则查问题会非常难受。5. 写给刚入坑的人我的设备树检查习惯我现在拿到一块新板子第一步一定是先把dtsi里的关键节点翻一遍找出根节点、chosen、内存节点、串口节点记录下它们的地址和状态。第二步编译新dts之后必做反编译确认改动真正进入dtb。第三步上板验证时先在/proc/device-tree里确认节点树再配合i2c/spi/gpio工具做最终验证。这套流程看起来慢但极大降低返工率。另外建议每个硬件外设节点都保持固定格式compatible、reg、interrupts、status写在一起并且加注释标明硬件手册出处。设备树源码很多是从厂商BSP继承来的不写注释三个月后自己都会忘。最后说一个小技巧拿到一块板子如果你不确定某个外设的兼容字符串到底应该填什么可以先去内核源码的drivers目录下搜对应驱动的of_match_table里面写了什么字符串你就填什么。这个习惯帮我避开了很多“照抄其它平台dts但驱动完全不认”的尴尬。设备树语法看起来琐碎实际掌握起来并不复杂核心就是节点、属性、值、引用四个概念外加一套编译和验证流程。多改几次、多踩几次坑自然会形成自己的判断。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑