DeviceTree

1.   文档结构介绍

首先对Device Tree的背景进行介绍, 描述为何要使用device-tree, 它有什么作用.

然后介绍Device Tree的语法格式, 让你能看懂一个device tree source file, 并修改这个file.

然后介绍如何把人能读懂的device tree source file编译成机器能读懂的device tree binary file.

最后介绍内核是如何解析device tree binary file.

2.   Device Tree介绍

本章我们需要弄清楚两个问题:

1. Device Tree有什么作用, 为什么要用device tree(why-device-tree)

2. 如何使用Device Tree(how-device-tree)

2.1             why-device-tree

在《统一的设备模型》一文中我们提到过bus, device, driver的概念.

device用于描述一个设备, driver用于驱动一个设备, 使其能表现出相应的功能.

 

打个比方: 某个ARM CPU上的SPI控制器, 我们称之为一个设备. 通常

用一个platform_device结构体来描述它: 包括寄存器地址, 使用的GPIO引脚, 中断号等, platform_device一般放在板级文件里面.

用一个platform_driver作为驱动: 驱动主要使设备表现出相应的功能, 比如如何通过该SPI控制器收发数据, 驱动代码一般存在于drivers/目录下.

 

市场上一个ARM CPU往往存在很多个板子, 比如FSLIMX.6 CPU, 做了不下10块板子. 每块板子只有些微小的差异, 比如SPI控制器在这块板子上用的是某几个引脚做为数据线, 在另外一块板子上用的是另外几个引脚. 这种情况下我们往往就需要去修改板级文件中的platform_device. 或者给每块板子新建一个自己的板级文件.

 

看上去还不错是吧, Linux内核本身就是设计用于兼容多种硬件的, 每个板子一个板级文件, 大家相互不冲突, 挺好的! 旧版本的内核一直是这样做的, 直到有一天Linux的作者Linus觉得不爽了, 他觉得这些相同CPU的不同板级文件都是垃圾, 又众多又没内涵, 应该从内核代码中移除, 所以就有了现在的device-tree机制.

 

该机制相当于把那些platform_device都放到device tree, 然后bootloader把这个tree传递给内核, 内核在解析这个tree以获取相关信息.

这样做有很多好处, 例如: 想象一下, 如果不用该机制, 那么针对同一CPU的两块不同板, 需要做两个板级文件, 编译两次内核, 然后分别放在不同的板子上运行; 如果用device tree, 则只需要一个板级文件, 编译一个内核镜像, 传递不同的tree即可, 方便用同一个内核兼容不同的硬件.

 

device tree中除了可以存放一些platform_device的信息, 还可以包含很多其他内容, 概况起来, 主要如下.

Device Tree的主要内容:

         内核里面可能会有一个板级文件支持所有FSL IMX.6的板子, 另外一个板级文件支持所有Atmel SAMA5D3的板子; device tree首先要告诉内核, 应该使用哪个板级文件. 相当于mach id.

         内核runtime parameters: 这些信息在旧的方式下, 都是uboot通过bootargs传递给内核的, 包括 mem, console, filesystem_type, 现在也可以放在device tree

         设备的拓扑结构以及特性: 用的什么ARM CPU, 这个CPU有哪些片上外设, 每个外设的寄存器地址, 用到的管脚, 中断等, 相当于platform_device

2.2             how-device-tree

如果要使用Device Tree:

首先用户要了解自己硬件的设备拓扑结构, 内核runtime parameters以及要使用哪个板级文件

然后把这些信息组织成Device Tree source file(DTS)

然后通过编译器Device Tree Compiler(DTC), 这些适合人类阅读的DTS变成适合机器处理的Device Tree binary file(DTB).

 

然后bootloader(例如uboot)会先把这个DTB装载到MEMORY1, 然后把内核装载到MEMORY2, 启动内核并告诉内核从MEMORY1的位置获取DTB.

 

很简单吧, 这个逻辑确实很简单, 麻烦的是这个DTS到底该如何编写, 后面我们马上就会介绍. 在此之前, 先想一个问题:

假设两块板子都用了同一个IMX.6CPU, 那么这两块板子的DTS文件会不会有很多重复的地方? 答案是肯定的. 这种情况下, 我们可以把公共的部分提取出来, 变成一个dtsi文件, 然后不同板子的DTS可以#include这个dtsi文件.

 

DTS这种层次结构遵循实际硬件的层次结构:

例如ARM公司设计了一种体系架构 Cortex-A9; FSL公司用这个体系架构设计了IMX6A, IMX6B, IMX6C 3CPU; Embest公司用IMX6A做了两块板子 IMX6A-BOARD1 IMX6A-BOARD2.

那么DTS的层次结构就是

一个dtsi文件描述Cortex-A9体系架构, 一般官方名称是skeleton.dtsi

一个dtsi文件描述3CPU的共同部分, imx6x.dtsi

3个不用的dtsi描述3CPU: imx6a.dtsi, imx6b.dtsi, imx6c.dtsi

2个不通的DTS描述2款板: imx6a-board1.dts, imx6a-board2.dts

依次include前一级的dtsi.

