linux 设备树详解

转载:https://www.cnblogs.com/xiaojiang1025/p/6131381.html

概念

Linux内核从3.x开始引入设备树的概念,用于实现驱动代码与设备信息相分离。在设备树出现以前,所有关于设备的具体信息都要写在驱动里,一旦外围设备变化,驱动代码就要重写。引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。比如在ARM Linux内,一个.dts(device tree source)文件对应一个ARM的machine,一般放置在内核的"arch/arm/boot/dts/"目录内,比如exynos4412参考板的板级设备树文件就是"arch/arm/boot/dts/exynos4412-origen.dts"。这个文件可以通过$make dtbs命令编译成二进制的.dtb文件供内核驱动使用。

基于同样的软件分层设计的思想,由于一个SoC可能对应多个machine,如果每个machine的设备树都写成一个完全独立的.dts文件,那么势必相当一些.dts文件有重复的部分,为了解决这个问题,Linux设备树目录把一个SoC公用的部分或者多个machine共同的部分提炼为相应的.dtsi文件。这样每个.dts就只有自己差异的部分,公有的部分只需要"include"相应的.dtsi文件, 这样就是整个设备树的管理更加有序。我这里用`Linux4.8.5源码自带的dm9000网卡为例来分析设备树的使用和移植。这个网卡的设备树节点信息在"Documentation/devicetree/bindings/net/davicom-dm9000.txt"有详细说明,其网卡驱动源码是"drivers/net/ethernet/davicom/dm9000.c"

设备树框架

设备树用树状结构描述设备信息,它有以下几种特性

  1. 每个设备树文件都有一个根节点,每个设备都是一个节点。
  2. 节点间可以嵌套,形成父子关系,这样就可以方便的描述设备间的关系。
  3. 每个设备的属性都用一组key-value对(键值对)来描述。
  4. 每个属性的描述用;结束

所以,一个设备树的基本框架可以写成下面这个样子,一般来说,/表示板子,它的子节点node1表示SoC上的某个控制器,控制器中的子节点node2表示挂接在这个控制器上的设备(们)

/{                                  //根节点
    node1{                          //node1是节点名,是/的子节点
        key=value;                  //node1的属性
        ...
        node2{                      //node2是node1的子节点
            key=value;              //node2的属性
            ...
        }
    }                               //node1的描述到此为止
    node3{
        key=value;
        ...
    }
}

节点名

理论个节点名只要是长度不超过31个字符的ASCII字符串即可,此外
Linux内核还约定设备名应写成形如<name>[@<unit_address>]的形式,其中name就是设备名,最长可以是31个字符长度。unit_address一般是设备地址,用来唯一标识一个节点,下面就是典型节点名的写法

上面的节点名是firmware,节点路径是/firmware@0203f000,这点要注意,因为根据节点名查找节点的API的参数是不能有"@xxx"这部分的

Linux中的设备树还包括几个特殊的节点,比如chosen,chosen节点不描述一个真实设备,而是用于firmware传递一些数据给OS,比如bootloader传递内核启动参数给内核

引用

当我们找一个节点的时候,我们必须书写完整的节点路径,这样当一个节点嵌套比较深的时候就不是很方便,所以,设备树允许我们用下面的形式为节点标注引用(起别名),借以省去冗长的路径。这样就可以实现类似函数调用的效果。编译设备树的时候,相同的节点的不同属性信息都会被合并,相同节点的相同的属性会被重写,使用引用可以避免移植者四处找节点,直接在板级.dts增改即可。

下面的例子中就是直接引用了dtsi中的一个节点,并向其中添加/修改新的属性信息

KEY

在设备树中,键值对是描述属性的方式,比如,Linux驱动中可以通过设备节点中的"compatible"这个属性查找设备节点。
Linux设备树语法中定义了一些具有规范意义的属性,包括:compatibleaddressinterrupt等,这些信息能够在内核初始化找到节点的时候,自动解析生成相应的设备信息。此外,还有一些Linux内核定义好的,一类设备通用的有默认意义的属性,这些属性一般不能被内核自动解析生成相应的设备信息,但是内核已经编写的相应的解析提取函数,常见的有 "mac_addr""gpio""clock""power""regulator" 等等。

compatible

设备节点中对应的节点信息已经被内核构造成struct platform_device。驱动可以通过相应的函数从中提取信息。compatible属性是用来查找节点的方法之一,另外还可以通过节点名或节点路径查找指定节点。dm9000驱动中就是使用下面这个函数通过设备节点中的"compatible"属性提取相应的信息,所以二者的字符串需要严格匹配。
在下面的这个dm9000的例子中,我们在相应的板级dts中找到了这样的代码块:

然后我们取内核源码中找到dm9000的网卡驱动,从中可以发现这个驱动是使用的设备树描述的设备信息(这不废话么,显然用设备树好处多多)。我们可以找到它用来描述设备信息的结构体,可以看出,驱动中用于匹配的结构使用的compatible和设备树中一模一样,否则就可能无法匹配,这里另外的一点是struct of_device_id数组的最后一个成员一定是空,因为相关的操作API会读取这个数组直到遇到一个

address

(几乎)所有的设备都需要与CPU的IO口相连,所以其IO端口信息就需要在设备节点节点中说明。常用的属性有

  • #address-cells,用来描述子节点"reg"属性的地址表中用来描述首地址的cell的数量
  • #size-cells,用来描述子节点"reg"属性的地址表中用来描述地址长度的cell的数量

有了这两个属性,子节点中的"reg"就可以描述一块连续的地址区域。下例中,父节点中指定了#address-cells = <2>;#size-cells = <1>,则子节点dev-bootscs0中的reg中的前两个数表示一个地址,即MBUS_ID(0xf0, 0x01)0x1045C,最后一个数的表示地址跨度,即是0x4

interrupts

一个计算机系统中大量设备都是通过中断请求CPU服务的,所以设备节点中就需要在指定中断号。常用的属性有

  • interrupt-controller 一个空属性用来声明这个node接收中断信号,即这个node是一个中断控制器。
  • #interrupt-cells,是中断控制器节点的属性,用来标识这个控制器需要几个单位做中断描述符,用来描述子节点中"interrupts"属性使用了父节点中的interrupts属性的具体的哪个值。一般,如果父节点的该属性的值是3,则子节点的interrupts一个cell的三个32bits整数值分别为:<中断域 中断 触发方式>,如果父节点的该属性是2,则是<中断 触发方式>
  • interrupt-parent,标识此设备节点属于哪一个中断控制器,如果没有设置这个属性,会自动依附父节点的
  • interrupts,一个中断标识符列表,表示每一个中断输出信号

设备树中中断的部分涉及的部分比较多,interrupt-controller表示这个节点是一个中断控制器,需要注意的是,一个SoC中可能有不止一个中断控制器,这就会涉及到设备树中断组织的很多概念,下面是在文件"arch/arm/boot/dts/exynos4.dtsi"中对exynos4412的中断控制器(GIC)节点描述:

要说interrupt-parent,就得首先讲讲Linux设备管理中对中断的设计思路演变。随着linux kernel的发展,在内核中将interrupt controller抽象成irqchip这个概念越来越流行,甚至GPIO controller也可以被看出一个interrupt controller chip,这样,系统中至少有两个中断控制器了,另外,在硬件上,随着系统复杂度加大,外设中断数据增加,实际上系统可以需要多个中断控制器进行级联,形成事实上的硬件中断处理结构:

在这种趋势下,内核中原本的中断源直接到中断号的方式已经很难继续发展了,为了解决这些问题,linux kernel的大牛们就创造了irq domain(中断域)这个概念。domain在内核中有很多,除了irqdomain,还有power domain,clock domain等等,所谓domain,就是领域,范围的意思,也就是说,任何的定义出了这个范围就没有意义了。如上所述,系统中所有的interrupt controller会形成树状结构,对于每个interrupt controller都可以连接若干个外设的中断请求(interrupt source,中断源),interrupt controller会对连接其上的interrupt source(根据其在Interrupt controller中物理特性)进行编号(也就是HW interrupt ID了)。有了irq domain这个概念之后,这个编号仅仅限制在本interrupt controller范围内,有了这样的设计,CPU(Linux 内核)就可以根据级联的规则一级一级的找到想要访问的中断。当然,通常我们关心的只是内核中的中断号,具体这个中断号是怎么找到相应的中断源的,我们作为程序员往往不需要关心,除了在写设备树的时候,设备树就是要描述嵌入式软件开发中涉及的所有硬件信息,所以,设备树就需要准确的描述硬件上处理中断的这种树状结构,如此,就有了我们的interrupt-parant这样的概念:用来连接这样的树状结构的上下级,用于表示这个中断归属于哪个interrupt controller,比如,一个接在GPIO上的按键,它的组织形式就是:

中断源--interrupt parent-->GPIO--interrupt parent-->GIC1--interrupt parent-->GIC2--...-->CPU

有了parant,我们就可以使用一级一级的偏移量来最终获得当前中断的绝对编号,这里,可以看出,在我板子上的dm9000的的设备节点中,它的"interrupt-parent"引用了"exynos4x12-pinctrl.dtsi"(被板级设备树的exynos4412.dtsi包含)中的gpx0节点:

而在gpx0节点中,指定了"#interrupt-cells = <2>;",所以在dm9000中的属性"interrupts = <6 4>;"表示dm9000的的中断在作为irq parant的gpx0中的中断偏移量,即gpx0中的属性"interrupts"中的"<0 22 0>",通过查阅exynos4412的手册知道,对应的中断号是EINT[6]。

gpio

gpio也是最常见的IO口,常用的属性有

  • "gpio-controller",用来说明该节点描述的是一个gpio控制器
  • "#gpio-cells",用来描述gpio使用节点的属性一个cell的内容,即 `属性 = <&引用GPIO节点别名 GPIO标号 工作模式>

