Linux设备树学习(一)基本知识点
参考:https://blog.csdn.net/qq_35031421/article/details/105002645
一、设备树结构示例
jz2440 的设备树:
s3c2440-jz2440.dts
(include)----> s3c2440.dtsi
(include)----> s3c24xx.dtsi
(include)----> skeleton.dtsi
(include)----> s3c2440-pinctrl.dtsi
make dtbs 后的设备树:
s3c2440-jz2440.dtb
设备树是一种描述硬件的数据结构。
(1) DTS: .dts文件是设备树的源文件。
(2) DTSI:一个系列的多款开发板可能包含很多共同的部分,共同的部分一般被提炼为一个或多个.dtsi 文件。
(3) DTC: DTC是将.dts编译为.dtb的工具,相当于gcc。
(4) DTB: .dtb文件是 .dts 被 DTC 编译后的二进制格式的设备树文件,它可以被linux内核解析。
设备树像上述一层层包含的好处:
(1)在同名节点中,后出现的同种属性会覆盖前面的属性,比如,
s3c2440-jz2440.dts
(include)----> s3c2440.dtsi
这两个设备树中假如都有/uart0节点,但是status属性不同,那么会以s3c2440.dtsi的为准!!!!!!
(2)在同名节点中,不同种属性会合并到同名节点中,比如,
s3c2440-jz2440.dts有/uart0节点的status属性,s3c2440.dtsi有/uart0节点的reg属性,那么会合并为/uart0的属性!
二、节点
设备树中节点命名格式
node-name@unit-address
node-name: 设备节点的名称。节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是UART1外设;
unit-address: 一般表示设备寄存器首地址或者index。如果某个节点没有寄存器或者地址的话 “unit-address” 可以不要;
节点示例:
在上图中:多个cpu 和 ethernet依靠不同的unit-address 来分辨;可见,node-name相同的情况下,可以通过不同的unit-address定义不同的设备节点。
2.1 特殊节点
/ 根节点
没有node-name 或者 unit-address,它被定义为 /。
/aliases 子节点
aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。例如:定义 flexcan1 和 flexcan2 的别名是 can0 和 can1。
aliases {
can0 = &flexcan1;
can1 = &flexcan2;
};
/memory 子节点
所有设备树都需要一个memory设备节点,它描述了系统的物理内存布局。如果系统有多个内存块,可以创建多个memory节点,或者可以在单个memory节点的reg属性中指定这些地址范围和内存空间大小。
例如:一个64位的系统有两块内存空间:RAM1: 起始地址是0x0,地址空间是 0x80000000;RAM2: 起始地址是0x10000000,地址空间也是0x80000000;同时根节点/
下的 #address-cells = <2>和#size-cells = <2>,这个memory节点描述为:
memory@0 {
device_type = "memory";
reg = <0x00000000 0x00000000 0x00000000 0x80000000
0x00000000 0x10000000 0x00000000 0x80000000>;
};
或者:
memory@0 {
device_type = "memory";
reg = <0x00000000 0x00000000 0x00000000 0x80000000>;
};
memory@10000000 {
device_type = "memory";
reg = <0x00000000 0x10000000 0x00000000 0x80000000>;
};
/chosen 子节点
chosen 并不是一个真实的设备, 主要是为了向 Linux 内核传递数据,重点是 bootargs 参数。
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};
/cpus 和 /cpus/cpu* 子节点
/cpus节点下有1个或多个cpu子节点,
/cpus节点的 #size-cells
必须设置为0,
/cpus/cpu* 子节点中用reg属性用来标明自己是哪一个cpu。
// RK3288:
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@500 {
device_type = "cpu";
compatible = "arm,cortex-a12";
reg = <0x500>;
resets = <&cru SRST_CORE0>;
operating-points = <
/* KHz uV */
1608000 1350000
1512000 1300000
>;
#cooling-cells = <2>; /* min followed by max */
clock-latency = <40000>;
clocks = <&cru ARMCLK>;
};
cpu1: cpu@501 {
device_type = "cpu";
compatible = "arm,cortex-a12";
reg = <0x501>;
resets = <&cru SRST_CORE1>;
};
cpu2: cpu@502 {
device_type = "cpu";
compatible = "arm,cortex-a12";
reg = <0x502>;
resets = <&cru SRST_CORE2>;
};
cpu3: cpu@503 {
device_type = "cpu";
compatible = "arm,cortex-a12";
reg = <0x503>;
resets = <&cru SRST_CORE3>;
};
};
2.2 节点属性
compatible
manufacturer,model
manufacturer : 表示厂商; model : 一般是模块对应的驱动名字。设备树中的设备 与 内核中的驱动 利用该值进行绑定。
例如:
compatible = "fsl,mpc8641", "ns16550";
上面的compatible有两个属性,分别是 “fsl,mpc8641” 和 “ns16550”;设备首先会使用第一个属性值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件;如果没找到,就使用第二个属性值查找。
注:一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。
model
用于标识开发板(名字),推荐的格式是“manufacturer,model-name”
model = "Samsung S3C2416 SoC";
phandle
phandle属性为devicetree中唯一的节点指定一个数字标识符,节点中的phandle属性,它的取值必须是唯一的(不要跟其他的phandle值一样),例如:
pic@10000000 {
phandle = <1>;
interrupt-controller;
};
another-device-node {
interrupt-parent = <1>; // 使用phandle值为1来引用上述节点
};
注:DTS中的大多数设备树将不包含显式的phandle属性,当DTS被编译成二进制DTB格式时,DTC工具会自动插入phandle属性。
status
字符串,描述设备的状态信息,可选的状态如下表所示:
status值 描述
“okay” 表明设备是可操作的。
“disabled” 表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后。至于 disabled 的具体含义还要看设备的绑定文档。
“fail” 表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作。
“fail-sss” 含义和“fail”相同,后面的 sss 部分是检测到的错误内容
#address-cells 和 #size-cells
#address-cells
和 #size-cells
表明了子节点应该如何编写 reg 属性值:起始地址(address)和地址长度(size)。
#address-cells
决定了在它的子节点的reg属性中, 使用多少个u32整数来描述地址(address),
#size-cells
决定了在它的子节点的reg属性中, 使用多少个u32整数来描述大小(size)、(len)。
reg = <address1 size1 address2 size2 address3 size3 ……>
例如一个64位的处理器:
soc {
#address-cells = <2>; /*子节点的reg中,addr占64位*/
#size-cells = <1>; /*子节点的reg中,size占32位*/
serial {
compatible = "xxx";
reg = <0x4600 0x5000 0x100>; /*地址信息是64位:0x00004600 00005000,长度信息是32位:0x00000100*/
};
};
reg 属性
reg 属性的值一般是 (address, length) 对,reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息。
例如:一个设备有两个寄存器块,一个的地址是0x3000,占据32字节;另一个的地址是0xFE00,占据256字节,表示如下:
reg = <0x3000 0x20 0xFE00 0x100>;
注:上述对应#address-cells = <1>;(32位) #size-cells = <1>;(32位)
。
device_type 属性
此属性只能用于 cpu 节点或者 memory 节点。
#include "rk3288.dtsi"
/ {
memory@0 {
device_type = "memory";
/*高32 低32 高32 低32*/
reg = <0x0 0x0 0x0 0x80000000>;
};
ranges 属性(略)
ranges属性值可以为空或者按照 (child-bus-address,parent-bus-address,length) 格式编写的数字矩阵, ranges 是一个地址映射/转换表, ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成:
child-bus-address: 子总线地址空间的物理地址,由父节点的 #address-cells 确定此物理地址所占用的字长。
parent-bus-address: 父总线地址空间的物理地址,同样由父节点的 #address-cells 确定此物理地址所占用的字长。
length: 子地址空间的长度,由父节点的 #size-cells 确定此地址长度所占用的字长。
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0xe0000000 0x00100000>;
serial {
device_type = "serial";
compatible = "ns16550";
reg = <0x4600 0x100>;
clock-frequency = <0>;
interrupts = <0xA 0x8>;
interrupt-parent = <&ipic>;
};
};
soc的ranges值 <0x0 0xe0000000 0x00100000>,指定了一个 1024KB(0x00100000) 的地址范围,子地址空间的物理起始地址为 0x0,父地址空间的物理起始地址为 0xe0000000。
serial是串口设备节点, 它的reg 属性定义了 serial 设备寄存器的起始地址为 0x4600,寄存器长度为 0x100。经过ranges属性的地址转换, serial 设备可以从 0xe0004600 开始进行读写操作,0xe0004600=0x4600+0xe0000000。
name 属性(略)
name 属性值为字符串, name 属性用于记录节点名字, name 属性已经被弃用,不推荐使用name 属性,一些老的设备树文件可能会使用此属性。
2.3 引用其他节点
phandle
pic@10000000 {
phandle = <1>;
interrupt-controller;
};
another-device-node {
interrupt-parent = <1>; // 使用phandle值为1来引用上述节点
};
label
PIC: pic@10000000 {
interrupt-controller;
};
another-device-node {
interrupt-parent = <&PIC>; // 使用label来引用上述节点,
// 使用lable时实际上也是使用phandle来引用,
// 在编译dts文件为dtb文件时, 编译器dtc会在dtb中插入phandle属性
};
三、DTB格式
DTB文件主要包含四部分内容:
① struct ftd_header: 用来表明各个分部的偏移地址,整个文件的大小,版本号等;
② memory reservation block: 在设备树中使用/memreserve/ 定义的保留内存信息;
③ structure block: 保存节点的信息,节点的结构;
④ strings block: 保存属性的名字,单独作为字符串保存;
总结:
(1) DTB文件可以分为四个部分:struct ftd_header、memory reservation block、structure block、strings block;
(2) 最开始的为struct ftd_header,包含其它三个部分的偏移地址;
(3) memory reservation block记录保留内存信息;
(4) structure block保存节点的信息,节点的结构;
(5) strings block保存属性的名字,将属性名字单独作为字符串保存;
struct ftd_header结构体的定义如下:
struct fdt_header {
uint32_t magic; /*它的值为0xd00dfeed,以大端模式保存*/
uint32_t totalsize; /*整个DTB文件的大小*/
uint32_t off_dt_struct; /*structure block的偏移地址*/
uint32_t off_dt_strings; /*strings block的偏移地址*/
uint32_t off_mem_rsvmap; /*memory reservation block的偏移地址*/
uint32_t version; /*设备树版本信息*/
uint32_t last_comp_version; /*向后兼容的最低设备树版本信息*/
uint32_t boot_cpuid_phys; /*CPU ID*/
uint32_t size_dt_strings; /*strings block的大小*/
uint32_t size_dt_struct; /*structure block的大小*/
};
fdt_reserve_entry结构体如下:
该结构体用于表示memreserve的起始地址和内存空间的大小,它紧跟在struct ftd_header结构体后面。例如:/memreserve/ 0x33000000 0x10000,fdt_reserve_entry 结构体的成员 address = 0x33000000,size = 0x10000。
struct fdt_reserve_entry {
uint64_t address; /*64bit 的地址*/
uint64_t size; /*保留的内存空间的大小*/
};
structure block是用于描述设备树节点的结构,保存着节点的信息、节点的结构,它有5种标记类型:
① FDT_BEGIN_NODE (0x00000001):表示节点的开始,它的后面紧跟的是节点的名字;
② FDT_END_NODE (0x00000002):表示节点的结束;
③ FDT_PROP (0x00000003) :表示开始描述节点里面的一个属性,在FDT_PROP后面紧跟一个结构体如下所示:
struct {
uint32_t len; /*表示属性值的长度*/
uint32_t nameoff; /*属性的名字在string block的偏移*/
}
注:这个结构体后紧跟着是属性值,属性的名字保存在字符串块(Strings block)中。
④ FDT_END (0x00000009):表示structure block的结束。