Note: 后一级别的dts可以override前一级的内容.

3.   Device Tree Source语法格式

DTS的基本单元是node, node里面包含property; 然后把这些nodetree的方式组织. 如下:

/ {

    node1 {

        a-string-property = "A string";

        a-string-list-property = "first string", "second string";

        a-cell-property = <1>;

        a-byte-data-property = [0x01 0x23 0x34 0x56];

        mixed-property = "a string", [0x01 0x23 0x45 0x67], <0x12345678>;

        child-node1 {

            first-child-property;

            second-child-property = <1>;

            a-string-property = "Hello, world";

        };

        child-node2 {

        };

    };

    node2 {

        an-empty-property;

        n-cell-property = <1 2 3>; /* each number (cell) is a uint32 */

        child-node1 {

        };

    };

};

接下来的小节中:

首先会介绍nodeproperty的语法格式

然后介绍几种特殊的node

然后介绍几种特殊的property

然后介绍如何自定义nodeproperty

3.1             Node & Property

node的由node-name加一对{ }组成. { }中可以包含用于描述该nodepropertychild-node.

[label:] node-name[@unit-address] {

    [properties definitions]

    [child nodes]

}

[ ]表示可选内容

         node-name: 必须的, 它由简单的ascii字符组成, 最多31个字符. node-name一般代表这个node用于描述什么设备: 例如一个 3com509Ethernet控制器, node-name应该是ethernet, 而非3com509.

node-name在同一级别中应该unique, 不同级别中可以同名, 类似文件夹.

         @unit-address: 可选的, 如果一个node下面有reg这个property, 那么@unit-address就是必须的; 否则, 就一定不能有@unit-address

         label: 可选的, 如果我们要引用某一个node, 在没有label的情况下, 必须给定full path才行. label的作用就是你在引用某个node的时候, 直接用&label的方式就行, 不需要敲那么长一串full path.

label在整个DTS中必须unique

         properties definitions: 代表该node的属性, property的编写规则我们下面专门介绍

         child nodes: node, node遵循一样的规则. 除了root node, 每一个node parent就是它的上级node

 

Properties的定义采用key value;的形式, 不同的key代表类型的property, value的形态有以下几种:

         可以为空

an-empty-property;

         可以是text string或者string list

a-string-property = "A string";

a-string-list-property = "first string", "second string";

         可以是1个或多个32 bit unsigned integers, 每个32bit的整数称为1cell

1 cell: a-cell-property = <1>;

3 cells: n-cell-property = <1 2 3>;

         可以是binary data

a-byte-data-property = [0x01 0x23 0x34 0x56];

         也可以是以上类型的mix

mixed-property = "a string", [0x01 0x23 0x45 0x67], <0x12345678>;

3.2             root node

每个DTS中都必须一个root node.

/ {

    compatible = "acme,coyotes-revenge";

};

         root nodenode-name必须是 /

         root node没有parent, 所有其他node都是它的child-node

         root node下包含一些必要的property, 关于这些property的细节, 参考对应章节

         compatible : 用于匹配板级文件, 参考compatible property》一节

         #address-cells #size-cells, 它俩的细节参考《reg&cell property》一节. 这两个property是可选的

3.3             CPUs node

每个DTS中都必须有一个cpus node来描述芯片的CPU, 例如一个Cortex-A9的双核CPU:

    cpus {

        #address-cells = <1>;

        #size-cells = <0>;

        cpu@0 {

            compatible = "arm,cortex-a9";

            device_type = "cpu";

            reg = <0>;

        };

        cpu@1 {

            compatible = "arm,cortex-a9";

            device_type = "cpu";

            reg = <1>;

        };

    };

         cpus nodenode-name必须是cpus

         它的parentroot node, 每个child-node描述一个CPU

         它一般包含两个属性#address-cells, #size-cells, 用于描述它的child-nodereg属性, reg属性的具体细节参考对应章节

         每一个child-node中必须包含device_type, 它的属性值是cpu.

3.4             memory node

每个DTS中都必须有一个memory node来描述内存状况.

    memory {

        device_type = "memory";

        reg = <0x80000000 0x10000000>; /* 256 MB */

    };

         如果memory node下有一个device_type属性, 并且属性值是memory. node-name没有特殊限制.

如果没有这个属性, node-name必须是memory@0

         它的parent虽然不强制为root node, 但是强烈推荐使其parent-noderoot node.

3.5             device node

device node用于描述那些platform-device, 例如:

/ {

    #address-cells = <1>;

    #size-cells = <1>;

 

    serial@101F0000 {

        compatible = "arm,pl011";

        reg = <0x101F0000 0x8000>;

    };

 

    spi@10115000 {

        compatible = "arm,pl022";

        reg = <0x10115000 0x8000>;

    };

 

};

         reg&cell  property的意思, 请参考对应章节

         compatible用于匹配device driver

3.6             interrupt node & related property

每个CPU都有一个中断控制器, 处理来至DMA, SPI, UART, GPIO等各个外设的中断; 其中GPIO又有很多个管脚, 每个管脚都可以产生中断, 不过GPIO所有管脚的中断在CPU这端共享一个中断号.

