Linux设备树


logo

设备树Logo

简介


  在Linux3.x版本下,Linux内核中ARM架构的板级信息大量放置在 arch/arm/mach-xxx arch/arm/plat-xxx 文件夹下,例如platform设备、resource、spi_board_info以及各种硬件的platform_data,这些信息对Linux内核来说无关紧要,会造成大量的冗余编码,导致ARM的merge工作量较大。当采用设备树(device tree)之后,许多硬件的细节可以直接透过设备树传递给Linux内核,大大减少了Linux内核的冗余代码量。

  设备树并不是在这时被重新发明,在Linux内核的其他架构如PowerPC,很早便开始使用设备树来对硬件进行描述。

  在Linux内核中ARM相关文件存放位置如下:

  • ARM核心代码存储在 arch/arm 文件夹下;
  • ARM SoC核心架构代码存储在 arch/arm 文件夹下;
  • ARM SoC周边外设模块驱动存储在 drivers 文件夹下;
  • ARM SoC特定代码存储在 arch/arm/mach-xxx 文件夹下;
  • ARM SoC板级代码被移除,由设备树机制来负责传递硬件拓扑和硬件资源信息。

设备树实例

设备树实例

  Linux设备树可描述的信息如下:

  • CPU的数量和类别;
  • SoC内存基地址和大小;
  • SoC内部总线和桥;
  • SoC外设连接;
  • 中断控制器和中断使用情况;
  • 各种外设控制器和外设使用情况;

  在系统上电后,BootLoader会将设备树传递给Linux内核,内核根据识别的树信息展开为platform_device、spi_device等设备,并且这些设备用到的内存、中断、等资源也被传递给内核,内核会将这些资源绑定到相应的设备中。

Device Tree组成与结构

DTS(Device Tree Source)

  .dts文件是一种ASCII文本格式的Device Tree描述。在ARM Linux中,一个.dts文件对应一个ARM的machine,一般被放置在 arch/arm/boot/dts/ 目录。

DTSI(Device Tree Source Include)

  由于一个SoC基本都会对应多个machine,这样便会存在许多共同的部分,Device Tree将一些公用的部分使用.dtsi文件保存,类似于C语言的头文件。特定于machine的.dts文件一般都会引用这个.dtsi文件。

DTC(Device Tree Compiler)

  dtc是编译dts的工具,可以将dts文件转换为二进制.dtb文件。DTC的源代码位于内核的scripts/dtc目录,在Linux内核使能了Device Tree的情况下, 编译内核的时候主机工具dtc会被编译出来。

DTB(Device Tree Block)

  .dtb文件是 .dts 被 DTC 编译后的二进制格式的设备树文件,它可以被linux内核解析。

DTS存储结构

  Device Tree的基本单元是node,这些node会被组装成树结构,除了根节点,每个节点都只有一个父节点。一个设备树中只有一个根节点,每个节点包含若干个属性值来描述节点的一些特性。

  上图设备树实例图的对应.dts文件如下所示

/ {
    module="fsl,mpc8572ds"
    compatible="fsl,mpc8572ds"
    #address-cells=<1>
    #size-cells=<1>

    cpus{
        #address-cells=<1>
        #size-cells=<0>

        cpu@0{
            device_type="cpu"
            reg=<0>
            timebase-frequency=<825000000>
            clock-frequency=<825000000>
        };

        cpu@1{
            device_type="cpu"
            reg=<1>
            timebase-frequency=<825000000>
            clock-frequency=<825000000>
        };
    };

    memory@0{
        device_type="memory"
        reg=<0 0x20000000>
    };

    uart@fe001000{
        compatible="ns16550"
        reg=<0xfe001000 0x100>
    };

    chose{
        bootarg="root=/dev/sda2";
    };

    aliases{
        serial0="/uart@fe001000"
    };
};

  根据上面的.dts文件,可简单概括为以下几个部分:

  • 根节点: \
  • 设备节点:例如: uart@fe001000
    • 节点名称:例如: uart
    • 节点地址:例如: reg=<0xfe001000 0x100>
    • 子节点:例如: cpu@0
  • 属性:属性名称(Property name)和属性值(Property value)
  • 标签:例如 uart@fe001000

设备树语法

头文件

  设备树的头文件引用和C语言类似,使用#include来进行引用,.dts文件可以引用.h、.dts以及.dtsi文件。

\arch\arm\boot\dts\imx6ull.dts
#include <dt-bindings/clock/imx6ul-clock.h>
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/arm-gic.h>
#include "imx6ull-pinfunc.h"
#include "imx6ull-pinfunc-snvs.h"
#include "skeleton.dtsi"

