1、设备树基本知识
1.1 什么是设备树
设备树就是用来描述硬件资源的文本文件。该文件的组织结构(语法结构)类似于一棵倒树形结构。
1.2 为什么会出现设备树
Linux内核中大量的驱动程序使用总线驱动模型,即总线、设备、设备驱动。设备(device)专门用来描述设备所占有的资源信息, 设备驱动(driver)专门用于描述驱动逻辑,驱动逻辑中需要使用资源信息时动态去device中取,从而实现了软硬件的分离。
一般device 部分的代码会放在内核源码arch/arm/plat-xxx和arch/arm/mach-xxx等板级支持包文件中。但是随着Linux内核支持的硬件(开发板)越来越多,内核中用于描述硬件的代码也就越来越多,这使得内核维护者在发布一个新内核时有大量的工作要做,而这些代码对内核本身又没有帮助(垃圾代码)。这就有了Linus Torvalds的 “this whole ARM thing is a f*cking pain in the ass”。 为了解决这个问题,设备树被引入进来用于描述硬件信息,替换掉device部分的代码。设备树文件被编译成二级制文件,系统启动时通过bootloader传递给内核。
1.3 dts文件、dtb文件是什么
名词 | 全称 | 说明 |
---|---|---|
DT | Device Tree | 设备树 |
FDT | Flattened Device Tree | 开发设备树,起源于OpenFirmware(OF) |
dts | device tree source | 设备树源码 |
dtsi | device tree source include | 通用设备树源码 |
dtb | device tree blob | 编译设备树源码得到的文件 |
dtc | device tree compiler | 设备树编译器 |
dts源码: arch/arm/boot/dts/
DTC源码: scripts/dtc/
2、设备树语法及编译设备树
2.1 如何编译设备树
语法格式:
dtc -I dts -O dtb -o xxx.dtb xxx.dts #编译设备树
dtc -I dtb -O dts -o xxx.dts xxx.dtb #反编译设备树
举例:
scripts/dtc/dtc -I dts -O dtb -o my.dtb arch/arm/boot/dts/zynq-zed.dts
scripts/dtc/dtc -I dtb -O dts -o my.dts my.dtb
make dtbs
2.2 设备树的语法
官方文档: https://www.devicetree.org/releases/
2.2.1 根节点
根节点是设备树中必须包含的节点。根节点的名字是 / 。
/dts-v1/; //表示dts文件的版本
/{ //根节点
};
2.2.2 子节点
语法格式:
[label:] node-name[@unit-address]{
[properties definitions];
[child nodes];
};
例如:
/dts-v1/;
/{
node1{
child_node1{
};
};
node2{
};
};
注意:同级节点下节点名称不能相同。
节点名称:
在对节点命名时一般要体现设备的类型,比如网卡一般命名为ethernet,串口命名为uart,一般遵循下面的命名格式:
[标签:] <名称>[@设备地址]
/dts-v1/;
/{
node1{
child_node1{
};
};
node2{
};
uart:serial@20230505{
};
};
2.2.3 节点基本属性
reg属性
reg属性可以用来描述地址信息。比如寄存器地址。语法格式如下:
reg = <address1 length1 address2 length2 ....>
#address-cells和size-cells属性
#address-cells和size-cells用来描述子节点中的reg信息中的地址和长度信息
node1{
#address-cells = <1>;
#size-cells = <0>;
node1-child{
reg = <0>; //0为地址
};
};
model属性
model属性的值是一个字符串,一般model描述一些信息。比如设备的名称等。
model = "my board";
/dts-v1/;
/{
model = "xxx board device tree";
node1{
child_node1{
};
};
node2{
};
uart:serial@20230505{
};
};
status属性
status属性和设备的状态有关系,status属性的值是字符串,有以下几个可选状态:
属性值 | 描述 |
---|---|
okay | 设备是可用状态 |
disable | 设备是不可用状态 |
fail | 设备是不可用状态并且设备检测到了错误 |
fail-sss | 设备是不可用状态并且设备检测到了错误,sss是错误信息 |
compatible属性
compatible用于和driver进行匹配,匹配成功会调用driver中的probe函数。
compatible="mybuttons", "buttons"; //匹配时先用第一个进行匹配,不成功再使用第二个值进行匹配
/dts-v1/;
/{
model = "xxx board device tree";
#address-cells = <2>;
#size-cells = <2>;
node1{
#address-cells = <1>;
#size-cells = <1>;
child_node1{
reg = <0 0>;
};
};
node2{
};
uart:serial@20230505{
reg = <20230505 1
20220808 8>;
status = "okay";
compatible = "uart", "serial";
};
};
2.2.4 特殊节点
特殊节点aliases:
特殊节点aliases用来定义别名。定义别名的目的就是为了方便引用节点。当然,除了使用aliases来命名别名,还可以在对节点命名时添加标签来命名别名。
aliases{
mmc0 = &sdmmc0;
mmc1 = &sdmmc1;
mmc2 = &sdhci;
serial0 = "/serial@20230505";
};
/dts-v1/;
/{
model = "xxx board device tree";
#address-cells = <2>;
#size-cells = <2>;
aliases{
serial0="/node1";
serial1=&uart;
};
node1{
#address-cells = <1>;
#size-cells = <1>;
child_node1{
reg = <0 0>;
};
};
node2{
};
uart:serial@20230505{
reg = <20230505 1
20220808 8>;
status = "okay";
compatible = "uart", "serial";
};
};
chosen节点:
指定uboot给内核传递的参数。注意,chosen节点必须是根节点的子节点。
chosen{
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.3 console=ttyS0, 115200";
};
2.2.5特殊属性
device_type属性
device_type属性的值是字符串,只能用于对cpu或者memory节点的描述。
/dts-v1/;
/{
model = "xxx board device tree";
#address-cells = <1>;
#size-cells = <1>;
aliases{
serial0="/node1";
serial1=&uart;
};
memory@300000{
device_type="memory";
reg =<300000 100000>;
};
cpu1:cpu@1{
device_type = "cpu1";
compatible = "arm, cortex-a53";
};
chosen{
bootargs = "root=/dev/nfs nfsroot=192.168.1.8:/opt/rootfs";
};
node1{
#address-cells = <1>;
#size-cells = <1>;
child_node1{
reg = <0 0>;
};
};
node2{
};
uart:serial@20230505{
reg = <20230505 1>;
status = "okay";
compatible = "uart", "serial";
};
};
2.2.6自定义属性
允许自定义属性。
自定义一个管脚编号的属性pinnum;
pinnum = <0 1 2 3 4>;
/dts-v1/;
/{
model = "xxx board device tree";
#address-cells = <1>;
#size-cells = <1>;
aliases{
serial0="/node1";
serial1=&uart;
};
memory@300000{
device_type="memory";
reg =<300000 100000>;
};
cpu1:cpu@1{
device_type = "cpu1";
compatible = "arm, cortex-a53";
};
chosen{
bootargs = "root=/dev/nfs nfsroot=192.168.1.8:/opt/rootfs";
};
node1{
#address-cells = <1>;
#size-cells = <1>;
child_node1{
reg = <0 0>;
};
};
node2{
pinnum=<0 1 2 3 4>;
};
uart:serial@20230505{
reg = <20230505 1>;
status = "okay";
compatible = "uart", "serial";
};
};
3、实例分析
3.1 中断
3.2 时钟
3.3 CPU
3.4 GPIO
4、设备树DTB格式及内核的使用
/dts-v1/
/{
model1 = "this is my devicetree";
#address-cells = <1>;
#size-cells = <1>;
chosen{
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};
cpu1:cpu@1{
device_type = "cpu";
compatible = "arm,cortex-a35", "arm,armv8";
reg = <0x0 0x01>;
};
aliases{
led1 = "/gpio@22030101";
};
node1{
#address-cells = <1>;
#size-cells = <1>;
gpio@22030102{
reg = <0x20220102 0x40>;
};
};
node2{
node1-child{
pinnum = <0 1 2 3 4>;
};
};
gpio@22030101{
compatible = "led";
reg =<0x20230101 0x40>;
status = "okay";
};
};
dtc -I dts -O dtb -o test.dtb test.dts
hexdump test.dtb -C | less
4.1 设备树DTB文件的格式
DTB文件格式主要分为四个部分:small header(头部),memory reservation block(内存预留块),structure block(结构块),strings block(字符串块)。free space(自由空间)不一定存在。
4.1.1 header头部
注意: 所有成员类型均为u32, 大端模式。
totalsize
表示dtb文件的大小
off_dt_struct
表示struct block(结构块)在dtb文件中的偏移地址。
size_dt_struct
表示struct block(结构块)的大小。
off_dt_strings
表示string block(字符串块)在dtb文件中的偏移地址。
size_dt_strings
表示string block(字符串块)的大小
off_mem_rsvmap
表示memory reservation block(内存预留块)在dtb文件中的偏移
version
版本号
last_comp_version
表示向后兼容的版本。
4.1.2 内存保留块
如果在dts文件中使用memreserve描述保留的内存,保留的内存大小就会在这部分保存。
4.1.3 结构块
结构块描述的是设备树的结构,也就是设备树的节点。使用0x00000001表示节点的开始,然后跟上节点的名字(根节点的名字用0表示),使用0x00000003表示 一个属性的开始,属性的名字和值使用结构体表示。
len表示属性值的长度,nameoff表示属性名字在字符串块中的偏移。使用0x00000002表示节点的结束。使用0x00000009表示根节点的结束。也就是整个结构块的结束。
4.1.4 字符串块
字符串块用来存放属性的名字,比如compatible,reg等。通过分析dtb的头部,我们已经知道了字符串块的位置。
4.2 uboot如何把设备树传递给内核
uboot将dtb文件的地址放在了X0寄存器中传递给内核。
4.3 内核是如何把设备树展开成device_node
内核会将dtb形式的设备树展开为device_node。
struct device_node {
const char *name;
const char *type;
phandle phandle;
const char *full_name;
struct property *properties; /*节点属性 (链式)*/
struct property *deadprops; /* removed properties */
struct device_node *parent;
struct device_node *child;
struct device_node *sibling; //兄弟节点
struct device_node *next; /* next device of same type */
struct device_node *allnext; /* next in list of all nodes */
struct proc_dir_entry *pde; /* this node's proc directory */
struct kref kref;
unsigned long _flags;
void *data;
};
struct property {
char *name;
int length;
void *value;
struct property *next;
unsigned long _flags;
unsigned int unique_id;
};
源码分析:
asmlinkage void __init start_kernel(void){
setup_arch(&command_line);{
void __init setup_arch(char **cmdline_p){
//__fdt_pointer ,dtb位于内存中的地址
//在head.S中, mov x21, x0 // x21=FDT
setup_machine_fdt(__fdt_pointer);{
}
*cmdline_p = boot_command_line; //记录了uboot传递给内核的参数
}
}
}
4.4 device_node树是如何转换成platform_device
平台设备(platform_device)在驱动程序中用于描述硬件使用的资源信息,所以内核最终将内核认识的device_node树转换成了platform_device。可以通过/sys/bus/platform/devices查看。
注意,并不是所有的节点都会被转换为platform_device, 被转换的节点需要满足以下规则:
-
根节点下包含compatible属性的子节点
-
节点中compatible属性包含simple-bus, simple-mfd, isa其中之一的节点下包含compatible属性的子节点
上例中node1和gpio都会被转换为plaltform_device。
-
如果节点compatible属性包含arm,primecell值,不会被转换为platform_device,而是转换为amba设备。
-
int of_platform_populate(struct device_node *root,
const struct of_device_id *matches,const struct of_dev_auxdata *lookup,
struct device *parent){
of_platform_bus_create(child, matches, lookup, parent, true){
if (of_device_is_compatible(bus, "arm,primecell")) {
/*
* Don't return an error here to keep compatibility with older
* device tree files.
*/
of_amba_device_create(bus, bus_id, platform_data, parent);
return 0;
}
of_platform_device_create_pdata(bus, bus_id, platform_data, parent);{
}
}
}
注意:device和driver匹配时, 如果使用了设备树匹配函数为of_match_table
5、设备树中的of操作函数
Linux系统中使用device_node结构体来描述一个节点,定义在linux/include/of.h文件中。
5.1 获取节点
struct device_node *of_find_node_by_name(struct device_node *from,const char *name)
from,从哪个节点开始搜索
struct device_node *of_find_node_by_path(const char *path)
path, 从根节点开始的全路径
struct device_node *of_get_parent(const struct device_node *node)
struct device_node *of_get_next_child(const struct device_node *node,
struct device_node *prev)
prev,为NULL 则从第一个子节点开始查找
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type, const char *compatible)
type, 要查找节点的device_type属性, 为NULL则忽略该属性
compatible,要查找节点的compatible属性列表
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)
5.2 获取属性
Linux中使用呢property结构体来描述属性
struct property {
char *name;
int length;
void *value;
struct property *next;
unsigned long _flags;
unsigned int unique_id;
};
struct property *of_find_property(const struct device_node *np,const char *name,int *lenp)
lenp,获取到的属性值的字节数
int of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index, u32 *out_value)
int of_property_read_u32_array(const struct device_node *np,
const char *propname, u32 *out_values,
size_t sz)
int of_property_read_string(struct device_node *np, const char *propname,
const char **out_string)
以上功能如果需要获取IORESOURCE_IRQ或者IORESOURCE_MEM类型资源时也可以考虑使用早期的
platform_get_resource
5.3 获取中断号
unsigned int irq_of_parse_and_map(struct device_node *node, int index)
static inline u32 irqd_get_trigger_type(struct irq_data *d)
注意:对于自定义属性一种方法是研究驱动源码
另外一种方法就是内核源码Documentation/devicetree/bindings/目录下有相应的帮助文档
propname,
u32 index, u32 *out_value)
int of_property_read_u32_array(const struct device_node *np,
const char *propname, u32 *out_values,
size_t sz)
int of_property_read_string(struct device_node *np, const char *propname,
const char **out_string)
以上功能如果需要获取IORESOURCE_IRQ或者IORESOURCE_MEM类型资源时也可以考虑使用早期的
```c
platform_get_resource
5.3 获取中断号
unsigned int irq_of_parse_and_map(struct device_node *node, int index)
static inline u32 irqd_get_trigger_type(struct irq_data *d)
注意:对于自定义属性一种方法是研究驱动源码
另外一种方法就是内核源码Documentation/devicetree/bindings/目录下有相应的帮助文档