首页 > 代码库 > Device Tree
Device Tree
设备树笔记
参考资料:http://www.wowotech.net/linux_kenrel/why-dt.html
一、背景
设想一下:bootloader将Linux内核复制到内存中,然后跳到内核的入口点开始执行。此时内核就像运行在处理器上的一个裸机程序。需要配置处理器,设置虚拟内存,向控制台打印一些信息。但是这些事情如何完成?所有的这些操作都要通过写寄存器来实现,但Linux内核如何知道这些寄存器的地址?如何知道当前有多少个CPU核可以使用?有多少内存可以访问?最直接的办法就是在内核代码里为指定平台写好这些代码,由内核配置参数决定哪些平台代码将被启用。但每一块ARM芯片都有自己的寄存器地址和不同的配置方式以及外设,这导致内核充斥大量垃圾代码,所以人们希望内核能以某种方式识别硬件并加载驱动,于是设备树出现了,他用于指明系统所使用的设备及相应的配置信息。
随着越来越多的芯片厂商加入ARM阵营,各个ARM 供应商的SOC 家族的CPU越来越多,不同厂商的周边硬件设备又各不相同,加之芯片供应商开发人员为了更快的开发效率,使得很多SOC 特定的代码都是通过复制现有代码并稍作修改就提交到ARM Linux。这导致
1、越来越多的ARM 平台相关的代码被加入到Linux内核, #ifdef充斥在各个源代码中,让ARM mach-和plat-目录下的代码有些不忍直视。
2、系统充斥大量重复代码。
因此,内核社区成立了一个“ARM 子架构”的团队,该团队主要负责协调各个ARM厂商的代码(not ARM core part),检查各个子架构维护者提交的代码,并建立一个ARM 平台合并树来维护这些代码。针对不同的SOC共用的IP block(知识产权块,例如I2C controller),将其驱动代码从各个arch/arm/mach-xxx中独立出来,变成通用的模块移动到kernel/drivers目录。而如clock control、interrupt control等并不是ARM特殊部分,将其驱动放在linux/kernel目录下,属于core-Linux-kernel frameworks。此外,对于ARM平台,需要保存一些和framework交互的代码,这些代码叫做ARM SoC core architecture code。总结如下:
1、ARM的核心代码仍然保存在arch/arm目录下,ARM SoC core architecture code(与系统内核交互的代码)也保存在arch/arm目录下;
2、ARM SOC的周边外设模块(如I2C控制器)的驱动保存在drivers目录下,通用的设备(如中断控制器)驱动直接集成到系统内核中;
3、ARM SOC的专有代码在arch/arm/mach-xxx目录下;
4、ARM SOC board specific的代码被移除,由设备树机制来负责传递硬件拓扑和硬件资源信息。
本质上,设备树改变了原来用hardcode方式将硬件配置信息嵌入到内核代码的方法,改用bootloader传递一个DB的形式。对于基于ARM CPU的嵌入式系统,我们习惯于针对每一个平台进行内核的编译。但是我们期望ARM能够像X86那样用一个kernel image来支持多个平台。在这种情况下,如果我们认为内核是一个黑盒,那么其输入参数应该包括:识别平台的信息、runtime的配置参数、设备的拓扑结构以及特性。在linux kernel中,设备树就是为了把上述的三个参数信息通过bootloader传递给kernel,以便kernel可以有较大的灵活性。
为一个外设写一个设备树entry(http://blog.csdn.net/klaus_wei/article/details/42915545):
1、为"compatible"赋一个字符串"magicstring",自动生成工具的生成格式一般是:名字+版本。
2、在数据手册里查看总线上设备的地址分配信息, 写一条 "reg=" 语句。
3、"interrupt-parent=<&gic>"
4、中断号 "interrupt="
5、最后加上一些设备的自定义参数
Porting操作系统到硬件平台: 1、自己撰写一个bootloader并传递适当的参数给kernel。除了传统的 command line以及tag list之类的,最重要的是申请一个machine type,当拿到属于自己项目的machine type ID的时候,当时心情雀跃,似乎自己已经是开源社区的一份子了(其实当时是有意愿,或者说有目标是想将大家的代码并入到linux kernel main line的)。 2、在内核的arch/arm目录下建立mach-xxx目录,这个目录下,放入该SOC的相关代码,例如中断 controller的代码,时间相关的代 码,内存映射,睡眠相关的代码等等。此外,最重要的是建立一个board specific文件,定义一个machine的宏: MACHINE_START(project name, "xxx公司的xxx硬件平台") 在xxx_init函数中,一般会加入很多的platformdevice。因此,伴随这个board specific文件中是大量的静态table,描述了各种硬件设备信息。 3、调通了system level的driver(timer,中断处理,clock等)以及串口terminal之后,linux kernel基本是可以起来了,后续各种driver不断的添加,直到系统软件支持所有的硬件。 |
二、设备树
2.1 概念
如果要使用Device Tree,首先用户要了解自己的硬件配置和系统运行参数,并把这些信息组织成Device Tree source file。通过DTC(Device Tree Compiler),编译为Device Tree binary file(有一个更好听的名字,DTB,device tree blob)。系统启动时被加载到内存并将起始地址传给OS内核。
另外,设备树中不用描述所有硬件信息,对于可以动态探测到的设备不必在其中描述,如USB设备、PCI 设备,但usb host controller是无法动态识别的,PCI bridge如果不能被探测,则需要在device tree中描述。对于同一系列的SOC家族,通常将公共的硬件描述保存在一个单独的dtsi文件中,方便大家include共用,省去代码的重复。
2.2 设备树源文件
2.2.1 语法
device tree的基本单元是node。这些node被组织成树状结构,除了root node,每个node都只有一个parent。一个device tree文件中只能有一个root node。每个node中包含了若干的property/value来描述该node的一些特性。在linux kernel中,扩展名是dts的文件就是描述硬件信息的device tree source file,在dts文件中,一个node被定义成:
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
}
说明:
[]表示可选项;
label方便在dts文件中引用;
每个node用节点名字(node name)标识,节点名字的格式是node-name@unit-address;@unit-address的格式和设备挂在哪个bus上相关,如cpu,其unit-address就是从0开始编址,如以太网控制器,其unit-address就是寄存器地址,如果该node没有reg属性(寻址需求属性),那么该节点名字中必须不能包括@和unit-address。root node的node name是确定的,必须是“/”。
属性(property)值标识了设备的特性,它的值(value)是多种多样的:
1、可能是空,也就是没有值的定义。
2、可能是一个u32、u64的数值(值得一提的是cell这个术语,在Device Tree表示32bit的信息单位)。例如#address-cells = <1> 。当然,可能是一个数组。例如<0x00000000 0x00000000 0x00000000 0x20000000>
3、可能是一个字符串。例如device_type = "memory" ,当然也可能是一个string list。例如"PowerPC,970"
child node的格式和node是完全一样的。
2.2.2 节点和属性
根节点/ | 节点 | 属性 | 属性说明 | 节点说明 |
| #address-cells | #是数量的意思,#address-cells属性用来描述sub node中的reg属性的地址域特性,也就是说需要用多少个u32的cell来描述该地址域。 | 如果节点中包含了有寻址需求reg的子节点,则需要定义这两个属性, | |
#size-cells | ||||
Compatible 该属性的值是string list,定义了一系列的modle(每个string是一个model)。这些字符串列表被操作系统用来选择用哪一个driver来驱动该设备。 | model属性指明了该设备属于哪个设备生产商的哪一个model。一般model赋值“生产商,模型(系列)” 。对于root node,compatible属性用来匹配machine type,对于普通的HW block的节点,如中断控制器,属性被用来匹配适合的driver。 |
| ||
interrupt-parent | 用于标识能产生中断的设备连接到哪个中断控制器 |
| ||
chosen { } | bootargs | 传递命令行参数 | 描述由系统firmware指定的runtime parameter。如果存在chosen这个node,其parent node必须是根节点。 | |
initrd-start | 传递initrd的开始地址 | |||
aliases { } |
|
| 定义了一些别名,方便引用节点时省写完整路径 | |
memory { } | device_type | 对于memory node,其device_type必须为memory。 | 是所有设备树文件的必备节点,它定义了系统物理内存的布局 | |
reg属性定义了访问该device node的地址信息, | 该属性的值被解析成任意长度的(address,size)数组, address和size在其父节点中定义(#address-cells和#size-cells)。对于device node,reg描述了memory-mapped IO register的offset和length。对于memory node,定义了该memory的起始地址和长度。 | |||
interrupt-controller @4a000000{} | #interrupt-cells | 用多少个u32(即cells)来标识一个interrupt source | 中断控制器节点,其中包含属性值。4a000000表示中断控制器寄存器的起始地址 | |
Serial @50000000{} | interrupts | 对于一个能产生中断的设备,必须定义interrupts这个属性。也可以定义interrupt-parent这个属性,如果不定义,则继承其parent node的interrupt-parent属性。 |
| |
status |
| |||
|
|
|
| |
Cpus{} |
| 对于cpus node,#address-cells 是1,而#size-cells是0。 | 对于根节点,必须有一个cpus的child node来描述系统中的CPU信息。 |
2.3 设备树二进制文件
设备树二进制文件的组织格式如下:
说明:
1 DTB header其各个成员解释如下:
header field name | description |
magic | 用来识别DTB的。通过这个magic,kernel可以确定bootloader传递的参数block是一个DTB还是tag list。 |
totalsize | DTB的total size |
off_dt_struct | device tree structure block的offset |
off_dt_strings | device tree strings block的offset |
off_mem_rsvmap | offset to memory reserve map。有些系统,我们也许会保留一些memory有特殊用途(例如DTB或者initrd image),或者在有些DSP+ARM的SOC platform上,有写memory被保留用于ARM和DSP进行信息交互。这些保留内存不会进入内存管理系统。 |
version | 该DTB的版本。 |
last_comp_version | 兼容版本信息 |
boot_cpuid_phys | 我们在哪一个CPU(用ID标识)上booting |
dt_strings_size | device tree strings block的size。和off_dt_strings一起确定了strings block在内存中的位置 |
dt_struct_size | device tree structure block的size。和和off_dt_struct一起确定了device tree structure block在内存中的位置 |
3、 memory reserve map的格式描述
这个区域包括了若干的reserve memory描述符。每个reserve memory描述符是由address和size组成。其中address和size都是用U64来描述。
4、device tree structure block的格式描述
device tree structure block区域是由若干的分片组成,每个分片开始位置都是保存了token,以此来描述该分片的属性和内容。共计有5种token:
(1)FDT_BEGIN_NODE (0x00000001)。该token描述了一个node的开始位置,紧挨着该token的就是node name(包括unit address)
(2)FDT_END_NODE (0x00000002)。该token描述了一个node的结束位置。
(3)FDT_PROP (0x00000003)。该token描述了一个property的开始位置,该token之后是两个u32的数据,分别是length和name offset。length表示该property value data的size。name offset表示该属性字符串在device tree strings block的偏移值。length和name offset之后就是长度为length具体的属性值数据。
(4)FDT_NOP (0x00000004)。
(5)FDT_END (0x00000009)。该token标识了一个DTB的结束位置。
一个可能的DTB的结构如下:
(1)若干个FDT_NOP(可选)
(2)FDT_BEGIN_NODE
node name
paddings
(3)若干属性定义。
(4)若干子节点定义。(被FDT_BEGIN_NODE和FDT_END_NODE包围)
(5)若干个FDT_NOP(可选)
(6)FDT_END_NODE
(7)FDT_END
5、device tree strings bloc的格式描述
device tree strings bloc定义了各个node中使用的属性的字符串表。由于很多属性会出现在多个node中,因此,所有的属性字符串组成了一个string block。这样可以压缩DTB的size。
2.4 设备树数据流
1、在ARM的汇编启动代码中,定义了两个变量_machine_rach_type(保存了机器类型ID)、_atags_pointer(保存了设备树/标签列表指针);
2、上电后,引导加载程序被加载到内存(ARM没有BIOS这类固件程序),在将控制权交给内核时,将设备树指针传给内核;
3、将DTB转换为树状结构,节点用一个结构体标识;
4、扫描DTB,获取chosen节点的bootargs、initrd属性的值,并保存在全局变量中,其中保存了一些系统参数;
5、内核根据机器类型ID扫描机器描述符列表(机器描述符在编译时被保存在一个特殊段,并使用一个数据结构标识),确定机器描述符。
三、代码分析
请参考http://www.wowotech.net/linux_kenrel/dt-code-analysis.html
Device Tree