节点

  设备树中的基本单元称为节点(node)

  设备节点格式:

[label:] node-name[@unit-address] 
{
   [properties definitions]
   [child nodes]
};

  node-name 是设备的节点名称,为ASCII字符串,一般节点的名字能清晰描述出节点的功能,例如“uart1”表示这个节点是UART1外设。

  unit-address 一般表示设备的地址活寄存器首地址,如果某个节点没有地址或寄存器的话,此处可省略,例如:“cpu0: cpu@0”。

  label 表示节点标签,冒号后的才是节点的名称。引入标签是为了更方便的访问节点,再次访问节点是可以直接通过&label来访问这个节点。

  {} 里用于节点内容的描述,其中 [properties definitions] 表示节点属性。[child nodes] 是这个挂在设备节点上的子节点。

  例:

ecspi2: ecspi@0200c000 {
    #address-cells = <1>;
    #size-cells = <0>;
    compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
    reg = <0x0200c000 0x4000>;
    interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_ECSPI2>,
            <&clks IMX6UL_CLK_ECSPI2>;
    clock-names = "ipg", "per";
    dmas = <&sdma 5 7 1>, <&sdma 6 7 2>;
    dma-names = "rx", "tx";
    status = "disabled";
};

常用的节点属性

  • compatible

  compatible表示“兼容”,每个节点都会有一个compatible属性,用来标识特定的设备。内核启动时,会根据描述的先后顺序找到可以使用的驱动程序。
  根节点也会有compatible属性,它用来选择哪一个“machine desc”,一个内核可以支持machine A也可以支持machine B,内核启动会根据节点的compatible属性找到相对应的machine desc结构体,执行其中的初始化函数。

  • address-cells、#size-cells

  cell指一个32位的数值,address-cells表示address要用多少个32位来表示;size-cell表示size要用多少个32位数来表示。

  例:

/ {
	#address-cells = <1>;
	#size-cells = <1>;

	memory {
		reg = <0x00000000 0x04000000>,
		    <0x08000000 0x04000000>;
	};
}

  上例中,address-cells为1,所以reg中用1个数来表示地址,即用0x00000000和0x08000000表示内存地址,size-cells为1,所以reg中用1个数表示大小,即用0x04000000表示大小。

  • model

  model属性与compatible属性有些类似,compatible它是一个字符列表,表示可以与硬件兼容A、B、C等驱动;model用来准确定义这个硬件是什么。

例:

/ {
    model = "Freescale i.MX6 ULL DDR3 ARM2 Board";
    compatible = "fsl,imx6ull-ddr3-arm2", "fsl,imx6ull";
}

  在上例中,表示可以兼容imx6ull-ddr3-arm2,也兼容imx6ull。

  • status

  dtsi文件定义了很多设备,若在编写时不希望或者这些设备没有,可以添加status属性,并设备为“disabled”。

描述
“okay”设备正常运行
“disabled”表示该设备目前尚未运行,但将来可能会运行
“fail”表示设备无法运行。在设备中检测到严重错误,需修复
“fail-sss”设备无法运行。在设备中检测到严重错误,需修复,sss表示错误信息
  • reg

  reg属性描述了设备资源在其父总线定义的地址空间内的地址,如果一个节点存在reg属性,那么节点的名字必须包含unit-address,unit-address值来源于reg属性的第一个地址值。

  例:

intc: interrupt-controller@00a01000 {
         compatible = "arm,cortex-a7-gic";
         #interrupt-cells = <3>;
         interrupt-controller;
         reg = <0x00a01000 0x1000>,
               <0x00a02000 0x100>;
     };
  • ranges

  ranges可以理解为地址映射/转换表,其目的主要是显示的把设备地址映射为CPU可以使用的地址。ranges属性值可以为空或者按照(child-bus-address, parent-bus-address, length)格式编写的数字矩阵。

  child-bus-address: 子总线地址空间的物理地址,由子节点的#address-cells确定此物理地址所占用的字长;

  parent-bus-address:父总线地址空间的物理地址,同样由父节点的#address-cells确定此物理地址所占用的字长。

  length:子地址空间的长度,由父节点的#size-cells确定此地址长度所占用的字长。

  如果ranges为空,表示子地址空间和父地址空间一一对应,不需要地址翻译。

注意:如果某个节点缺少ranges,除了其父节点,其他任何节点都不能直接访问。

  例:

