目录
一、什么是设备树
用于描述一个机器或平台上所有设备信息的独立文件(一个结构体变量)
例如:一块开发板有SOC、CPU、CLOCK,SOC中又存在外设BUS,LCD等
二、DTS、DTB和DTC的关系
1.DTS 是设备树源码文件,DTB 是将DTS 编译以后得到的二进制文件。将.c 文件编译为.o 需要用到 gcc 编译器
2.DTC工具相当于gcc编译器,将.dts编译成.dtb。DTC 工具源码在 Linux 内核的 scripts/dtc 目录下。
3.DTB相当于bin文件,或可执行文件。
通过make dtbs编译所有的dts文件。如果要编译指定的dtbs
make xxx.dtb
三、DTS基本语法
1. .dtsi 头文件
设备树也有头文件,扩展名为.dtsi。可以将一款SOC的其他所有设备/平台的共有的信息提出来,作为一个通用的.dtsi文件。
2.设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设
备节点,每个节点都通过一些属性信息来描述节点信息。从/根节点开始描述设备信息,在/根节点外有一些&cpu0这样的语句是“追加“。
示例代码:
1 / {
2 aliases {
3 can0 = &flexcan1;
4 };
5
6 cpus {
7 #address-cells = <1>;
8 #size-cells = <0>;
9
10 cpu0: cpu@0 {
11 compatible = "arm,cortex-a7";
12 device_type = "cpu";
13 reg = <0>;
14 };
15 };
16
17 intc: interrupt-controller@00a01000 {
18 compatible = "arm,cortex-a7-gic";
19 #interrupt-cells = <3>;
20 interrupt-controller;
21 reg = <0x00a01000 0x1000>,
22 <0x00a02000 0x100>;
23 };
24 }
1.第 1 行, “/”是根节点,表示根,每个设备树文件只有一个根节点
2.第 2、6 和 17 行,aliases、cpus 和 intc 是三个子节点,在设备树中节点命名格式如下:
(1)node-name@unit-address
其中“node-name”是节点名字,为 ASCII 字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是 UART1 外设。“unit-address”一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,如“cpu@0”
(2)label: node-name@unit-address
上述命令并不是“node-name@unit-address”这样的格式,而是用“:”隔开成了两部分,“:”前面的是节点标签(label),“:”后面的才是节点名字。
引入 label 的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点,比如节点 “intc:interrupt-controller@00a01000”,节点 label 是 intc,而节点名字就很长了,为“interrupt-controller@00a01000”。很明显通过&intc 来访问“interrupt-controller@00a01000”这个节点要方便很多!
3. 第 10 行,cpu0 也是一个节点,只是 cpu0 是 cpus 的子节点。每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。设备树源码中常用的几种数据形式如下所示:
(1)字符串
compatible = "arm,cortex-a7";
上述代码设置 compatible 属性的值为字符串“arm,cortex-a7”。
(2)32位无符号整数
reg = <0>;
上述代码设置 reg 属性的值为 0,reg 的值也可以设置为一组值,比如:reg = <0 0x123456 100>;
(3)字符串列表
属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开,如下所示:
compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
上述代码设置属性 compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。
3.标准属性
(1)compatible 属性(匹配驱动)
compatible 属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible 属性的值是
一个字符串列表,compatible 属性用于将设备和驱动绑定起来。
格式:compatible = "manufacturer,model";
其中 manufacturer 表示厂商,model 一般是模块对应的驱动名字。
(2)model 属性
model 属性值也是一个字符串,一般 model 属性描述设备模块信息,比如名字什么的,比
如:
model = "wm8960-audio";
(3)status 属性
status 属性看名字就知道是和设备状态有关的,status 属性值也是字符串,字符串是设备的
状态信息,可选的状态如图所示:
(4)#address-cells 和#size-cells 属性
这两个属性的值都是无符号 32 位整形,#address-cells 和#size-cells 这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。
#address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),#size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。
reg 属性的格式:
reg = <address1 length1 address2 length2 address3 length3……>
每个“address length”组合表示一个地址范围,其中 address 是起始地址,length 是地址长
度aips3: aips-bus@02200000 { compatible = "fsl,aips-bus", "simple-bus"; #address-cells = <1>; #size-cells = <1>; dcp: dcp@02280000 { compatible = "fsl,imx6sl-dcp"; reg = <0x02280000 0x4000>; };
设置 aips3: aips-bus@02200000 节点#address-cells = <1>,#size-cells = <1>,说明 aips3: aips-bus@02200000 节点起始地址长度所占用的字长为 1,地址长度所占用的字长也为 1。
子节点 dcp: dcp@02280000 的 reg 属性值为<0x02280000 0x4000>,因为父节点设置了#address-cells = <1>,#size-cells = <1>,相当于设置了起始地址为 0x02280000,地址长度为 0x40000。
(5)reg 属性
reg 属性前面已经提到过了,reg 属性的值一般是(address,length)对。reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息。
(6) ranges 属性(范围)
ranges 是一个地址映射/转换表,ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成:
1.child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长。
2.parent-bus-address :父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长。
3.length :子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长。
a.如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换
示例代码:
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&gpc>;
ranges;
}
b.ranges 属性值不为空值
示例代码:
soc {
2 compatible = "simple-bus";
3 #address-cells = <1>;
4 #size-cells = <1>;
5 ranges = <0x0 0xe0000000 0x00100000>;
6
7 serial {
8 device_type = "serial";
9 compatible = "ns16550";
10 reg = <0x4600 0x100>;
11 clock-frequency = <0>;
12 interrupts = <0xA 0x8>;
13 interrupt-parent = <&ipic>;
14 };
15 };
第 5 行,节点 soc 定义的 ranges 属性,值为<0x0 0xe0000000 0x00100000>,此属性值指定了一个 1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为 0x0,父地址空间的物理起始地址为 0xe0000000。
第 10 行,serial 是串口设备节点,reg 属性定义了 serial 设备寄存器的起始地址为 0x4600,寄存器长度为 0x100。经过地址转换,serial 设备可以从 0xe0004600 开始进行读写操作0xe0004600=0x4600+0xe0000000。
(7)name 属性
name 属性值为字符串,name 属性用于记录节点名字
4. 根节点 compatible 属性(支持性)
每个节点都有 compatible 属性,根节点“/”也不例外,设备节点的 compatible 属性值是为了匹配 Linux 内核中的驱动程序,而根节点的compatible 属性值是为了查看是否支持此设备。
一般第一个值描述了所使用的硬件设备名字,第二个值描述了设备所使用的 SOC。Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。
(1)不适用设备树设备匹配方法
在没有使用设备树以前,uboot 会向 Linux 内核传递一个叫做 machine id 的值,machine id也就是设备 ID,告诉 Linux 内核自己是个什么设备,看看 Linux 内核是否支持。
Linux内核都用MACHINE_START和MACHINE_END来定义一个 machine_desc 结构体来描述这个设备。
uboot 会给 Linux 内核传递 machine id 这个参数,Linux 内核会检查这个 machineid,其实就是将 machine id 与Linux内核的主机id进行对比,看看有没有相等的,如果相等的话就表示 Linux 内核支持这个设备,如果不支持的话那么这个设备就没法启动 Linux 内核。
(2)使用设备树设备匹配方法
Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。
四、设备树在系统中的体现
Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-
tree 目录下根据节点名字创建不同文件夹,如图所示:
五、特殊节点
在根节点“/”中有两个特殊的子节点:aliases 和 chosen。
1.aliases
单词 aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目
的就是为了方便访问节点。比如SOC上有多个IIC控制器,aliases相当于给每个I2C控制器分配一个唯一的编号。如:i2c@13880000对应的alias是i2c2,那么这个编号就是2.将来可以在/dev下看到名为i2c-2的设备节点。
不过我们一般会在节点命名的时候会加上 label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点。
2.chosen
chosen 并不是一个真实的设备,主要目的就是将uboot里面bootargs环境变量值,传递给Linux内核作为命令行参数,cmd line。
uboot是如何向kernel传递bootargs?
经过查看发现chosen节点中包含bootargs属性,属性值和uboot的bootargs一致。
我们通过 bootz 命令启动 Linux 内核的时候会运行 do_bootm_linux 函数,do_bootm_linux 函数会通过一系列复杂的调用,最终通过 fdt_chosen 函数在 chosen 节点中加入了 bootargs 属性。
六、绑定信息文档
设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属性不同。那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在Linux 内核源码中有详细的.txt 文档描述了如何添加节点,这些.txt 文档叫做绑定文档。
Linux 源码目录一般为:/Documentation/devicetree/bindings如图所示:(找不到对应的文档,则咨询芯片的提供商。)
七、Linux内核的OF操作函数
1.查找节点的 OF 函数
设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux 内核使用 device_node 结构体来描述一个节点。与查找节点有关的OF 函数有 5 个:
(1)of_find_node_by_name 函数
函数原型:
通过节点名字查找指定的节点:
struct device_node *of_find_node_by_name(struct device_node *from,const char *name);
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name:要查找的节点名字。
返回值:找到的节点,如果为 NULL 表示查找失败。
(2) of_find_node_by_type 函数
函数原型:
通过 device_type 属性查找指定的节点:
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值。
返回值:找到的节点,如果为 NULL 表示查找失败。
(3)of_find_compatible_node 函数
函数原型:
根据 device_type 和 compatible 这两个属性查找指定的节点:
struct device_node *of_find_compatible_node(struct device_node *from,const char *type,
const char *compatible)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值,可以为 NULL,表示忽略掉 device_type 属性。
compatible :要查找的节点所对应的 compatible 属性列表。
返回值:找到的节点,如果为 NULL 表示查找失败
(4)of_find_matching_node_and_match 函数
函数原型:
通过 of_device_id 匹配表来查找指定的节点:
struct device_node *of_find_matching_node_and_match(struct device_node *from,const struct of_device_id *matches,const struct of_device_id **match)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
matches:of_device_id 匹配表,也就是在此匹配表里面查找节点。
match :找到的匹配的 of_device_id。
返回值:找到的节点,如果为 NULL 表示查找失败
(5)of_find_node_by_path 函数
函数原型:
通过路径来查找指定的节点:
inline struct device_node *of_find_node_by_path(const char *path)
path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径。
返回值:找到的节点,如果为 NULL 表示查找失败
2.查找父/ 子节点的 OF 函数
(1)of_get_parent 函数
函数原型:
用于获取指定节点的父节点(如果有父节点的话):
struct device_node *of_get_parent(const struct device_node *node)
node:要查找的父节点的节点。
返回值:找到的父节点。
(2)of_get_next_child 函数
函数原型:
用迭代的方式查找子节点:
struct device_node *of_get_next_child(const struct device_node *node,
struct device_node *prev)
node:父节点。
prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为
NULL,表示从第一个子节点开始。
返回值:找到的下一个子节点。
3.提取属性值的 OF 函数
(1)of_find_property 函数
函数原型:
查找指定的属性:
property *of_find_property(const struct device_node *np,const char *name,int *lenp)
np:设备节点。
name: 属性名字。
lenp:属性值的字节数
返回值:找到的属性。
(2)of_property_count_elems_of_size 函数
函数原型:
获取属性中元素的数量:
int of_property_count_elems_of_size(const struct device_node *np,const char *propname,
np:设备节点。
proname: 需要统计元素数量的属性名字。
elem_size:元素长度。
返回值:得到的属性元素数量。
(3)of_property_read_u32_index 函数
函数原型:
从属性中获取指定标号的 u32 类型数据值:
int of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index,
u32 *out_value)
np:设备节点。
proname: 要读取的属性名字。
index:要读取的值标号。
out_value:读取到的值
返回值:0 读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。
(4) of_property_read_u8_array 函数
of_property_read_u16_array 函数
of_property_read_u32_array 函数
of_property_read_u64_array 函数
函数原型:
分别是读取属性中 u8、u16、u32 和 u64 类型的数组数据:
int of_property_read_u8_array(const struct device_node *np,const char *propname,u8 *out_values,size_t sz)
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值,分别为 u8、u16、u32 和 u64。
sz :要读取的数组元素数量。
返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。
(5)of_property_read_u8 函数
of_property_read_u16 函数
of_property_read_u32 函数
of_property_read_u64 函数
函数原型:
读取这种只有一个整形值的属性:
int of_property_read_u8(const struct device_node *np,const char *propname,u8 *out_value)
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值。
返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。
(6) of_property_read_string 函数
函数原型:
读取属性中字符串值
int of_property_read_string(struct device_node *np,const char *propname,const char**out_string)
np:设备节点。
proname: 要读取的属性名字。
out_string:读取到的字符串值。
返回值:0,读取成功,负值,读取失败。
(7)of_n_addr_cells 函数
函数原型:
获取#address-cells 属性值:
int of_n_addr_cells(struct device_node *np)
np:设备节点。
返回值:获取到的#address-cells 属性值。
(8) of_n_size_cells 函数
函数原型:
获取#size-cells 属性值
int of_n_size_cells(struct device_node *np)
np:设备节点。
返回值:获取到的#size-cells 属性值。
4.其他常用的 OF 函数
(1)of_device_is_compatible 函数
函数原型:
查看节点的 compatible 属性是否有包含 compat 指定的字符串,也就是检查设备节点的兼容性:
int of_device_is_compatible(const struct device_node *device,const char *compat)
device:设备节点。
compat:要查看的字符串。
返回值:0,节点的 compatible 属性中不包含 compat 指定的字符串;正数,节点的 compatible属性中包含 compat 指定的字符串。
(2)of_get_address 函数
函数原型:
获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值:
const __be32 *of_get_address(struct device_node *dev,int index,u64 *size,unsigned int *flags)
dev:设备节点。
index:要读取的地址标号。
size:地址长度。
flags:参数,比如 IORESOURCE_IO、IORESOURCE_MEM 等
返回值:读取到的地址数据首地址,为 NULL 的话表示读取失败。
(3)of_translate_address 函数
函数原型:
将从设备树读取到的地址转换为物理地址:
u64 of_translate_address(struct device_node *dev,const __be32 *in_addr)
dev:设备节点。
in_addr:要转换的地址。
返回值:得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。
(4)of_address_to_resource 函数
函数原型:
将 reg 属性值,然后将其转换为 resource 结构体类型:
int of_address_to_resource(struct device_node *dev,int index,struct resource *r)
dev:设备节点。
index:地址资源标号。
r:得到的 resource 类型的资源值。
返回值:0,成功;负值,失败。
(5)of_iomap 函数
函数原型:
用于直接内存映射,以前我们会通过 ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址。
void __iomem *of_iomap(struct device_node *np,int index)
np:设备节点。
index:reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
返回值:经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。