ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RK3568设备树DTS实战:从编写到编译调试的完整指南

RK3568设备树DTS实战:从编写到编译调试的完整指南 做嵌入式Linux开发提到“dts设备树文件”基本就是“这块板子的硬件说明书”。我前几年第一次接触RK3568设备树时光搞清楚 dts、dtsi、dtb 这几个后缀之间的关系就绕了一大圈后面自己动手改节点、加外设、调pinctrl每一步都是踩坑踩出来的经验。这篇文章不整虚的直接说dts设备树文件怎么组织、怎么写、怎么编译、怎么调尽量口语化但内容保证能落地。面向刚接手Linux驱动的开发者也适合正在RK3568这类瑞芯微平台上快速上手的工程师只要看完能自己改一个GPIO点灯节点或者挂上一个I2C外设这篇文章就没白写。1. 设备树到底是什么先搞懂这几个概念1.1 为什么会有设备树这个“万恶之源”在设备树出现之前ARM Linux内核每支持一块板子就要在arch/arm/mach-xxx/下塞一堆board-xxx.c文件里面全是硬编码的 platform_device、GPIO申请、中断注册表。芯片种类一多这些文件膨胀到基本没法维护而且一个内核镜像只能服务一块板子换个评估板就得重新编译内核。设备树Device Tree说白了就是把“硬件怎么接”的信息从C代码里剥离出来做成一份独立的数据描述文件内核启动时再根据这份文件来生成平台设备。同一个内核配上不同的dtb就能适配不同硬件这就是为什么现在几乎所有ARM Linux平台都默认走设备树。对普通开发者来说你并不需要关心设备树是怎么被发明出来的你只需要记住一件事dts设备树文件描述的是“有什么硬件、硬件挂在哪个地址、用什么中断、占哪些引脚”驱动则是通过节点里的compatible字符串来找自己的设备。写错dts驱动就找不到设备表现出来就是“设备没注册、不probe、没反应”。1.2 dts、dtsi、dtb、dtbo 别再傻傻分不清很多新手一进内核目录就被这些后缀搞晕其实它们的关系很好理解后缀全称/含义在项目中的作用.dtsDevice Tree Source板级设备树源文件描述一整块具体板子.dtsiDevice Tree Source IncludeSoC级公共片段可被多个板级dts引用.dtbDevice Tree Blobdts经过预处理和编译后的二进制启动时传给内核.dtboDevice Tree Blob Overlayoverlay格式的设备树增量用于运行时动态修改设备树可以这么类比dtsi是芯片原厂给的“标准户型图”dts是你的“装修方案”dtb是“最终施工图纸”dtbo是“后续加装家具的增补图纸”。我们平时改外设绝大部分都是在板级dts里覆盖或者追加SoC级dtsi中的节点比如rk3568.dtsi是瑞芯微官方定义芯片内部外设你的板子dts再#include rk3568.dtsi然后打开你要用的串口、I2C、SPI等资源。1.3 启动流程里谁是中间人设备树文件从源码到真正生效大致链路是这样的dts/dtsi 先经过C预处理器展开宏和头文件再由DTC编译器编成dtbU-Boot引导时读入dtb将它放到内存某个地址然后把地址通过寄存器传给内核ARM Linux约定r2寄存器内核启动早期解析dtb把节点转换成设备模型接着驱动通过compatible匹配并执行probe。这里有个最容易被忽略的点内核镜像本身的CONFIG_OF要开U-Boot环境变量里选的dtb文件名要正确。很多“改了dts没生效”的案例并不是语法写错而是你改了内核的dts烧录时却只更新了内核镜像dtb还是旧文件或者U-Boot里fdt_file指向了另一个dtb文件。我自己的经验是先确认启动日志里打印的 model 和你期望的值是否一致再开始排查节点问题。2. dts基本语法和必备属性2.1 节点、属性、值的基本写法先看一个最简设备树源文件骨架/dts-v1/; #include dt-bindings/gpio/gpio.h #include dt-bindings/pinctrl/rockchip.h #include dt-bindings/interrupt-controller/irq.h #include rk3566.dtsi / { model My RK3568 Board; compatible my,rk3568-board, rockchip,rk3568; #address-cells 2; #size-cells 2; chosen { stdout-path uart2; }; memorya0000000 { device_type memory; reg 0x0 0xa0000000 0x0 0x80000000; }; };/dts-v1/;是版本声明必须有。根节点用/ { };表示里面是一堆子节点。节点名的格式是“节点名unit-address”比如memorya0000000其中后面的地址要和该节点reg属性的第一个地址一致虽然不一致也能编译但属于有病不改内核和社区工具解析时会留下隐患。属性值常见就几种字符串写model My Board;整数数组写reg 0x0 0xa0000000;字符串列表写compatible a, b;字节数组写binary [00 12 34];。父节点里的#address-cells和#size-cells决定了子节点reg里地址和长度分别占几个32位单元RK3568这类64位SoC顶层通常是2个cell表示64位地址千万不要拍脑袋乱写。2.2 compatible、status、reg 这些属性怎么用compatible是设备树里最重要的属性驱动通过它来找设备。格式通常是“厂商,型号”比如st,lsm6ds3代表意法半导体的lsm6ds3。内核驱动源码里的of_device_id表会逐个比较匹配上就probe。建议写两个值第一个是精确型号第二个是通用兼容型号比如compatible my,board, rockchip,rk3568;这样即使后续板卡改版内核兼容性也更好。status只有几个合法值okay表示启用disabled表示禁用。原厂dtsi里很多外设为了省电或者避免资源冲突都是disabled板级dts里当你确认控制器没被占用再打开它。reg描述设备在总线上的地址或者寄存器偏移范围具体几个cell由父节点决定。比如I2C子设备里reg 0x6b;就是该外设在I2C总线上的7位地址。另外顶层根节点还会写model字符串这个会显示在启动日志里方便确认当前跑的是哪份设备树。chosen节点不是真实硬件它保存内核运行参数比如stdout-path指向串口节点、bootargs放命令行参数调试设备树时优先确认chosen。2.3 中断、GPIO、时钟、pinctrl 的描述方法中断描述有三个核心点中断控制器节点必须声明interrupt-controller;和#interrupt-cells N;使用中断的外设通过interrupt-parent指定控制器然后写interrupts。ARM GIC 一般3个cell第1个是中断类型SPI或PPI第2个是中断号第3个是触发类型。RK3568里常见写法是uart0 { interrupt-parent gic; interrupts GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH; };GPIO中断则是把GPIO控制器当作中断控制器用比如按键button { interrupt-parent gpio3; interrupts RK_PA2 IRQ_TYPE_LEVEL_LOW; };GPIO描述也很固定。GPIO控制器节点要有gpio-controller;和#gpio-cells 2;消费设备里写gpios gpio2 RK_PB2 GPIO_ACTIVE_HIGH;第1个cell是引脚名第2个cell是极性标志。时钟入门写法是clocks cru CLK_UART0;加clock-names baudclk;驱动里用devm_clk_get(dev, baudclk)取时钟。pinctrl则描述引脚复用板级dts里一般只写pinctrl-names default; pinctrl-0 uart0_xfer;这里uart0_xfer是在dtsi里已经定义好的引脚复用组。记住一个原则某个引脚如果被pinctrl配置成外设功能就不能在GPIO节点里再用同一个引脚内核会报pin busy。2.4 怎么引用和覆盖 dtsi 里的节点想修改原厂dtsi里的节点不能在板级dts里重新写一个同名节点那样会重复定义导致编译或解析问题。正确做法是使用 label 引用比如原厂在dtsi里写了uart0: serialfdd50000 { ... };你在板级dts里用uart0 { status okay; };添加或覆盖属性。如果要往某个控制器下挂子设备也在引用块里追加i2c1 { status okay; clock-frequency 400000; lsm6ds36b { compatible st,lsm6ds3; reg 0x6b; interrupt-parent gpio1; interrupts RK_PA2 IRQ_TYPE_LEVEL_LOW; vdd-supply vcc3v3_sys; }; };这套“引用覆盖”的写法是设备树区别于普通配置文件的核心优美之处SoC级文件保持不动板级dts只做增量修改既能跟随原厂更新又能保持自己的定制内容。3. 实操编写从一个RK3568外设节点开始3.1 动手写dts之前先做这三件事第一拿到板子原理图确认外设挂在哪个控制器上。以RK3568为例芯片有6组UART、多组I2C/SPI但引脚复用不是随便接的必须先看datasheet里的复用表确定要用哪个MUX组否则pinctrl配置就是错的。第二看原厂rk3568.dtsi里这个节点默认是什么状态很多控制器默认disabled你必须知道要打开哪些时钟、哪些引脚组。第三看内核源码里对应驱动的of_match_id确认compatible字符串和驱动匹配表完全一致尤其是大小写和逗号前后都不能错。很多人习惯直接抄老项目的节点但不同内核版本、不同芯片原厂甚至不同SDK分支节点名、时钟名、pinctrl组名都会变化。我踩过最大的坑就是拿RK3399的I2C节点硬套到RK3568上结果时钟名不对probe一直失败。3.2 先写一个GPIO LED节点练手GPIO LED是最容易上手的实验目标因为它不依赖复杂时钟和pinctrl只要一个GPIO空闲就能亮。RK3568板级dts里添加/ { leds { compatible gpio-leds; work_led: led-0 { label work; gpios gpio2 RK_PB2 GPIO_ACTIVE_HIGH; linux,default-trigger heartbeat; default-state on; }; }; };这段代码的意思是在根节点下创建一个leds子节点compatible gpio-leds交给内核的leds-gpio驱动处理。label会出现在/sys/class/leds/下面写成work后操作的就是/sys/class/leds/work。linux,default-trigger驱动会自动关联到触发源这里设成心跳灯可以直观看到内核活着。如果你想验证GPIO极性把default-state改成on如果灯不亮而反而不亮就把GPIO_ACTIVE_HIGH换成GPIO_ACTIVE_LOW。3.3 挂一个I2C外设LSM6DS3加速度计如果GPIO已经会写了下一步我强烈建议练挂I2C从设备。I2C外设在设备树里无非三件事控制器节点要打开、pinctrl要选对、子设备的地点和中断要确定。下面是一个完整示例i2c1 { status okay; clock-frequency 400000; lsm6ds36b { compatible st,lsm6ds3; reg 0x6b; interrupt-parent gpio1; interrupts RK_PA2 IRQ_TYPE_LEVEL_LOW; vdd-supply vcc3v3_sys; vddio-supply vcc3v3_sys; }; };我特别强调reg 0x6b这个值。数据手册里写地址经常是8位形式比如0xD6但设备树里I2C地址统一用7位表示0xD6右移一位就是0x6B。很多新手直接抄手册的0xD6结果I2C总线扫描时在0x6B能看到设备驱动却去访问0xD6自然失败。中断脚也要看原理图确认这个引脚有没有被别的外设占用同样的引脚如果已经在某个pinctrl组里被用作UART或者SPIprobe时就会报中断申请失败。3.4 从dts到dtb的编译过程独立编译一个dts可以用DTC工具dtc -I dts -O dtb -o rk3568-my.dtb rk3568-my.dts但实际内核项目里不建议这么干因为你的dts里有一堆#include和宏必须让内核对它做预处理。正确做法是在内核根目录执行make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- dtbs如果只编自己板子的dtb可以先确认arch/arm64/boot/dts/rockchip/Makefile里有没有把你的dts目标加进去然后执行make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- rockchip/rk3568-my.dtb构建系统会自动先调用C预处理器展开所有#include和宏再调用DTC生成二进制。看到这里你应该明白了为什么很多人在纯命令行下用dtc编译报“无法识别宏”的错——因为少了预处理这一步。3.5 U-Boot和PetaLinux环境下怎么处理设备树U-Boot 2018以后的版本默认也带设备树arch/arm/dts/目录下的dts用来描述U-Boot阶段需要的外设比如PMIC、DDR、网络、串口。它和内核设备树是两棵独立的树修改时必须分清你改的是哪一棵。U-Boot源码配置里可以通过环境变量fdt_file指定加载哪个dtb文件名比如fdt_filerockchip/rk3568-my.dtb这样U-Boot启动时会从boot分区读取对应的dtb再传给内核。如果你在用PetaLinux这种上层构建工具设备树的处理又会包一层。通常工具链会生成一个基础设备树然后允许你在user层覆盖自定义dts或dtsi比如PetaLinux工程里把自定义dts放到project-spec/meta-user/recipes-bsp/device-tree/files/执行petalinux-build -c device-tree重新打包。这类工具的坑在于它会做二次处理直接改内核源码里的dts不一定会进最终的镜像你要养成反编译最终dtb确认内容的习惯。4. 设备树调试的几个实战技巧4.1 反编译dtb确认最终烧进去的是什么设备树这行最实用的一句话一切以最终dtb为准。很多时候你以为自己改了但烧进去还是旧文件。拿到一个dtb后先用命令反编译成可读的dtsdtc -I dtb -O dts -o dump.dts boot.dtb然后直接看compatible、model、status这些关键属性。想快速读某个属性也可以用fdtgetfdtget boot.dtb / leds-node fdtget boot.dtb /model如果手头没有独立dtc可以用fdtdumpfdtdump boot.dtb | less调试状态下我一般会先看启动日志里打印的 model 字符串是不是我预期的如果不是说明U-Boot加载的并不是当前目录下的dtb优先排查分区烧录和环境变量。4.2 运行时设备树 /proc/device-tree 和 /sys/firmware/devicetree设备树被内核解析后会以目录树形式暴露出来。/proc/device-tree和/sys/firmware/devicetree/base指向同一个东西。你可以直接在板子上查看ls /proc/device-tree/ cat /proc/device-tree/model cat /proc/device-tree/leds/led-0/compatible属性文件末尾带\0直接cat有时会看到乱码建议用tr -d \0 /proc/device-tree/model这个运行时树是极好的调试入口。比如驱动没probe你可以进这个目录检查对应节点在不在节点在但status是disabled那就一目了然。另一个实用点是配合watch命令动态观察热插拔设备的overlay节点是否加载成功。此方法比反复重启快太多我已经习惯了上板第一件事就是看/proc/device-tree。4.3 用overlay实现动态设备插拔有些场景不适合改动主dtb比如你想在运行时不重新编译内核的情况下挂一个临时的I2C外设设备树overlay就是干这个的。overlay的dts写法多了一层fragment和__overlay__/dts-v1/; /plugin/; / { fragment0 { target-path /; __overlay__ { test-led { compatible gpio-leds; }; }; }; };编译overlay用DTC的-选项保留符号表dtc - -I dts -O dtb -o test-led.dtbo test-led.dts然后通过configfs把dtbo写入内核。我建议初学者先不搞overlay它虽然看起来很酷但调试难度比直接改主dtb高不少。先熟练掌握整体修改再研究动态加载。5. 常见问题与排查思路5.1 问题速查表现象可能原因优先排查方向改了dts没反应dtb没重新编译/没烧录/U-Boot加载了别的dtb反编译最终dtb确认model驱动不probecompatible不匹配/节点status是disabled/驱动模块没加载查of_device_id与dts兼容字符串pinctrl报pin busy引脚被多个节点占用全局搜索GPIO号清掉冲突节点中断申请失败interrupt-parent选错/中断号越界/触发类型不对对照SoC手册GIC中断表I2C找不到设备地址写错/从设备供电没开/时钟频率过高用i2cdetect扫描确认7位地址编译dts报宏错误直接用dtc裸编没走内核预处理用内核make dtbs带预处理流程5.2 改完不生效99%是流程问题这是出现频率最高的问题。每次改动dts后我的固定节奏是重新编译dtb确认产物时间戳反编译看关键节点烧录到正确分区启动日志确认model进入/proc/device-tree验证节点存在。如果做了全套还是没有才回去查语法。千万不要只在你的电脑上编译完说“明明改了”因为板子跑的很可能还是U-Boot从某个固定分区加载的旧dtb。另外注意部分bootloader会从FIT镜像或者boot分区内嵌dtb这时即使你擦除了根分区的dtb文件也没用得重新打包镜像。5.3 compatible匹配不上驱动到底在找什么内核驱动和设备树的匹配非常简单驱动源码里有of_device_id表比如static const struct of_device_id lsm6ds3_of_match[] { { .compatible st,lsm6ds3 }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, lsm6ds3_of_match);dts节点里的compatible只要任何一个字符串能和表里的匹配驱动就会probe。匹配不上的常见原因抄错了内核版本比如老内核驱动还叫st,lsm6ds2或者你在compatible里写了空格或者内核编译时根本没把驱动编进去成了“即使匹配上也没probe”。我建议遇到不probe先做两件事grep compatible在内核驱动源码里搜一遍确认关键字完全一致再执行modprobe或看/sys/bus/i2c/drivers/下有没有对应驱动。5.4 GPIO和pinctrl冲突同一个脚不能干两份活你在dts里既把某个引脚定义成GPIO输出又在某个串口的pinctrl-0里把它复用成UART引脚内核启动时会报类似pin 10 already requested by serial0的错。这种问题最隐蔽因为它不会导致内核崩溃只会在创建GPIO设备时悄悄失败。排查方法是全局搜索这个GPIO编号在dts里的所有引用然后用cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins查看当前占用情况。一般的处理思路是如果外设必须用这个引脚就删除或disbale冲突的GPIO节点如果只是调试就换一个空闲引脚。5.5 启动阶段解析设备树失败会怎样设备树本身有语法错误或者地址范围设置不对内核启动早期可能直接panic连串口日志都来不及完整打印。这种时候不要慌先检查dtc编译阶段有没有警告特别留意unit address mismatch和DTC: Warning (reg_format)这类信息。另一个常见原因是reg的cell数写错比如在#address-cells 1的父节点下写了两个地址cell内核解析时会把寄存器基址和长度读错后续任何外设的ioremap都会异常。养成每次编译都盯警告的习惯能省下大量启动调试时间。最后再分享一点我的实操心得设备树没有想象中神秘它本质上就是一份有约定格式的硬件清单而dts设备树文件写得对不对考验的不是语法背得熟不熟而是你会不会看原理图和SoC手册。我个人项目里最常用的操作其实是grep拿到一个外设节点后先在内核源码里grep它的compatible找到驱动实现再反向去读dts比凭空猜测高效得多。如果你刚接触设备树我的建议是先拿GPIO LED这类最简单的节点把完整流程跑通从改dts到编dtb、烧录、看/proc/device-tree过程中感受一次“改动生效”后面再碰复杂外设就有底气了。还有一个我自己长期保留的好习惯每次修改前都先把原厂dtsi打开放在旁边对照因为我发现至少一半错误不是语法问题而是没有理解原厂已经帮你做了什么。希望这篇文章能让你在遇到dts文件时少一点烦躁多一点“原来如此”的感觉。
返回列表