本文目录
设备树(Device Tree,简称 DT)是描述硬件配置信息的一种数据结构,常用于嵌入式系统,尤其是在 ARM 架构的系统中。设备树描述了系统的硬件布局,使得操作系统可以在不依赖具体硬件信息的情况下进行硬件初始化和驱动加载。
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/f59a7bc2d4ed4829b3231efc2395beca.png)
一、相关知识点
1. 为什么要引入设备树?
在讲平台总线模型的时候。平台总线模型是把驱动分成了俩个部分,一部分是device
,一部分是driver
,设备信息和驱动分离这个设计非常的好。device部分是描述硬件的。但是随着Linux文持的硬件越来越多,在内核源码下关于硬件描述的代码也越来越多。并且每修改一下就要编译一次内核。
长此以往Linux内核里面就存在了大量”垃圾代码”,而且非常多,这里说的“垃圾代码”是关于对硬件描述的代码。从长远看,这些代码对Linux内核本身并没有帮助,所以相当于Linux内核是“垃圾代码”。但是并不是说平台总线这种方法不好。
为了解决这个问题,设备树就被引入到了Linux上。使用设备树来代替之前编译进内核的device
文件,并从内核中分离了出来,成为了一个单独的部分。对硬件修改以后不必重新编译内核。直接需要将设备树文件编译成二进制文件,在通过bootloader传递给内核即可。虽然用设备树替换了原来的device部分,但是平台总线模型的匹配和使用基本不变。所以设备树就是用来描述硬件资源的文件。
2. 基本概念
dts(Device Tree Source): 设备树源码,使用一种结构化的语言来描述硬件。文件扩展名为 .dts
。
dtsi(Device Tree Source include): 用于描述一些通用的设备树文件。
dtc(Device Tree Compiler): 设备树编译器,将 .dts
文件编译成二进制设备树文件(.dtb
)。
dtb(Device Tree Blob): 设备树被编译后的二进制文件,内核在启动时加载并解析该文件。
解释:dtsi
文件的作用
假设我们有两个开发板,一个开发板上有led、beep等硬件设备,另一个开发板上有led、screen等硬件设备,那么我们就可以在dtsi
文件中写两个开发板共有的硬件设备的设备树,即led的设备树。这样我们在单独编写某个开发板的设备树时,只需要包含这个dtsi
文件,并描述自己开发板独有的硬件信息即可。
二、设备树的结构及基本语法
设备树的基本结构是由节点(nodes)和属性(properties)构成的。每个节点表示一个硬件设备或一个逻辑设备,每个节点包含若干属性,用来描述该节点的详细信息。
1. 最简设备树的结构
设备树是一棵树形结构,下述第三行的/
为根节点表示整个系统,即设备树的树根。子节点在根节点下定义和描述,每个子节点表示一个设备或设备的一部分。每个子节点有属性,属性包含该设备的具体配置信息。子节点下也可以有子子节点,以此类推。
my_tree.dts
/des-v1/; //设备树的版本,这个是必须要写的!不写的话编译会报错。
/{ //根节点
[子节点1]
[子节点2]
...
};
2. 子节点名称
子节点名称中[ ]
为可选项。[标签]
为子节点名称的别名,可以通过引用标签来引用该节点。[@<设备地址>]
主要用于区分设备,让节点名称更人性化,方便阅读,并无实际意义。
注意:同级节点名称不能相同,不同级的节点名称可以相同。
[标签]: <名称> [@<设备地址>]
3. 子节点格式
[label]: node [@unit-address ]{
[属性]
[子子节点]
};
例如:
led:goio@0{
[属性]
node-child{
};
};
4. 节点常用属性
在设备树中,节点属性用于描述硬件设备的具体信息和配置。
(1)model属性
model属性的值是一个字符串,一般用model描述一些信息。比如设备的名称、名字等。例如:model=“this is my DT”;
。该属性通常出现在设备树的根节点中,提供一个简洁的描述,帮助识别设备的具体型号和版本,并无实际意义。
/des-v1/; //设备树的版本,这个是必须要写的!不写的话编译会报错。
/{ //根节点
model=“this is my DT”;
[子节点]
...
};
(2)reg属性
指定设备在内存中的地址和大小。比如寄存器地址。
reg = <起始地址,地址长度>;
例如:reg=<0x202002, 0x40>
,描述了一端起始地址为0x202002
,地址长度为0x40
的地址信息。
●重要: reg属性为可变参,具体参数长度由另外两个属性#address-cell
和#size-cells
决定。
(3)#address-cell、#size-cells属性
该属性用于描述子节点中地址和大小信息的格式。#address-cell
用来描述reg属性中用几个数来表示起始地址,#size-cells
用来描述reg属性中用几个数来表示地址的大小。注意:描述的是其子节点中的地址格式!!
举例:
#address-cell= <1>; //reg属性中用1个数表示地址
#size-cells= <0>; //0个数表示地址大小
reg=<0x2202>; //则0x2202为地址。
#address-cell= <2>; //reg属性中用2个数表示地址
#size-cells= <2>; //2个数表示地址大小
reg=<0x00 0x2202 0x00 0x40>; //则0x00 0x2202描述地址,0x00 0x40描述地址大小。
(4)status属性
status属性是和设备的状态有关系的,status的属性值是字符串。属性值有下面几个状态可选。
属性值 | 描述 |
---|---|
okay | 设备是可用设备 |
disabled | 设备是不可用状态 |
fail | 设备是不可用状态且检测到了错误 |
(5)compatible属性
compatible属性是非常重要的一个属性。 compatible是用来和驱动进行匹配的。匹配成功以后会执行驱动中的probe函数。该属性用于指定设备的类型和兼容性信息。操作系统利用这个属性来识别和初始化设备。例如compatible = "arm,cortex-a9";
举例:compatible="qjl-beep","qjl-led";
,在匹配的时候会先使用第一个值qjl-beep进行匹配,如果没有就会使用第二个值qjl-led进行匹配。
(6)device_type属性
在某些设备树文件中,可以看到device_type
属性,device_type属性的值是字符串,只用于cpu节点或者memory节点进行描述。
作用:操作系统在启动过程中读取设备树,以识别和初始化系统中的各种硬件设备。device_type 属性可以帮助操作系统快速识别特定类型的设备,例如 CPU 和内存。
/dts-v1/;
/ {
model = "Example Board";
compatible = "example,board";
cpus {
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a9";
reg = <0>;
};
};
memory {
device_type = "memory";
reg = <0x80000000 0x4000000>;
};
};
(7)自定义属性
自定义属性可以添加到设备树的任何节点中,只要它们符合设备树语法。
例如:
memory {
device_type = "memory";
reg = <0x80000000 0x4000000>;
myselfinfo="qjl"; //自定义属性。
};
5. 特殊节点
特殊节点在设备树中通常必须作为根节点的子节点。这是设备树规范的一部分。
(1) aliases节点:定义别名
特殊节点aliases
用来统一给其他节点定义别名。定义别名的目的就是为了方便引用节点。当然,除了使用aliases来命名别用,也可以在对节点命名的适合添加标签来命名别名。
aliases{
led0 = &him_led; //&节点名称
ledl = &your_led;
led2 = &my_led;
serial0 ="/simple@fe000000/serial@llc500" //或者使用绝对路径
};
(2) chosen节点:配置启动参数
chosen节点用来uboot给内核传递参数。重点是bootargs参数。在设备树(Device Tree)中,chosen 节点用于指定系统启动时所需的一些关键参数和配置。这个节点通常包含与操作系统引导过程相关的重要信息,如根文件系统的位置、控制台设备、内核命令行参数等。
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rw";
};
三、设备树实例分析
1.GPIO控制器
(1)在GPIO控制器中,必须有一个属性gpio-controller
,表示他是GPIO控制器。
(2)在GPIO控制器中,必须有一个属性#gpio-cells
,表示其他节点如果使用这个GPIO控制器需要几个cell来描述哪一个GPIO。
●示例:
原厂GPIO控制器代码
gpio0:gpioc {
compatible="controller";
gpio-controller; //GPIO控制器
#gpio-cells =<2>; //引用该控制器时,要用2个cell描述。
}
开发者使用GPIO控制器:假设led的引脚为GPIO0_PB5 ,默认引脚为低电平。
led:gpio { //便于演示,名称随便起的。
compatible="led"; //自定
led_gpios=<&gpio0 RX_PB5 0> //使用gpio0控制器,PB5引脚(代码里为宏定义) 0表示低电平。
}
2.pinctrl 引脚复用
上一个内容中,我们只是配置了GPIO控制器,而没有确定GPIO引脚的功能,有时一个引脚可能会有多个可用功能。例如串口、IIC、GPIO等。这时我们需要将引脚复用为我们需要的功能。如果不设置,默认为GPIO功能,
●设置引脚功能的节点:将GPIO引脚设置什么功能。
rk_led {
rk_led_gpio: rk-led-gpio {
rockchip,pins = <0 RK_PB7 RK_FUNC_GPIO &pcfg_pull_none>;
/* 0: 表示引脚所在的引脚组(或引脚控制器)。
RK_PB7 : 表示具体的引脚编号。
RK_FUNC_GPIO: 表示引脚的功能,这里是配置为 GPIO。这里为宏定义,实际值为0。
&pcfg_pull_none: 表示引脚的配置参数,这里表示没有上拉或下拉电阻。
*/
};
};
●使用该功能节点。
myled: led {
compatible = "topeet,led";
gpios = <&gpio0 RK_PB7 1>; //使用gpio0控制器,PB7引脚(代码里为宏定义) 1表示高电平。
pinctrl-names = "default"; //这个属性定义了引脚控制器配置的名称,default 表示默认配置。
pinctrl-0 = <&rk_led_gpio>; //使用这个节点的配置
};
3. 中断
(1)在中断控制器(GIC)中,必须有一个属性interrupt-controller
,表示他是中断控制器。
(2)在中断控制器(GIC)中,必须有一个属性#interrupt-cells
,表示其他节点如果使用这个中断控制器需要几个cell来表示使用哪一个中断。
中断控制器的代码通常是由芯片原厂来编写的,我们用户只需要在设备树文件中使用即可。可通过原理图来查看使用的引脚,然后在源码中查找到该引脚的中断控制器是哪个。
(3)在设备树中使用中断,需要使用属性interrupt-parent=<&XXXX>
表示中断信号链接的是哪个中断控制器。XXXX为中断控制器名称。接着使用interrupts
属性来表示中断引脚和触发方式。触发方式的宏定义通常在编译源码后的include/dt-bindings/interrupt-controller
中定义。
注:interrupts里有几个cell,是由interrupt-parent对应的中断控制器里面的#interrupt-cells属性决定。
●示例:
处理器原厂代码
gpio_c:gpioc {
compatible="controller";
gpio-controller; //GPIO控制器
#gpio-cells =<2>;
interrupt-controller; //表示这是一个中断控制器。
#interrupt-cells =<2>; //其他节点引用该中断控制器时,interrupts需要有两个值来描述。
}
设备树使用中断控制器代码
beep:gpio { //便于演示,名称随便起的。
compatible="beep_controller"; //自定
interrupt-parent=<&gpio_c>; //使用gpio_c中断控制器。
interrupts=<36 1>; //中断号为36,触发方式为上升沿触发。
}
4. 时钟
绝大部分的外设工作都需要时钟,时钟一般以时钟树的形式呈现。在arm平台中可以使用设备树来描述时钟树。如时钟的结构,时钟的属性等。再由驱动来解析设备树中时钟树的信息。从而完成时钟的初始化
和使用。
在设备树中,时钟分为消费者(consumers)和生产者(providers)。时钟生产者是定义时钟控制器的基本信息。时钟消费者是使用时钟控制器的硬件模块或设备。
●生产者属性:
(1)#clock-cells
:属性代表时钟输出的路数。当#clock-cells值为0时,代表仅有1路时钟输出,当#clock-cells值大于或等于1时,代表输出多路时钟。
#clock-cells=<0>; //1路时钟输出
#clock-cells=<1>; //多路时钟输出
(2)clock-output-names
:属性定义了输出时钟的名字。
clock-output-names="gpio_clock";
(3)clock-frequency
:属性可以指定时钟频率的大小。
clock-frequency = <24000000>; // 时钟频率为:24 MHz
●消费者属性:
只需要记住clocks
属性和clock-names
属性即可。分别用来指定使用的时钟源和在消费者中时钟的名字。
●示例:
时钟生产者节点代码(时钟控制器):
clock-controller@10000000 {
compatible = "example,clock-controller";
reg = <0x10000000 0x1000>;
#clock-cells = <1>; //多路时钟
clk1: clock@0 {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <24000000>; // 24 MHz
clock-output-names = "clk1";
};
clk2: clock@1 {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <48000000>; // 48 MHz
clock-output-names = "clk2";
};
};
时钟消费者节点代码(具体模块):
uart@101f1000 {
compatible = "arm,pl011";
reg = <0x101f1000 0x1000>;
clocks = <&clk1>; //获取时钟源
clock-names = "uartclk"; //时钟源别名
};
三、DTC编译设备树文件
编译设备树:dtc -I dts -O dtb -o xxx.dtb xxx.dts
反编译设备树:dtc -I dtb -O dts -o xxx.dts xxx.dtb
●具体步骤:
-
使用命令查找源码路径下的dtc编译器,如:
sudo find /opt/Ascend310B-source-opi -name "dtc"
。如果没有找到,则编译内核源码,会生成dtc
可执行文件。
-
编写设备树文件,使用命令进行编译。注意:要使用dtc的绝对路径,或者将dtc路径设置为环境变量加入
/etc/profile
文件中。