ARM Linux设备树实战:从DTS编译到驱动匹配与调试

发布时间:2026/9/8 9:51:46
ARM Linux设备树实战:从DTS编译到驱动匹配与调试 1. 为什么ARM Linux离不开设备树从arch/arm/mach-xxx的板级文件说起1.1 设备树要解决的根本问题板级代码泛滥先说个实际经历。前段时间帮朋友排查一块RK3568的板子内核起来后一路看dmesg发现串口0没注册、网口也没起来。查遍rootfs、uboot之后最后在设备树里找到了问题uart0节点statusdisabledpinctrl引到了别的功能上。这种问题在嵌入式Linux里太典型了——只要你的板子和设备树信息对不上后面所有工作都白搭。要理解设备树为什么会存在得先看一段历史。在设备树引入ARM Linux之前每换一块开发板内核里就得新增一个mach-xxx.c文件板子的内存大小、时钟频率、串口地址、GPIO初始化全部硬编码在C代码里。那时候arch/arm目录下躺着上百个板级文件内核被一堆板级代码拖累得越来越臃肿。更致命的是社区想合入一个新板子的支持就得被迫接收一大堆“只有这块板子能用”的初始化代码代码质量和可维护性都很难控制。PCI和x86平台为什么没有这个问题因为PCI总线有标准枚举机制设备自己会报“我是谁、我需要什么资源”操作系统启动时扫描总线就能拿到完整信息根本不需要为每块主板单独写内核代码。但ARM的嵌入式平台五花八门SoC内部外设基本是内存映射没法靠总线自动枚举。所以内核社区最终采用了设备树这套方案把硬件描述和内核代码彻底解耦。板子是什么样就写一个dts文件告诉你驱动代码只负责“按名找资源”不关心具体板子怎么接的线。维度x86/PCI平台ARM平台硬件识别方式总线枚举ACPI表设备树节点描述板级差异隔离BIOS/ACPI层处理dts文件处理更换硬件需改什么一般不需要改内核改/换设备树文件驱动获取硬件信息PCI配置空间查询解析设备树节点资源1.2 DTS、DTSI、DTB、DTC这些文件到底各管什么实际开发中你接触最多的就是.dts和.dtsi后缀的文件还有编译出来的.dtb。很多人一开始分不清这三者的关系我用最直白的话解释一遍。.dts描述“一块具体板子”的设备树源文件类似于“我手上这块开发板用了哪个触摸屏、哪个网卡芯片、GPIO按键怎么接”。.dtsi被.dts包含的公共代码片段描述“同一颗SoC有哪些外设、默认的寄存器地址、时钟、中断”在SoC厂商的SDK里你会看到rk3568.dtsi这种文件。.dtbdts被DTC编译后生成的二进制文件内核启动时bootloader会把它加载到内存然后传给内核解析。DTC设备树编译器通常在Linux内核源码的scripts/dtc目录下也可以单独安装设备树编译器工具包。它们的关系可以理解为.dtsi是芯片原厂画好的“户型图基础框架”.dts是“针对某一套具体硬件的装修方案”编译后得到.dtb这一个“最终施工图”。内核启动时拿到的就是.dtb。修改设备树时你一般不需要动dtsi而是新建或修改dts用include把公共dtsi导入再覆盖其中节点。1.3 设备树的边界它到底管什么、不管什么设备树里放的是“硬件长什么样”但驱动真正的执行逻辑、协议栈、算法处理都不在设备树里。设备树是一个描述了设备地址、中断、时钟、GPIO、DMA、电源域等信息的“静态数据库”内核根据这些信息创建设备、匹配驱动、注册中断、配置时钟。所以写设备树你不需要关系驱动里怎么操作寄存器但你要确保节点里的地址、中断号、时钟索引和SoC手册一致否则驱动就算写得再好也取不到正确资源。这种“描述与代码分离”的设计是理解设备树的钥匙。驱动通过接口去查询“我的中断是几号”“我用的时钟是哪个”而不是自己硬编码数值。说得夸张一点同一份驱动代码换一个dts文件就能适配完全不同的板卡外设连接方案。这也是为什么懂设备树的人调试新板子特别快——硬件改动了优先改描述而不是改驱动。2. 设备树的核心语法与解析流程从dts到dtb再到probe2.1 节点、属性、compatible三大件必须吃透设备树本质上是一棵树根节点是“/”下面挂各种子节点。每个节点就是一个设备或总线用花括号包裹节点里有一些key-value对叫做属性property。最重要的属性就是compatible它就像设备的“身份证姓名”驱动靠它来认亲。看一个最简单的RK3568开发板上串口节点的写法/ { model MyBoard RK3568 Board; compatible myboard,rk3568, rockchip,rk3568; chosen { stdout-path uart2; }; }; uart2 { status okay; pinctrl-names default; pinctrl-0 uart2m1_xfer; };先说compatible。内核匹配驱动的顺序是“设备树节点中的compatible值”和“驱动结构体里of_device_id表中的compatible值”逐一比对。上面这个板级节点有两个compatible第一个是“myboard,rk3568”更具体第二个“rockchip,rk3568”更通用。匹配时先与第一个尝试匹配不中再试第二个。再看uart2这种写法。它并不是创建一个新节点而是引用并覆盖dtsi里已经定义好的uart2节点。dtsi里通常已经写好了寄存器地址、中断号、时钟等dts里只需要改status和pinctrl就能把某个外设“激活”并接到正确的引脚上。这也是为什么嵌入式开发中改设备树大部分工作都是在做引用覆盖而不是重头写一棵新树。2.2 从dts到dtbDTC编译与反编译实操在Linux内核源码目录下编译设备树常用命令是make ARCHarm64 dtbs也可以单独编译某一个dtb文件以RK3568为例make ARCHarm64 qcom_defconfig # 先加载一个配置如果SDK里已有config可跳过 make ARCHarm64 dtbs # 编译全部dtb make ARCHarm64 rk3568-evb1-v10.dtb # 只编其中一个如果只是验证dts语法、不依赖完整内核代码树也可以直接用dtc命令dtc -I dts -O dtb -o myboard.dtb myboard.dts dtc -I dtb -O dts -o myboard.dts myboard.dtb第一行把dts编译成dtb第二行把dtb反编译成可读的dts。反编译在排查问题时很有用你手上的固件不确定用的是哪个dts或者怀疑烧进去的dtb和源文件对不上反编译一下看节点内容立刻就知道。需要提醒的是dtc编译报错时出错信息往往是“unit name warn”这类的warning而不是error。很多人看到warning就跳过结果跑起来发现外设没注册。我的习惯是打开-fix-suppress-phandle-errors这类选项并且保证编译过程尽量零warning尤其是地址长度、phandle引用这种实际问题后面调试起来代价更大。2.3 内核如何解析设备树从device_node到platform_device当bootloader把dtb传给内核后内核在启动早期会做一次完整的解析把dtb展开成一颗device_node树存到全局链表里。这个阶段并不创建设备只是把“硬件描述”登记到内核的“台账”里。等对应的总线初始化时内核会遍历device_node根据节点状态创建设备。最典型的是平台设备platform_device很多SoC内部外设挂在虚拟的platform bus上内核启动时会调用of_platform_default_populate把设备树里statusokay的节点自动变成platform_device注册到总线上之后总线再去匹配驱动。对于I2C、SPI这类带控制器的总线流程稍有不同。I2C控制器节点会被解析成i2c_adapter然后系统遍历该控制器的子节点为每个子节点创建i2c_client。所以你在设备树里写“触摸屏挂在i2c2上地址是0x38”内核就会在i2c2总线上注册一个地址为0x38的i2c_client驱动再通过i2c_client访问设备。这个过程完全由设备树驱动驱动代码里不需要硬编码“哪个设备在哪个总线哪个地址”。内核解析完设备树之后sysfs里也能看到结果。路径/sys/firmware/devicetree/base/对应设备树的根节点每个节点就是一个目录属性就是文件。查看节点内容可以用cat /sys/firmware/devicetree/base/compatible这个命令直接输出板级的compatible字符串可以用来确认内核实际加载到的设备树版本和你预期是否一致。3. 设备树与驱动的匹配链路compatible、of_device_id与probe的协同3.1 驱动怎么被“喊”起来of_device_id匹配过程设备树节点有了驱动怎么知道哪个设备是自己的菜关键在驱动结构体里的of_device_id表。以常见的I2C触摸屏驱动为例static const struct of_device_id goodix_ts_of_match[] { { .compatible goodix,gt9147 }, { .compatible goodix,gt9271 }, { .compatible goodix,gt9286 }, { .compatible goodix,gt967 }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, goodix_ts_of_match);当内核创建好设备无论是platform_device还是i2c_client后总线会调用驱动的_match接口将设备的compatible属性和驱动of_device_id表中的compatible逐一比较。匹配成功后调用驱动的probe函数。probe里面做的事情一般是“获取设备树里配置的资源、注册中断、初始化硬件、创建设备节点”。这里有个初学者最容易忽略的点compatible字符串必须完全一致。设备树里写“goodix,gt9147”和驱动表里写“goodix,GT9147”都不行大小写、逗号后的空格都会被当成不同字符串。所以当你的驱动明明加载了却不走probe时第一步永远是比对两边compatible是不是一字不差。3.2 资源获取不再靠硬编码从设备树读出地址、中断、GPIO设备树不仅承担“匹配”职责还负责把设备需要的外部资源告诉驱动。就拿最基本的platform driver为例驱动里获取物理地址和中断号的经典写法是struct resource *res; void __iomem *base; int irq; res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(pdev-dev, res); irq platform_get_irq(pdev, 0);这里的资源哪来的就是内核在解析设备树节点时把reg属性转换成了IORESOURCE_MEM资源把interrupts属性转换成了中断号。所以你修改设备树里reg的地址驱动不需要重新编译只要重新编译dtb并更新到板子上驱动读取到的资源就会跟着变。GPIO和时钟也是同样套路。设备树里配置一个LED节点myled: my-led { compatible myboard,led; gpios gpio4 22 GPIO_ACTIVE_LOW; default-state off; };驱动里用gpiod_get获取GPIO描述符用devm_clk_get获取时钟句柄完全通过名字去找。这样硬件连接变了只改dts就行。资源类型dts属性驱动获取接口寄存器地址/长度reg #address-cells/#size-cellsplatform_get_resource中断号interruptsplatform_get_irqGPIOgpios / gpio-namesdevm_gpiod_get / gpiod_get_optional时钟clocks / clock-namesdevm_clk_get复位resets / reset-namesdevm_reset_control_get电源vdd-supplydevm_regulator_get3.3 一个常见误区改了dts为什么驱动probe没执行很多人改完设备树把status改成okay了compatible也对上了但驱动就是不走probe。这时需要按这个顺序排查。第一确认dtb有没有真的被bootloader加载。很多板子在uboot里固化了一个dtb路径或dtb分区你改了内核源码树里的dts但没重新打包/烧写resource分区或boot.img加载的还是老dtb。第二确认节点status是不是okay。很多人只看自己写的部分却忘了dtsi里某个父节点或者对应的pinctrl节点是disabled。第三确认compatible是否确实能被设备树反编译看到。用前面说的反编译方法或者直接看/sys/firmware/devicetree/base/对应路径下的compatible文件内容。第四确认设备是否真的创建了。platform总线可以用ls /sys/bus/platform/devices/查看I2C设备可以通过i2cdetect -y 总线号查看。设备都没创建probe自然永远不触发。这四步是设备树驱动开发最常碰到的排查链路按顺序走下来90%的“probe不执行”问题都能定位到根因。4. RK3568平台的设备树实战多dtb选择、修改与部署4.1 openHarmony和Linux SDK里的多设备树到底咋选最近很多人问瑞芯微rk3568设备树在SDK里一堆openharmony的rk3568也有许多设备树到底该选哪个其实这层困惑不怪大家——RK3568这颗芯片被太多方案商用了内存颗粒不同、显示接口不同、摄像头不同、Wi-Fi模组不同都会导致设备树内容有差异。设备树文件名的规律通常是“芯片名-板子名-版本号.dts”比如rk3568-evb1-ddr4-v10.dtsrk3568-evb2-lpddr4-v10.dtsrk3568-nvr-demo-v10.dts选择的关键不是“哪个新选哪个”而是“你的板和哪个文件名描述的内容最接近”。先从几个维度去抠差异板子用的是DDR4还是LPDDR4/LPDDR4X显示输出是HDMI、eDP、LVDS还是MIPI DSIMIPI摄像头用的sensor型号是什么Wi-Fi模组是AP6255、AP6398还是RTL8821每个变量都会影响dtb的最终选择。还有一种更稳妥的办法是把所有候选dtb反编译出来grep一下关键节点dtc -I dtb -O dts -o rk3568-evb1.dts rk3568-evb1.dtb grep -n hdmi\|edp\|lvds\|dsi rk3568-evb1.dts对比完就明白每个dts的差异点基本集中在显示、摄像头、无线模组这几个主板上最容易变化的区域。选错dtb的表现也很典型屏幕不亮、触摸没反应、Wi-Fi扫不到热点但串口和控制台往往是好的因为串口节点在所有variant里基本保持一致。4.2 在Ubuntu环境下修改RK3568设备树的常规流程如果用的是官方或方案商的SDK修改设备树的流程基本是三步改dts、编译dtb、打包更新。以Ubuntu开发机环境为例假设SDK放在kernel目录下cd kernel vim arch/arm64/boot/dts/rockchip/rk3568-evb1-ddr4-v10.dts make ARCHarm64 rockchip_linux_defconfig make ARCHarm64 dtbs编译完成后生成的dtb在arch/arm64/boot/dts/rockchip/目录下。下一步是把dtb替换到boot镜像或resource分区里。不同SDK打包方式有差异但不少RK平台的SDK是这样做的./mkimage.sh或者单独更新dtb分区upgrade_tool di -resource out/.../resource.img还有一种最常见的场景是只想在Ubuntu里快速改一下设备树、验证某个外设能不能被识别并不想重编整个SDK。这时可以直接用dtc把现有dtb反编译成dts改完再编译成dtb然后用rkdeveloptool或upgrade_tool烧写到对应分区。这种方式适合小改动比如把一个GPIO口的状态改一下、把某个外设status从disabled改成okay完全不需要动内核代码。4.3 常见修改动作示例加一个SPI设备、打开一个串口改设备树最终要落到具体动作上。举一个我经常做的例子在一块RK3568板子上挂一个SPI接口的Flash需要新增节点。先看板子上SPI控制器挂哪个总线查原理图确认CS脚、时钟引脚然后在dts里加spi1 { status okay; spidev0: spi-flash0 { compatible jedec,spi-nor; reg 0; spi-max-frequency 50000000; }; };这里reg 0表示片选0macimum频率根据Flash手册设置。如果是自研板需要特别确认form factor。改完编译烧写后在/sys/class/spi_master/spi1/下就能看到spi1.0对应的MTD设备也会出现在/dev/mtd*里。再举个打开串口的例子。RK3568有很多组UART但大多数默认是disabled。如果板子上引出了uart4的TX/RX那么要去dtsi里找到uart4节点然后在板级dts里uart4 { status okay; pinctrl-names default; pinctrl-0 uart4m1_xfer; };这里pinctrl-names和pinctrl-0是最关键的它决定了芯片的引脚复用功能到底配置成UART还是GPIO。很多人打开串口后发现引脚不出数据大概率就是pinctrl配置和实际引出的引脚对应不上。5. 高频驱动问题排查USB转串口、调试器与GPU硬件解码5.1 USB转串口芯片驱动CH340、CP2102、FTDI在Linux下的机制热词里CH340串口驱动、cp2102驱动、ftdi串口驱动的搜索量一直很高说明这是个真实痛点。先说结论在主流Linux发行版和ARM Linux SDK里这些常见的USB转串口芯片驱动大多已经编进内核或者以模块形式存在正常情况下不需要自己去下载第三方驱动。以USB设备的行为来看芯片插入后内核先识别到USB设备然后根据VID/PID匹配对应驱动创建设备节点。常见列表如下芯片型号内核模块常见VID:PID生成的设备节点CH340/CH341ch3411a86:7523/dev/ttyUSB0CP2102cp210x10c4:ea60/dev/ttyUSB0FTDI FT232Rftdi_sio0403:6001/dev/ttyUSB0通用USB转串口cdc_acm因设备而异/dev/ttyACM0排查这类驱动问题的思路也非常固定。第一步插上USB执行lsusb看设备ID是否被系统识别。如果lsusb里都没有先检查硬件线缆和USB口问题不在驱动。第二步执行dmesg | tail看内核有没有打印出类似ch341-uart或cp210x的注册日志。有日志说明驱动已加载。第三步看/dev/ttyUSB0或/dev/ttyACM0是否出现。出现说明设备节点创建成功。第四步如果设备节点出现但操作权限不足提示permission denied就要把用户加入dialout组或配置udev规则sudo usermod -aG dialout $USER这条命令在很多发行版上都适用加完重新登录生效。我遇到过太多人卡在最后一步原本以为驱动没装好其实只是/dev/ttyUSB0的读写权限问题。5.2 ST-Link、J-Link在Linux下的驱动与权限配置调试器在Linux下的问题也很有代表性。ST-Link、J-Link这类调试器本质上也是USB设备内核可能已经有驱动或者只需安装厂商提供的工具链。很多人以为要专门去装驱动其实关键点在于权限和工具链。ST-Link在Linux下最常用的工具链是OpenOCD和stlink-tools。连接ST-Link后lsusb能看到STMicroelectronics设备。如果OpenOCD提示无法连接大概率是权限问题当前用户没有访问USB设备的权限。解决方法同样是配置udev规则sudo cat /etc/udev/rules.d/99-stlink.rules EOF SUBSYSTEMusb, ATTR{idVendor}0483, ATTR{idProduct}3748, MODE0666, GROUPplugdev EOF sudo udevadm control --reload-rules sudo udevadm triggerJ-Link的流程差不多。SEGGER官方提供Linux版JLink软件包解压后直接运行JLinkExe即可。如果JLink也提示权限问题需要注意SEGGER自己安装时会写入/opt/SEGGER下的udev规则但使用自编译OpenOCD时要确保udev规则中的设备ID覆盖了你手中J-Link的版本。这里额外提醒一点不少调试器无法连接其实不是驱动或权限问题而是USB线质量问题或者Type-C接口没有插入到位。先看lsusb输出再谈权限和驱动这是调试USB设备的基本思路。5.3 GPU设备树和视频硬件解码为什么只改设备树不够热词里出现gpu驱动开发和chromium rockchip硬件解码这属于比较进阶的话题。在RK3568平台上GPU是Mali-G52设备树里有一个gpu节点内核通过devfreq框架管理GPU频率驱动可以是内核自带的panfrost也可能是Rockchip提供的Mali驱动。设备树里的GPU节点并不复杂无非是compatible、reg、interrupts、clocks这些属性。但如果想让Chromium在RK3568上实现完整的硬件视频解码单纯改设备树根本不够。硬件解码链路涉及用户态MPPMedia Process Platform、V4L2的m2m设备、DRM显示框架以及Chromium侧的视频解码器实现。设备树这里的作用只是告诉内核“VPU/GPU/显示控制器分别在哪、用什么中断和时钟”真正能不能硬解还要看用户态组件和内核驱动版本是否配套。所以当你在RK3568上遇到Chromium不能硬解视频时先把dmesg里mpp_service、vpu_service的注册信息打印出来确认内核是否识别到了编解码硬件再去排查用户态库。设备树只是第一道门门后面还有好几道坎。6. 设备树开发中最容易踩的坑与实用检查方法6.1 status disabled和节点覆盖改了却不生效的常见原因设备树开发里最经典的问题就是我明明把节点status改成okay了怎么reboot之后还是没生效这种问题十有七八是“改错了文件”。很多SoC的dtsi里已经把某个外设定义好了但板级dts里可能引用了它也可能没有。你在dtsi文件里改status板级dts一旦对这个节点做了引用覆盖优先级以板级dts为准。反过来如果你在另一个专门的dts文件里改但这个文件根本没被最终的dts包含那就当然不生效。排查方法也简单编译完dtb后反编译查看该节点的status是不是你期望的okay。例如dtc -I dtb -O dts -o out.dts out.dtb grep -n -A 5 uart4 out.dts如果反编译结果显示status还是disabled那说明你改的文件不在编译路径里。这种情况再往下查Makefile或者dts的include关系。还有一个很隐蔽的坑节点名和label搞混。比如uart4节点label是uart4但你直接在根节点下新建了一个“uart4”而不是用uart4引用这样内核就会把它当成一个不存在的地址设备驱动也不会匹配。6.2 引脚复用冲突两个功能抢同一个物理引脚pinctrl是设备树里最容易踩雷的部分。同一个物理引脚不同时间段可能复用为UART、I2C、SPI或GPIO。当你把两个外设都配到同一个pin上时后注册或者先注册的那个驱动可能会拿到pinctrl配置但实际电气连接已经乱套了。典型表现外设A和B功能都能在设备树里看到但A工作正常、B完全没反应或者A/B互相干扰、系统启动时pinctrl报错。这类问题的排查思路不是看代码而是去读芯片的引脚复用表。在RK3568平台上你可以通过内核日志搜pinctrl相关错误。同时在设备树里要留意pinctrl-0引用的是不是同一个节点uart4 { pinctrl-0 uart4m1_xfer; };如果把UART引脚配置成了uart4m1_xfer而另一处又用uart4m1_cts_rts或其他外设引用了同一个pin group就会冲突。排查这类问题时我习惯把候选dts反编译后用grep搜索同一个pin定义被哪个节点引用了交叉对比pin group名称。6.3 一套实用的“设备树体检”命令清单调试设备树相关问题时我有一套固定的体检流程每个新板子来了都会先跑一遍。这套流程能快速筛选出内核对设备树的解析结果帮你判断“是设备树写错了、还是没加载对、还是驱动压根没匹配上”。# 查看内核实际解析到的设备树根节点compatible cat /sys/firmware/devicetree/base/compatible # 列出所有platform设备 ls /sys/bus/platform/devices/ # 查看某个具体设备是否注册成功 ls /sys/bus/platform/devices/ | grep mydevice # 查看中断请求是否有设备挂上 cat /proc/interrupts | grep myirq # 查看内核启动阶段设备树解析日志 dmesg | grep -i of_ | head -50 dmesg | grep -i Device Tree多加一句很多板子在bootloader阶段会有意挑选不同dtb比如Rockchip的Uboot会读取extlinux.conf或parameter分区里的dtb路径。如果发现自己反复编译烧写还是旧配置先去uboot环境变量里看boot_fdtfile或者kernel命令行有没有指定dtb名称。6.4 设备树调试的最后一招在驱动里打印of_match_table匹配状态如果上述检查都正常但驱动就是不probe可以临时在驱动代码的probe入口和匹配函数里加打印。具体做法是在platform_driver结构体初始化时检查drv-driver.of_match_table是否被正确指向你的of_device_id数组。常见BUG是只写了MODULE_DEVICE_TABLE宏但忘了给platform_driver设置of_match_table字段。static struct platform_driver my_driver { .probe my_probe, .driver { .name my_device, .of_match_table my_of_match, }, };如果是I2C设备则对应i2c_driver里也要有of_match_table。很多时候驱动模块加载后没有输出任何错误但就是没触发probe原因就是驱动侧没把匹配表挂上。这个问题在复制代码时特别容易发生因为新旧内核版本的driver结构体字段名有时候会不一样编译过了不一定代表挂载对了。写在最后我的设备树调试习惯做了这么多年嵌入式Linux我越来越觉得设备树调试的核心心法就一句话先确认内核实际看到什么再谈修改什么。很多人一上来就改代码、改配置改了半天reboot起来发现还是老样子其实就是没确认dtb有没有加载对、节点有没有生效。我现在拿到一块新板子不是先急着写驱动而是先花十五到二十分钟把设备树过一遍内存节点容量对不对、串口pinctrl有没有配错、关键外设status是不是okay、uboot环境变量引导的dtb路径是什么。这套习惯帮我省了太多debug时间也希望这篇文章能帮你少踩几个坑。