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往往存在很多个板子, 比如FSL的IMX.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.6的CPU, 那么这两块板子的DTS文件会不会有很多重复的地方? 答案是肯定的. 这种情况下, 我们可以把公共的部分提取出来, 变成一个dtsi文件, 然后不同板子的DTS可以#include这个dtsi文件.
DTS这种层次结构遵循实际硬件的层次结构:
例如ARM公司设计了一种体系架构 Cortex-A9; FSL公司用这个体系架构设计了IMX6A, IMX6B, IMX6C 3中CPU; Embest公司用IMX6A做了两块板子 IMX6A-BOARD1, IMX6A-BOARD2.
那么DTS的层次结构就是
一个dtsi文件描述Cortex-A9体系架构, 一般官方名称是skeleton.dtsi
一个dtsi文件描述3款CPU的共同部分, imx6x.dtsi
3个不用的dtsi描述3款CPU: 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; 然后把这些node用tree的方式组织. 如下:
/ {
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 {
};
};
};
接下来的小节中:
首先会介绍node和property的语法格式
然后介绍几种特殊的node
然后介绍几种特殊的property
然后介绍如何自定义node和property
3.1 Node & Property
node的由node-name加一对{ }组成. { }中可以包含用于描述该node的property和child-node.
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
}
[ ]表示可选内容
node-name: 必须的, 它由简单的ascii字符组成, 最多31个字符. node-name一般代表这个node用于描述什么设备: 例如一个 3com509的Ethernet控制器, 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;
a-string-property = "A string";
a-string-list-property = "first string", "second string";
可以是1个或多个32 bit unsigned integers, 每个32bit的整数称为1个cell
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
/ {
compatible = "acme,coyotes-revenge";
};
root node的node-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>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0>;
};
cpu@1 {
compatible = "arm,cortex-a9";
device_type = "cpu";
reg = <1>;
};
};
cpus node的node-name必须是cpus
它的parent是root node, 每个child-node描述一个CPU
它一般包含两个属性#address-cells, #size-cells, 用于描述它的child-node的reg属性, 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-node为root node.
3.5 device node
device node用于描述那些”platform-device”, 例如:
/ {
#address-cells = <1>;
#size-cells = <1>;
serial@101F0000 {
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 {
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>; 代表spi0在intc中的中断号是65, 因为intc要求用1个cell描述中断, 因此这里只用1个cell即可
gpio0这个node比较特殊:
首先, 它使用intc做为中断控制器, 中断号是96;
其次, 它本身也是一个中断控制器, 并且要求用2个cell来描述中断.
而wlcore这个node就是用的gpio0做为中断控制器, 并且应gpio0的要求, 用两个cell来描述中断: 第一个cell代表中断号, 第二个cell代表中断触发方式
3.7 aliases node
这是一个特殊的node, 定义了一些别名. 它的形式一般如下:
node-name必须是aliases
属性的value即可以是lable, 也可以是full path.
若是lable, 则相当于给lable取个别名
若是full path, 则相当于路径的缩写, 方便其他node引用. 这一点上作用跟”lable”类似
注意, 形式是ethernet0 = ð0; 而不是 ethernet0 = <ð0>;!! 前者是取别名, 后者是引用某一个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.
如果某个child-node中包含来了地址信息, 则其parent-node有两个重要属性
#address-cells: 代表child-node的reg属性中, 用几个uint32的整数代表基地址, 如果是32位CPU, 一般1个cell就可以了; 如果是64位CPU, 则需要2个cell
#size-cells: 代表child-node的reg属性中, 用几个uint32的整数代表地址长度
reg = <0xfc069000 0x800 [address2 length2] ....>
结合上述, 用1个uint32代表基地址, 基地址就是0xfc069000
用1个uint32代表长度, 长度就是0x800, 也就是2K
[ ]里面是可选的, 有的reg里面可能需要描述多段地址, 它们的基地址不一样, 长度不一样. 不过多段地址都遵循同样的规则
如果node下包含reg信息, 则node-name一定要包含@unit-address, unit-address的值是reg中的第一个地址.
这里的address是从parent-node的角度来看, 而且仅限于该parent-node.
如果某个address是CPU需要访问的, 例如SPI控制的寄存器地址是fc069000, 则需要从root-node的角度去看, 确保看到的地址是fc069000
那么node与node直接的地址域该如何转换呢? 需要使用rangs property, 下一节专门介绍它.
3.11 ranges property
ranges属性用于node与node之间地址域的切换. 我们用一个例子来了解它吧.
假设我们有一个external-bus, 下面挂载了DDRAM和Norflash两个控制器:
DDRAM的片选是0, 地址空间是128M;
Norfalsh的片选是1, 地址空间是64M.
那么描述external-bus的node应该是这个样子:
external-bus {
#address-cells = <2>
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>: 代表用2个cell来描述external-bus的child-node的reg, 第一个cell代表片选, 第二个cell代表从片选开始的偏移量
#size-cells = <1>代表用1个cell来描述地址长度
很完美是不是, 但是上述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
compatible = "ddram-manufacture, ddram-model";
reg = <0 0 0x8000000>;
};
norflash@1,0 {
compatible = "norflash-manufacture, norflash-model";
reg = <1 0 0x4000000>;
};
};
};
上述红色部分就是增加的代码, ranges把external-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 node下child-node的值一一对应, 例如 0 0; 1 0
parent-address: 它该用几个cell表示呢? external-bus node的parent-node的#address-cells是多少, 就要用多少个cell.
parent-address的值就是parent-node能访问的地址, 例如0x10100000, 就表示CPU在访问DDRAM的时候, 用的是这个地址
size-of-the-region-in-the-child-address-space: 它该用几个cell表示呢? external-bus node的parent-node的#size-cells是多少, 就要用多少个cell.
0x8000000就代表从CPU的角度, 可以访问基地址为0x10100000, 地址范围为128M的区域.
这个值可以比ddram@0,0这个node的地址范围大, 这种情况的意思就是从CPU的角度, 可以访问一块大的地址范围(例如512M), 但是external-bus下只挂载了个128M的DDRAM.
以上基本上就讲清楚了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没有任何意义, 不需要ranges到root-node下.
3.12 自定义 node和property
除了上述一些特殊的nodes和properties, 我们还可以自定义一些nodes或者properties. 自定义的node或property也要遵循我们在3.1节列出的语法格式.
自定义的node和property一般用于某些特定的驱动, 对于这些node和property所代表的意义, 我们需要做一个txt文档, 存放于Documentation/devicetree/bindings目录下.
对于内核代码中已经存在的DTS, 如果里面有自定义的node或property, 你也可以在bindings目录下找到解释.
4. Device Tree Binary编译
暂时不需要了解细节
5. 内核代码如何使用Device Tree
5.1 UBOOT如何将dtb传递给内核
内核启动过程中, 会进入到linux/arch/arm/kernel/head.S这个文件 (内核启动的详细流程我们会有一篇专门的文章来介绍).
head.S中有一段注释, 定义了bootloader和kernel的参数传递要求:
/*
* 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要传递给内核之前要copy到memory中), 也可以能是tag list的指针.
在ARM的汇编部分的启动代码中(主要是head.S和head-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.c中的setup_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的, 它的逻辑是: 从dtb的root-node中获取compatible属性的值, 然后从".arch.info.init"这个section中依次取出machine_desc; 比较compatible-value和machine_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-node的1级child-node, 并且node-name是否为”memory@0”, 如条件满足, 则此node也是一个memory node
针对所有的memory node(大多数DTB中只有1个memory-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_type是cpu的node
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保存了系统中所有CPU的MPIDR值(CPU ID值),
* 具体的index的编码规则是:
* tmp_map[0]保存了booting CPU的id值,
* 其余的CPU的ID值保存在1~NR_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 node的compatible属性值是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都创建好, 但status是disable
* 在板级文件中, 如果我们要使用某个片上外设, 则将其status override为enable
*/
if (!of_device_is_available(np))
return NULL;
/* of_device_alloc除了分配struct platform_device的内存,
* 还分配了该platform device需要的resource的内存(参考struct platform_device 中的resource成员).
* 当然, 这就需要解析该device node的interrupt属性以及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
Comment |
|
const char *name |
node-name |
const char *type |
“device_type”属性的属性值 |
phandle phandle |
该节点的phandle, 可以理解成节点所在位置的指针 |
const char *full_name |
node的full path |
structproperty *properties |
该节点的属性列表 |
structproperty *deadprops |
如果需要删除某些属性, kernel并非真的删除, 而是挂入到deadprops的列表 |
structdevice_node *parent |
|
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-boot把DTB传递到内核之后, 内核会解析该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 |
… |
不一个个列了, 后续在添加 |