这种情况下:

如何描述CPU的中断控制器;

如果指定每个外设的中断号;

如何描述GPIO每个管脚的中断?

 

下面用一个例子说明:

/ {

    compatible = "acme,coyotes-revenge";

    #address-cells = <1>;

    #size-cells = <1>;

    interrupt-parent = <&intc>;

 

    intc: interrupt-controller@48200000 {

        compatible = "ti,am33xx-intc";

        interrupt-controller;

        #interrupt-cells = <1>;

        reg = <0x48200000 0x1000>;

    };

 

    spi0: spi@48030000 {

        compatible = "ti,omap4-mcspi";

        #address-cells = <1>;

        #size-cells = <0>;

        reg = <0x48030000 0x400>;

        interrupts = <65>;

    }

 

    gpio0: gpio@44e07000 {

        compatible = "ti,omap4-gpio";

        ti,hwmods = "gpio1";

        gpio-controller;

        #gpio-cells = <2>;

        interrupt-controller;

        #interrupt-cells = <2>;

        reg = <0x44e07000 0x1000>;

        interrupts = <96>;

    };

 

    wlcore: wlcore {

        interrupt-parent = <&gpio0>;

        interrupts = <31 IRQ_TYPE_LEVEL_HIGH>; /* gpio 31 */

    };

 

 

};

intc这个node描述了一个中断控制器, 我们先来看看如何描述一个中断控制器:

         interrupt-controller; 这个空属性, 代表本node是中断控制器

         #interrupt-cells = <1>; 这个属性代表本中断控制器要用几个cell来描述中断

 

中断控制器描述好了, 那么如何使用它呢, root-node下有个属性:

         interrupt-parent = <&intc>; 这个属性代表本node使用哪个中断控制器. 如果某个node下没interrupt-parent属性, 则它会集成parent-node的中断控制器

         例如spi0这个node没有interrupt-parent属性, 则使用root-node的中断控制器, 也就是&intc

         interrupts = <65>; 代表spi0intc中的中断号是65, 因为intc要求用1cell描述中断, 因此这里只用1cell即可

 

gpio0这个node比较特殊:

首先, 它使用intc做为中断控制器, 中断号是96;

其次, 它本身也是一个中断控制器, 并且要求用2cell来描述中断.

 

wlcore这个node就是用的gpio0做为中断控制器, 并且应gpio0的要求, 用两个cell来描述中断: 第一个cell代表中断号, 第二个cell代表中断触发方式

3.7             aliases node

这是一个特殊的node, 定义了一些别名. 它的形式一般如下:

    aliases {

        ethernet0 = &eth0;

        serial0 = &serial0;

    };

         node-name必须是aliases

         属性的value即可以是lable, 也可以是full path.

         若是lable, 则相当于给lable取个别名

         若是full path, 则相当于路径的缩写, 方便其他node引用. 这一点上作用跟lable类似

         注意, 形式是ethernet0 = &eth0; 而不是 ethernet0 = <&eth0>;!! 前者是取别名, 后者是引用某一个node

3.8             chosen node

chosen node主要用来描述由系统bootloader指定的runtime parameter.

    chosen {

        bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";

    };

         如果存在chosen这个node, parent-node必须是root-node

         node-name必须是chosen或者chosen@0

         bootargs: 以前是通过u-boot传递给内核的

3.9             compatible property

它是一个text string或者string list; 一般包含在root node device node.

root node, 它用于匹配使用哪个板级文件.

device node, 它用于匹配device driver.

compatible = "manufacturer, model1"[, "manufacturer, model2"];

         compatible一般由1 string组成, 形如 manufacturer, model1. [ ]中是可选内容

         manufacturer: 代表厂商, 例如freescale

         mode1: 代表厂商的某个设备, 例如imx6

         合起来就是: compatible = "freescale, imx6";

3.10       reg&cell property

什么时候需要用到reg property? 假设某个node下面包含地址信息, 则需要用到reg.

例如:

当我们用一个node描述SPI控制器的时候, node里面必须包含SPI控制器的寄存器地址;

在比如, 当我们用一个node描述DDRAM控制器的时候, 需要给定DDRAM的访问地址, 这个地址跟硬件连接有关系(用哪几根地址线, 用哪个片选管脚等).

前一种情况相对简单, 后一种需要用到memory map, 也就是需要用到ranges property.

 

先来看看reg property的语法格式, 要描述一个reg, 还需要另外两个property: #address-cells, #size-cells.

    parent-node {

        #address-cells = <1>;

        #size-cells = <1>;

        child-noe@fc069000 {

            reg = <0xfc069000 0x800 [address2 length2] ....>;

        };

    };

         如果某个child-node中包含来了地址信息, 则其parent-node有两个重要属性

         #address-cells: 代表child-nodereg属性中, 用几个uint32的整数代表基地址, 如果是32CPU, 一般1cell就可以了; 如果是64CPU, 则需要2cell

         #size-cells: 代表child-nodereg属性中, 用几个uint32的整数代表地址长度

         reg = <0xfc069000 0x800 [address2 length2] ....>

         结合上述, 1uint32代表基地址, 基地址就是0xfc069000

         1uint32代表长度, 长度就是0x800, 也就是2K

         [ ]里面是可选的, 有的reg里面可能需要描述多段地址, 它们的基地址不一样, 长度不一样. 不过多段地址都遵循同样的规则

         如果node下包含reg信息, node-name一定要包含@unit-address, unit-address的值是reg中的第一个地址.

         这里的address是从parent-node的角度来看, 而且仅限于该parent-node.