GPIO的设置同样采用了上述偏移量的思想,比如下面的这个led的设备书,表示使用GPX2组的第7个引脚:

驱动自定义key

针对具体的设备,有部分属性很难做到通用,需要驱动自己定义好,通过内核的属性提取解析函数进行值的获取,比如dm9000节点中的下面这句就是自定义的节点属性,用以表示配置EEPROM不可用。

VALUE

dts描述一个键的值有多种方式,当然,一个键也可以没有值

字符串信息

32bit无符号整型数组信息

二进制数数组

字符串哈希表

混合形式

上述几种的混合形式

设备树/驱动移植实例

设备树就是为驱动服务的,配置好设备树之后还需要配置相应的驱动才能检测配置是否正确。比如dm9000网卡,就需要首先将示例信息挂接到我们的板级设备树上,并根据芯片手册和电路原理图将相应的属性进行配置,再配置相应的驱动。需要注意的是,dm9000的地址线一般是接在片选线上的,所以设备树中就应该归属与相应片选线节点,我这里用的exynos4412,接在了bank1,所以是"<0x50000000 0x2 0x50000004 0x2>"
最终的配置结果是:

勾选相应的选项将dm9000的驱动编译进内核。

make menuconfig
[*] Networking support  --->
    Networking options  --->
        <*> Packet socket
        <*>Unix domain sockets 
        [*] TCP/IP networking
        [*]   IP: kernel level autoconfiguration
