嵌入式Linux设备树完全指南:从dts/dtsi到驱动匹配与调试

发布时间:2026/10/22 6:28:19
嵌入式Linux设备树完全指南:从dts/dtsi到驱动匹配与调试 要说嵌入式Linux里最让人又爱又恨的东西设备树Device Tree绝对排得上号。很多开发者第一次接触时面对那一堆dts、dtsi文件会忍不住问这不就是一堆描述硬件的“配置文件”吗怎么里面的门道这么多我当时也有同样的困惑。但真正搞清楚之后会发现设备树不是内核的附属品而是连接“硬件事实”和“软件抽象”的关键一环。本文不讲空洞的概念我会从设备树存在的意义、语法细节、编译加载链路、驱动匹配机制到实际调试中的坑完整拆一遍适合刚接触内核移植的开发者也适合被设备树折磨过想系统捋一遍的同行。1. 从板级文件的地狱到设备树的春天一场硬编码革命1.1 没有设备树的年代内核里发生了什么在设备树大规模进入主线内核之前ARM平台的内核里塞满了平台相关的代码。每一个板子、每一款开发板都要在arch/arm/mach-xxx目录下单独维护一份板级文件里面用一大段C代码描述硬件资源。比如某块板子上有串口、网卡、LED、按键开发者需要在platform_device的数组里一个个手动注册还要把内存地址、中断号、时钟信息写死在代码里。这种做法带来的问题很现实。同一个SoC往往衍生出几十上百种评估板和商业产品而这些板子之间的差异往往就是某个外设换个地址、换根中断、多加一颗PMIC。如果每块板子都要维护一份板级文件内核里就会出现大量高度重复的代码。更麻烦的是这些代码一旦合并进内核主线后面想清理就非常困难——不是不能删而是没人敢动因为你不知道谁还在依赖这份文件。我当时接手的一块老平台光是mach-xxx目录下的板级文件就有将近三十份其中很多唯一的工作就是“重新注册一块网卡驱动”。当时我就在想这种模式怎么可能持续。事实证明它确实没有持续下去。1.2 设备树到底解决了什么核心问题设备树的思路很直接把硬件的描述信息从源代码里剥离出来放到一份独立的数据结构里。内核启动时拿到这份数据动态创建出对应的platform_device和资源描述驱动只需要按compatible字符串去匹配自己负责的硬件就行。这套机制带来的好处有三个层次。第一层是代码瘦身。同一颗SoC可以只有一个核心的dtsi文件描述通用部分具体板卡只需要一个很小的dts文件覆盖掉跟默认配置不同的部分。新增一款板子基本不用动内核代码写一个dts然后编译进去就行了。第二层是内核、驱动、硬件三者解耦。SoC厂商负责维护dtsi板卡厂商维护dts用户维护自己的设备树覆盖层各管各的。驱动作者不再需要关心自己的驱动会被注册多少次只需要写明compatible支持哪些设备名。第三层是系统更接近“通用内核”的思路。一套内核镜像通过更换不同的dtb文件就能适配不同硬件这对预编译内核的发行版方案尤其重要。后面Android生态、整机厂商大规模拥抱设备树本质上都是看中了这个灵活性。实际上即使你是做x86平台的理解设备树也有价值因为U-Boot、TF-A、OpenSBI等固件侧的技术栈都大量使用了设备树或device tree相关的概念。它不是某一个平台的小工具而是硬件描述的一种通用语言。2. dts/dtsi里的构件规则把一棵硬件树读薄2.1 公共部分放dtsi板级差异放dts很多新手翻开内核里的arch/arm/boot/dts或者arch/arm64/boot/dts目录第一反应是文件多到让人崩溃。其实文件的组织逻辑很简单记住一条原则就行dtsi是“可复用部分”dts是“最终板级描述”。以某颗常见的SoC为例内核里通常有一个xxx.dtsi描述CPU核心、总线上挂载的各个通用外设控制器还有一个xxx-pinctrl.dtsi专门描述引脚复用选项这些都是同一SoC所有平台共用的。而具体的开发板文件比如xxx-board.dts里面#include这些dtsi然后补充这个板子上真实存在的外设名、地址、中断、GPIO配置。这里必须特别注意一个细节设备树里的#include可以用C语言的预处理器处理所以你经常能看到#define宏定义也可以用/include/这种旧语法。用C预处理器好处很多最常见的用法是引用厂商标定好的引脚宏例如GPIO_ACTIVE_LOW这类常量。实际工程中几乎都是#include 预处理器宏的组合裸写/include/的反而越来越少了。2.2 节点、属性、地址设备树的三个基本构件设备树的结构是一棵树根节点用/表示下面挂cpus、memory、soc、chosen等子节点。每个节点可以有属性属性就是一个键值对。地址相关的属性被单独拎出来重点对待原因很简单设备树要解决的第一个问题就是让内核知道硬件资源在哪。先看一个最基础的串口节点uart1 { status okay; pinctrl-names default; pinctrl-0 pinctrl_uart1; };这个写法是“引用”根树干或SoC dtsi里已经定义好的uart1节点然后覆盖属性。如果你的板级文件想开启某个外设就把status改写成okay想关闭就写成disabled。很多板级调试时外设不工作查了一圈发现status没有打开这种案例我见得太多了。再来看地址描述相关的模型。设备树里一个带寄存器的节点必须有reg属性同时其父节点要通过#address-cells和#size-cells声明每个地址和长度码用了多少个u32。这里是最容易出错的地方因为代码里看到的reg 0x1000 0x200到底代表什么完全取决于父节点的地址单元定义。soc { compatible simple-bus; #address-cells 1; #size-cells 1; uart0: serial10000000 { compatible vendor,uart; reg 0x10000000 0x100; interrupts 0 38 4; }; };这个例子很典型。#address-cells 1表示地址宽1个cell#size-cells 1表示长度宽1个cell所以reg 0x10000000 0x100表示起始地址是0x10000000映射长度是0x100。如果父节点两个属性都写成了2那同一行reg的语义就完全变了驱动拿到的地址就会错乱。2.3 核心属性速查compatible、model、phandle与中断compatible是设备树里最重要的属性它是一份“候选驱动名单”。驱动侧用of_match_table列出自己支持的所有设备名内核在扫描设备树时逐个比对。比如一个以太网控制器节点可以写compatible vendor,eth-v2, vendor,eth;前面的字符串代表精确型号后面的代表通用型号。匹配时按顺序优先匹配更精确的。所以驱动里必须注意如果你只写了vendor,eth-v2那只有精确匹配这一条生效如果你想做到旧新兼容最好在驱动里把多个兼容字符串都写全。厂商名加型号名的格式不只是习惯问题它能在一定程度上避免命名冲突建议大家都遵守。中断相关的属性是另一大块。设备树里描述中断通常需要interrupt-parent指定中断控制器interrupts指定中断号。但interrupts的具体语法没有一个“统一中断号”每个中断控制器都可以通过#interrupt-cells来自定义单元个数。对ARM GIC来说典型写法是interrupts 0 42 4;三个cell分别是中断类型0代表SPI共享外设中断1代表PPI私有外设中断、中断号、触发标志4代表高电平触发。这里有个经典误区很多人直接把数据手册里的中断号填进去结果驱动死活不触发其实是因为GIC的两个cell的第一个值写错导致内核把SPI当成PPI去路由了。phandle是设备树里的“指针”。当你在某个节点里写clocks clk_uart时clk_uart编译后就是一条phandle引用指向标签为clk_uart的节点。标签label和phandle的关系是编译器在编译时会自动为带label的节点生成一个唯一的phandle你在引用时只需要写label即可。如果手写phandle属性很容易跟自动生成的冲突实际项目中基本不需要手写。3. 从源码到内核运行时编译、加载与展开的完整链路3.1 DTC编译器和三种格式的来回折腾设备树的源码格式是.dts编译后的二进制格式是.dtb同时还有一个文本格式.dtsi。编译工具叫DTCDevice Tree Compiler大部分平台的基础软件包里都已经装了。如果没装一般通过包管理器装一个device-tree-compiler即可。基本的编译命令是这样的dtc -I dts -O dtb -o output.dtb input.dts反编译也就是从dtb还原成可读的dtsdtc -I dtb -O dts -o output.dts input.dtb内核源代码树里编译设备树的路径通常不用你手动敲命令。在arch/arm64/boot/dts/下新增一个.dts文件后只要把它加进该目录下的Makefile然后编译内核dtb文件就会自动生成。实际项目里最常用的反编译场景是为了验证线上运行的固件里到底烧录了什么配置可以用dtc把固件提取出来的dtb还原。我曾经调试过一个问题板子内核启动时串口地址和驱动对不上最终就是通过反编译dtb才发现设备树里寄存器的值已经被人为改过根因是启动时环境变量覆盖了某些内存区域。除了dtc之外fdtdump也是快速查看dtb内容的好工具它不会帮你保存成漂亮格式但能快速看结构。3.2 Bootloader如何把dtb交到内核手上设备树从磁盘到内核手里要经过Bootloader。现在绝大多数平台用的Bootloader都是基于U-BootU-Boot启动内核时通常会把dtb加载到内存的某个地址然后通过启动参数传递地址。ARM64平台常见做法是把dtb地址放在一个固定的寄存器里U-Boot里也经常能看到这样的命令setenv fdt_addr_r 0x40000000 load mmc 0:1 ${fdt_addr_r} /boot/board.dtb booti ${kernel_addr_r} - ${fdt_addr_r}这个流程有几种常见问题。最常见的是fdt_addr_r和内核镜像加载地址重叠结果内核启动时把设备树的区域覆盖了内核后来读设备树数据时读到的全是垃圾。所以做启动调试时第一件事应该是确认内存布局确保kernel、ramdisk、fdt三个区域的地址互相之间没有冲突。U-Boot还支持对dtb做一些“修改”后再传给内核。比如fdt set命令可以临时改变某个属性的值。以前我调试过一款板子上的PMIC默认设备树里status是disabledU-Boot启动脚本里执行fdt set /pmic status okay后内核才正常初始化了电源芯片。这类操作适合临时验证但正式方案一定要改源码不要在启动脚本里堆一堆fdt set否则固件里的设备树和实际生效的设备树会越漂越远。3.3 内核如何把dtb变成运行时设备模型内核拿到的dtb是一份紧凑的二进制数据。在启动早期内核栈还没完全准备好时它用一套精简的内存操作去遍历节点定位内存范围、选择rootfs、准备cmdline等。之后在设备模型尚未完善阶段把整个dtb展开成一套树状结构这个过程在源码里对应unflatten函数。展开后的树会挂在根节点下并且在每个展开节点上附上相应的资源和属性指针。展开完成后内核会沿着设备树创建platform_device。任何一个设备树节点如果它的compatible匹配上了内核里的驱动或者它属于simple-bus这类已知总线节点都有可能被实例化成设备。对内核开发者来说设备树展开后最直观的观察窗口是/proc/device-tree和/sys/firmware/devicetree/base里面有每个节点的属性和子节点。如果/proc/device-tree里看不到某个节点说明它要么没进编译要么在某个环节被丢弃了。有个细节值得注意设备树节点并不是每个都会变成platform_device。内核会跳过那些没有compatible的纯容器节点也会跳过chosen、aliases、memory这类特殊节点。很多开发者新建外设节点后发现驱动匹配不上却忽略了节点本身可能根本没被实例化成设备。如果你在/sys/bus/platform/devices/里找不到对应的设备那问题大概率出在节点定义上。4. 驱动与设备树对接的关键机制匹配、probe与资源转换4.1 compatible匹配到底是怎么发生的设备树的威力最终要通过驱动体现出来。驱动侧最核心的数据结构是of_match_table。一个典型的platform_driver会这样注册static const struct of_device_id my_uart_of_match[] { { .compatible vendor,uart-v2, }, { .compatible vendor,uart, }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, my_uart_of_match); static struct platform_driver my_uart_driver { .probe my_uart_probe, .remove my_uart_remove, .driver { .name my_uart, .of_match_table my_uart_of_match, }, }; module_platform_driver(my_uart_driver);当内核创建设备时platform_match函数会依次尝试多种匹配方式设备树匹配是其中最关键的一条。它会遍历驱动的of_match_table用of_device_is_compatible逐个比较设备树节点的compatible属性。比较规则是如果设备树节点compatible的第一个字符串跟驱动表里的某一项完全相等则匹配成功否则继续尝试下一个。这里有一个很多人容易忽略的坑compatible比较是单向的。设备树里的compatible写成vendor,uart-v2, vendor,uart驱动表里只有vendor,uart时内核依然能匹配成功因为它会依次拿设备树里的每个字符串去比对。但如果设备树里只写了vendor,uart-v2而驱动表里只有vendor,uart那匹配就会失败。也就是说设备树里应该写最具体的版本号在前通用型号在后驱动表则应该把通用的兜底型号也写进去这样才能兼容后续的小版本升级。4.2 reg、中断、GPIO如何变成驱动可以用的资源设备树节点被实例化成platform_device后原来写在reg里的地址信息会被转换成一个或多个struct resource。驱动里用platform_get_resource(pdev, IORESOURCE_MEM, 0)就能拿到第一个内存资源。注意这个资源的名字就是节点名resource的顺序跟reg属性里的条目顺序完全一致。如果你的reg里写了两个寄存器段第一个是控制寄存器、第二个是数据缓冲那索引0和索引1的语义千万不能搞反。中断的转换相对复杂。设备树里interrupts属性经过内核的中断子系统转换后会映射成Linux的IRQ number。在驱动里通过platform_get_irq(pdev, 0)拿到中断号再通过request_irq注册中断处理函数。在设备树模式下你不需要手动计算中断号因为GIC等中断控制器通过irq_domain建立了硬件中断与Linux中断号之间的映射关系。调试关键时候可以在驱动里打印一下platform_get_irq的返回值跟/proc/interrupts里看到的中断号对比能快速确认是不是同一个中断。GPIO资源则走另一套API。设备树里GPIO的引用通常是通过gpios属性描述的属性里包含一个指向GPIO控制器的phandle后面跟上几个cell数据具体格式由#gpio-cells决定。led0: led { gpios gpio4 10 GPIO_ACTIVE_HIGH; };内核里用devm_gpiod_get系列API请求GPIO时会解析这个属性并通过gpio_chip里的映射关系把设备树里的描述转换成实际的GPIO编号。这里建议不要在老的gpio_*接口里硬编码GPIO号一定要从设备树里取。4.3 在驱动里读取自定义属性实测中最好用的一套API除了标准角色之外设备树还能承载很多板级差异化的配置参数。比如一个LCD驱动可能需要知道屏的物理尺寸、时序参数、背光亮度上限这些都可以写在设备树节点里。驱动侧用of_property_read_u32、of_property_read_string等API去读取。u32 max_brightness; struct device_node *np pdev-dev.of_node; ret of_property_read_u32(np, max-brightness, max_brightness); if (ret) { dev_warn(pdev-dev, max-brightness not found, use default 255\n); max_brightness 255; }读取字符串数组时要注意设备树里字符串属性通常以string list形式存在比如clock-output-names clk_uart, clk_dma;驱动里用of_property_count_strings和of_property_read_string_index遍历。真正踩坑的是很多人用of_property_read_string去读一个数组属性结果只拿到第一个字符串后面全是乱码。这是典型的API使用错误读取复数属性必须用_index系列。5. 调设备树时我踩过的坑和最终沉淀的排查路线5.1 dtc编译和运行时最容易翻车的几个报错设备树编译不像C语言那样有严格的类型检查很多错误是运行期才暴露的。但有几个编译期报错非常典型值得提前记下来。第一个是地址单元不匹配。在某个节点下写了reg属性但父节点的#address-cells和#size-cells没有定义或者值定义错了dtc会报类似“node has a reg property but no #address-cells”的错误。这个错误多半不是你写的那个节点的问题而是它的祖父节点或者更上级的容器节点少了属性。第二个是phandle冲突。如果你手动写了phandle同时又用label去引用同一个节点编译器可能不报错但展开后指向混乱。所以建议永远不要手写phandle全部通过label来自动生成。第三个是语法错误被C预处理器放大。因为设备树文件要经过C预处理器一旦你忘记#include某个头文件宏展开不出来报错位置往往在文件末尾非常难定位。我的经验是先单独跑一遍cpp把展开后的纯dts文件拿出来看错误一目了然。5.2 设备树叠加层overlay与运行时验证设备树叠加层dtbo是调试和产品定制的一把利器。内核支持在运行时动态加载一个叠加二进制把一组新的节点或属性“叠加”到现有设备树上这在FPGA工程、客制化配件、原型开发中非常常见。编译overlay的命令类似dtc -I dts -O dtb - -o custom.dtbo custom.dts-选项会生成__symbols__节点这是叠加层能够引用基础树上节点的必要条件。使用叠加层时板级dtb和dtbo的内存布局要提前规划好内核侧的configfs接口会负责加载和卸载。我在实际调试中更常用的验证方式是反编译。拿到线上运行的dtb后第一件事永远是dtc -I dtb -O dts还原成可读文件跟源码做对比。很多所谓“设备树改了没生效”的问题反编译之后立刻水落石出——要么根本没编译进固件要么编译的是旧版文件要么U-Boot启动时被脚本改写了。5.3 设备树排查问题清单与常用查看点这里给出一份我自己整理的排查清单每次设备树相关的异常基本按这个顺序过检查点具体操作常见问题节点是否存在ls /proc/device-tree/node节点缺失或不在树中参数值是否生效xxd /proc/device-tree/node/prop属性值被覆盖或地址错误设备是否实例化ls /sys/bus/platform/devices/compatible不匹配或节点未展开驱动是否绑定查看/sys/bus/platform/drivers/drv/驱动没有该设备的绑定入口中断是否注册cat /proc/interrupts中断号映射错误或未触发注册资源是否申请驱动probe日志中打印资源地址reg解析错误我自己调试时最常看的窗口是/sys/firmware/devicetree/base。比如怀疑某个外设的时钟配置有问题就先执行cat /sys/firmware/devicetree/base/soc/serial10000000/clocks看看当前生效的phandle指向的是不是预想中的那个时钟节点。再看/sys/kernel/debug/clk/clk_summary确认时钟是否已经打开、频率是否正确。这套链路走一遍大部分问题都能定位到具体环节。5.4 一个真实案例复盘从网卡不工作到定位属性链断裂最后说一个印象比较深的实际案例。当时某款板子在量产测试中以太网控制器时灵时不灵。用ifconfig看到接口起来了但ping外网不通重启后又好了过一段时间又挂。一开始怀疑驱动问题增加打印反复测试发现驱动probe正常中断也注册了但mii通信一直报超时。后来我从设备树入手。先反编译量产固件里的dtb发现以太网节点的phy-handle指向的PHY节点是正常的但PHY节点里挂着reset-gpios属性指向一个GPIO控制器。顺着这个GPIO查下去发现它在板级dts里的定义和SoC dtsi里的引脚复用配置冲突了。进一步对比两个版本引脚复用发现某次改用新PCB时PLD的逻辑做了调整但设备树里的pinctrl引脚编号没同步更新。问题本质是设备树里“GPIO控制器引用”和“引脚复用配置”两套独立属性引用的其实是同一个引脚但配置内容互相矛盾。整个排查过程里最花时间的不是设备树解析而是把“驱动显示正常”和“底层集成错误”的边界理清楚。设备树里改了reset-gpios使用另一个引脚后问题彻底消失。复盘下来就是一个典型的“设备树属性链”问题eth - phy - reset - gpio - pinctrl每层单独看都合理串起来就不对了。所以做设备树调试一定要有“链路思维”。看节点不能停留在节点本身要顺着phandle、pinctrl、clocks、interrupts这些引用走完整条链确认每个环节的属性和父节点的cells定义是一致的。很多时候问题根源不在你怀疑的那个节点而在它引用的某个上游或下游节点。把这条链跑顺设备树的很多“诡异问题”都会变成逻辑清晰的配置问题。pinctrl和GPIO复用的问题尤其隐蔽。这两个子系统各自维护一套描述内核里没有强校验去保证二者不冲突。我在后面的项目里养成了一个习惯硬件改板后第一件事就是拿着原理图把GPIO复用表格过一遍设备树确认没有引脚被同时声明成两个外设。设备树不是写完编译过就完事它必须和实际硬件一一对应这一点比代码规范更重要因为编译器不会替你检查硬件事实。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询