如果某个addressCPU需要访问的, 例如SPI控制的寄存器地址是fc069000, 则需要从root-node的角度去看, 确保看到的地址是fc069000

那么nodenode直接的地址域该如何转换呢? 需要使用rangs property, 下一节专门介绍它.

3.11       ranges property

ranges属性用于nodenode之间地址域的切换. 我们用一个例子来了解它吧.

 

假设我们有一个external-bus, 下面挂载了DDRAMNorflash两个控制器:

DDRAM的片选是0, 地址空间是128M;

Norfalsh的片选是1, 地址空间是64M.

那么描述external-busnode应该是这个样子:

    external-bus {

        #address-cells = <2>

        #size-cells = <1>;

 

        ddram@0,0 {

            compatible = "ddram-manufacture, ddram-model";

            reg = <0 0 0x8000000>;

        };

 

        norflash@1,0 {

            compatible = "norflash-manufacture, norflash-model";

            reg = <1 0 0x4000000>;

        };

    };

         #address-cells = <2>: 代表用2cell来描述external-buschild-nodereg, 第一个cell代表片选, 第二个cell代表从片选开始的偏移量

#size-cells = <1>代表用1cell来描述地址长度

 

很完美是不是, 但是上述reg描述的地址, CPU是无法访问的. CPU只知道去控制它的地址线, 你要告诉CPU送什么地址能访问到DDRAM, 送什么地址能访问到Norflash. 这些地址就是从root-node的角度看到的地址.

这时你就需要用到ranges属性, 我们加上ranges属性, 然后代码就变成了这个样子:

/ {

    compatible = "cpu-manufacture, cpu-model";

    #address-cells = <1>;

    #size-cells = <1>;

 

    external-bus {

        #address-cells = <2>

        #size-cells = <1>;

        ranges = <0 0  0x10100000  0x8000000    //Chipselect 1, DDRAM

                  1 0  0x18100000  0x4000000>;  //Chipselect 2, Norflash

 

        ddram@0,0 {

            compatible = "ddram-manufacture, ddram-model";

            reg = <0 0 0x8000000>;

        };

 

        norflash@1,0 {

            compatible = "norflash-manufacture, norflash-model";

            reg = <1 0 0x4000000>;

        };

    };

};

上述红色部分就是增加的代码, rangesexternal-bus  node下的地址域转换到roo-node.

         ranges = <child-address  parent-address  size-of-the-region-in-the-child-address-space>

         child-address : 它该用几个cell表示呢? external-bus node下的#address-cells是多少, 就要用多少个cell.

child-address的值与external-bus nodechild-node的值一一对应, 例如 0 0; 1 0

         parent-address: 它该用几个cell表示呢? external-bus nodeparent-node#address-cells是多少, 就要用多少个cell.

parent-address的值就是parent-node能访问的地址, 例如0x10100000, 就表示CPU在访问DDRAM的时候, 用的是这个地址

         size-of-the-region-in-the-child-address-space: 它该用几个cell表示呢? external-bus nodeparent-node#size-cells是多少, 就要用多少个cell.

0x8000000就代表从CPU的角度, 可以访问基地址为0x10100000, 地址范围为128M的区域.

这个值可以比ddram@0,0这个node的地址范围大, 这种情况的意思就是从CPU的角度, 可以访问一块大的地址范围(例如512M), 但是external-bus下只挂载了个128MDDRAM.

 

以上基本上就讲清楚了ranges属性的语法格式, 除了上述情况, ranges还可以为空, 如下:

/ {

    compatible = "cpu-manufacture, cpu-model";

 

    platform-node {

        #address-cells = <1>;

        #size-cells = <1>;

        ranges;

 

        spi@fc069000 {

            reg = <0xfc069000 0x800>;

        };

 

        i2c@44e0b000 {

            #address-cells = <1>;

            #size-cells = <0>;

 

            reg = <0x44e0b000 0x1000>;

 

            tps@24 {

                reg = <0x24>;

            };

        };

    };

};

上述例子中, platform-node只是一个虚拟的概念, 它不是一个真实的总线, 创建这样一个node只是为了把下面的child-node都归类, 让逻辑更清晰一点.

对于spi, CPU直接访问寄存器地址fc069000就可以了, 这种情况下, ranges就会空, 代表1 : 1的地址域切换.

 

另外你注意到了吗, i2c@44e0b000这个node下并没有ranges这个属性, 这种情况说明该node下的地址域不需要转换到parent node.