Device Drivers  --->
    [*] Network device support  --->
        [*]   Ethernet driver support (NEW)  --->
            <*>   DM9000 support
File systems  --->
    [*] Network File Systems (NEW)  --->
        <*>   NFS client support
        [*]     NFS client support for NFS version 3
        [*]       NFS client support for the NFSv3 ACL protocol extension
        [*]   Root file system on NFS

执行make uImage;make dtbs,tftp下载,成功加载nfs根文件系统并进入系统,表示网卡移植成功

分类:  Linux驱动开发
<think>嗯,用户要求详细解释Linux设备树,这个问题在嵌入式Linux开发中确实是个核心难点。用户可能正在学习驱动开发或系统移植,被设备树的概念困扰了——毕竟它把硬件描述从内核代码抽离出来,和传统驱动编写方式很不一样。 刚才提供的三个引用很有价值:[^1]强调设备树在嵌入式系统的关键作用,[^2]说明其分离硬件描述和驱动的本质,[^3]则具体解释了树形结构和文件类型。需要把这些点系统化组织起来。 核心要讲清楚三点:为什么需要设备树(解决ARM板级描述混乱问题),它是什么(硬件连接的树形数据结构),以及怎么用(DTS编译为DTB的过程)。用户特别提到“详细解释”,所以每个技术术语都要展开,比如DTS、DTB、DTC的区别。 注意到用户引用里提到“初学者晦涩难懂”,解释时要避免内核源码细节,多用生活化比喻——比如把设备树比作房屋的电路图纸就很贴切。另外必须强调设备树与ACPI的区别,这是嵌入式与x86系统的关键差异点。 最后考虑设计的问题既要覆盖基本概念(文件类型和作用),也要包含实用技巧(验证和调试),这样能帮用户建立从理论到实践的完整路径。特别是设备树覆盖机制这个进阶功能,对实际开发很有帮助。 </think>好的,我们来详细解释一下 Linux 设备树(Device Tree)的核心概念、工作原理和应用场景。 ### 1. 什么是 Linux 设备树? * **核心概念:** Linux 设备树是一种**描述硬件配置信息**的数据结构。它以一种与操作系统内核**无关的格式**,详细说明了系统所连接的硬件组件(CPU、内存、总线、外设等)以及它们之间的**连接关系**。 * **目的:** 设备树的核心目标是**将硬件描述与操作系统内核代码解耦**。在没有设备树的时代(尤其是 ARM 平台),内核需要为每种不同的开发板或设备包含大量特定于板子的代码来描述其硬件(称为“Board File”)。这使得内核代码臃肿,难以维护,移植到新硬件平台非常麻烦。 * **表现形式:** 设备树信息最初以人类可读的文本文件(**.dts** - Device Tree Source)形式编写。然后,这些 `.dts` 文件被编译成紧凑的二进制格式(**.dtb** - Device Tree Blob 或 Device Tree Binary)。内核在启动早期阶段加载并解析这个 `.dtb` 文件来获取硬件信息。 * **数据结构:** 正如其名“Device **Tree**”,设备树采用**树形结构**来描述硬件。树根是整个系统平台,分支代表总线(如 PCI、I2C、SPI、USB),叶子节点代表挂在总线上的具体设备(如传感器、网卡、显示器控制器等)。 ### 2. 为什么需要设备树? * **解决硬件多样性问题:** 嵌入式 ARM/MIPS/RISC-V 等平台硬件配置千差万别(CPU 型号、内存大小、外设类型和连接方式)。 * **消除“Board File”弊端:** 传统方式下,内核需要为每个特定的板子编写大量的 C 代码结构体(`arch/arm/mach-xxx/board-xxx.c`),导致: * **内核臃肿:** 内核镜像包含大量可能不会被使用的板级信息。 * **维护困难:** 添加新板子需要修改内核源码并重新编译整个内核。 * **移植复杂:** 移植内核到新硬件需要大量重复工作。 * **代码冗余:** 不同板子上相同的外设也需要重复定义。 * **统一描述语言:** 设备树提供了一种**标准化的硬件描述语言**,允许开发者用一种方式描述几乎所有硬件连接。 ### 3. 设备树的关键组成部分 * **节点(Node):** 树的基本单元,代表一个设备或总线。根节点是 `/`。 * **标准节点:** 由内核或绑定规范预定义含义的节点类型(如 `cpus`, `memory`, `soc`, `i2c0`, `spi1`, `usb0`)。 * **绑定(Binding):** 定义特定类型设备节点应包含哪些属性及其含义的文档。 * **属性(Property):** 依附于节点的键值对(`key = value;`),用于描述节点的具体特性或配置。 * **关键属性:** * `compatible`: **最重要**的属性。它是一个字符串或字符串列表,指定节点所代表设备的类型。内核驱动程序通过匹配设备的 `compatible` 属性来决定是否能够驱动该设备。例如:`compatible = "vendor,device-model", "generic-driver-type";`。 * `reg`: 描述设备在父总线地址空间内的寄存器地址范围(通常是起始地址和长度)。 * `#address-cells` / `#size-cells`: **用于父节点**,定义其子节点 `reg` 属性中 `<地址>` 和 `<大小>` 字段所需的 32 位单元(cell)数量。这是解析地址的关键。 * `model`: 描述设备/平台的型号名称。 * `status`: 设备状态(如 `"okay"`, `"disabled"`)。 * `interrupt-parent` / `interrupts`: 指定设备的中断控制器父节点及其具体中断号/触发方式。 * `clocks` / `clock-names`: 指定设备使用的时钟源。 * `pinctrl-*`: 用于配置引脚复用(Pinmux)。 * `phandle`: 节点的唯一标识符,允许其他节点通过 `&label` 或 `<phandle>` 引用它。 * **标签(Label):** 在节点定义前添加标签(如 `&uart0`),允许在设备树的其他地方通过 `&label` 方便地引用该节点。 ### 4. 设备树文件的生命周期(DTS -> DTB) 1. **编写 .dts / .dtsi 文件:** * `.dts`:针对特定硬件平台(通常是特定开发板或设备)的主设备树源文件。 * `.dtsi`:设备树包含文件(类似于 C 头文件)。包含通用部分(如 SoC 芯片级的定义),可以被多个 `.dts` 文件包含复用。例如,`board-mydevice.dts` 会包含 `soc-somechip.dtsi` 和 `common-connectors.dtsi`。 2. **预处理:** `.dts` 文件会经过 C 预处理器(`cpp`),处理 `#include`, `#define` 等指令,展开 `.dtsi` 文件,生成一个临时的 `preprocessed.dts` 文件。 3. **编译:** 使用 **DTC (Device Tree Compiler)** 工具将预处理后的 `.dts` 文件编译成二进制格式的 `.dtb` 文件。`DTC` 会验证语法和一定程度的结构正确性。命令通常为 `dtc -I dts -O dtb -o myboard.dtb myboard.dts`。 4. **加载(Bootloader 阶段):** 在系统启动过程中,Bootloader(如 U-Boot)负责将内核镜像(`zImage`/`uImage`)和 `.dtb` 文件加载到内存的指定位置(有时两者会被打包成一个 FIT 镜像)。 5. **解析(内核启动阶段):** Linux 内核在初始化早期阶段(通常在 ARM 平台上是 `setup_arch()` 函数中),会从 Bootloader 传递来的信息(或固件如 UEFI ACPI 表)中找到 `.dtb` 在内存中的位置,并调用设备树解析器将其转换成内核内部易于操作的树形数据结构(`struct device_node`)。 6. **匹配与初始化(内核驱动初始化):** * 平台总线和设备驱动使用 `of_*`(Open Firmware)系列 API 遍历设备树节点。 * 驱动程序在其 `of_device_id` 表中声明它能支持的 `compatible` 字符串。 * 内核的驱动匹配机制(`Device<->Driver Matching`)会遍历设备树节点,将节点的 `compatible` 属性与驱动支持的 `compatible` 字符串进行匹配。 * 匹配成功后,内核会为该设备节点创建 `struct platform_device` 等结构体,并调用驱动程序的 `probe()` 函数进行设备初始化和驱动加载。 * 驱动程序在 `probe()` 函数中,通过设备树 API(如 `of_get_property()`, `of_iomap()`, `of_irq_get()`, `of_clk_get()` 等)从设备树节点中提取配置信息(寄存器地址、中断号、时钟频率、GPIO 设置等),并使用这些信息来初始化和操作硬件。 ### 5. 设备树的主要优点 * **单一内核支持多硬件:** 一个内核镜像(编译时通常不包含特定板级信息)可以搭配不同的 `.dtb` 文件,就能启动在不同的硬件平台上。 * **硬件描述与内核分离:** 添加新硬件或修改硬件配置(如更换传感器型号)通常只需修改 `.dts` 文件并重新编译生成 `.dtb`,无需修改和重新编译内核源码本身。 * **提高内核可维护性:** 内核源码不再充斥大量板级定制代码,更加精简和通用化。 * **清晰的硬件视图:** `.dts` 文件以结构化的方式清晰地描述了整个系统的硬件拓扑和配置,易于理解和调试。 * **动态性:** 内核可以在运行时解析设备树(虽然最常见的是启动时解析)。 * **设备树覆盖(Overlay):** 在系统运行时,动态加载额外的 `.dtbo`(Device Tree Overlay)片段来修改或添加设备树节点,常用于支持可插拔硬件模块(如 HATs、Capes)。 ### 6. 设备树 vs. ACPI * **设备树:** 主要用于**嵌入式系统**(ARM、MIPS、RISC-V)、桌面/服务器 ARM 系统(如 Apple M-Series Macs)。提供静态硬件描述。 * **ACPI (Advanced Configuration and Power Interface):** 主要用于 **x86/x86_64** 架构的 PC、服务器。除了描述静态硬件,还管理电源状态、热事件、即插即用等高级功能。 ### 7. 总结 Linux 设备树是一种革命性的机制,它解决了嵌入式 Linux 平台硬件多样性带来的内核维护和移植难题。通过使用 `.dts` 源文件描述硬件连接和属性,编译成 `.dtb` 二进制文件,并在内核启动时加载解析,设备树实现了 **硬件配置信息与内核代码的分离**。它基于树形结构组织硬件节点,关键属性如 `compatible` 用于驱动匹配,`reg` / `#*-cells` 用于地址解析。设备树使得单一内核镜像支持多种硬件成为可能,极大地提高了嵌入式 Linux 的开发效率和可维护性,是现代嵌入式 Linux 开发不可或缺的组成部分[^1][^2][^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值