目录
③DTS是DT的源文件,描述Device Tree中的设备(Device)的具体内容和拓扑结构 2
④DTC , Device Tree Compiler,设备树编译器 3
⑤DTB, Device Tree BLOB设备树二进制对象 3
-
设备树简介
-
问题一:为什么需要设备树?
在目前广泛使用的Linux kernel2.6版本中,对于不同平台、不同硬件,往往存在着大量的不同的、移植性差的板级描述代码,以达到对这些不同平台和不同硬件特殊适配的需求。但是过多的平台、过多的不同硬件导致这样的板级代码越来越多,比如arch/arm/plat-xxx和arch/arm/mach-xxx中充斥着大量的垃圾代码,platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data。
使用Device Tree之后,可以实现不改动内核代码(不改动zImage)的情况下,不同产品,只更换相应的*.dtb文件即可启动系统。即相同的zImage使用不同的*.dtb文件,可以启动不同的产品。
①名词解释:
-
-
DT: Device Tree 设备树
-
DTS: Device Tree Source设备树源码
-
DTC: Device Tree Compiler设备树编译器
-
DTB: Device Tree BLOB设备树二进制文件
-
SOC: 基于某款CPU的平台
-
machine: 基于平台的某个产品
②DT详细介绍:
组成:Device Tree由两部分组成,
--node 节点\结点\设备(节点可包含子节点)
--property 属性(都是成对出现的name和value)
DT的节点主要描述设备的存储空间、总线、中断、GPIO、clock等信息。
③DTS是DT的源文件,描述Device Tree中的设备(Device)的具体内容和拓扑结构
DTS源文件,其规范不是c语言编写。都是ASCII结构的文本格式文件,保存在arch/arm/boot/dts目录中。DTS源文件主要有两类:*.dts文件和*.dtsi文件。
*.dtsi文件,记录公共、可通用的device信息。可被其他*.dts或*.dtsi文件引用。引用方式/include/"xxx.dtsi"。
一个machine,必有一个*.dts文件对应其DTS入口。多个*.dts文件和*.dtsi文件共同描述一个machine。
④DTC , Device Tree Compiler,设备树编译器
DTC是负责把*.dts文件编译成*.dtb镜像的编译工具。源码位script/dtc目录中
⑤DTB, Device Tree BLOB设备树二进制对象
BLOB,Binary Large Object,二进制对象。*.dtb文件可被Linux内核解析成二进制文件,*.dtb文件由描述一个machine的多个*.dts和*.dtsi文件编译而成。
⑥dtb文件的编译
在执行内核编译的过程中,最先编译的就是Device Tree。单独编译内核时,会将/arch/arm/boot下的含根节点的dts,编译成一个个*.dtb文件。高通平台生成boot.img过程中,会由高通提供的dtbtool工具将所有含"qcom,msm-id"根节点的dtb文件,编译成一整个dt.img文件。dt.img文件是按页大小(如2048)编译而成的。android提供的mkbootimg工具在打包boot.img过程中,会参照"--bt dt.img"参数,将指定的dt.img放置在ramdisk.img之后。
⑦boot image简介
-
添加dt.img之后,boot.img由四部分组成:
-
boot image header
-
kernel image
-
ramdisk image
-
device tree image
-
每一部分的大小都是以page为单位。
-
emmc:1 page(例如:2048 byte)。
-
DTS设备树文件的加载及驱动注册过程
①lk引导之前的准备
简介中介绍,一个包含足够多驱动的内核镜像,匹配不同的machine的dtb文件,可以支持不同的machine。实际上,高通做的更彻底:一个boot.img镜像中,包含足够多驱动的内核镜像,加上一个包含所有machine的dt.img。实际上这样一个boot.img,可以支持多个machine。Lk根据platformid/boardid等id加载最匹配当前产品的dtb。
②bootloader的引导
❶lk运行过程中,对boot.img中的dt.img做如下操作:
--从dt.img中解析出包含的所有的dtb文件(设备树二进制文件)
--判断dtb文件中的platform ID,匹配出适合的dtb文件
--将匹配的dtb文件加载到DDR存储器中
--修改部分节点的属性,内核的启动信息
--将dtb在DDR中的首地址,传递给内核。
❷lk传递给内核的参数如下:
--r0 :不变,仍为0
--r1 :不再使用,通常为oxffff ffff
--r2 :设备树二进制文件dtb在内存中的首地址
❸lk引导后的地址:
内核:基址+0x0000 0000
ramdisk :基址+0x0200 0000
DTB :基址+0x01E0 0000
x之后的Android版本,允许修改ramdisk_offset、tags_offset(即dtb_offset)的偏移地址。
❹内核的启动
-
函数调用过程:
-
1、start_kernel //内核执行的一个C函数
-
2、setup_arch //创建arm的架构
-
3-1、setup_machine_fdt //负责加载DT
-
3-2、unflatten_device_tree //负责解析DT
-
当DT被解析成FDT后,保存在allnodes/of_allnodes全局变量中。
-
allnodes是一个指针变量,指向device tree的根节点。
-
allnodes定义在drivers/of/base.c文件中;
-
setup_machine_fdt定义在arch/arm/kernel/devtree.c文件中;
-
unflatten_device_tree定义在drivers/of/fdt.c文件中。
❺machine的启动
3.x内核中,DT_MACHINE_START是启动machine的函数入口。
DT_MACHINE_START定义中含有一个.dt_compat成员。
.dt_compat表示DTS根节点中的compatible属性。
DTS根节点中的compatible属性的结构:
<manufacture>, <model>
例:compatible = "qcom, msm8939"
3.x内核中,.dt_compat是决定启动哪一个DT_MACHINE_START的标准。
3.x内核中, .dt_compat是由bootloader通过dtb首地址( .dt_compat 包含在dtb文件中)传递给内核的。
-
③device的注册
当匹配DT_MACHINE_START成功之后, DT_MACHINE_START定义的init_machine即会运行。
函数调用过程如下:
init_machine -> msm8xxx_init -> board_dt_populate -> of_platform_populate
of_platform_populate函数完成的工作,就是将保存在allnodes/of_allnodes全局变量中的FDT信息,注册一个个sysfs系统下的device设备。
of_platform_populate函数不会将所有的节点都注册成device,只有status属性为"ok"和"okay"的才会注册。
如果device tree中节点的status属性为"disabled",则此节点不会被注册成device。
④probe函数的执行
适配device和drivier的match函数找到相同的name时,会自动调用driver注册的probe函数
-
DTS语法结构
①语法简介:
采用device tree后,许多硬件的细节可以直接透过它传递给Linux,而不需要在kernel中进行大量的冗余代码。当前的device tree主要应用于ARM, X86, Microblaze,PowerPC等架构中并向其他的平台进行扩展,以便在基于kernel架构下完成平台设备描述的统一化,所有的设备在平台上都以设备树的形式进行注册,而系统可以通过设备树的解析来完成相关设备的查找和注册。Device Tree由一系列被命名的节点(node)和属性(property)组成,而节点本身可以包含子节点。所谓属性,其实就是成对出现的name和value。在Device Tree中,可描述的信息包括(原先这些信息大多被写hard code到kernel中,现在是通过设备树的形式描述传递到内核):
-
CPU的数量和类别
-
内存基地址和大小
-
总线和桥
-
外设连接
-
中断控制器和中断使用情况
-
GPIO控制器和GPIO使用情况
-
Clock控制器和Clock使用情况
它基本上就是画一棵电路板上CPU、总线、设备组成的树,Booloader会将这棵树传递到内核,然后内核可以识别这棵树,并根据它站开出Linux内核中的platform device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给了内核,内核会将这些资源绑定到展开的相应设备上。
.dts文件是一种ASCII文件格式的Device Tree描述,此文本非常人性化,适合人类的阅读习惯。基本上,在ARM Linux中,一个.dts文件对应一个ARM的machine,一般放置在内核的arch/arm/boot/dts/目录。由于一个SOC可能对应多个machine(一个SOC可以对应多个产品和电路板),势必这些.dts文件需包含许多共同的部分,Linux内核为了简化,把SOC公用的部分或者多个machine共同的部分提炼为.dtsi。类似于c语言的头文件。其他的machine对应的.dts就include这个.dtsi。例如,对于MSM8974而言,msm8974-v2.0.1-mtp.dts有如下一行:/include/"msm8974-v2.0-1.dtsi",这个.dtsi中就包含和machine共同的部分。当然,和C语言的头文件类似,.dtsi也可以include其他的.dtsi,譬如几乎所有的ARM SoC的.dtsi都引用了skeleton.dtsi。
②设备树基本组成和结构
我们准备提供一个有关设备树概念的概述和如何使用这些设备树来描述一个机器。完整的设备树数据格式的技术说明书请参照ePAPR规范。
-
基本数据格式
设备树是一个包含节点和属性的简单树状结构。属性就是键-值对,而节点可以同时包含属性和子节点。例如,如下就是一个.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>; a-string-property = "Hello, world"; }; child-node2 { }; }; node2 { an-empty-property; a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */ child-node1 {
}; }; }; |
这棵树它并没有描述任何东西,但它却是体现了节点的一些属性:
属性是简单的键值对,他的值可以为空或者包含一个任意字节流。虽然数据类型没有编码进数据结构,但在设备树源文件中有几个基本的数据表示形式。 文本字符串(无结束符)可以用双引号表示:
\'Cells\'是32位无符号整数,用尖括号限定:
二进制数据用方括号限定:
不同表示形式的数据可以使用逗号连在一起:
逗号也可以用于创建字符串列表:
|
-
初始结构
第一步就是要为这个"模型机"构建一个基本结构,这是一个有效的设备树最基本的结构。在这个阶段你需要唯一的标识该机器。
首先,编译会找到
Z:\project\kernel\msm8953.dtsi文件
此段代码中"/"是根节点。compatible指定了系统的名称,它包含一个 "<制造商>,<型号>"形式的字符串。重要的是要指定一个确切的设备,并且包括制造商的名字,以避免命名空间冲突。由于操作系统会使用compatible的值来决定如何在机器上运行,所以正确的设置这个属性变得非常重要。理论上,兼容性(compatible)就是操作系统需要的所有数据都唯一的标识一个机器。本段代码中,制定了芯片的制造商是高通qcom,型号是msm8953。 |
-
*处理器节点添加
接下来就应该描述每个CPU了。先添加一个名为"cpus"的容器节点,然后为每个CPU分别添加子节点。具体到我们的情况是八核的ARM系统。
首先,在msm8953.dtsi中添加了如下代码将"msm8953-cpu.dtsi"加入进来。
………… |
每个cpu节点的compatible属性是一个"<制造商>,<型号>"形式的字符串,并制定了确切的cpu,就像顶层的compatible属性一样。 |
-
节点名称简介
每个节点必须有一个"<名称>[@设备地址]"形式的名字。<名称>就是一个不超过31位的简单ASCII字符串。通常,节点的命名应该根据它所体现的是什么样的设备,如果该节点描述的设备有一个地址的话还应该加上设备地址(unit-address)。通常,设备地址就是用来访问该设备的主地址,并且该地址也在节点的reg属性中列出。本文档中我们将在稍后涉及到reg属性。同级节点命名必须是唯一的,但只要地址不同,多个节点也可以使用一样的通用名称(例如serial@101f 1000和serial@101f 2000)。关于节点命名的更多细节请参考ePAPR规范2.2.1节。如:
节点的名称为i2c,而其地址为"78b7000" |
-
设备节点简介
系统中每个设备都表示为一个设备树节点。所以接下来就应该为这个设备树填充设备节点。现在将这些新节点设置为空。
/ { model = "Qualcomm MSM 8974"; compatible = "qcom,msm8974"; interrupt-parent = <&intc>; cpus { #size-cells = <0>; #address-cells = <1>; CPU0: cpu@0 { device_type = "cpu"; compatible = "qcom,krait"; reg = <0x0>; }; CPU1: cpu@1 { device_type = "cpu"; compatible = "qcom,krait"; reg = <0x1>; }; CPU2: cpu@2 { device_type = "cpu"; compatible = "qcom,krait"; reg = <0x2>; }; CPU3: cpu@3 { device_type = "cpu"; compatible = "qcom,krait"; reg = <0x3>; }; }; memory { secure_mem: secure_region { linux,contiguous-region; reg = <0 0xFC00000>; label = "secure_mem"; }; adsp_mem: adsp_region { linux,contiguous-region; reg = <0 0x3F00000>; label = "adsp_mem"; }; qsecom_mem: qsecom_region { linux,contiguous-region; reg = <0 0x1100000>; label = "qseecom_mem"; }; }; soc: soc { }; }; |
在此树中,已经为系统中的每个设备添加了节点,而且这个层次结构也反应了设备与系统的连接方式。例如:外部总线上的设备就是外部总线节点的子节点,i2c设备就是i2c总线节点的子节点。通常,这个层次结构表现的是CPU视角的系统视图。在这棵树中,应该注意这些事情;每个设备节点都拥有一个compatible属性。正如前面所述,节点的命名应当反映设备的类型而不是特定的型号。 |
-
compatible属性介绍
树中每个表示一个设备的节点都需要一个compatible属性。compatible属性是操作系统用来决定使用哪个设备驱动来绑定到一个设备上的关键因素。
compatible是一个字符串列表,第一个字符串指定了这个节点所表示的确切的设备,该字符串的格式为:"<制造商>,<型号>"。剩下的字符串的则表示其它与之相兼容的设备。例如:
compatible = "qcom,apq8084-sim", "qcom,apq8084", "qcom,sim";
在这里,apq8084-sim指定了确切的设备,而apq8084则说明这是与apq8084-sim的寄存器级兼容,sim则说明了是与apq8084-sim的寄存器级的兼容。
这种做法可以使现有的设备驱动能够绑定到新设备上,并仍然唯一的指定确切的设备。
-
设备地址简介
可编址设备使用以下属性将地址信息编码进设备树:
■ reg
■ #address-cells
■ #size-cells
每个可编址设备都有一个元组列表的reg:
元组的形式:reg = <地址1 长度1 [地址2 长度2] [地址3 长度3] ... >
每个元组都表示一个该设备使用的地址范围。每个地址值是一个或多个 32 位整型数列表,称为 cell。同样,长度值也可以是一个 cell 列表或者为空。
由于地址和长度字段都是可变大小的变量,那么父节点的 #address-cells 和 #size-cells 属性就用来声明各个字段的 cell 的数量。换句话说,正确解释一个 reg 属性需要用到父节点的 #address-cells 和 #size-cells 的值。要知道这一切是如何运作的,我们将给模型机添加编址属性,就从 CPU 开始。
-
CPU节点表示了一个关于编址的最简单的例子。每个CPU都分配了一个唯一的ID,并且没有CPU id相关的大小信息。
在cpu节点中,#address-cells设置为1,#size-cells设置为0.这意味着子节点的reg值是一个单一的uint32,这是一个不包含长度大小字段的地址,为CPU0分配的地址是0x0。cpu节点的#size-cells为0是因为只为每个cpu分配一个单独的地址。
你可能还会注意到reg的值和节点名字是相同的。按照惯例,如果一个节点有reg属性,那么该节点的名字就必须包含设备地址,这个设备地址就是reg属性里第一个地址值。如上cpu描述中:
cpu0的描述中:因为cpu0的描述中存在reg属性。所以对于CPU0:其节点名字为cpu,节点地址为0,即cpu@0。节点地址对应reg的属性的地址值。 |
-
内存映射设备
与cpu节点里单一地址值不同,应该分配给内存设备一个地址范围。
#size-cells声明每个子节点的reg元组中长度字段的大小。在接下来的例子中,每个地址值是2 cell,每个cell都是32位地址;每个长度为2 cell,每个cell都是32位地址,因为是2个,所以是64位编址,即64位的机器则可以使用值为2的#address-cells和#size-cells来获得在设备树中的64位编址。
reserved-memory { #address-cells = <2>; #size-cells = <2>; ranges;
other_ext_mem: other_ext_region@0 { compatible = "removed-dma-pool"; no-map; reg = <0x0 0x84a00000 0x0 0x1e00000>; };
modem_mem: modem_region@0 { compatible = "removed-dma-pool"; no-map; reg = <0x0 0x86c00000 0x0 0x6a00000>; };
adsp_fw_mem: adsp_fw_region@0 { compatible = "removed-dma-pool"; no-map; reg = <0x0 0x8d600000 0x0 0x1100000>; };
wcnss_fw_mem: wcnss_fw_region@0 { compatible = "removed-dma-pool"; no-map; reg = <0x0 0x8e700000 0x0 0x700000>; };
venus_mem: venus_region@0 { compatible = "shared-dma-pool"; reusable; alloc-ranges = <0x0 0x80000000 0x0 0x10000000>; alignment = <0 0x400000>; size = <0 0x0800000>; };
secure_mem: secure_region@0 { compatible = "shared-dma-pool"; reusable; alignment = <0 0x400000>; size = <0 0x09800000>; };
qseecom_mem: qseecom_region@0 { compatible = "shared-dma-pool"; reusable; alignment = <0 0x400000>; size = <0 0x1000000>; };
adsp_mem: adsp_region@0 { compatible = "shared-dma-pool"; reusable; size = <0 0x400000>; };
dfps_data_mem: dfps_data_mem@90000000 { reg = <0 0x90000000 0 0x1000>; label = "dfps_data_mem"; };
cont_splash_mem: splash_region@0x90001000 { reg = <0x0 0x90001000 0x0 0x13ff000>; label = "cont_splash_mem"; };
gpu_mem: gpu_region@0 { compatible = "shared-dma-pool"; reusable; alloc-ranges = <0x0 0x80000000 0x0 0x10000000>; alignment = <0 0x400000>; size = <0 0x800000>; };
boot_log_mem: boot_log_region@B0000000 { compatible = "removed-dma-pool"; no-map; reg = <0x0 0xB0000000 0x0 0x80000>; };
/* NOTE: * reserve 5M momory before other_ext_mem * It maybe need modify when modify other_ext_mem */ subsys_trap_mem: subsys_trap_region@0 { compatible = "removed-dma-pool"; no-map; reg = <0 0xB0080000 0 0x100000>; label = "subsys_trap_mem"; };
rs_recorder_mem: rs_recorder_region@0 { compatible = "removed-dma-pool"; no-map; reg = <0 0xB0180000 0 0x400000>; label = "rs_recorder_mem"; }; }; |
-
地址映射属性ranges
由于地址域是包含于一个节点及其子节点的,所以父节点可以*的定义任何对于该总线来说有意义的编址方案。那些在直接父节点和子节点以外的节点通常不关心本地地址域,而地址应该从一个域映射到另一个域。
我们前面所述的为设备分配的地址都是设备节点的本地地址,而不是CPU可用的地址,因此我们需要将这些地址映射成为CPU可使用的地址,此处,我们就需要采用地址映射的方式来完成,即Device Tree中的ranges属性。根节点描述的都是CPU视角的地址空间,而且根节点的子节点使用的是CPU的地址域,不需要直接的映射即可使用,如msmgpio: gpio@fd510000使用的就是CPU直接分配的fd510000地址。对于非根节点的直接子节点的设备节点就需要地址的映射,使其映射到CPU可访问的地址空间范围内。Ranges属性就制定了设备树中从一个地址域转换到另外一个地址域的方法。ranges 是地址转换表,其中的每个项目是一个子地址、父地址以及在子地址空间的大小的映射。映射表中的子地址、父地址分别采用子地址空间的#address- cells和父地址空间#address-cells大小。如:
#address-cells = <1>; #size-cells = <1>; ranges = <0x0 0xfec00000 0x180000>; |
子地址空间的cell为1,因此0x0 0xfec00000 0x180000的第一个cell表示的是片选地址为0,第二个cell表示片选地址0的地址空间映射到CPU的地址空间为0xfec00000,第三个cell表示的是地址空间的大小。 |
#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 |
子地址空间的#address-cells为2,父地址空间的#address-cells值为1,因此0 0 0x10100000 0x10000的前2个cell为设备节点后片选0上偏移0,第3个cell表示设备节点后片选0上偏移0的地址空间被映射到CPU的0x10100000位置,第4个cell表示映射的大小为0x10000。ranges的后面2个项目的含义可以类推。 |
-
中断属性及工作方式
与遵循树的自然结构而进行的地址转换不同,机器上的任何设备都可以发起和终止中断信号。另外地址的编址也不同于中断信号,前者是设备树的自然表示,而后者表现为独立于设备树结构的节点之间的链接。描述中断连接需要四个属性:
interrupt-controller - 一个空的属性定义该节点作为一个接收中断信号的设备。 #interrupt-cells - 这是一个中断控制器节点的属性。它声明了该中断控制器的中断指示符中 cell 的个数(类似于 #address-cells 和 #size-cells)。 interrupt-parent - 这是一个设备节点的属性,包含一个指向该设备连接的中断控制器的 phandle。那些没有 interrupt-parent 的节点则从它们的父节点中继承该属性。 interrupts - 一个设备节点属性,包含一个中断指示符的列表,对应于该设备上的每个中断输出信号。 |
中断指示符是一个或多个 cell 的数据(由 #interrupt-cells 指定),这些数据指定了该设备连接至哪些输入中断。在以下的例子中,大部分设备都只有一个输出中断,但也有可能在一个设备上有多个输出中断。一个中断指示符的意义完全取决于与中断控制器设备的 binding。每个中断控制器可以决定使用几个 cell 来唯一的定义一个输入中断。 |