ARTICLE DETAIL

资讯详情

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

嵌入式Linux设备树实战指南:从DTS语法到驱动匹配与调试

嵌入式Linux设备树实战指南:从DTS语法到驱动匹配与调试 我现在带的两个新人上周同时进了同一个坑改完一块 RK3568 板子的设备树重新编译内核烧进去之后屏幕死活不亮。一个以为是显示驱动有问题翻了一下午 DRM 代码另一个怀疑是触摸屏驱动没加载反复 insmod/rmmod。结果呢两个人谁都没先去看设备树到底改对了没有——一个改错了节点路径另一个改完了没重新编 dtb。这不怪他们设备树这玩意儿入门文档看着浅实际操作全是暗坑。这篇文章就一次把设备树讲透。从它为什么会出现、语法结构长什么样到内核启动时怎么解析、驱动怎么写才能拿到你要的资源最后再给几个真实板子上摸爬滚打出来的排查套路。适合刚接触嵌入式 Linux 的初学者也适合那些已经被 dts/dtsi 折磨过几轮的驱动开发。看完你至少能做到拿到一块新板子知道从设备树哪个节点下手改完设备树知道怎么确认它真的生效了。1. 设备树到底是个啥为什么 Linux 内核非要搞这么个东西1.1 没有设备树之前驱动是“写死”硬件信息的现在的年轻人可能很难想象2010 年之前的 ARM Linux 内核每个开发板都在 arch/arm/mach-xxx 目录下躺着十几个 C 文件。那会儿驱动要描述硬件不是靠一张配置文件而是直接在 C 代码里把寄存器地址、中断号、时钟资源全部写死。举个例子以前要注册一个 UART 平台设备你得写类似这样的代码static struct resource uart_resources[] { [0] { .start 0x10000000, .end 0x10000fff, .flags IORESOURCE_MEM, }, [1] { .start 42, .end 42, .flags IORESOURCE_IRQ, }, }; static struct platform_device uart_device { .name vendor-uart, .resource uart_resources, .num_resources ARRAY_SIZE(uart_resources), };这种代码最大的问题不是丑是没法复用。厂商每出一个新板子内核源码树里就多一个 mach-xxx 文件夹同一个内核镜像想在几块不同板子上跑几乎不可能。更痛苦的是这些硬件描述代码和驱动逻辑混在一起主线内核想维护都维护不动。所以 ARM Linux 社区最后学乖了决定全面转向 Device Tree。这套东西最早来自 PowerPC思路其实特别简单既然硬件描述和驱动逻辑老打架那就把它们俩彻底分开——硬件长什么样写在一个独立的数据结构里驱动只负责按图索骥。设备树就是这张图。1.2 设备树的核心思路把硬件描述从代码里拆出来设备树本质上是一棵描述硬件拓扑的树形数据结构用文本格式书写相当于一块板子的“硬件配置表”。它的基本组成单位是节点node和属性property节点表示一个硬件设备或者总线属性描述这个设备的具体参数比如寄存器基地址、中断号、时钟频率、引脚编号。围绕设备树有四个缩写先分清它们后面所有讨论都有共识缩写全称作用DTSDevice Tree Source设备树源文件纯文本格式描述一块具体板卡DTSIDevice Tree Source Include设备树公共片段可以被 DTS 通过 include 引用用于存放 SoC 公共部分DTBDevice Tree Blob由 DTS/DTSI 编译出来的二进制文件内核启动时实际使用的就是它DTCDevice Tree Compiler编译 DTS 生成 DTB 的工具叫“编译器”但功能远没有 gcc 复杂打个比方设备树就像装修设计图驱动是施工队。施工队不会在进场前就自己脑补墙往哪砌而是照着设计图干活如果业主要改个插座位置只需要改设计图不需要把施工队全员换掉。对应到嵌入式开发里换板子改硬件大多数时候只需要改 DTS驱动代码可以原封不动。设备树里最核心的一个属性叫 compatible。它的格式是“厂商,型号”比如 rockchip,rk3568-uart、atmel,24c02。这个字符串就是硬件的身份证驱动靠它来识别“这设备归我管”。后面讲驱动匹配的时候所有问题几乎都绕不开 compatible 这三个单词。2. 设备树语法与文件结构看懂一个 dts 不只靠猜2.1 最小的设备树长什么样不整虚的直接看一个最简设备树把结构骨架先立起来/dts-v1/; / { compatible vendor,my-board; #address-cells 1; #size-cells 1; chosen { bootargs consolettyS0,115200 root/dev/mmcblk0p2; }; memory60000000 { device_type memory; reg 0x60000000 0x20000000; }; soc { compatible simple-bus; #address-cells 1; #size-cells 1; ranges; uart0: serial10000000 { compatible vendor,uart; reg 0x10000000 0x1000; interrupts 0 42 4; }; }; };这段代码虽然简单但设备树里的关键概念基本全带到了。/dts-v1/;是版本声明表示当前文件遵循设备树规范 v1必须写在最前面。根节点/下面直接挂了一个 compatible用来标识整块板子的型号比如 rockchip,rk3568-evb。#address-cells和#size-cells是给下一级子节点看的分别表示 reg 中地址字段和长度字段需要用几个 32 位整数来描述。这里都是 1说明地址和长度都只需要一个 32 位整数即可。reg 0x60000000 0x20000000表示寄存器或内存区域的物理起始地址 0x60000000、大小 0x20000000具体怎么解析就看父节点定义的那两个 cells。uart0: serial10000000里的uart0是标签label后面可以用uart0引用它serial10000000才是节点名 后面的部分一般是对应的起始地址。interrupts 0 42 4这种格式不是随便写的它的含义取决于父节点的#interrupt-cells定义。比如值里面可能包含中断域标识、中断号、触发方式等必须对着具体的 interrupt-controller 节点去理解。2.2 节点、属性、标签与 compatible 的匹配关系设备树的节点是分层的层级关系通过花括号嵌套表达。每个节点可以有任意多个属性属性值类型常见的有这么几种字符串compatible vendor,device;字符串列表compatible rockchip,rk3568-uart, snps,dw-apb-uart;32 位无符号整数数组reg 0x10000000 0x1000;用尖括号包起来多个值用空格分隔。64 位整数数组clock-frequency 0x00 0x5F5E100;实际是两个 32 位拼成一个 64 位。二进制数据local-mac-address [00 11 22 33 44 55];用方括号包裹每个字节两位十六进制数。属性值的空格、引号、尖括号、方括号全是语法的一部分写错一个字符dtc 就会直接报错。关于 compatible再提醒一点它是个字符串列表匹配的时候内核会按顺序依次尝试。所以常见的写法是把最具体的型号写在最前面然后把兼容的通用型号跟在后头。比如某颗 PHY 芯片节点 compatible microchip,ksz9031, ethernet-phy-ieee802.3-c22前一个是驱动精确匹配用的后一个是内核通用 PHY 框架兜底用的。这种“先精确定位再兼容兜底”的模式在设备树里非常常见。2.3 dtsi 与 dts 的组织方式include 到底怎么用真实项目中你不会只看到一个孤零零的 dts 文件而是一个 dtsi 家族。习惯做法是SoC 厂商提供 SoC 级别的 dtsi比如 rk3568.dtsi里面定义好 CPU、中断控制器、时钟、I2C/SPI/UART 控制器等所有内部外设状态默认为 disabled。核心板厂商再提供核心板级别的 dtsi比如 rk3568-evb.dtsi在里面把部分外设打开加上 DDR、eMMC 等配置。具体产品工程师写板级 dts比如 my-product.dts里面用#include包含核心板 dtsi然后只改自己跟别的板子不一样的地方。这种层级组织方式极大提高了复用性。但初学者最容易踩的坑是uart0 {}这种语法在 dts 里到底是怎么生效的uart0 { status okay; pinctrl-names default; pinctrl-0 uart0_xfer; };uart0表示“引用标签为 uart0 的那个节点并在它原有的内容基础上追加/覆盖属性”。注意这里不是新创建一个节点而是修改节点。如果你在 rk3568.dtsi 里把serial10000000定义为 disabled而在板级 dts 里打开它最终生效的就是板级 dts 里的值。这就是设备树“越具体的文件优先级越高”的基本原则之一。#include在设备树里不是设备树语法而是 C 预处理指令。也就是说dts 文件在真正编译前会先经过 C 预处理器展开头文件、处理宏定义。这也是为什么 dts 里可以写#include dt-bindings/gpio/gpio.h然后直接引用GPIO_ACTIVE_HIGH——这个宏就是在头文件里定义的预处理阶段会被替换成数字 1。2.4 dtc 编译与反编译dtb 怎么来、怎么查设备树编译本身不复杂如果你只想把一份 dts 编成 dtbdtc -I dts -O dtb -o my-board.dtb my-board.dts但在实际内核工程里一般不需要手动敲 dtc你只要在内核源码目录下执行make ARCHarm64 my-board.dtb # 或者 make ARCHarm64 dtbs内核的 Makefile 会自动找到对应的 dts并用内核自己携带的 dtc 版本编译。这里有个经验之谈尽量用内核源码目录里的 scripts/dtc/dtc别图省事用系统安装的独立 dtc。因为不同版本对设备树新语法比如 interrupt-map、gpio-hog 这类支持程度不一样内核里的版本总是跟当前内核源码最匹配。还有一招是反编译。拿到一个别人给的 dtb怎么快速知道它里面写了什么直接反编译dtc -I dtb -O dts -o dump.dts boot.dtb我拿到一块新板子第一反应不是翻厂商 SDK 里的 dts 源文件而是先把 bootloader 实际加载的 dtb 反编译出来看一眼。因为有时你手里的 dts 源码和烧进板子的 dtb 并不是同一个版本这种“源码和实物不一致”的破事我见过太多次了。反编译出来的 dump.dts 就是板子上真正生效的硬件描述以它为准排查问题事半功倍。3. 设备树在内核里是怎么被“吃”掉的启动链路与 API3.1 从 Bootloader 到内核DTB 怎么交接设备树文件编译成 dtb 之后要由 bootloader多数情况是 U-Boot加载到内存然后再把控制权交给内核。在 ARM64 平台上约定是通过特定寄存器把 dtb 在内存中的物理地址传给内核内核启动早期会去这个地址解析设备树。这个过程说复杂也复杂说简单也简单只要 bootloader 和内核约定好 dtb 放哪、怎么传剩下的事内核自己搞定。U-Boot 里常见的操作大致是这样# 加载 dtb 到内存比如 tftp 或者从 mmc 读 load mmc 0:1 0x4000000 my-board.dtb # 加载内核镜像 load mmc 0:1 0x4008000 Image # 启动把 dtb 地址和内核地址都传进去 booti 0x4008000 - 0x4000000这里踩坑非常多。常见的一个是 bootloader 里加载 dtb 的地址不对导致内核读到的是一堆垃圾数据启动卡死或随机崩溃。另一个是 U-Boot 环境变量里的 fdt_addr 和实际加载地址不一致。排查这种问题建议先在 U-Boot 命令行执行fdt addr 0x4000000再执行fdt print /如果设备树结构能正常打印说明这块 dtb 是完好的如果连根节点都打印不出来那就是加载阶段已经错了。另外U-Boot 还可能在启动前对 dtb 做修改。比如根据开机拨码开关动态改 mac 地址、根据 EEPROM 里的配置开关某个外设。这类修改是在 fdt set/fdt rm 这类命令里完成的。所以板上跑的 dtb 大概率已经被 U-Boot 动过刀了这就是为什么我前面强调要反编译“板子上实际加载的 dtb”而不是盯着源码文件看。3.2 驱动怎么通过 of_ 系列 API 解析硬件信息设备树是给驱动提供硬件描述信息的数据源但驱动不可能在 probe 里把整个树遍历一遍。Linux 内核提供了一大套 of_ 开头的 API让驱动按节点路径、按属性名去读取想要的数据。在 platform_driver 的 probe 回调里你通常拿到的struct platform_device *pdev中的pdev-dev.of_node就指向了这个设备对应的设备树节点。有了 of_node就能读取各种属性static int my_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct resource *res; void __iomem *base; u32 irq; u32 param; int ret; // 1. 获取 mem 资源reg 属性中的第 0 段 res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(dev, res); if (IS_ERR(base)) return PTR_ERR(base); // 2. 获取中断号中断属性中的第 0 个 irq platform_get_irq(pdev, 0); if (irq 0) return irq; // 3. 读取自定义属性 ret of_property_read_u32(dev-of_node, vendor,some-param, param); if (ret) { dev_warn(dev, missing vendor,some-param\n); param 0; } return 0; }platform_get_resource和platform_get_irq其实底层也是解析设备树上的 reg 属性和 interrupt 属性再把它转成传统内核里 platform_device 的 resource 结构体。所以你会发现设备树虽然外面长得很陌生但到驱动这一层很多老 API 依然能用——这就是内核为了平滑过渡做的兼容设计。另外一类常用的 of_ API 是of_property_read_*系列比如of_property_read_string、of_property_read_u32_array还有of_get_named_gpio读取 GPIO 引脚号、of_clk_get读取时钟。这些 API 的返回值值得特别重视但凡返回负数基本都是属性不存在、类型不匹配、索引越界多数情况是设备树和驱动没有对齐。我一般会强制团队在读取属性后马上打印一次 dev_info把关键参数打出来方便后面排查。3.3 设备与驱动的匹配过程compatible 怎么对上驱动何时被调用内核里有总线bus的匹配机制。对 platform 总线设备来说驱动注册时会提供一张匹配表of_device_id里面放着一组 compatible 字符串static const struct of_device_id my_dt_ids[] { { .compatible vendor,my-device, }, { } }; MODULE_DEVICE_TABLE(of, my_dt_ids); static struct platform_driver my_driver { .probe my_probe, .driver { .name my-device, .of_match_table my_dt_ids, }, }; module_platform_driver(my_driver);内核在创建设备也就是把设备树节点转换成 platform_device 的过程时会把设备树节点的 compatible 拿出来和驱动注册的 of_match_table 里的 compatible 逐一比较。只要有一个能对上就会调用这个驱动的 probe。这里有个特别容易忽略的坑of_match_table 里写的 compatible 必须和设备树节点里的完全一致包括大小写、厂商前缀、下划线。别小看这个实际中因为拼写不一致导致驱动的 probe 根本没被调用的案例多得数不过来。你要是发现“驱动明明编进内核了但 probe 没打出来”第一反应就应该是查 of_match_table 和设备树节点的 compatible 是否一致。在系统运行时怎么确认设备和驱动是否绑定成功两个方法# 查看该设备被哪个驱动接管了 ls -l /sys/bus/platform/devices/10000000.serial/driver # 查看所有 platform 设备与驱动的绑定关系 cat /sys/bus/platform/drivers/my-device/如果绑定成功在 /sys/bus/platform/drivers/ 对应驱动目录下能看到设备子目录的符号链接如果设备节点存在但 driver 下面没有它说明匹配失败除了拼写问题还要检查 of_match_table 是否导出、模块是否真的加载。4. 设备树配置实战从 GPIO 点亮到外设适配4.1 GPIO 与中断配置先搞清 pinctrl再谈亮灯设备树里配置 GPIO 最容易犯的错误就是只写了 GPIO 号忘了考虑引脚复用和上下拉。以 RK3568 为例一个引脚往往有多个功能可能是 GPIO也可能是 UART TX还可能是 I2C SCL。你想把它当 GPIO 用必须先通过 pinctrl 把引脚 mux 到 GPIO 功能同时配置好默认的电平状态。看一个典型的 LED 节点配置/ { leds { compatible gpio-leds; work-led { label work; gpios gpio4 RK_PA0 GPIO_ACTIVE_HIGH; linux,default-trigger heartbeat; }; }; }; pinctrl { leds { work_led: work-led { rockchip,pins 4 RK_PA0 RK_FUNC_GPIO pcfg_pull_none; }; }; };gpios gpio4 RK_PA0 GPIO_ACTIVE_HIGH这一行的意思是引用的 gpio 控制器是 gpio4引脚编号是 RK_PA0也就是组内引脚 0高电平激活。中间的引脚编号和后面的 flag 在 include/dt-bindings/pinctrl/rockchip.h 和 include/dt-bindings/gpio/gpio.h 里都有宏定义凡是在 dts/dtsi 里能看到宏就说明这个文件最终会被 C 预处理器处理。重点说下 pinctrl 那段。rockchip,pins 4 RK_PA0 RK_FUNC_GPIO pcfg_pull_none 表达的是bank 4、pin A0、复用功能为 GPIO、上下拉配置为无。这里如果漏了 pinctrl-0 的引用或者引用的 label 不存在最常见的结果就是引脚功能不对本来想点灯引脚却被内部默认复用成了别的功能灯怎么也不亮。排查这类问题时优先看 /sys/kernel/debug/gpio确认引脚当前是被谁占用的cat /sys/kernel/debug/gpio cat /sys/kernel/debug/pinctrl/*/pinsGPIO 中断也是一样的逻辑中断控制器节点上必须有interrupt-controller;标记GPIO 节点还必须定义#interrupt-cells 2子设备节点里引用中断时通过interrupt-parent gpio4指定中断挂在哪个控制器上。少了 interrupt-parent 或者拼错标签设备的中断申请直接就失败了。4.2 I2C/SPI 外设节点怎么加完整可抄的例子I2C 外设在设备树里通常挂在某个 i2c 控制器节点下作为子节点出现。加一颗 AT24C02 EEPROM 的节点大致长这样i2c2 { status okay; clock-frequency 400000; eeprom50 { compatible atmel,24c02; reg 0x50; pagesize 8; }; };I2C 子节点的 reg 是设备地址注意必须是 7 位地址。很多人直接把数据手册里的 8 位地址比如 0xA0写进来然后发现设备怎么都枚举不到——因为 8 位地址最低位已经是读写标志位了7 位地址实际是 0xA0 右移一位即 0x50。这种地址换算的坑网上随便一搜就是一堆血泪史。挂 SPI 外设也类似挂在 spi 控制器下面spi1 { status okay; num-cs 1; spiflash0 { compatible jedec,spi-nor; reg 0; spi-max-frequency 50000000; }; };这里 SPI 子节点的 reg 是片选编号也就是说reg 0表示使用第 0 个片选。spi-max-frequency是内核 SPI 框架需要的一个重要参数它直接决定控制器以多快的时钟频率和设备通信配得太高可能通信不稳定配得太低浪费性能。还有一个容易忽略的是如果 SPI 控制器下面有多个设备片选号千万不要重复否则两个设备会互相干扰。我建议新手把“设备和驱动都没问题就是设备树加错了”作为第一怀疑对象。每次加完一个外设节点至少做三件事确认第一看内核日志里有没有这个设备的 probe 信息第二看/sys/bus/i2c/devices/或者/sys/bus/spi/devices/下有没有生成新的设备目录第三把寄存器调试工具接上去确认总线波形真的在闪。三步走完基本就能定位是驱动没写对还是树没配对。4.3 RK3568 触摸竖屏改横屏一个完整的显示设备树实战瑞芯微 RK3568 平台上很多人会遇到一个需求板子本来竖着放产品要求改成横屏显示同时触摸方向和画面方向必须一致。这个需求横跨显示和触摸两条链路设备树要改的地方其实很明确。首先说显示侧。RK3568 有 VOP/DPU 显示控制器panel 等显示链路的配置一般在 disp 设备树节点里。如果面板本身是竖屏你想在横屏窗口下显示一般两条路一是在 DRM/KMS 层配置 panel 的旋转rotation二是在合成器层面做 framebuffer 的旋转。设备树里常见的是给 panel 或者 connector 节点配旋转属性比如dsi0 { status okay; panel0 { compatible xx,yy-panel; reg 0; rotation 90; }; };注意这个 rotation 到底认不认取决于你用的 DRM 驱动支不支持。有些方案是在 U-Boot 的 logo 阶段、内核 DRM 阶段分别都要设置两者不一致就会启动过程中画面转了一下。其次说触摸侧。触摸屏输出的是触摸点的坐标逻辑上它可以被驱动/设备树做坐标变换。以 Goodix GT911/GT9xx 这类触摸屏为例设备树里通常有这几个属性i2c3 { status okay; gt911: touchscreen5d { compatible goodix,gt911; reg 0x5d; interrupt-parent gpio4; interrupts RK_PB0 IRQ_TYPE_LEVEL_LOW; reset-gpios gpio4 RK_PB1 GPIO_ACTIVE_HIGH; irq-gpios gpio4 RK_PB0 GPIO_ACTIVE_LOW; touchscreen-inverted-x; touchscreen-swapped-x-y; }; };这几个触摸属性的含义是touchscreen-inverted-x把 X 轴方向反转。touchscreen-inverted-y把 Y 轴方向反转。touchscreen-swapped-x-y交换 X 和 Y 坐标也就是把竖屏触摸映射到横屏显示上。竖屏改横屏最典型的需求就是 X/Y 交换加某一个轴反转。具体怎么组合没有固定公式只能试。我的做法是先只加 swapped-x-y然后点一个点看位置对不对不对就再依次加上 inverted-x、inverted-y直到四个角和中心点全部对齐。这里强调一点触点对准之后还要验证边缘滑动方向因为轴反转只改变坐标原点不改变滑动手感。整个调整过程不需要重新编译内核只需要重新编译 dtb、烧录、重启一轮大概几十秒比改触摸驱动快得多。还有一种情况是产品用的电容屏驱动自己不带旋转配置那就要在主控端做转换。这属于应用层/中间件层的事不在设备树讨论范围内但务必知道设备树的触摸方向和显示方向必须保持逻辑一致。显示转了 90 度触摸也要转 90 度否则用户看到的是横屏画面手指却得按竖屏逻辑去点体验非常糟糕。4.4 PHY 设备树配置网口不通先查这里网络是嵌入式开发里绕不开的环节PHY 芯片的设备树配置也常年处于“看着简单、出事麻烦”的状态。常见的有线网口 MAC 和 PHY 分离方案设备树里要解决两个问题MAC 怎么找到 PHY以及 PHY 工作在什么接口模式下。RK3568 的 GMAC 节点配置经常长这样gmac0 { status okay; phy-mode rgmii-id; phy-handle phy0; phy0: ethernet-phy1 { reg 1; compatible ethernet-phy-ieee802.3-c22; }; };逐项解释一下这几个关键点phy-mode用来描述 MAC 和 PHY 之间的接口模式。rgmii-id表示 RGMII 接口且 RX/TX 时钟延迟由 PHY 侧处理rgmii则表示不处理延迟延迟要靠硬件走线长度控制或由 MAC 侧处理。这个参数配错最典型的症状是网络能 link 上但丢包率极高甚至完全收不到数据。phy-handle是一个 phandle 引用指向 MDIO 总线上的 PHY 设备节点。ethernet-phy1的 reg 是 PHY 的 MDIO 地址这个地址由 PHY 芯片的上电地址引脚决定不是随便写的。比如 KSZ9031 在某些板子上地址是 0在某些板子上是 1、4、7不看原理图和上电默认配置可能 PHY 连枚举都枚举不出来。新建 PHY 设备树节点后系统起来先做这几步排查# 查看 MDIO 总线上枚举到的 PHY ls /sys/bus/mdio_bus/devices/ # 读 PHY 状态 mii-tool eth0 ethtool eth0如果 /sys/bus/mdio_bus/devices/ 下根本找不到 PHY 节点优先怀疑 MDIO 地址不对或者 PHY 的复位脚没有正常释放。如果地址对、PHY 能读出来但 eth0 起不来再看 phy-mode 和时钟/RGMII 延迟配置。光电转换器是不讲情面的它只认物理层参数对不对。5. 常见问题与排查技巧实录5.1 设备树编译报错我从错误信息里看到的问题本质设备树语法相对简单但编译报错依然很常见。看到 dtc 输出的错误别慌大部分都能从错误信息直接定位。常见的几类报错和原因报错信息特征根本原因解决方法Error: syntax error / ERROR: Input tree has errors少了分号、括号不匹配、引号没闭合去报错行附近检查标点符号ERROR: Node has a reg or ranges property, but no #address-cells某个节点写了 reg/ranges但父节点没定义 #address-cells给父节点补上对应的 #address-cells 和 #size-cellsERROR: label not found引用了 xxx但 xxx 标签不存在检查标签拼写及该标签所在文件是否真的被 includeERROR: phandle is not a number属性引用方式错误把 xxx 直接写在字符串里检查该属性是否应该用 phandle 类型尖括号表示ERROR: (dts): too many cells in property interruptinterrupt 属性的 cell 数量和中断控制器 #interrupt-cells 不匹配检查 interrupt-parent 对应节点的 #interrupt-cells 值我实际工作中最多的是第一种漏了分号或括号。因为 dts 文件动辄上千行嵌套层级又多少一个分号dtc 报的行号有时候会偏得离谱。好在现在编辑器基本都能配对括号写之前先检查一遍标点能省很多时间。5.2 改完设备树“没生效”一套标准排查流程设备树改完了、重新编译了、烧进去了发现行为完全没变。怎么办先不要怀疑“设备树没生效”这个笼统的结论要拆开来看是 dtb 没换还是节点没被解析还是解析了但驱动没反应第一步确认板子上运行的 dtb 到底是哪个。进入系统后执行ls /sys/firmware/devicetree/base/如果能看到完整的设备树节点目录说明内核确实通过设备树启动了。接着用cat /sys/firmware/devicetree/base/xxx/status查看你的目标节点当前的实际状态。这里的 status 值就是 dtb 里最终生效的值包含 bootloader 修改后的结果。第二步确认你的 dtb 是真的更新了。有些情况下bootloader 烧写的是另一个位置的 dtb比如你编译了新 dtb 但烧到了 recovery 分区或者 TFTP 启动时加载的还是旧文件。一个笨但可靠的办法在设备树里随便加一个属性比如/ { vendor,test hello-world; };系统起来后cat /sys/firmware/devicetree/base/vendor,test如果打印出 hello-world说明新 dtb 生效否则就去查 bootloader 到底加载了哪个文件。这个办法看似幼稚但定位问题极其高效。第三步确认节点没有被其他逻辑覆盖或禁用。比如你想用 i2c2但另一个 dtsi 里有个 i2c2 { status disabled; }; 的覆盖最终生效的可能是后面那个。设备树里重复引用的覆盖顺序和文件 include 顺序有关别以为写了 status okay 就一定没问题要追查最终 dtb 里的实际值。5.3 资源冲突、引脚被占用设备树“打架”现场设备树的一个常见报错是资源冲突典型日志包括failed to request GPIO 127 for ... resource busy这类问题几乎都是两个设备节点引用了同一个引脚资源。可能是两个节点都配了同一个 GPIO 作为中断脚可能是 GPIO 和 pinctrl 里的引脚复用冲突也可能是某个外设已经被占用你在另一个节点里又把它引用了一遍。排查方法并不难打开内核的 debugfs 就能看到谁在占用cat /sys/kernel/debug/gpio cat /sys/kernel/debug/pinctrl/pinctrl-dev/pinmux-pins cat /sys/kernel/debug/pinctrl/pinctrl-dev/pinconf-pinsGPIO 调试信息里会列出每个 gpiochip 下的引脚是被哪个设备占用的pinctrl 的 pinmux-pins 会显示当前引脚的复用功能。这两份信息结合起来基本可以定位冲突点。还有一种更隐蔽的冲突你在设备树里给 A 设备配了 pinctrl给 B 设备也配了 pinctrl但两个配置引用了同一个物理引脚。这种情况内核不一定报错因为 pinctrl 框架可能是按“最后一个生效”来处理的但最终结果是硬件功能错乱。解决思路只有一个回到原理图确认每个引脚只能有一种功能这是硬件层面就定死的事软件再花哨也改变不了。5.4 我私藏的几个设备树调试技巧最后分享几个不太会写进文档、但我实际用得很顺手的调试技巧。第一个技巧反编译运行中的设备树。不一定非要找源文件系统起来后整个设备树就是一棵活目录树dtc -I fs -O dts /sys/firmware/devicetree/base -o running.dts这个命令能把手头板子真实运行的设备树完整导出成 dts 文本。对于那种“厂商给了一堆 dtsi 东拼西凑、根本不知道最终生效版本是什么”的情况这招直接治本。第二个技巧在内核里动态看设备树匹配情况。如果你怀疑驱动和设备树没匹配上打开内核动态调试或者简单粗暴地在驱动 probe 入口加一行打印static int my_probe(struct platform_device *pdev) { dev_info(pdev-dev, probe, node%pOF\n, pdev-dev.of_node); return 0; }%pOF 是内核专门打印设备树节点的格式化扩展能直接打印节点全路径。有了这行日志驱动有没有进来、匹配的是哪个节点一眼就看清楚了。第三个技巧批量检查设备树节点内存。写脚本盯着 /sys/firmware/devicetree/base 下的目录变化比如设备被 probe 后某节点的 status 有没有从 disabled 变成 okay某些属性有没有被驱动修改。虽然这类运行时修改比较少但真碰上了脚本比自己一个个 cat 高效得多。第四个技巧是经验性的改设备树之前先git diff确认自己改了哪些行。设备树文件之间通过 include 和 label 产生错综复杂的关联有时候你以为只改了一行实际上可能影响了多个节点反过来有时候你改了 A 节点的属性却被另一个文件里更靠后的 A 覆盖了。提交前把 diff 拉出来逐行看能避免一大批低级错误。设备树这个东西第一次接触感觉像天书接触多了就会发现它的逻辑其实很简单一棵树、若干属性、一堆 compatible 映射。个人最大的体会是设备树调试不怕问题怪就怕你手里没有“真实生效的树”和“当前引脚占用情况”这两份信息。只要能把板子上真正跑的设备树一五一十看明白绝大多数配置问题都能在半小时内定位到根因。后面你还会遇到更多跟设备树相关的场景修改内存映射做内核裁剪、适配新 sensor、调整 DDR 参数、配置安全启动……设备树这套基本功值得你花时间彻底吃透。
返回列表