例如tps@24描述了一个用i2c控制的触屏, 该屏的i2c设备地址是0x24, 这个地址只对i2c控制器有意义, CPU没有任何意义, 不需要rangesroot-node.

3.12       自定义 nodeproperty

除了上述一些特殊的nodesproperties, 我们还可以自定义一些nodes或者properties. 自定义的nodeproperty也要遵循我们在3.1节列出的语法格式.

 

自定义的nodeproperty一般用于某些特定的驱动, 对于这些nodeproperty所代表的意义, 我们需要做一个txt文档, 存放于Documentation/devicetree/bindings目录下.

 

对于内核代码中已经存在的DTS, 如果里面有自定义的nodeproperty, 你也可以在bindings目录下找到解释.

4.   Device Tree Binary编译

暂时不需要了解细节

5.   内核代码如何使用Device Tree

5.1             UBOOT如何将dtb传递给内核

内核启动过程中, 会进入到linux/arch/arm/kernel/head.S这个文件 (内核启动的详细流程我们会有一篇专门的文章来介绍).

head.S中有一段注释, 定义了bootloaderkernel的参数传递要求:

/*

* Kernel startup entry point.

* ---------------------------

*

* This is normally called from the decompressor code.  The requirements

* are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,

* r1 = machine nr, r2 = atags or dtb pointer.

*

目前的kernel支持旧的tag list的方式, 同时也支持device tree的方式.

r2可能是DTB的指针(bootloader要传递给内核之前要copymemory中), 也可以能是tag list的指针.

ARM的汇编部分的启动代码中(主要是head.Shead-common.S, machine nr和指向DTB或者atags的指针被保存在变量__machine_arch_type__atags_pointer, 这么做是为了后续c代码进行处理.

5.2             内核解析dtb的流程

NOTE: 内核的代码逻辑主要放在这一小节描述; 关于dtb相关的代码细节, 例如内核里面用什么数据结构描述一个node, 以什么样的方式去扫描device-tree, 放在下一节.

 

内核启动过程中, 在执行完上述汇编代码后, 会做一些别的初始化动作, 然后会运行到init/main.c, 执行start_kernel函数,  start_kernel会调用arch/arm/kernel/setup.csetup_arch函数.

setup_arch会调用相应的函数来解析dtb. 这些函数如下:

void __init setup_arch(char **cmdline_p)

{

    const struct machine_desc *mdesc;

 

    setup_processor();

 

    /* 匹配板级文件, 解析runtime parameters, 解析memory node信息,

     * __atags_pointer是在汇编代码中赋值的dtb的指针

     */

    mdesc = setup_machine_fdt(__atags_pointer);

 

    /* 如果没有使用dtb, 则使用旧的方式匹配板级文件, 旧的方式主要靠u-boot传过来的machine id*/

    if (!mdesc)

        mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);

 

    ......

 

    /* 针对每一个node, 生成一个struct device_node

     * 并把所有的device_node挂载到全局链表of_allnodes

     */

    unflatten_device_tree();

 

    /* 解析DTB中的CPUs node*/

    arm_dt_init_cpu_maps();

 

    ......

}

如何匹配板级文件

旧的方式靠u-boot传过来的machine id匹配板级文件, 新的方式用dtb来匹配. 内核系统默认优先使用dtb的方式.

 

dtb方式下, setup_machine_fdt函数用于匹配板级文件, 下面我们看看他的实现细节.

 

实现文件: arch/arm/kernel/devtree.c

const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)

{

    const struct machine_desc *mdesc, *mdesc_best = NULL;

 

#ifdef CONFIG_ARCH_MULTIPLATFORM

    DT_MACHINE_START(GENERIC_DT, "Generic DT based system")

    MACHINE_END

 

    mdesc_best = &__mach_desc_GENERIC_DT;

#endif

 

    /* 1th: 验证传过来的dtb是否有效, 验证完毕后, 会把转换为虚拟地址后的dtb指针保存在initial_boot_params, 供后续代码使用 */

    if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))

        return NULL;

 

    /* 2th: 寻找最合适的machine_desc */

    mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);

 

    /* 3th: 如果没找到最合适的machine_desc, 则用early_print打印出相关消息.

     * early_print底层是用汇编操作串口打印消息

     */

    if (!mdesc) {

        const char *prop;

        int size;

        unsigned long dt_root;

 

        early_print("\nError: unrecognized/unsupported "

                "device tree compatible list:\n[ ");

 

        dt_root = of_get_flat_dt_root();

        prop = of_get_flat_dt_prop(dt_root, "compatible", &size);

        while (size > 0) {

            early_print("'%s' ", prop);

            size -= strlen(prop) + 1;

            prop += strlen(prop) + 1;

        }

        early_print("]\n\n");

 

        dump_machine_table(); /* does not return */

    }

 

    /* We really don't want to do this, but sometimes firmware provides buggy data */

    if (mdesc->dt_fixup)

        mdesc->dt_fixup();

 

    /* 4th: 如果找到了machine_desc, 则进行一些其他处理, 这个函数后面单独说 */

    early_init_dt_scan_nodes();

 

    /* Change machine number to match the mdesc we're using */

    __machine_arch_type = mdesc->nr;

 

    return mdesc;

}