crypto: caam@30900000 {
    compatible = "fsl,imx7d-caam", "fsl,sec-v4.0";
    #address-cells = <1>;
    #size-cells = <1>;
    reg = <0x30900000 0x40000>;
    ranges = <0 0x30900000 0x40000>;
    interrupts = <GIC_SPI 91 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX7D_CAAM_CLK>,
        <&clks IMX7D_AHB_CHANNEL_ROOT_CLK>;
    clock-names = "caam_ipg", "caam_aclk";

    sec_jr0: jr0@1000 {
            compatible = "fsl,sec-v4.0-job-ring";
            reg = <0x1000 0x1000>;
            interrupts = <GIC_SPI 105 IRQ_TYPE_LEVEL_HIGH>;
    };

    sec_jr1: jr1@2000 {
            compatible = "fsl,sec-v4.0-job-ring";
            reg = <0x2000 0x1000>;
            interrupts = <GIC_SPI 106 IRQ_TYPE_LEVEL_HIGH>;
    };

    sec_jr2: jr2@3000 {
            compatible = "fsl,sec-v4.0-job-ring";
            reg = <0x3000 0x1000>;
            interrupts = <GIC_SPI 114 IRQ_TYPE_LEVEL_HIGH>;
    };
};

  如上例所示,sec_jr0的地址起始地址为(0x1000-0x0)+0x30900000,大小为0x1000;sec_jr1的地址起始地址为(0x2000-0x0)+0x30900000,大小为0x1000;sec_jr2的地址起始地址为(0x3000-0x0)+0x30900000,大小为0x1000。

/ {
    compatible = "acme,coyotes-revenge";
    #address-cells = <1>;
    #size-cells = <1>;
    interrupt-parent = <&intc>;

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            compatible = "arm,cortex-a9";
            reg = <0>;
        };
        cpu@1 {
            compatible = "arm,cortex-a9";
            reg = <1>;
        };
    };

    serial@101f0000 {
        compatible = "arm,pl011";
        reg = <0x101f0000 0x1000 >;
        interrupts = < 1 0 >;
    };

    serial@101f2000 {
        compatible = "arm,pl011";
        reg = <0x101f2000 0x1000 >;
        interrupts = < 2 0 >;
    };

    gpio@101f3000 {
        compatible = "arm,pl061";
        reg = <0x101f3000 0x1000
               0x101f4000 0x0010>;
        interrupts = < 3 0 >;
    };

    intc: interrupt-controller@10140000 {
        compatible = "arm,pl190";
        reg = <0x10140000 0x1000 >;
        interrupt-controller;
        #interrupt-cells = <2>;
    };

    spi@10115000 {
        compatible = "arm,pl022";
        reg = <0x10115000 0x1000 >;
        interrupts = < 4 0 >;
    };

    external-bus {
        #address-cells = <2>
        #size-cells = <1>;
        ranges = <0 0  0x10100000   0x10000     // Chipselect 1, Ethernet
                  1 0  0x10160000   0x10000     // Chipselect 2, i2c controller
                  2 0  0x30000000   0x1000000>; // Chipselect 3, NOR Flash

        ethernet@0,0 {
            compatible = "smc,smc91c111";
            reg = <0 0 0x1000>;
            interrupts = < 5 2 >;
        };

        i2c@1,0 {
            compatible = "acme,a1234-i2c-bus";
            #address-cells = <1>;
            #size-cells = <0>;
            reg = <1 0 0x1000>;
            interrupts = < 6 2 >;
            rtc@58 {
                compatible = "maxim,ds1338";
                reg = <58>;
                interrupts = < 7 3 >;
            };
        };

        flash@2,0 {
            compatible = "samsung,k8f1315ebm", "cfi-flash";
            reg = <2 0 0x4000000>;
        };
    };
};

  由上例可知:external-bus子地址空间的#address-cells为2,父地址空间的#address-cells值为1,因此 0 0 0x10100000 0x10000 的前两个cell为external-bus片选0上相对该片选的基地址移0,第三个cell表示external-bus片选0上地址偏移0的地址空间被映射到0x10100000位置,第四个cell表示映射的大小为0x10000。

  • device_type

  device_type属性值为字符串,IEEE 1275 会用到此属性,用于描述设备的 FCode ,但是设 备树没有 FCode ,所以此属性也被抛弃了。此属性只能用于 cpu 节点或者 memory 节点。

  例:

cpus {
    #address-cells = <1>;
    #size-cells = <0>;

    cpu0: cpu@A00 {
        device_type = "cpu";
        compatible = "arm,cortex-a9";
        reg = <0xA00>;
        cooling-min-level = <13>;
        cooling-max-level = <7>;
        #cooling-cells = <2>; /* min followed by max */
    };

    cpu@A01 {
        device_type = "cpu";
        compatible = "arm,cortex-a9";
        reg = <0xA01>;
    };
};

