ARTICLE DETAIL

资讯详情

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

ARM Linux设备树从入门到实战:DTS语法、编址与中断映射全解析

ARM Linux设备树从入门到实战:DTS语法、编址与中断映射全解析 简介设备树在ARM架构Linux开发中承担描述硬件平台配置的关键角色掌握其节点、属性、compatible机制是驱动与内核移植者绕不开的基本功。这份文档从基础数据格式讲起逐步拆解CPU与内存映射设备的编址方式、Ranges地址转换、中断控制器与中断线路映射并覆盖设备特定数据、aliases与chosen特殊节点同时专门展开PCI主机桥接、PCI总线编号与地址翻译、高级中断映射等进阶主题。内容兼具原理讲解和代码示例适合嵌入式Linux开发者从零理解设备树也适合已有基础者系统查漏补缺、应对实际调试中遇到的硬件描述问题。资源以docx形式提供共1个文件压缩包整体约68KB轻量便于收藏查阅。目前已有1078人学习下载说明其在设备树入门与进阶场景中得到一定认可。阅读后可快速建立设备树整体知识框架并借助文中解析思路解决实际板级适配中的常见难点。1. 设备树不是配置文件是硬件的“寄存器级自述”拿到一块新板子最怕的不是驱动代码报错而是内核启动后串口控制台一片死寂连个 oops 都没有。我遇到过最典型的一次双核 Cortex-A9 的板子内核版本和驱动全部就绪唯独设备树里cpus节点忘了写#address-cells 1导致后续所有reg解析错位中断控制器和串口的地址全部读偏系统直接挂在 earlycon 之前。那一刻才意识到设备树在 ARM Linux 里不是文档不是配置文件而是内核与硬件之间唯一可信的“寄存器级自述”。它描述的是 CPU 视角下的整机拓扑哪些设备存在、挂在哪个总线上、地址是多少、中断怎么路由、驱动该匹配哪个 compatible。本文面向嵌入式 BSP 和驱动开发工程师从 ePAPR 的规范骨架出发把节点语法、编址规则、中断映射和 PCI 这类高级场景逐个拆开最后给出验证和排错的具体手段。2. 设备树数据格式节点、属性与五种编码表示2.1 最小的树长什么样设备树本质上是一棵由节点node和属性property构成的树。节点对应系统中的设备或总线容器属性则是键值对用来描述该设备的具体特征。先看一个最小但完整的.dts片段/ { node1 { a-string-property A string; a-string-list-property first string, second string; a-byte-data-property [0x01 0x23 0x34 0x56]; child-node1 { first-child-property; second-child-property 1; }; child-node2 { }; }; node2 { an-empty-property; a-cell-property 1 2 3 4; }; };这段树不描述任何真实硬件但展示了设备树的全部结构元素根节点/、子节点node1、孙节点child-node1以及分散在各层的属性。注意an-empty-property没有值这种布尔型属性在驱动里通常表示“存在即使能”比如interrupt-controller和dma-coherent。设备树源文件本身是文本经过dtc编译成二进制 DTB 后由内核解析文本结构只是给人阅读和修改的形态。2.2 属性值的五种基本编码属性值在.dts里虽然看起来只是字符串但底层是不同的字节流。理解这五种形式是读懂任何一篇设备树源码的前提string-property a string; /* 文本字符串null 结尾 */ cell-property 0xbeef 123 0xabcd1234; /* 32 位无符号整数一个数占 4 字节 */ binary-property [0x01 0x23 0x45 0x67]; /* 二进制字节流方括号内逐字节给出 */ mixed-property a string, [0x01 0x23], 0x12345678; /* 逗号拼接不同类型 */ string-list red fish, blue fish; /* 字符串列表逗号分隔 */cell是设备树里最容易踩坑的地方。1 2 3 4表示四个 32 位无符号整数每个叫一个 cell占 4 字节。reg 0x101F1000 0x1000中的两个数字分别表示起始地址和长度含义由父节点的#address-cells和#size-cells决定这是第 4 章的重点。二进制数据方括号内每个字节一个十六进制数可以连续书写常用于 MAC 地址、中断号表这类定长数据。逗号拼接的混合属性较少见但 ePAPR 明确允许适用场景是同一属性里既有字符串描述又有数值参数。2.3 从 dts 到 dtb编译与反编译回路写设备树第一步是掌握 dtc 工具链。内核源码树的scripts/dtc/dtc或系统自带的 dtc 都可以用dtc -I dts -O dtb -o board.dtb board.dts dtc -I dtb -O dts -o board_reverse.dts board.dtb第一条命令把文本源文件编译成二进制 DTB第二条把内核实际加载的 DTB 反编译回文本。我在定位问题时常做的是先取内核运行时实际使用的 DTB 反编译再和源码 diff这样能立刻发现是编译流程没更新、还是 bootloader 覆盖了某段设备树。-选项可以在编译时生成symbols节点供 overlay 和 phandle 引用在模块化设备树调试里非常有用。3. 节点命名规则与 compatible 驱动匹配机制3.1 命名的两个组成部分名字与设备地址每个节点必须遵循name[unit-address]的命名形式。name是不超过 31 个字符的 ASCII 字符串描述设备类型而不是具体型号。例如 3com 的以太网适配器节点应该叫ethernet而不是3com509。[unit-address]部分在节点描述的设备有地址时必须出现且该地址通常是节点reg属性里的主地址。同级的兄弟节点名称必须唯一但不同地址的同类设备可以用同一个通用名比如serial101F1000和serial101F2000。这个地址不是厂商随意填的它必须和节点reg属性的第一项指向同一位置否则 dtc 在编译时会报unit_address_vs_reg警告而这类警告在板级 bring-up 阶段往往预示着后续驱动资源申请错乱。3.2 一棵完整的设备树骨架把第 2 章的语法套到一个真实机器上。假设一块基于 ARM Cortex-A9 的板子有 CPU、串口、GPIO、中断控制器、SPI 和一条外部总线那么骨架如下/ { compatible acme,coyotes-revenge; cpus { #address-cells 1; #size-cells 0; cpu0 { compatible arm,cortex-a9; reg 0; }; cpu1 { compatible arm,cortex-a9; reg 1; }; }; serial101F1000 { compatible arm,pl011; }; serial101F2000 { compatible arm,pl011; }; gpio101F3000 { compatible arm,pl061; }; interrupt-controller10140000 { compatible arm,pl190; }; spi10115000 { compatible arm,pl022; }; external-bus { ethernet0,0 { compatible smc,smc91c111; }; i2c1,0 { compatible acme,a1234-i2c-bus; rtc58 { compatible maxim,ds1338; }; }; flash2,0 { compatible samsung,k8f1315ebm, cfi-flash; }; }; };这棵树目前还不能直接启动因为设备之间的连接关系中断线、地址映射还没描述但层次结构已经能正确反映 CPU 视角的硬件拓扑。串口、GPIO 这些内存映射设备挂在根下外部总线设备挂到external-bus节点下i2c 设备挂到 i2c 控制器下。在瑞芯微 RK3568 这类主流 ARM SoC 的设备树里同样沿用的是这套结构SoC 级外设放在根节点下板级外设挂在对应总线上。这种“内聚节点”写法让同一款 SoC 的不同板卡可以复用同一个soc.dtsi板级差异只体现在根节点和总线下挂的设备上。3.3 compatible 的匹配顺序与驱动绑定每个表示设备的节点都必须有compatible属性它是操作系统选择设备驱动的唯一依据。属性值是一个字符串列表第一个字符串指定精确型号格式为制造商,型号后面的字符串声明它兼容哪些其他设备。节点compatible 写法说明顶层机器acme,coyotes-revenge制造商前缀 精确型号避免命名空间冲突CPUarm,cortex-a9精确 CPU 型号串口arm,pl011精确外设型号以太网smc,smc91c111精确芯片型号NOR Flashsamsung,k8f1315ebm, cfi-flash精确型号 兼容通用标准接口flash节点有两个字符串这是兼容性列表的典型用法。内核在匹配驱动时按顺序遍历of_match_table把节点 compatible 的第一个字符串优先匹配精确驱动如果找不到再用第二个字符串去匹配通用驱动。以cfi-flash为例它对应 Linux 内核里通用的 CFI 接口 Flash 驱动任何遵循 CFI 规范的 Flash 芯片都能被识别。驱动侧of_device_id表中同样按顺序排列匹配成功就绑定因此 compatible 的顺序在驱动开发中非常重要。关于这条属性我一直坚持一个原则不要使用通配符形式比如fsl,mpc83xx-uart。硅片厂商在后续版本中改变寄存器行为是常态一旦compatible里写了通配字符串旧驱动会被错误匹配到新一代芯片上等到发现问题时硬件已经没法改回。正确做法是选择某一代具体芯片写入 compatible后续所有新芯片都通过追加fsl,具体型号来声明兼容而不是覆盖掉精确型号。4. 编址机制address-cells、size-cells 与 ranges 的配合4.1 CPU 编址最简单的 tuple 形式可编址设备通过reg属性描述地址范围reg是一个 tuple 列表每个 tuple 由地址, 长度构成。但地址和长度到底占几个 32 位 cell不由节点自己决定而由父节点的#address-cells和#size-cells决定。CPU 节点是最简单的例子。CPU 的 ID 是一个单一的整数没有“大小”概念cpus { #address-cells 1; #size-cells 0; cpu0 { compatible arm,cortex-a9; reg 0; }; cpu1 { compatible arm,cortex-a9; reg 1; }; };这里的#size-cells 0表示reg中不包含长度字段每个 CPU 只占一个 cell 的 ID 值。cpu0的unit-address必须和reg 0对应dtc 依赖这一规则校验节点命名。对于多核系统CPU 的 ID 顺序直接映射到内核的cpu logical id如果reg和硬件实际的 MPIDR 不匹配会引发smp_prepare_cpus阶段的启动异常。4.2 内存映射设备地址加长度的标准写法内存映射外设的reg通常包含起始地址和地址块大小。父节点的#address-cells 1、#size-cells 1是最常见的组合表示地址和长度各占一个 cellserial101F1000 { compatible arm,pl011; reg 0x101F1000 0x1000; }; gpio101F3000 { compatible arm,pl061; reg 0x101F3000 0x1000; };0x101F1000 0x1000的意思是串口的寄存器基址在 0x101F1000寄存器块占据 0x10004KB空间。这里的长度不是必须精确到最后一个寄存器而是按总线对齐规则给出整段区域内核的ioremap会严格按这个长度映射。曾见过有人把长度写成 0x100驱动读写越界后访问到了相邻外设的寄存器空间表现是串口数据偶发错乱查了很久才发现是长度值抄错了数据手册。4.3 非内存映射设备地址即总线上的从地址I2C、SPI 这类总线上的设备不支持内存映射它们的“地址”是总线协议层的从设备地址没有长度概念。看 i2c 下的 RTC 节点i2c1,0 { compatible acme,a1234-i2c-bus; #address-cells 1; #size-cells 0; rtc58 { compatible maxim,ds1338; reg 0x58; }; };i2c 控制器节点下设置了#size-cells 0因此子节点rtc58的reg 0x58只包含一个 cell 的从地址 0x58即 Maxim DS1338 的 7 位 I2C 从地址。这类总线在reg中没有长度是因为总线上没有地址空间的概念驱动的i2c_get_clientdata拿到的是addr字段而不是资源。注意这里 rtc 节点命名里的58是十进制还是十六进制设备树里unit-address按十六进制解析rtc58表示十六进制 0x58这个约定在整个设备树里一致写错进制会导致命名校验失败。4.4 ranges父子地址空间之间的翻译桥ranges属性是设备树处理地址翻译的核心。当子节点挂在某个总线桥上子节点的reg地址是总线本地地址而不是 CPU 视角的地址时父节点必须通过ranges建立“本地地址 → CPU 地址”的映射。外部总线桥就是典型场景。桥的本地地址空间从 0 开始而 CPU 把这段空间映射到了 0x10100000 和 0x30000000external-bus { #address-cells 2; #size-cells 1; ranges 0 0 0x10100000 0x1000, 1 0 0x10160000 0x10000, 2 0 0x30000000 0x4000000; ethernet0,0 { compatible smc,smc91c111; reg 0 0 0x1000; }; i2c1,0 { compatible acme,a1234-i2c-bus; reg 1 0 0x10000; }; flash2,0 { compatible samsung,k8f1315ebm, cfi-flash; reg 2 0 0x4000000; }; };ranges的每个子项格式是子地址, 父地址, 长度本例中子地址由两个 cell 组成片选号 偏移父地址一个 cell长度一个 cell。0 0 0x10100000 0x1000的含义是片选 0、偏移 0 的本地地址对应 CPU 侧的 0x10100000映射长度 0x1000。reg 0 0 0x1000里的0 0正是 ranges 第一项的本地地址内核通过遍历 ranges 表完成翻译。这里有一个容易忽略的细节当ranges属性存在但为空ranges;时表示父子地址空间完全 1:1 映射没有偏移当节点完全没有ranges属性时子设备对 CPU 来说不可访问属于纯内部总线。调试外部设备 probe 失败时先确认 ranges 翻译后的地址是否符合预期比直接追驱动代码快得多。你可以用of_translate_address在内核里手动验证某个子节点的地址也可以在用户态读/proc/device-tree下的对应节点核对原始值。5. 中断工作方式与设备特定数据的编码5.1 中断控制器节点如何声明自己中断在设备树里同样用属性描述核心是三个角色中断控制器、引用中断的设备、以及连接两者的路由信息。中断控制器节点必须声明自己是控制器并说明每个中断请求线占用几个 cellinterrupt-controller10140000 { compatible arm,pl190; reg 0x10140000 0x1000; interrupt-controller; #interrupt-cells 1; };interrupt-controller;是空属性存在即表示该节点是中断控制器#interrupt-cells 1表示该控制器下一个中断号用一个 32 位 cell 表示。对 ARM 的 VICPL190这类控制器一个 cell 就够用了但 GIC通用中断控制器通常需要三个 cell分别编码中断类型SPI/PPI、中断号和触发方式。#interrupt-cells的值直接决定下游设备节点interrupts属性里要写几个数写错一个数整个中断路径全部解析错乱。5.2 设备节点如何申请中断设备节点用interrupt-parent指定中断控制器用interrupts指定中断号。interrupt-parent可以显式写在设备节点里也可以沿父节点链向上继承intc: interrupt-controller10140000 { compatible arm,pl190; reg 0x10140000 0x1000; interrupt-controller; #interrupt-cells 1; }; serial101F1000 { compatible arm,pl011; reg 0x101F1000 0x1000; interrupts 7; interrupt-parent intc; }; gpio101F3000 { compatible arm,pl061; reg 0x101F3000 0x1000; interrupts 9; };interrupts 7表示该串口连接到 intc 的中断输入线 7。显式写interrupt-parent intc时intc是对interrupt-controller10140000节点的 phandle 引用这是设备树最常用的节点引用语法。没有显式声明interrupt-parent的节点内核会沿着父节点链向上查找第一个声明了interrupt-parent的节点比如gpio101F3000会向上找到根节点如果根节点配置了 default 的interrupt-parent就使用那个默认控制器。实际开发中我推荐每个驱动都让设备树明确给出interrupt-parent而不是依赖继承。因为一旦中间某层节点增加了interrupt-parent属性而你没有注意子节点的中断会突然路由到另一个控制器现象是驱动申请中断成功但永远没有中断到来。5.3 中断映射的高级路由interrupt-map 与 PCIPCI 这类让设备自己枚举的中总线中断路由要交给interrupt-map属性。它把设备侧的中断引脚INTA/B/C/D映射到上层中断控制器的具体中断号pci10100000 { compatible arm,versatile-pci; #interrupt-cells 1; interrupt-map-mask 0xf800 0 0 7; interrupt-map 0x0000 0 0 1 intc 17, 0x0000 0 0 2 intc 18, 0x0000 0 0 3 intc 19, 0x0000 0 0 4 intc 20; };interrupt-map每行由 “PCI 设备地址 中断号 父控制器 父中断号” 组成interrupt-map-mask指定哪些位参与匹配。以0x0000 0 0 1 intc 17为例PCI 地址bus 0、dev 0、fn 0的 INTA 引脚映射到 intc 控制器的中断 17。这样同一个 PCI 设备在不同槽位上中断号会随槽位自动路由不需要为每个槽位写死中断号。对于带device_type pci的节点这套机制是必备的否则设备上电后 Linux 的 PCI 子系统无法把硬件中断号正确分发给驱动。5.4 设备特定数据厂商前缀与内核读取方式任何设备都可能需要额外的、非标准化的配置参数。设备树的处理原则是非标准属性必须带厂商前缀。RTC 的报警中断引脚、OLED 控制器的复位 GPIO、PHY 芯片的寄存器偏移都属于这类。以一块常见的 I2C 接口 OLED 控制器为例i2c0 { oled3c { compatible solomon,ssd1306-i2c; reg 0x3c; solomon,height 32; solomon,width 128; reset-gpios gpio1 3 GPIO_ACTIVE_LOW; }; };solomon,height和solomon,width是厂商自定义属性给驱动提供屏幕尺寸参数reset-gpios是内核标准的 GPIO 描述属性由gpiod_get接口解析。驱动侧读取自定义属性用of_property_read_u32读取 GPIO 用of_get_named_gpio前者在属性缺失时返回-EINVAL后者会返回无效 GPIO 错误码。ETH PHY 子节点也常见这种组合reg表示 MDIO 地址interrupt-parent和interrupts把 PHY 的中断引脚接到 SoC 的 GPIO 控制器设备树里把 PHY 描述清楚phy_device才能在mdio总线上被正确 probe。6. 特殊节点、PCI 地址翻译与三板斧排查手段6.1 aliases 与 chosen两个影响启动流程的节点aliases节点不为硬件服务而是给系统里的关键节点提供短名字避免启动代码在整棵树里做深度搜索。它的值是节点路径字符串不是 phandlealiases { serial0 /serial101F1000; ethernet0 /external-bus/ethernet0,0; };chosen节点存放的是“运行期”参数而不是硬件属性最常用的是bootargs和stdout-path。内核启动时把stdout-path指向的串口作为早期控制台不需要驱动做任何 device probe 就能输出日志chosen { bootargs root/dev/mmcblk0p2 rw consolettyAMA0,115200; stdout-path serial0:115200n8; };stdout-path里可以引用aliases里的名字也可以直接写完整路径。调试内核启动阶段的早期崩溃时我一般会先确认chosen节点有没有被 bootloader 正确填充——很多板卡启动异常是因为 bootloader 用自己生成的 DTB 覆盖了内核里的chosen导致 console 参数丢失表现是内核日志在看到printk之前就消失。6.2 PCI 总线编号与地址翻译的补充规则PCI 节点比普通总线多一层总线编号管理。bus-range 0 0声明该 PCI 域下的总线号从 0 到 0只包含主总线如果桥下有桥后面数字要扩展。PCI 的地址类型通过ranges子项高位的type字段区分三种常见类型分别对应 PCI 的 IO 空间、32 位非预取内存和 32 位预取内存pci10100000 { compatible arm,versatile-pci; device_type pci; bus-range 0 0; ranges 0x01000000 0 0x41000000 0x41000000 0 0x00100000, 0x02000000 0 0x42000000 0x42000000 0 0x10000000; };0x02000000 0 0x42000000 0x42000000 0 0x10000000的六个 cell 分别表示类型0x02000000 表示 32 位非预取内存、PCI 高地址、PCI 低地址、CPU 高地址、CPU 低地址、长度。在 32 位系统上高地址部分恒为 0六个数中每个地址占 2 个 cell这由父节点的#address-cells决定。PCI 总线是自枚举的设备树不需要列出每个 PCI 设备只需描述 Host Bridge 的资源窗口和中断路由剩下的交pci_scan_bus完成。6.3 验证设备树的三板斧设备树写完了上板前先做这三件事能省去大量启动阶段的排查时间。# 第一板斧编译时严格检查 dtc -I dts -O dtb -o board.dtb board.dts 21 | grep -E Warning|Error # 第二板斧从内核实际加载的 DTB 反编译确认有没有被 bootloader 改过 dtc -I dtb -O dts -o runtime.dts /sys/firmware/fdt # 第三板斧查看运行时设备树内容与驱动匹配情况 ls -l /sys/firmware/devicetree/base cat /sys/firmware/devicetree/base/compatible cat /proc/interrupts | grep serial第一板斧重点看unit_address_vs_reg和graph_port类警告这些多半意味着节点命名和reg不一致后续解析必然出问题。第二板斧解决“启动时到底加载了什么”的问题bootloader 可能根据环境变量动态改写 DTB只有运行时看到的才是真实的。第三板斧确认内核视角的设备树内容/proc/interrupts能直观显示某个设备的中断是否被分配到对应控制器。配合这几条命令设备树相关问题基本能在一个 debug 周期内定位到具体节点。本文还有配套的精品资源点击获取
返回列表