setup_machine_fdt的大致流程已经在上述代码中用中文注释了.

该函数返回的是一个machine_desc, 我们可以把machine_desc简单理解为用于描述一个板级文件. 应该能大致猜到该函数的作用了吧: 根据dtb的从内核的一堆machine_desc, 选出最合适的那个machine_desc. (上述代码中的of_flat_dt_match_machine就是用于实现这套逻辑的).

 

那么内核中的那一堆machine_desc是如何产生的呢? 又是根据什么规则来选择最合适的machine_desc的呢?

 

先回答第一个问题: 那么内核中的那一堆machine_desc是如何产生的

内核的板级文件中, 会用DT_MACHINE_START , MACHINE_END来定义一个machine_desc. 例如如下代码(arch\arm\mach-omap2\board-generic.c):

#ifdef CONFIG_SOC_OMAP2420

static const char *const omap242x_boards_compat[] __initconst = {

    "ti,omap2420",

    NULL,

};

 

DT_MACHINE_START(OMAP242X_DT, "Generic OMAP2420 (Flattened Device Tree)")

    .reserve    = omap_reserve,

    .map_io     = omap242x_map_io,

    .init_early = omap2420_init_early,

    .init_machine   = omap_generic_init,

    .init_time  = omap2_sync32k_timer_init,

    .dt_compat  = omap242x_boards_compat,

    .restart    = omap2xxx_restart,

MACHINE_END

#endif

DT_MACHINE_START是一个宏, 它会定义一个machine_desc, 并把该machine_desc存放在".arch.info.init"这个段(section).

如果多个板级文件中都用到了DT_MACHINE_START, 那么".arch.info.init"这个section中就会有一堆的machine_desc.

 

在回答第二个问题: 根据什么规则来选择最合适的machine_desc

of_flat_dt_match_machine就是用来选择machine_desc, 它的逻辑是: dtbroot-node中获取compatible属性的值, 然后从".arch.info.init"这个section中依次取出machine_desc; 比较compatible-valuemachine_desc-> dt_compat, 找到最合适的machine_desc. 所谓最合适就是字符串最匹配的那个.

找到之后, 就知道使用哪个板级文件了.

如何解析runtime parameters

setup_machine_fdt函数中, 如果找到了合适的machine_desc, 紧接着就会调用early_init_dt_scan_nodes来做后续处理.

 

early_init_dt_scan_nodes中所做的处理如下:

         解析runtime parameters.

         保存root-node下的#address-cells#size-cells的值

         解析memory-node, 并将内存信息保存下来供内核系统使用.

 

本节只分析前两个问题, 最后一个问题放在下一节描述.

void __init early_init_dt_scan_nodes(void)

{

    /* Retrieve various information from the /chosen node */

    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);

 

    /* Initialize {size,address}-cells info */

    of_scan_flat_dt(early_init_dt_scan_root, NULL);

 

    /* Setup memory, calling early_init_dt_add_memory_arch */

    of_scan_flat_dt(early_init_dt_scan_memory, NULL);

}

of_scan_flat_dt函数的意思是扫描dtb里面的每一个node, 针对每一个node, 调用第一个参数传递过来的函数指针.

 

如何解析runtime parameters: 针对每一个node调用early_init_dt_scan_chosen, 并将结果保存在boot_command_line这个全局变量中.

early_init_dt_scan_chosen

         查找root-node下面的1(depth == 1)child-node, 有没有node-name"chosen"或者"chosen@0"node

         如果有这个node, 则获取该node下的bootargs属性的值. 并把这个值保存在boot_command_line这个全局变量中.

如果dtb中没有定义chosen node或者该node下没有bootargs这个属性, 则内核会使用默认的CONFIG_CMDLINE

 

如何保存root-node下的#address-cells#size-cells的值: 针对每一个node调用early_init_dt_scan_root.

early_init_dt_scan_root

         首先判断是不是root-node (depth 是否等于 0)

         如果是root-node, 则获取该node下的两个属性值

         获取#size-cells这个属性的值, 并将结果保存在dt_root_size_cells

         获取#address-cells这个属性的值, 并将结果保存在dt_root_addr_cells

如果root-node下没有定义这两个属性, 则内核系统会使用默认值(OF_ROOT_NODE_SIZE_CELLS_DEFAULT, OF_ROOT_NODE_ADDR_CELLS_DEFAULT)

memory mode如何处理

接着上一节的内容, 如何解析memory-node: 针对每一个node调用early_init_dt_scan_memory函数.

early_init_dt_scan_memory

         查看node下是否有一个名为device_type的属性:

如果有, 判断属性值是否为memory, 如是, 则此node是一个memory node

如果没有device_type这个属性, 则判断node是否为root-node1child-node, 并且node-name是否为memory@0, 如条件满足, 则此node也是一个memory node

         针对所有的memory node(大多数DTB中只有1memory-node), 获取node下的reg属性, 然后结合dt_root_addr_cells dt_root_size_cells, reg中解析出memory的起始地址和大小, 最后将此memory的信息添加到内核系统中

