ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

RK3568设备树与Linux驱动匹配机制实战解析

2026/9/4 13:03:10 拓冰建站 浏览量
RK3568设备树与Linux驱动匹配机制实战解析 你可能也遇过这种场面RK3568 板子拿回来驱动的 .c 文件写好了menuconfig 也打开了结果点灯死活不亮/sys/class/leds 下面空荡荡的。最后检查来检查去发现问题根本不在驱动而在设备树——某个节点 status 还是 disabled驱动压根没被 probe。这件事给我的教训是搞 Linux 设备树和驱动的人光会写.c不行得先把设备树和驱动的“耦合关系”吃透。设备树不是一堆给内核看的配置文件它是决定驱动能不能运行、怎么运行的“硬件说明书”。这篇文章想聊的就是设备树和 Linux 驱动之间那点事。我会从设备树为什么会出现讲起拿实际 RK3568 板子上的点灯节点做例子把这套机制拆开揉碎适合刚接触设备树的嵌入式开发也适合那些整天调驱动但始终没把设备树匹配逻辑捋顺的同学。1. 没有设备树的日子里Linux 是怎么被硬件变体拖垮的1.1 板级文件时代的“一片混乱”现在的年轻人可能体会不到Linux 早期在 ARM 平台上维护代码有多痛苦。那时候每出一个新的开发板内核里就要加一个arch/arm/mach-xxx/board-xxx.c里面写满了这块板子上的外设注册代码。比如你板子上有一个 I2C 触摸屏、一个 GPIO 按键、一个网卡都得在 board 文件里用platform_device_register()一个一个手动注册。这样做最直接的后果是内核源码里堆了成百上千个 board 文件。每换一块板子哪怕只是把 GPIO 按键从 GPIO1_12 改到 GPIO1_13也要复制一份 board 文件再改几行。代码仓库越来越臃肿提交历史里大量内容都是“new board support”真正有意义的驱动改动被淹没。更麻烦的是uboot 和内核之间也没有统一的硬件描述格式都是各自维护各自的配置。我一个朋友早年维护过某个公司的 BSP他跟我说最怕的就是客户说“我们换了个 DDR 型号帮我适配一下”。听起来只是改内存参数但由于历史代码里板级信息四处耦合经常上午改完下午就冒出新问题。这种痛苦我试过几次就明白了硬件的多样性不可能靠“增加板级代码”解决必须把硬件描述从驱动里抽离出去。1.2 设备树到底是什么一份内核和 Bootloader 共享的硬件清单设备树Device Tree本质上就是一套描述硬件资源的“数据结构”它从 Open Firmware 那套规范发展而来目的是让内核通过一份扁平化的树形描述知道CPU 是什么、内存多大、有哪些外设、外设挂在哪个地址上、中断线接到哪里、引脚复用到什么功能。这份描述是二进制格式的叫 DTBDevice Tree Blob。DTB 的源文件是 DTSDevice Tree Source公共部分拆成 DTSIDevice Tree Source Include。编译工具叫 DTC。整条链路是这样.dts / .dtsi --- dtc 编译 --- .dtb --- bootloader 加载 --- 内核解析Linux 内核在启动早期会把 DTB 解析成一颗struct device_node组成的树然后顺着树上的节点去创建平台设备platform_device。后面驱动注册时总线去匹配这些设备匹配上了就调用probe()。设备树最大的价值不是“让驱动少写代码”而是让驱动不再和具体板子绑定。同样一个 GPIO 控制器驱动可以服务一百种不同接法的板子只要设备树里描述清楚就行。有一点必须提醒设备树不是“万能配置中心”它能描述资源但不能凭空创造驱动。如果你的外设内核里根本没有驱动设备树写得再漂亮硬件也不会工作。设备树提供的是“位置”和“资源”驱动的职责才是“行为”。2. 设备树文件的真实结构从一份 gpio-leds 设备树开始逐行读2.1 最小可用的点灯节点拆解我习惯用点灯来入门设备树因为一个最简单的 GPIO LED 外设能把设备树里最核心的属性全部带出来。下面是一份真实 RK3568 SDK 里常见的节点片段/ { compatible rockchip,rk3568-evb1-ddr4-v10; model Rockchip RK3568 EVB1 DDR4 V10 Board; gpio-leds { compatible gpio-leds; pinctrl-names default; pinctrl-0 led_work_pin; led_work: led-0 { label work; gpios gpio0 RK_PC5 GPIO_ACTIVE_LOW; default-state on; linux,default-trigger heartbeat; }; }; }; pinctrl { led_work_pin: led-work-pin { rockchip,pins 0 RK_PC5 RK_FUNC_GPIO pcfg_pull_none; }; };看起来是不是有点懵我把它拆开解释。/表示根节点设备树是一棵以/为起点的树。根节点里通常有compatible和model。model是给人看的板子名字compatible是给内核做板级匹配用的格式一般是厂商,型号。gpio-leds这个节点没有reg属性也没有status属性。它不是一个“地址设备”而是一个“纯平台设备”。内核的 GPIO LED 驱动drivers/leds/leds-gpio.c通过节点的compatible gpio-leds找到它然后解析下面的子节点每个子节点代表一颗 LED。gpios gpio0 RK_PC5 GPIO_ACTIVE_LOW是关键。这里的gpio0是一个“引用”phandle指向 SoC 里 GPIO0 控制器的节点。RK_PC5是瑞芯微平台定义好的宏表示 GPIO0 组的 C 组第 5 脚。GPIO_ACTIVE_LOW表示低电平点亮。这些宏定义通常来自内核里的 dt-bindings 头文件所以设备树源文件里经常能看到一堆#include。上电后这个节点被解析成 platform_device内核的 gpio-leds 驱动匹配成功后就会在/sys/class/leds/work下生成一个设备。之后想点灯就简单了echo 0 /sys/class/leds/work/brightness echo 1 /sys/class/leds/work/brightness如果一切都是正常的这就是设备树和驱动配合工作的完整闭环。2.2 dtsi 和 dts 的分工以及那些让你头疼的宏实际工程里设备树文件名五花八门但文件组织有规律SoC 厂商提供的是 dtsi开发者只需要写一个“板级 dts”。RK3568 平台典型的样子大概是rk3568.dtsi # SoC 级CPU、中断控制器、UART、I2C等控制器 rk3568-pinctrl.dtsi # 引脚控制器 rk3568-evb.dtsi # EVB 公共板级配置 rk3568-evb1-ddr4-v10.dts # 具体硬件版本板子最终编译对象#include把层级串起来。板级 dts 里会覆盖 dtsi 中某些节点的状态常见写法是uart3 { status okay; pinctrl-names default; pinctrl-0 uart3_xfer; };这种写法表示“我要打开 uart3并且给它配好默认引脚。”如果 dtsi 里 uart3 默认是status disabled你没有在板级文件里“翻成 okay”后面配置再多也没用。宏的理解也特别重要。很多刚从单片机转过来的朋友看到gpio0 RK_PC5 GPIO_ACTIVE_LOW里面全是宏第一反应是去代码里搜#define RK_PC5到底等于几。这种做法没有意义。设备树不是让你背数字而是通过宏避免写错。真正要关心的是父节点的#address-cells、#size-cells决定了一个reg里地址和长度各占几个 32 位格子。如果父节点是soc { #address-cells 2; #size-cells 2; };那么它下面的外设节点写reg 0x0 0xfdd60000 0x0 0x1000才是正确的地址是高 32 位、低 32 位分别两个 cell长度同理。少了前面那个 0地址很可能就错位了。这种问题在 64 位 ARM 平台上特别容易踩后面讲排错时会再展开。3. 驱动与设备树自动“结婚”的那套算法match 与 platform_device 生成全过程3.1 compatible 匹配规则驱动是怎么认领自己设备的设备树节点不是天然“属于”某个驱动的。Linux 的设备模型核心是 bus总线、device设备、device_driver驱动三者之间的关系。在平台设备platform_device这条线上platform bus 是虚拟总线他的匹配函数到处找“哪个驱动应该接管这个设备”。对设备树场景来说匹配的核心就是compatible字符串。驱动里会维护一张“我能支持哪些设备”的表设备树节点用compatible说“我是什么硬件”两边相等就有戏。驱动里典型的写法是static const struct of_device_id led_demo_match[] { { .compatible myvendor,gpio-demo-led, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_demo_match); static struct platform_driver led_demo_driver { .probe led_demo_probe, .remove led_demo_remove, .driver { .name my-gpio-demo, .of_match_table led_demo_match, }, }; module_platform_driver(led_demo_driver);设备树里写成gpio-demo { compatible myvendor,gpio-demo-led; status okay; >static int led_demo_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct gpio_desc *gpio; int ret; gpio devm_gpiod_get(dev, data, GPIOD_OUT_LOW); if (IS_ERR(gpio)) { dev_err(dev, failed to get data gpio\n); return PTR_ERR(gpio); } ret device_property_read_u32(dev, max-timeout-ms, timeout); if (ret) timeout 500; return 0; }注意我读>dev_info(dev, %s enter\n, __func__);这一行打印能帮你判断问题出在“匹配失败”还是“资源获取失败”这两个方向的排查路径完全不同。后面第五部分会继续展开这里先记住一个原则内核日志是最诚实的调试对象设备树写没写对看 dmesg 远比盯代码有效。4. RK3568 平台选设备树、改配置、烧进板子的完整链路4.1 为什么板子资料里有一堆设备树文件你到底该改哪一个RK3568 作为一颗使用非常广泛的 SoC同一个 SDK 里有几十份 dts 文件毫不奇怪。第一次看到这种文件夹的人基本都会问到底哪个是我要改的其实大多数 SDK 的文件名已经把答案写在脸上了比如rk3568-evb1-ddr4-v10.dts rk3568-evb2-lpddr4-v10.dts rk3568-nvr-demo.dts rk3568-aiot-demo.dts每个名字对应一个具体的硬件形态。关键是看板子上的丝印、内存类型、DDR 版本、屏幕接口。比如你拿到的是 RK3568 EVB1 且内存是 DDR4那就优先找带evb1-ddr4的 dts如果板子是你们公司自己画的硬件工程师一般会明确告诉你兼容哪份 EVB 版本的布局。最怕的是“凭感觉选”。在 OpenHarmony 移植、Linux SDK 开发、甚至某些第三方 BSP 里选错默认 dts 导致的启动问题非常常见而且症状很阴间开机卡在不同阶段、触摸屏没反应、网卡不通。原因是引脚复用和电源域配置完全不同。一个比较可靠的判断方法是看设备树根节点的model和compatible# 板子启动后用 adb 或串口进入 shell cat /proc/device-tree/model cat /proc/device-tree/compatible这两个文件会存放字符串。如果启动到一半就挂了看不到就先烧一个已知可用的固件进去再对照 SDK 里的文件名反推。4.2 改一次设备树并生效的完整操作路径从 dts 到 dtb 到分区假设你确定了目标设备树是rk3568-evb1-ddr4-v10.dts现在想在上面加一个自定义 GPIO 点灯节点完整路径是这样第一步找到源文件。通常路径类似kernel/arch/arm64/boot/dts/rockchip/rk3568-evb1-ddr4-v10.dts第二步在根节点下加节点。注意保持缩进和格式添加完先做语法自查。第三步编译 dtb。最推荐的是跟随内核的编译系统。在 kernel 目录下执行make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- rockchip/rk3568-evb1-ddr4-v10.dtb这会生成arch/arm64/boot/dts/rockchip/rk3568-evb1-ddr4-v10.dtb为什么不用单独命令直接跑dtc因为设备树里大量#include的头文件和宏依赖 C 预处理器展开直接用dtc xxx.dts会报一堆宏未定义。你当然可以手动先cpp预处理再喂给 dtc但工程上没必要这么折腾内核 make 系统已经把这些都处理好了。第四步把新 dtb 烧到板子对应分区。不同 SDK 烧录方式不一样常见的是 Android 工具链里的 resource 分区、boot.img 里的 dtb 段、U-Boot 引导加载后单独加载的 dtb 分区。你可以在 SDK 的文档里搜“resource.img”或“dtb.img”。这一环节最容易出的事就是改了 dtb也编译了烧录了但 bootloader 加载的根本不是这个文件。第五步启动后验证加载版本dmesg | grep -i device tree cat /proc/device-tree/model如果cat /proc/device-tree/model能显示你改过的 model说明你改的这棵树确实被内核用起来了。4.3 在 Ubuntu 主机上交叉编译 RK3568 设备树的实用做法很多 RK3568 开发者平时不用 Windows而是用 Ubuntu 服务器做编译。第一次在 Ubuntu 上编 RK3568 设备树最容易踩的是交叉编译器没装。编译 ARM64 设备树需要aarch64-linux-gnu-系列工具链。Ubuntu 上最直接的方式sudo apt install gcc-aarch64-linux-gnu device-tree-compiler如果 SDK 里已经配置好整个内核编译环境也可以直接在内核目录里跑export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- make rockchip_linux_defconfig # 不同 SDK 的 defconfig 名字不同 make dtbs注意上面只是常见套路不同厂家 SDK 的 defconfig 不一定叫这个名字可能叫rockchip_defconfig或evb_defconfig。最靠谱的方式是打开 SDK 里的编译脚本搜一下里面用的是什么 target。SDK 里通常有一键编译脚本比如./build.sh kernel它会负责 defconfig、交叉编译、打 dtb 包。不要为了“看起来专业”就绕过 SDK 自己乱编。单独验证某个 dts 时我偶尔也手动编译dtc -I dts -O dtb -o /tmp/test.dtb test.dts但这种方法处理不了带#include和宏的复杂文件。如果只是检验语法用dtc -I dts -O dtb -o /dev/null test.dts也是可以的看到一堆宏报错不用慌这不代表设备树逻辑错了只是缺少预处理。5. 设备树调试中的命中率最高的几个坑以及我的排查顺序5.1 现象一设备树里写了节点驱动 probe 就是不执行这是我日常帮人看问题最多的场景。通常驱动代码有节点也加了驱动加载时不报错就是 probe 不打印。我的排查顺序是固定的先看设备树节点有没有被内核真正识别到ls /proc/device-tree/ ls /proc/device-tree/gpio-demo/ cat /proc/device-tree/gpio-demo/compatible | tr \0 \n注意compatible属性本质是多个以\0结尾的字符串所以用cat不能直接看得舒服用tr把空字符替换成换行最方便。如果你在/proc/device-tree/下根本找不到这个节点说明你编译或烧录的不是这份 dts。再看节点的 status。我不止一次遇到过节点在某个 dtsi 里被定义了并且在另一个 dts 里又定义了 status但这两段代码最终合并时你眼睛盯的那份并不是生效那份。设备树多处节点修改同一个节点时后面的赋值会覆盖前面的。搜索整个 dts 文件确认status的最终值是okay而不是disabled。再确认驱动的of_match_table里有没有挂上挂对了没有。代码里常犯的错误是写了static const struct of_device_id led_demo_match[] { { .compatible myvendor,gpio-demo-led }, {} };但 platform_driver 里只写了.driver { .name my-gpio-demo, },忘了把of_match_table led_demo_match填进去。这样驱动只能靠 name 字符串匹配设备树节点的 compatible 就没有参与匹配过程probe 很大概率不触发。最后看总线归属。如果你的外设节点挂在 I2C 控制器下面那么它对应的不是 platform_device而是 i2c_client。此时内核 I2C 子系统用的是 i2c 的of_match_table匹配驱动也需要用i2c_driver而不是platform_driver。把 platform_driver 用在 I2C 设备上就像把钥匙插错门锁结构再完整也白搭。5.2 现象二probe 执行了但资源读取全是错的probe 能跑说明设备树和驱动已经匹配上了。接下来问题往往出在资源描述错误。比如我遇到过的案例某外设寄存器地址在 SoC 手册里是0xFDD60000开发者在设备树里写reg 0xfdd60000 0x1000;如果此时外设节点挂在soc下面而soc节点的#address-cells 1、#size-cells 1这么写确实没问题。但在 64 位 ARM 平台上很多 SoC 的地址总线描述会使用两个 cell比如soc { #address-cells 2; #size-cells 2; };这时正确写法是reg 0x0 0xfdd60000 0x0 0x1000;一旦地址 cell 数量不匹配内核解析出来的资源地址是错的ioremap 返回的虚拟地址自然对不上读写寄存器要么崩溃要么无效。再比如 GPIO 引脚冲突。设备树里配好的 pinctrl 如果和别的设备重叠启动日志里通常能看到类似pin 165 already requested的提示。这种问题经常在两个人同时往设备树里添加外设时发生A 用了 GPIO0_C5 做 LEDB 用了同一脚接按键两边在代码里都配了但物理上只有一个脚。排查方法是把 pinctrl 里的用的 bank、pin 编号逐个对照原理图不要只盯着 gpio 属性还要看rockchip,pins里的引脚号。5.3 现象三dmesg 里提示 pinctrl 相关的 init 失败设备树编写里 pinctrl 是一个大坑。很多节点在 dtsi 里会写pinctrl-names default; pinctrl-0 some_pin;这表示设备启动时引脚控制器要先把某些引脚切换到某种复用功能。如果你的某个外设对 pinctrl 节点引用了不存在的 pin或者 pin 配置里写的 function 冲突probe 阶段就可能失败。我之前就把一个 UART 的引脚误配成了 GPIO 功能结果串口设备注册时始终拿不到正确配置直接报pinctrl_select_state失败后面所有初始化都中止。所以每次新加节点时我习惯先打开 pinctrl 相关的 dtsi 确认这个 pin 有没有被别的节点占用而不是只改完自己这个节点就收工。全局搜索目标引脚名是一个很保值的好习惯。另外拿到一份外部 BSP 时第一件事是让板子跑起来后执行cat /sys/kernel/debug/gpio cat /sys/kernel/debug/pinctrl/pinctrl-handles看系统目前的 pin 占用全貌。这些 debugfs 信息对排查设备树引脚冲突的帮助比看十遍源码都好用。6. 进阶pstore 节点与 overlay 这种“把设备树当配置中心”的玩法6.1 给内核崩溃留后手RK3568 设备树里的 ramoops/pstore 配置驱动开发哪有不崩溃的。调试驱动时最无语的情况是内核 panic 后重启最后几行日志丢了完全看不出崩溃原因。ARM64 平台上常用的手段是 pstore/ramoops它可以把崩溃前的内核日志保存在一段保留内存里重启后读出来。设备树里通常这样配reserved-memory { #address-cells 2; #size-cells 2; ranges; ramoops110000 { compatible ramoops; reg 0x0 0x110000 0x0 0xf0000; record-size 0x20000; console-size 0x20000; ftrace-size 0x10000; pmsg-size 0x20000; }; };这段配置的含义是在物理内存起始 0x110000 的位置划出 0xf0000 大小作为保留区。record-size是保存 panic 日志的缓冲区大小console-size是内核 console 输出的缓冲区ftrace-size给 ftrace 用pmsg-size给用户空间 pmsg 用。具体地址需要根据板子内存布局调整绝不能和内核镜像、DDR 初始化参数冲突。内核侧还需要开启相关配置CONFIG_PSTOREy CONFIG_PSTORE_RAMy CONFIG_PSTORE_CONSOLEy CONFIG_PSTORE_FTRACEy CONFIG_PSTORE_PMSGy CONFIG_RAMOOPSy板子崩溃重启后去/sys/fs/pstore/下看ls /sys/fs/pstore/ cat /sys/fs/pstore/console-ramoops-0能看到之前的 panic backtrace就能定位问题。这个钩子不需要驱动代码配合纯粹是设备树和内核机制之间的配合也是“设备树描述保留内存资源”的一个典型用法。我在 RK3568 上调试一个崩溃在中断上下文里的驱动时就是靠 pstore 把最后那几行日志抓出来的。6.2 设备树 overlay 和“把板级差异写进运行时配置”的思路设备树并不是“只能烧死在 bootloader 里”。Linux 内核还支持 device tree overlay简单说就是允许运行时往根设备树上叠加一个新节点或修改现有节点。最常见的应用场景是一个主设备树适配多种外设扩展板而不想为每种组合重新编译整个 dtb。一个最小 overlay 的源码是这样的/dts-v1/; /plugin/; / { fragment0 { target-path /; __overlay__ { my-demo { compatible myvendor,gpio-demo-led; status okay; }; }; }; };编译成 dtbodtc -I dts -O dtb - -o demo.dtbo demo-overlay.dts加载到内核需要内核配置CONFIG_OF_OVERLAY和CONFIG_CONFIGFS_FSmkdir /sys/kernel/config/device-tree/overlays/demo cat demo.dtbo /sys/kernel/config/device-tree/overlays/demo/dtbo卸载时直接删除目录rmdir /sys/kernel/config/device-tree/overlays/demooverlay 的价值不在于让你“不用重新编译内核”而在于把板级外设差异从固定 dts 中解放出来。比如 RK3568 上做网关产品不同客户可能选不同的 4G 模块引脚排列也有差异。与其维护几十份完整 dts不如维护一份基础 dts 加几个 overlay运行时根据客户型号加载对应 dtbo。这样出厂固件可以统一硬件配置变成运行时策略。我个人的习惯是能用固定设备树解决的问题不引入 overlay。因为 overlay 的调试手段比固定 dts 少出问题定位也麻烦。但如果你们的硬件形态确实很多、组合爆炸overlay 这套机制很值得认真掌握。使用 overlay 时我还建议在启动脚本里记录加载顺序和文件 md5不然板子多了之后现场查“为什么有人加载了旧版本 overlay”会非常痛苦。最后再分享一个设备树调试的小技巧拿到一块新板子先别急着写复杂驱动从点灯开始把“改 dts - 编 dtb - 烧分区 - 看 /proc/device-tree - 验证 sysfs”这条链路完整跑通。这个过程看起来简单但能同时验证你的编译环境、烧录工具、内核设备模型和 pinctrl 配置是否全都正确。链路通了后面写什么驱动心里都有底。设备树不是洪水猛兽它就是一套需要你亲手“喂”给驱动的硬件描述协议跟着实际板子多走几遍踩过坑自然就通透了。