1. 设备树的引入和作用
设备树实际上就是用树形结构来描述板级设备的一种方式。如下图: 树的主干就是系统总线,在系统总线上面又长出了CPU、DDR、I2C控制器、SPI控制器等等。而I2C控制器又长出了具体的设备,如AT24C02。
在没有引入设备树之前,指定驱动程序所用到的硬件资源是直接使用某个单板相关的C文件来指定的,这些对板级硬件资源描述的代码,都充斥在内核源码目录的 /arch/arm/plat-xxx 和 /arch/arm/mach-xxx 的目录下。
而基于ARM芯片做出的开发板实在是太多了,每一个开发板就对应着一个用于描述硬件资源的C文件,而且这个C文件还是要编译进内核里面的,这会导致内核代码里面充斥着大量无用的、冗余的代码,导致内核越来越庞大。
为了解决这一问题,Linux内核在后来的版本(3.x版本以上吧,没有去深究)中引入了设备树,用设备树去指定驱动程序的硬件资源,而且设备树是不用编译进内核里面的,Linux内核会动态解析设备树从而生成硬件资源相关的结构体 platform_device 。
总结:
1、设备树是为了解决内核中充斥着过多的描述板级硬件资源相关的代码而引入的
2、设备树的作用:指定驱动程序所用到的硬件资源
2. 和设备树有关的几个单词缩写
1、DTC(device tree compiler):也就是设备树所使用的编译器,一般内核源码里面提供了
2、DTS(device tree source):就是设备树的源文件
3、dtsi(device tree source include):就是设备树源文件要包含的另外一个设备树文件,所以使用 i 标识,用于区分
4、DTB(device tree blob):就是设备树源文件经过编译后生产的二进制文件
3. 设备树的语法
设备树是由一个个的节点和对应节点的属性所组成的。
3.1 设备树文件布局:
/dts-v1/; // 表示版本
[memory reservations] // 格式为: /memreserve/ <address> <length>;
/ {
[property definitions]
[child nodes]
};
/ 表示该设备树的根节点,每一个设备都有一个根节点
[property definitions]:表示一个节点的属性,属性都是用一组 key-value (键值对)来描述
[child nodes]:表示子节点
3.2 dts文件和dtsi文件
设备树一般不需要我们从零写出来,内核支持了某款芯片比如 imx6ull,在内核的 arch/arm/boot/dts 目录下就有了能用的设备树模板,一般命名为 xxxx.dtsi。“i”表示“include”,被别的文件引用的。
比如我们自己做了一款板子,里面所使用到的硬件资源和 xxx.dtsi 文件定义的硬件资源基本一致的话,那么我们可以直接把 xxx.dtsi 文件包含到我们自己的设备树文件即可。
dts和dtsi设备树文件,在语法上是完全一样的。
dts 中可以包含.h 头文件,也可以包含 dtsi 文件,在.h 头文件中可以定义一些宏。
/dts-v1/;
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"
/ {
……
};
3.3 节点的语法格式
设备树的基本单元就是一个个的节点,被称为 “node”,语法格式如下:
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
};
1、label是标号,可以省略的。其作用就是为了方便引用该节点而存在的。比如我们定义了一个uart节点:
/dts-v1/;
/ {
uart0: uart@fe001000 {
compatible="ns16550";
reg=<0xfe001000 0x100>;
};
};
我们可以使用两种方法来修改这个节点:
// 在根节点之外使用 label 引用 node:
&uart0 {
status = “disabled”;
};
// 或在根节点之外使用全路径:
&{/uart@fe001000} {
status = “disabled”;
};
2、node-name:就是节点名称,是每个节点必须要有的。
3、unit-address:表示设备的地址或者寄存器的地址。如果某个设备没有地址或者寄存器的话,就可以省略。
3.4 属性(properties)的语法格式
在设备树中,属性的语法格式有两种:
// 其中label可以省略,property-name就是属性名,value就是属性跟的值
[label:] property-name = value;
// 这种格式则只有属性名,而值为空
[label:] property-name;
其中,value的取值只有3种格式:
-
array of cells:cells指的就是一个32位的数据,array of cells则表示1个或多个32位的数据。赋值时使用 <> 括起来的,如下所示: clock-frequency = <0x00000001 0x00000000>;
-
string:即字符串。使用时要用 “” 把字符串括起来,而且还可以使用逗号分隔多个字符串。示例如下: compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
-
bytestring:字节序列,可以使用1个或多个字节赋值。使用过程中需要用 [] 括号括起来。示例如下: local-mac-address = [00 00 12 34 56 78]; // 每个 byte 使用 2 个 16 进制数来表示
3.5 常用的属性
1、#address-cells、#size-cells
**address-cells:**cell指一个32位数值,而address-cells则表示要用多少个32位数来表示一个address。
**size-cells:**size(大小) 要用多少个 32 位数来表示。
比如在设备树中表示一段内存的大小:
/ {
#address-cells = <1>;
#size-cells = <1>;
memory {
reg = <0x80000000 0x20000000>;
};
};
其中,0x80000000用来表示首地址,0x20000000用来表示内存长度。
2、compatible
设备树非常重要的一个属性,表示“兼容属性”。比如对于某个 LED,内核中可能有 A、B、C 三个驱动都支持它,那可以这样写:
led {
compatible = “A”, “B”, “C”;
};
这里的意思就是内核中的 A B C 三个驱动都支持这个led节点。
compatible 取值的形式一般是:compatible = “manufacturer,model”,即”厂家名,模块名“。
3、model
model 属性与 compatible 属性有些类似,但是有差别。
compatible 属性是一个字符串列表,表示可以你的硬件兼容 A、B、C 等驱动;
model 用来准确地定义这个硬件是什么。
比如根节点中可以这样写:
/ {
compatible = "samsung,smdk2440", "samsung,mini2440";
model = "jz2440_v3";
};
它表示这个单板,可以兼容内核中的“smdk2440”,也兼容“mini2440”。
从 compatible 属性中可以知道它兼容哪些板,但是它到底是什么板?用 model 属性来明确。
4、status
用于描述设备节点状态的。比如我们使用 include 把dtsi文件包含进了你的设备树后,dtsi文件定义的一些节点在你的板子上是没有的。这时你就可以引用那个设备节点,然后把该节点的status属性设置为 “disable”。如下:
&uart1 {
status = "disabled";
};
status的取值也是一个字符串,有以下4种取值方法: 5、reg
reg,即register,表示寄存器的意思。
在设备树中,用reg来表示一段内存空间的大小的。反正对于 ARM 系统,寄存器和内存是统一编址的,即访问寄存器时用某块地址,访问内存时用某块地址,在访问方法上没有区别。
reg 属性的值,是一系列的“address size”,用多少个 32 位的数来表示 address 和 size,由其父节点的 #address-cells、#size-cells 决定。
示例如下:
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
memory {
reg = <0x80000000 0x20000000>;
};
};
3.6 常用节点
1、根节点
每个设备树文件必须要有一个根节点
/dts-v1/;
/ {
model = "SMDK24440";
compatible = "samsung,smdk2440";
#address-cells = <1>;
#size-cells = <1>;
};
2、CPU节点
一般不需要我们设置,在 dtsi 文件中都定义好了:
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
.......
}
};
3、memory节点
芯片厂家不可能事先确定你的板子使用多大的内存,所以 memory 节点需要板厂设置,比如:
memory {
reg = <0x80000000 0x20000000>;
};
4、chosen节点
我们可以通过设备树文件给内核传入一些参数,这要在 chosen 节点中设置 bootargs 属性:
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};
5、aliases节点
aliases 即别名的意思,该节点的作用就是定义别名,定义别名的目的就是为了更方便的访问节点。如下:
aliases {
can0 = &flexcan1;
can1 = &flexcan2;
ethernet0 = &fec1;
ethernet1 = &fec2;
gpio0 = &gpio1;
gpio1 = &gpio2;
};
4. 编译、更换和调试设备树
4.1 编译设备树
设备树需要编译成 dtb 格式的二进制文件,才能加载进内存,从而给内核进行解析。编译设备树有两种方法:
1、在内核源码根目录下编译
编译之前需要设置好 ARCH、CROSS_COMPILE、PATH 这三个环境变量,然后直接在内核源码目录下输入如下命令:
make dtbs V=1 # V=1就是打印出编译过程的详细信息
make xxx.dts # 指定编译某一个 dts 设备树源文件
2、手工编译
手工编译,就是直接使用内核源码目录下的 scripts/dtc/dtc 编译工具,进行编译。需要注意的是,如果设备树文件使用了 #include 这样的语法,包含了其他文件的话,内核的dtc工具是无法编译的,它不支持这种语法。要用 /include 包含其他文件才行。
编译、反编译的示例命令如下,“-I”指定输入格式,“-O”指定输出格式,“-o”指定输出文件:
./scripts/dtc/dtc -I dts -O dtb -o tmp.dtb arch/arm/boot/dts/xxx.dts // 编译 dts 为 dtb
./scripts/dtc/dtc -I dtb -O dts -o tmp.dts arch/arm/boot/dts/xxx.dtb // 反编译 dtb 为 dts
4.2 给开发板更换设备树
编译好设备树文件之后,我们就可以把生成的 dtb 文件烧写到开发板,给开发板更换设备树了。
我们如何确定哪个设备树文件对应自己的开发板?一般都会有开发板的介绍手册的。
如何烧写设备树到开发板?可以把设备树文件拷贝到板子的 /boot 目录下(不同的板子该目录可能不同,不过一般都在这个目录下),替换原来的设备树,然后重启板子。
或者我们可以使用一些烧写工具(原厂会提供),直接烧写到开发板也可以。
4.3 查看内核中的设备树
内核中的设备树在 /sys/firmware 目录下。输入如下命令:
# ls /sys/firmware/
devicetree fdt
可以看到列出了两个文件夹。
/sys/firmware/devicetree 目录下是以目录结构程现的 dtb 文件, 根节点对应 base 目录, 每一个节点对应一个目录, 每一个属性对应一个文件。这些属性的值如果是字符串,可以使用 cat 命令把它打印出来;对于数值,可以用 hexdump 把它打印出来。
还可以看到/sys/firmware/fdt 文件,它就是 dtb 格式的设备树文件,可以把它复制出来放到 ubuntu上,执行下面的命令反编译出来(-I dtb:输入格式是 dtb,-O dts:输出格式是 dts):
cd 板子所用的内核源码目录
./scripts/dtc/dtc -I dtb -O dts /从板子上/复制出来的/fdt -o tmp.dts
如何确定我们确实已经更换了设备树?
我们可以在设备树文件的根节点下定义一个测试节点:
/ {
test_node {
xxx_test = "hello";
};
};
然后编译已经修改了的设备树文件,更换到开发板后,重启板子。进入到 /sys/firmware/devicetree/base 目录下,查找一下是不是有一个文件夹名称和我们刚刚写的测试节点名称一样,如果有那么说明设备树文件确实替换了。
5. 内核对设备树的处理
总体处理流程:
从源代码 dts 文件开始,设备树的处理过程为:
- dts 在 PC 机上被编译为 dtb 文件;
- u-boot 把 dtb 文件传给内核;
- 内核解析 dtb 文件,把每一个节点都转换为 device_node 结构体;
- 对于某些 device_node 结构体,会被转换为 platform_device 结构体。
其中,根节点会被保存在内核全局变量的 of_root 中,从 of_root 开始就可以访问到任意的设备树节点了。
5.1 哪些设备树节点会被转换为 platform_device
-
根节点下含有 compatile 属性的子节点 -
含有特定 compatile 属性的节点的子节点 如 果 一 个 节 点 的compatile属 性 , 它 的 值 是 这4者 之 一 : “simple-bus”,“simple-mfd”,“isa”,“arm,amba-bus”,那么它的子结点(需含 compatile 属性)也可以转换为platform_device。 -
总线 I2C、SPI 节点下的子节点:不转换为 platform_device。 某个总线下到子节点,应该交给对应的总线驱动程序来处理, 它们不应该被转换为 platform_device。
例如:
/ {
mytest {
compatile = "mytest", "simple-bus";
mytest@0 {
compatile = "mytest_0";
};
};
i2c {
compatile = "samsung,i2c";
at24c02 {
compatile = "at24c02";
};
};
spi {
compatile = "samsung,spi";
flash@0 {
compatible = "winbond,w25q32dw";
spi-max-frequency = <25000000>;
reg = <0>;
};
};
};
1、上述的 /mytest 节点可以转换为 platform_device ,因为它含有compatile属性。而它的子节点/mytest/mytest@0 也会被转换为 platform_device ,因为它的父节点是 “simple-bus” 属性的。
2、/i2c 节点一般用来表示i2c控制器,可以转换为 platform_device 。但是它的子节点/i2c/at24c02不可以转换为 platform_device ,因为它的父节点没有声明是那4种属性之一。它被如何处理完全由父节点的 platform_driver 决定, 一般是被创建为一个 i2c_client。
3、/spi 节点一般用来表示spi控制器,可以被转换为 platform_device 。而他的子节点 /spi/flash@0 同样不会被转换为 platform_device, 它被如何处理完全由父节点的 platform_driver 决定, 一般是被创建为一个 spi_device。
5.2 设备节点怎么转换为 platform_device
这里只给出结论,内核处理设备树的函数调用过程不分析。
- platform_device 中含有 resource 数组, 它来自 device_node 的 reg, interrupts 属性;
- platform_device.dev.of_node 指向 device_node, 可以通过它获得其他属性
5.3 platform_device 如何与 platform_driver 配对
从设备树转换得来的 platform_device 会被注册进内核里,以后当我们每注册一个 platform_driver 时,它们就会两两确定能否配对,如果能配对成功就调用 platform_driver 的 probe 函数。
内核源码中有一个 platform_match 函数,该函数就是实现了 platform_device 与 platform_driver 的配对规则。该函数源码如下图: 配对规则主要有4步:
-
最先比较:是否强制选择某个 driver 。比较 platform_device.driver_override 和 platform_driver.driver.name 。可以设置 platform_device 的 driver_override成员,强制选择某个 platform_driver。 -
然后比较:设备树信息。比较:platform_device.dev.of_node 和 platform_driver.driver.of_match_table。 -
接下来比较:platform_device_id。比较 platform_device. name 和 platform_driver.id_table[i].name,id_table 中可能有多项。 -
最后比较:platform_device.name 和 platform_driver.driver.name。
5.4 没有转换为 platform_device 的节点,如何使用
任意驱动程序里,都可以直接访问设备树。就算没有被转换为 platform_device 的设备节点,我们也可以使用内核提供的 of 函数来访问设备节点,of 是 open firmware 的缩写,表示开放固件的意思。
这些 of 函数都可以在内核源码的 include/linux/of.h 头文件中找到。比如下面一些我们可能用到的函数:
找到节点:
1、of_find_node_by_name:通过设备节点的名字查找指定的节点
2、of_find_node_by_type :通过 device_type 属性查找指定的节点
3、of_find_compatible_node :通过 compatible 属性查找指定的节点
4、of_find_node_by_path :通过节点路径找到指定节点。比如 “/memory” 节点
找到属性:
1、of_find_property ,函数原型如下:
extern struct property *of_find_property(const struct device_node *np, const char *name, int *lenp);
参数 np 表示某个节点, name表示属性名字,lenp用来保存这个属性的长度。
比如下面一个节点:
xxx_node {
xxx_pp_name = “hello”;
};
xxx_pp_name 就是该节点属性的名字,值的长度就是6字节。
获取属性:
1、of_get_property :根据名字找到节点的属性,并且返回它的值。
2、of_property_count_elems_of_size :根据名字找到节点的属性,确定它的值有多少个元素(elem)。
6. 怎么修改设备树文件
一个写得好的驱动程序,它会尽量确定所用到的硬件资源。只有那些确实确定不了的硬件资源才会让设备树来指定。
开发人员会根据原理图确定"驱动程序无法确定的硬件资源", 再在设备树文件中填写对应内容。
那么, 所填写设备树内容的格式是什么?我们如何写?
1、使用芯片厂家提供的工具
有些芯片,厂家提供了对应的设备树生成工具,可以选择某个引脚用于某些功能,就可以自动生成设备树节点。然后我们再把这些节点复制到内核的设备树文件里即可。
2、看绑定文档
内核文档 Documentation/devicetree/bindings/
做得好的厂家也会提供设备树的说明文档
3、参考同类型单板的设备树文件
4、网上搜索
4、网上搜索
5、实在没办法时, 只能去研究驱动源码
|