CPUs node如何处理

setup_arch函数会调用arm_dt_init_cpu_maps来解析CPUs node, 具体细节如下:

void __init arm_dt_init_cpu_maps(void)

{

    /*寻找full path“/cpus”的那个device node.

     * cpus这个device node只是一个容器, 其中包括了各个child cpu node的定义以及所有cpu node共享的property.

     */

    cpus = of_find_node_by_path("/cpus");

 

    for_each_child_of_node(cpus, cpu) {           //遍历cpus的所有的child node

        u32 hwid;

        if (of_node_cmp(cpu->type, "cpu"))        //我们只关心那些device_typecpunode

            continue;

 

        if (of_property_read_u32(cpu, "reg", &hwid)) {    //读取reg属性的值并赋值给hwid

            return;

        }

        /* reg的属性值的8 MSBs必须设置为0, 这是ARM CPU binding定义的.*/

        if (hwid & ~MPIDR_HWID_BITMASK)  

            return;

 

        /*不允许重复的CPU id, 那是一个灾难性的设定 */

        for (j = 0; j < cpuidx; j++)

            if (WARN(tmp_map[j] == hwid, "Duplicate /cpu reg "

                             "properties in the DT\n"))

                return;

 

        /* 数组tmp_map保存了系统中所有CPUMPIDR值(CPU ID值),

         * 具体的index的编码规则是:

         *  tmp_map[0]保存了booting CPUid,

         *  其余的CPUID值保存在1NR_CPUS的位置.

         */

        if (hwid == mpidr) {

            i = 0;

            bootcpu_valid = true;

        } else {

            i = cpuidx++;

        }

        tmp_map[i] = hwid;

    }

 

    /* 根据DTB中的信息设定cpu logical map数组 */

    for (i = 0; i < cpuidx; i++) {

        set_cpu_possible(i, true);

        cpu_logical_map(i) = tmp_map[i];

    }

}

要理解这部分的内容,需要理解ARM CUPs binding的概念,可以参考Documentation/devicetree/bindings/arm目录下的CPU.txt文件的描述

interrupt node如何处理

初始化是通过start_kernel->init_IRQ->machine_desc->init_irq()实现的.

machine_desc就是我们找到的那个与DTB最配的板级文件中定义的.

init_irq()函数中一般会直接调用of_irq_init

 

of_irq_init是在drivers/of/irq.c中定义的, 它会扫描每一个node, 判断node中是否有一个"interrupt-controller"的属性. 如果有, 就会把它当做一个中断控制节点.

 

of_irq_init的实际代码细节比上述的要复杂, 理解它需要配合中断子系统的背景知识, 这里就详述了.

device node如何处理

本文开始部分就提到, device-tree就是相当于把原来在板级文件中定义的platform_device, 放到device-tree中来了.

device-tree的基本单元是node, 你应该能猜到, 每个device-node就代表一个platform_device. 但是内核代码到底是怎么处理的呢? 在设备模型一章中我们提到platform_device/bus/driver, 采用device-tree机制后, 设备模型还是没有改变, 这些node必须转换成一个个的struct platform_device, 并注册到platfrom_bus, 才能与driver配合工作.

 

OK, 下面我们就来看看内核代码是如何通过node生成platfrom_device.

代码执行路径如下: start_kernel -> rest_init -> kernel_init -> kernel_init_freeable -> do_basic_setup -> do_initcalls.

do_initcalls函数中, kernel会依次执行各个initcall函数, 在这个过程中, 会调用customize_machine, 具体如下:

static int __init customize_machine(void)

{

 

    if (machine_desc->init_machine)

        machine_desc->init_machine();

    else

        of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL);

 

    return 0;

}

arch_initcall(customize_machine);

customize_machine会首先调用machine_desc->init_machine, 如果machine_desc中没有定义init_machine, 则会调用of_platform_populate.

of_platform_populate就是用来将node转换成platfrom_device. 大多数板级文件中定义的init_machine里面, 也是直接调用of_platform_populate

 

下面我们看看of_platform_populate的具体实现(drivers/of/platform.c)

static int of_platform_bus_create(struct device_node *bus,--要创建的那个device node

                  const struct of_device_id *matches,-------要匹配的list

                  const struct of_dev_auxdata *lookup,------附属数据

                  struct device *parent, bool strict)---------------parent指向父节点。strict是否要求完全匹配

{

    const struct of_dev_auxdata *auxdata;

    struct device_node *child;

    struct platform_device *dev;

    const char *bus_id = NULL;

    void *platform_data = NULL;

    int rc = 0;

 

    /* Make sure it has a compatible property */

    /* 这段代码说明, 一个node如果代表platform_device, 则必须有compatible属性 */

    if (strict && (!of_get_property(bus, "compatible", NULL))) {

        pr_debug("%s() - skipping %s, no compatible prop\n",

        __func__, bus->full_name);

        return 0;

    }

 

    auxdata = of_dev_lookup(lookup, bus);  /*在传入的lookup table寻找和该device node匹配的附加数据*/