中断信息节点

  中断一般包括中断产生设备和中断处理设备。中断控制器负责处理中断,每一个中断都有对应的中断号及触发条件。中断产生设备可能有多个中断源,有时多个中断源对应中断控制器中的一个中断,这种情况中断产生设备的中断源称之为中断控制器中对应中断的子中断。一般情况中断产生设备数量要多于中断控制器,多个中断产生设备的中断都由一个中断控制器处理,这种多对一的关系也很像一个树形结构,所以在设备树中,中断也被描述成树,叫中断树。

  一般使用以下几个部分描述中断:

  • interrupt-controller

  表示中断控制器,其值为空,用来接收中断信号。

  • interrupt-cells

  跟#address-cells和#size-cells类似,用来说明有多少个中断说明符,即interrupt属性的大小,也就是一条信息有几个cells,每个cells都是32位整型。

  • interrupt-parent

  指向interrupt-controller的phandle,如果该节点没有该属性,默认继承其父节点的中断属性。

  • interrupts

  包含了许多中断描述符(interrupt specifier,也就是中断源),比如中断类型(PPI,SPI……)、中断号、中断触发类型等信息。

  例:

intc: interrupt-controller@00a01000 {
    compatible = "arm,cortex-a7-gic";
    #interrupt-cells = <3>;
    interrupt-controller;
    reg = <0x00a01000 0x1000>,
            <0x00a02000 0x100>;
};

soc {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "simple-bus";
    interrupt-parent = <&gpc>;
    ranges;

    ...

    gpmi: gpmi-nand@01806000{
        compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
        #address-cells = <1>;
        #size-cells = <1>;
        reg = <0x01806000 0x2000>, <0x01808000 0x4000>;
        reg-names = "gpmi-nand", "bch";
        interrupts = <GIC_SPI 15 IRQ_TYPE_LEVEL_HIGH>;
        interrupt-names = "bch";
        clocks = <&clks IMX6UL_CLK_GPMI_IO>,
                <&clks IMX6UL_CLK_GPMI_APB>,
                <&clks IMX6UL_CLK_GPMI_BCH>,
                <&clks IMX6UL_CLK_GPMI_BCH_APB>,
                <&clks IMX6UL_CLK_PER_BCH>;
        clock-names = "gpmi_io", "gpmi_apb", "gpmi_bch",
                    "gpmi_bch_apb", "per1_bch";
        dmas = <&dma_apbh 0>;
        dma-names = "rx-tx";
        status = "disabled";
    };

...

};

  由上例可知,中断控制器为 intc ,其中断信息为 3,在节点gpmi下interrupts为<GIC_SPI 15 IRQ_TYPE_LEVEL_HIGH>,符合中断控制器中断信息的描述。

alias节点

  alias节点用来引用其他节点的节点,一般情况下引用一个节点需要写出器相对根节点的完整路径,当需要新增许多节点时,还需要考虑节点具体的位置,所以这个时候在需要引用的节点前设置一个label,引用的时候只需要&label即可。别名尽量固定,更改真名的情况下,只需要更改alias处即可,不需要更改每一个用到的driver,和typedef定义类型有点类似。

  例:

aliases {
    can0 = &flexcan1;
    can1 = &flexcan2;
    ethernet0 = &fec1;
    ethernet1 = &fec2;
    gpio0 = &gpio1;
    gpio1 = &gpio2;
    gpio2 = &gpio3;
    gpio3 = &gpio4;
    gpio4 = &gpio5;
    i2c0 = &i2c1;
    i2c1 = &i2c2;
    i2c2 = &i2c3;
    i2c3 = &i2c4;
    mmc0 = &usdhc1;
    mmc1 = &usdhc2;
    serial0 = &uart1;
    serial1 = &uart2;
    serial2 = &uart3;
    serial3 = &uart4;
    serial4 = &uart5;
    serial5 = &uart6;
    serial6 = &uart7;
    serial7 = &uart8;
    spi0 = &ecspi1;
    spi1 = &ecspi2;
    spi2 = &ecspi3;
    spi3 = &ecspi4;
    usbphy0 = &usbphy1;
    usbphy1 = &usbphy2;
};

chose节点

  chose节点主要用于传递数据,一般用于传递启动参数。

  例:

chosen {
    bootargs = "console=ttyS0,115200 root=/dev/mtdblock1 rw rootfstype=jffs2";
};

设备树的编译、加载过程图

在这里插入图片描述

设备树编译、加载过程

参考

《devicetree-specification-v0.2.pdf》
《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.pdf》
《嵌入式Linux应用开发完全手册》
ARM Linux 3.x的设备树(Device Tree)
Device Tree常用方法解析
设备树详解


🍙

🤔

🥱

Logo

更多推荐