ARTICLE DETAIL

资讯详情

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

嵌入式Linux设备树实战:从dts语法到RK3568调试全解析

嵌入式Linux设备树实战:从dts语法到RK3568调试全解析 刚接触嵌入式Linux的设备树时很多人都会被那棵层层嵌套、各种标号和中断绑定的树吓到。说实话我最初拿到一份RK3568的dts也觉得像在看一张迷宫地图。但当你真正动手改过几次、被无启动日志折磨过几回后会发现设备树其实就是一份“硬件配置清单”它的价值恰恰在于把硬件资源和驱动代码之间的对应关系用一种树形结构直白地描述出来。这篇文章我会结合瑞芯微RK3568、PetaLinux、U-Boot 2018这些大家在真实项目中常碰到的环境把dts文件从语法到实操掰开揉碎地讲一遍不管你是刚入行的驱动新手还是被上游dtsi搞到头大的老开发都能直接拿走照用。1. 设备树是什么为什么现在绕不开它1.1 从板级文件到设备树的演进在Linux 3.x之前内核里维护着一堆arch/arm/mach-xxx目录每个平台都放着一堆板级初始化代码把板子上的GPIO、中断、寄存器基地址全都硬编码进去。那时候换一块新板子最直接的办法就是把相近的板级文件拷过来改一改结构体、调一调地址再编一遍内核。这样做不是不行但维护成本会随着芯片平台数量的增加滚雪球。各家SoC厂商的板级补丁满天飞社区每上一个新版本都要在一堆平台上做心里没底的回归测试。设备树正是为解决这个撕裂感而生的。它把硬件描述从内核C代码里彻底剥离出来变成一份数据文件dts内核只管解析这棵树然后根据节点里的compatible属性去匹配对应驱动。这样一来同一个内核二进制可以启动不同硬件换板卡只需要换一个dtb文件不需要重新编译内核。它的本质就是硬件与驱动之间的一道契约承上启下让BSP开发逻辑更清晰。1.2 dts、dtsi、dtb三兄弟到底怎么分工我先从文件名讲清楚dts是设备树源文件dtsi是设备树公共头文件dtb是编译后的二进制目标文件。它们的生成链路是先有一组dts文件和若干dtsi文件dts通过#include指令把需要的dtsi包含进来然后由DTCDevice Tree Compiler编译成一个dtb。大家习惯把“dts”作为整个设备树的代称但实际动手时你改得最多的往往是厂商提供的xxx.dtsi因为它包含了SoC内部几乎所有的外设节点、中断控制器、时钟和引脚定义。很多初学者会犯一个理解性错误以为板级dts是独立写出来的像写单片机寄存器配置一样从头写到尾。实际上dts的编写大量依赖dtsi里的公共描述你的任务更像是一个“覆盖者”在dts里引用dtsi中已有的控制器节点补充板级差异的属性比如外接通了什么设备、I2C地址是多少、GPIO脚接在哪一路LED、内存大小等等。这种设计就是设备树的复用逻辑跟你写C语言用头文件做抽象如出一辙。1.3 设备树能做什么不能做什么设备树能描述的硬件种类非常多CPU核心数量与频率、内存布局、中断控制器连接关系、外设总线拓扑、引脚复用、电源域、时钟、GPIO、DMA通道甚至板子上的背光亮度、风扇转速传感器也能作为属性写在节点里。凡是用通用语言能概括的硬件信息都可以放进设备树。但要注意设备树不是一个完整的硬件配置系统它取代不了驱动程序寄存器级的操作逻辑也和硬件抽象层HAL不是一回事。它的边界在于它只做静态描述。如果一个外设的工作模式需要在系统运行中频繁切换比如双角色USB要根据插入方向改变主从那这种动态行为是驱动代码的职责设备树只负责告诉驱动“我有这个USB控制器在哪个基地址、支持哪些模式”。把这些边界搞清楚你写dts时就不会把一堆状态逻辑强行塞进节点属性导致设备树文件变得臃肿又难查。2. dts文件的基本语法与节点结构2.1 节点、属性与值类型速记设备树的基本单位是节点起始于一个带有路径名或者 label:node-nameunit-address 的节点定义花括号里则放它的属性和子节点。根节点固定是“/”整棵树都从它长出来每个设备在树里的路径就是从根节点一路下来的斜杠全路径。属性是键值对键是字符串值支持几种常见类型字符串、字符串列表、u32数组、二进制数组还有更复杂的组合形式。我随手写个最小例子给你看/dts-v1/; / { compatible myvendor,myboard; model MyBoard v1; memory40000000 { device_type memory; reg 0x40000000 0x20000000; }; };这里的memory节点告诉内核物理内存起始地址是0x40000000大小是512MB。reg属性的两个u32分别是地址和大小但在64位ARM平台地址和大小会各占两个u32写成0x0 0x40000000 0x0 0x20000000这种形式。值的定义方式虽然固定但不同节点的属性含义并不一致所以撰写时一定要针对具体芯片的参考手册来写不能光记语法。2.2 中断属性与中断控制器中断是dts里最容易出错的部分。每个能动态产生中断的设备节点一般需要三个关键成员 interrupts 声明中断号与触发方式 interrupt-parent 指向它的上一级中断控制器而中断控制器本身要在dtsi里标记 interrupt-controller 并为 interrupts 值的个数设置 #interrupt-cells。以ARM常见的GIC为例GIC-400通常是类型序号标志三个cell而GICv3的中断描述往往要两个cell再加上一个标识额外组的cell。我们看到的interrupts属性好比一张“维修单据”中断类型相当于报修渠道中断号是工单编号触发性质是你希望什么时候被叫醒。你必须在设备树里明确这个中断是从哪个控制器来的否则内核在初始化时会一脸茫然。实战中如果两个中断控制器chip且节点没指定interrupt-parent则会继承父节点的默认设置要是父节点也含糊中断注册就会出乱七八糟的失败。所以只要板级外设和dtsi里的GIC关系变了我第一件事就是检查这三个字段齐不齐。2.3 引脚复用pinctrl与gpio关系写外设dts时GPIO和pinctrl经常是一对孪生兄弟。pinctrl负责让引脚工作在某个mux功能比如GPIO3_B6这个引脚到底是普通GPIO还是UART_TX还是I2C数据线就要通过pinctrl属性去选。节点里的pinctrl-names用来声明状态名称pinctrl-0/1则是在某一个状态下选择的一组脚。最常见的有default、sleep等状态系统启动时自动应用default状态。GPIO本身则通过“gpio-controller”节点来抽象外设节点中引用GPIO时需要给出“哪个控制器第几号引脚有效极性”三个信息。很多新手会有一个误区以为把GPIO的pin号写在设备节点里驱动里就能按数字吉凶直接操作了。实际上设备节点里写的这个偏移号是针对某个gpio-controller节点的相对偏移而不是SoC手册里那种“PA0、PB3”的全局逻辑编号。我在RK3568项目里就把这个偏移关系重新换算过一遍不然用GPIO子系统的框架函数时index老是错位。2.4 节点引用、修改与覆盖设备树的灵活性很大一部分来自节点的“可覆盖性”。dtsi里定义了一个i2c2节点i2c2 { status okay; clock-frequency 100000; };这里i2c2就是引用节点label然后为它补充板级属性。这种写法让SoC级的dtsi保持通用板级dts只管覆盖。同样地要禁用一个设备就把status写成“disabled”要修改某个已有终止设备的reg直接在引用的花括号里覆盖属性。某些场景下还可以用 /delete-node/ 和 /delete-property/ 来删除指定节点或属性多用于去掉SoC上未引出的功能模块。需要注意覆盖的原则是“以最后出现者为准”。一个属性可以被多次修改后面出现的值会覆盖前面。所以调试时如果发现改动不生效先检查是不是dts后面还有别的include也写了同名字段。我记得有人在两个dtsi里重复定义同一个i2c节点的status一个okay一个disabled结果内核心事重重地把它禁用了找问题找了整整半天。养成只在一处地方做覆盖的习惯能省很多无谓排障。3. RK3568设备树实战拿现成的板子写dts3.1 骨架compatible、model与memory我建议你拿到一块RK3568板子时先看板级dts的头几十行。厂商一般会在include前把根节点和关键说明放在最显眼的位置/dts-v1/; #include rk3568.dtsi #include rk3568-evb.dtsi #include dt-bindings/gpio/gpio.h #include dt-bindings/input/input.h / { model MyRouter RK3568; compatible myrouter,rk3568, rockchip,rk3568; chosen { stdout-path serial2:150200n8; }; };compatible列表的顺序是有讲究的第一个值通常写最精确的型号名内核在匹配machine时按顺序比较找到第一个匹配的machine_desc就停。model字符串则用来给人看串口里打印的板卡型号基本就是它。chosen节点里的stdout-path指向调试串口设备这个错了启动阶段内核与控制台就断了联系你看不到任何日志。所以拿到新板子先把串口对应关系捋清楚再往里加业务设备这是我反复强调的铁律。3.2 内存与保留内存区域RK3568的dtsi里一般会根据bootloader传的tags或者ATF编译配置来处理内存但板级dts也可以写死memory节点。很多量产产品会选择在dts里直接控制内存范围因为要让预留区域给MFC、ISP、DSP或者ATF使用。比如/ { reserved-memory { #address-cells 2; #size-cells 2; ranges; dsp_reserved: dsp10000000 { reg 0x0 0x10000000 0x0 0x8000000; }; }; }; dsp_reserved { status okay; };这里的reserved-memory节点是标准的内存保留机制。你的内核、DSP、安全固件各占哪一块都要在设备树里先约定好。如果两个保留区域或内核内存区重叠启动到一半大概率会发生诡异的数据踩踏表现为RAM校验失败、驱动注册失败甚至是随机memset死锁。实际在双系统方案里我常常在reserved-memory与dsp驱动之间来回比对确保地址范围严格不越界。3.3 调试串口与aliases最快跑通手段一开始写dts建议先把串口状态设置正确这是后续一切日志的前提。RK3568的调试口一般是uart2你需要确认板级dts里是否把uart2的status改成okay并且正确设置了pinctrl比如uart2 { status okay; pinctrl-names default; pinctrl-0 uart2m0_xfer; };同时根节点里的aliases最好把serial绑定好aliases { serial0 uart0; serial1 uart1; serial2 uart2; };别小看这个aliases。U-Boot和内核找console、找根文件系统设备时经常需要根据一个稳定的别名来定位而不是扫描一串可能乱序的设备路径。如果chosen里写的stdout-path是“serial2:150200n8”但aliases根本没把uart2映射成serial2控制台设备初始化时就会找不到目标打印直接丢失。这块我在调一块非标准主板时踩过连默认console路径都不认完全是靠看代码才发现的。3.4 I2C外设节点的写法与驱动匹配RK3568的I2C外设很常见比如板载触摸屏、温湿度传感器、RTC。一个典型的I2C设备节点长这样i2c1 { status okay; clock-frequency 400000; sensor76 { compatible national,lm75; reg 0x76; interrupt-parent gpio3; interrupts RK_PC1 IRQ_TYPE_LEVEL_LOW; pinctrl-names default; pinctrl-0 sensor_int_pin; }; };这里compatible是驱动里of_match_table要匹配的字符串。如果驱动名称是“lm75”它在match表里会写“national,lm75”你dts里就不能写成“lm75”或者“lm75a”。初学者最喜欢在这上面翻车节点写对了reg地址也对但驱动就是没probe。用ls /sys/bus/i2c/devices/看看会发现系统已经枚举出设备但始终没有driver竞态响应八成就是compatible没对上。在决定自己的兼容字符串时建议遵循vendor,device这种规范格式避免和社区既有命名冲突。3.5 GPIO-LED与按键的经典写法调试阶段往板子上挂LED是非常高效的。RK3568的GPIO可能有多个组比如GPIO0到GPIO4每组32个pin。LED节点写法gpio-leds { compatible gpio-leds; pinctrl-names default; pinctrl-0 green_led_pin; status-led { gpios gpio0 RK_PA0 GPIO_ACTIVE_HIGH; default-state on; label green:status; }; };其中的gpio0对应dtsi里的gpio0节点RK_PA0宏代表这一组里的第0个IO。GPIO_ACTIVE_HIGH是一种极性宏如果我们把LED接到VCC和GPIO之间点亮逻辑是GPIO拉低那就要写GPIO_ACTIVE_LOW。代码里用gpiod_set_value时内核会根据设备树里的极性自动转换。如果你板子接法写反了LED可能常亮不灭或者一亮一灭刚好反过来非常直观也方便回头对照原理图训练自己的设备树感觉。3.6 pinctrl配置mux、上拉与驱动强度引脚复用在RK3568上是个重点。同一个物理引脚可能支持几十种功能所以dtsi里往往把各个mux组定义成pinctrl节点。有时你需要在板级dts覆盖对应的pinctrl让引脚工作成特殊功能。比如一个一键休眠键我们可以把它定义成普通GPIO唤醒源pinctrl { power_key { power_key_pin: power_key-pin { rockchip,pins 2 RK_PB5 RK_FUNC_GPIO pcfg_pull_up; }; }; }; pmu_io_domains { // 相关电压域配置具体以芯片手册为准 };rockchip,pins数组最后一个元素是pcfg_xxx它引用了dtsi里的公共pin配置节点比如上拉、下拉、驱动强度、施密特触发等。这个数组的值比较多我通常用厂商提供的模板作为参考自己完全手写容易漏掉某项。配置完pinctrl后还得留意被复用到的原功能是否已经被禁用。比如同一个引脚之前被SDMMC用了你改成I2C功能就得先找到对应的SDMMC节点status设为disabled否则两个子系统都会争夺同一个pin启动时驱动初始化就会互相干架。4. 编译、烧录与运行时调试4.1 用dtc编译与反编译设备树当你改完dts需要把它变成dtb。常规方法是进入内核源码树执行make ARCHarm64 dtbs或者单独编译某个dtbmake ARCHarm64 rk3568-myboard.dtb如果只想快速验证语法也可以直接用dtc工具dtc -I dts -O dtb -o test.dtb test.dts反过来从现成dtb看别人怎么写的用反编译dtc -I dtb -O dts -o dump.dts boot.dtb对uImage/zImage启动的场景dtb通常要通过烧录工具单独烧到某个分区。如果是用U-Boot读取dtb要保证dtb地址和加载大小都对得上否则内核会读到残缺数据启动日志可能会在解压初期直接卡死或报“FDT:ERR_OVERLAP”之类的错误。我一般习惯在修改后先反编译dtb再搜一遍关键节点名确认改动确实编译进去了防止改错了文件却还在盲调。4.2 U-Boot 2018下的设备树加载流程U-Boot 2018这个版本很多RK和ZynqMP平台都在用。它加载设备树时不会停留在启动内核那一刻而是在环境变量里通过源码树和分区设定来指定dtb的位置。常见启动流程是setenv bootargs consolettyS2,1500000 root/dev/mmcblk0p1 rw rootwait fatload mmc 0:1 0x10000000 boot.img ext4load mmc 0:2 0x10080000 myboard.dtb booti 0x10080000 - 0x10000000也可以先用U-Boot的fdt命令对设备树现场打补丁比如修改mac地址或关闭某个节点fdt addr 0x10080000 fdt set /ethernet0 local-mac-address AA:BB:CC:DD:EE:FF fdt set /usbotg status disabled这在量产调试阶段特别管用不用重新编译dtb直接在U-Boot命令行改完再启动内核确认配置影响后再把改动固化到dts源文件。我调试RK3568网卡时经常这样临时切换phy地址。不过要看清楚U-Boot是在启动前用的fdt还是直接传递dtb给内核需要在板上的fdt_addr变量里写好对应内存地址改错了就是启动崩溃。4.3 PetaLinux环境里的设备树管理在Xilinx/AMD的PetaLinux工程里设备树文件通常位于项目目录的components/plnx_workspace/device-tree/device-tree/或者更常见的project/project-spec/meta-user/recipes-bsp/device-tree/files/。PetaLinux的优势是硬件平台描述和软件配置可以互相对应特别是PS侧外设如UART、I2C、CAN这些它会在生成设备树时从硬件的XML或者hdf中自动带出。但自动生成不等于百分之百满足你的定制需求所以你仍然要在meta-user的dts或dtsi里写覆盖。你需要修改PetaLinux设备树时可以运行petalinux-config -c device-tree或者直接编辑文件然后做一次完整的设备树重建petalinux-build -c device-tree需要注意PetaLinux工程里常常会有system-user.dtsi作为用户覆盖入口你把自定义的节点和属性放到这个文件里会减少和自动生成dtsi的冲突。如果直接去改那个自动生成的dtsi下次从Vivado导出hdf并重新build改动就会被打回原形。保持“自动生成不动用户覆盖另写”是最成熟的PetaLinux设备树工作流。4.4 常见编译错误与节点合法性检测设备树不仅会报语法错误还会报节点结构错误。比如“reg property size does not match #address-cells”就是典型的寄存器地址范围描述错乱往往发生在64位地址加32位size取值混用。需要先检查根节点的#address-cells和#size-cells再看具体节点的reg里给了几个u32。另一个高频报错是“interrupts property size does not match #interrupt-cells”说明中断属性cell数写错了尤其常见的是把GIC type/specifier和其他控制器混在一起抄。DTC还会提示“Label or path not found”说明你引用了不存在的节点label。这个我先检查是否漏了include再检查是不是拼错。设备树编译器给错误信息还算直白看多了就知道大部分问题集中在reg、interrupts、clocks这些带数量含义的属性上。建议每次改完都养成“改一个节点、编译一次”的好习惯不像C语言有完整IDE提示设备树错误堆栈信息稍弱小步快跑反而是提效关键。5. 常见问题与排查技巧实录5.1 compatible总是匹配不上驱动如果驱动里查得到of_match_table但dts里compatible不会触发probe先用/sys/firmware/devicetree/base/查看设备树解析后的实际节点内容确认节点里的compatible字符串和驱动match表完全一致。其次检查节点状态是不是“disabled”很多板级dts把默认外设都关了。还有一种易漏的情况是这个设备的父节点总线没有启用比如i2c master节点没设status为okay那么挂在它下面的子设备即使有compatible也不会被bus探测。5.2 启动日志里设备注册了但没输出有时候I2C子设备节点写对了驱动也匹配成功但是只有/dev/i2c-x没有生成实际的/dev/哑设备名。这往往是驱动里的总线探测没有成功比如设备上电时序不对此时读寄存器失败probe直接返回负值。跟设备树本身无关但你会误以为改dts能解决。我的经验是先看内核日志里有没有“xxx: probe of 1-0076 failed with error -11”如果是复位延迟不够就要在驱动或添加延时逻辑而不是继续改dts时间节点。5.3 GPIO数量对不上内核直接报错RK3568的GPIO组每个控制器有32个IOdtsi里用GPIO_ACTIVE_HIGH定义极性。如果你在gpio-leds里写了一个超出该控制器范围的gpio号内核启动时会报“invalid GPIO”并放弃这一项。排查方式非常直接对照SoC引脚表的行列号换算成控制器内相对偏移同时检查另一组控制器是否更像正确归属。建议先把一颗LED按原理图确认能亮再批量添加其他LED不然多个GPIO同时写错很难理清。5.4 中断卡死或频繁触发设备树中与中断有关的问题往往不会在编译时报错而是在运行中突然“irq xx: nobody cared”或者产生中断风暴。大概率是触发类型写错了比如一个电平触发的中断写成边沿触发或者设备树里中断号与硬件实际断点不一致。我调试底板时经常在IRQ handler里挂一个打印计数器通过中断次数判断触发极性是否正常。同时用cat /proc/interrupts确认中断是否注册到了预期编号这个编号对照设备树里的interrupts描述是最直观的。5.5 如何快速确认当前生效的dtb到底是谁生产环境最尴尬的事情不是写错dts而是不知道自己烧进去的dtb是哪一个。可以在U-Boot阶段把它反编译出来也可以用内核运行时检查ls /proc/device-tree这个虚拟文件系统会把当前内核正在使用的设备树导出成目录结构你可以直接查看compatible、model等属性内容。如果发现找不到你自己加的节点而盘里明明烧了新dtb别急大概率是U-Boot启动命令里dtb地址或分区选错。此时回到U-Boot用fdt命令逐个地址验证看哪个地址里的dtb包含你的模型字符串就能锁定真正加载的文件了。5.6 生产环境下建议的dts开发流程在我实际维护RK3568项目的过程中最推荐的工作流是先从厂商自带、功能完整的dtsi剪出一个最小可引导板级dts只留下串口、内存、看门狗确保能启动到shell然后每次加一类外设就做一个独立提交比如先加上I2C控制器再加I2C子设备再加相关pinctrl最后验证一遍引脚占用这样后期出了问题用二分法来回查效率高到不可思议。另一位很受用的习惯是把所有板级差异集中注释在一个dts文件的开头写上对应原理图版本号、核心板型号和分区表。当时同一条产线就出现过两版硬件共用一版软件dtb的经历排查下来才发现是dts里预留了完全不同的SARADC键值。硬件改版和软件版本必须呈一一对应关系这个纪律能避免大量潜在问题。设备树的本质本来就是把硬件差异说清楚你没把它当文档维护那迟早会被它反咬一口。最后分享一个小技巧不管是什么平台先学会反编译dtb再开始写自己的dts。因为厂商给你的成品dtb往往比文档表述得更诚实你能从中看到pinctrl实际选的是哪个mux组中断到底连到了GIC的哪个中断号aliases被安排成了什么顺序。把这些模板翻透再动手覆盖自己的板级差异比从零开始对着芯片手册拼一个dts要可靠得多。设备树这棵树看着盘根错节但只要你每次都从根节点出发顺着一条具体的硬件路径追下去它并不难驯服。
返回列表