    if (auxdata) {

        bus_id = auxdata->name;            /* 如果找到,那么就用附加数据中的静态定义的内容 */

        platform_data = auxdata->platform_data;

    }

 

    /* ARM公司提供了CPU core,除此之外,它设计了AMBA的总线来连接SOC内的各个block.

     * 符合这个总线标准的SOC上的外设叫做ARM Primecell Peripherals.

     * 如果一个device nodecompatible属性值是arm,primecell的话,

     * 可以调用of_amba_device_create来向amba总线上增加一个amba device

     */

    if (of_device_is_compatible(bus, "arm,primecell")) {

        of_amba_device_create(bus, bus_id, platform_data, parent);

        return 0;

    }

 

    /* 如果不是ARM Primecell Peripherals,

     * 那么我们就需要向platform bus上增加一个platform device

     */

    dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);

    if (!dev || !of_match_node(matches, bus))

        return 0;

 

    /* 一个device node可能有child-node,

     * 因此要重复调用of_platform_bus_create来把所有的device node处理掉

     */

    for_each_child_of_node(bus, child) {

        pr_debug("   create child: %s\n", child->full_name);

        rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);

        if (rc) {

            of_node_put(child);

            break;

        }

    }

    return rc;

}

 

OK, 上述代码中的of_platform_device_create_pdata就是用来创建platform_device.

static struct platform_device *of_platform_device_create_pdata(

                    struct device_node *np,

                    const char *bus_id,

                    void *platform_data,

                    struct device *parent)

{

    struct platform_device *dev;

 

    /* check status属性, 确保是enable或者OK

     * enable/OK代表该设备被启用

     * 一般来说, CPU对应的dtsi文件里面会把各个片上外设的node都创建, statusdisable

     * 在板级文件中, 如果我们要使用某个片上外设, 则将其status overrideenable

     */

    if (!of_device_is_available(np))

        return NULL;

 

   /* of_device_alloc除了分配struct platform_device的内存,

    * 还分配了该platform device需要的resource的内存(参考struct platform_device 中的resource成员).

    * 当然, 这就需要解析该device nodeinterrupt属性以及reg属性

    */

    dev = of_device_alloc(np, bus_id, parent);

    if (!dev)

        return NULL;

 

    /* 设定platform_device 中的其他成员 */

    dev->dev.coherent_dma_mask = DMA_BIT_MASK(32);

    if (!dev->dev.dma_mask)

        dev->dev.dma_mask = &dev->dev.coherent_dma_mask;

    dev->dev.bus = &platform_bus_type;

    dev->dev.platform_data = platform_data;

 

    /* 把这个platform device注册到platform_bus总线上 */

    if (of_device_add(dev) != 0) {

        platform_device_put(dev);

        return NULL;

    }

    return dev;

}

完成上述过程, 一个包含有compatible属性的node就被转换成了一个platform_device, 并注册到了platfrom_bus总线上

5.3             内核解析dtb的相关代码

这一节主要介绍dtb相关的代码细节, 数据结构.

数据结构

内核系统中, struct device_node 来抽象设备树中的一个node, 具体解释如下:

头文件: include/linux/of.h

struct  device_node

Comment

const char *name

node-name

const char *type

device_type属性的属性值

phandle phandle

该节点的phandle, 可以理解成节点所在位置的指针

const char *full_name

nodefull path

structproperty *properties

该节点的属性列表

structproperty *deadprops

如果需要删除某些属性, kernel并非真的删除, 而是挂入到deadprops的列表

structdevice_node *parent

指向parent node

structdevice_node *child

指向child node

structdevice_node *sibling

指向兄弟节点

structdevice_node *next

通过该指针可以获取相同类型的下一个node (目前还不是太明白它的具体意义)

structdevice_node *allnext

用于把自己挂载到node global list链表中

node global list是一个全局链表, DTB中所有的node都挂载在该链表

structkobject kobj

node的引用计数

unsigned long _flags

 

void*data

 

代码分析

头文件: include/linux/of.h

实现文件: drivers/of/fdt.c

 

宏观的逻辑来看, u-bootDTB传递到内核之后, 内核会解析该DTB: 针对每一个node, 生成一个struct device_node, 并初始化该device_node的相关内容; 然后把这个device_node挂载到内核系统中定义的一个全局变量of_allnodes.

执行上述逻辑的是fdt.c中定义的unflatten_device_tree函数, 如下

void __init unflatten_device_tree(void)

{

        __unflatten_device_tree(initial_boot_params, &of_allnodes,

                                early_init_dt_alloc_memory_arch);

 

        /* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */

        of_alias_scan(early_init_dt_alloc_memory_arch);

}

 

unflatten_device_tree执行完毕后, global list of_allnodes中就保存了所有的device_node的信息, 其它模块可以通过fdt.c提供的一系列API来查询node的相关信息.

这些API包括(只列举其中一部分):

API  device_node

Comment

struct device_node *of_find_node_by_path(const char *path)

根据给定的full path, 获取某个device_node

一个个列了, 后续在添加

 

posted @ 2020-12-13 17:56  johnliuxin  阅读(445)  评论(0)    收藏  举报