目录
🦋设备树的引入
在上一篇文章中讲了总线驱动模型,如下:
在总线驱动模型中,使用platform_device
结构体来描述不同类型的硬件资源:
- 在某个开发板的
board_XXX.c
文件中,定义一个或多个platform_device
结构体对象来给驱动程序提供硬件资源; - 不同开发板的硬件资源不同,所以多个开发板就会有多个
board_XXX.c
文件。
因此,如果要更换硬件资源信息,就需要修改board_XXX.c
文件,然后重新编译驱动,重新加载驱动。此时,对Linux内核来说,存在以下影响:
- 操作复杂:每次修改或添加硬件信息后,都需要重新编译和加载驱动。
- 内核冗余:每个开发板都需要一个
board_XXX.c
文件,这就导致内核中会充斥的大量的board_XXX.c
文件。
针对以上问题,Linux对总线驱动模型进行了改进——在内核中引入设备树。
设备树文件是位于linux内核之外的一个文件,用以给内核中的驱动程序指定硬件信息。设备树文件起着与board_XXX.c
文件相同的作用,但是设备树文件的优势在于:
不属于内核,不参与内核编译。
个人理解:修改设备树dts
文件后,只需要将dts
文件编译成dtb
文件即可,不需要再编译驱动程序,因为在驱动程序中,不再以头文件的形式包含board_XXX.c
文件,将驱动程序和描述硬件信息的设备树文件彻底分离开来。而驱动程序所需要的硬件信息,由内核读取设备树文件后,转化为platform_device结构体,提供给驱动程序。
🦋 设备树的语法
如上图所示,是设备树的模型,之所以叫“树”,是因为所有的设备节点都挂载在系统总线上,形成一个树状结构。其中,所有的设备节点都是“根节点”的子节点,且子节点还可以继续挂载子节点,可以无限挂载下去,所以该模型有着较强的扩展能力。
如下图所示是一个设备树的示例:
对应的dts文件如下:
🐞DTS文件布局
/{
[property definitions]
[child nodes]
};
如上所示,‘/’
表示根节点,后面紧跟一对花括号,根节点的属性和根节点下挂载的子节点在花括号内定义。
🐞node的格式
node
是设备树中的基本单元,被称为“设备节点”,其格式如下:
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
};
其中,label
是标号,可以省略,其作用是为了方便引用node
。unit-address
是设备节点的地址,也可省略。如下,是一个串口设备在设备树中的表示:
/ {
uart0: uart@fe001000 { //uart0是label, uart是node-name, fe001000是unit-adress。
compatible="ns16550";
reg=<0xfe001000 0x100>;
};
};
在根节点之外,可以使用以下2种方法来修改uart@fe001000
这个node
:
- 使用label引用node
&uart0 {
status = “disabled”; //修改status属性
};
- 使用全路径引用node
&{/uart@fe001000} {
status = “disabled”; //修改status属性
};
🐞properties的格式
在设备树中,无论是根节点还是其他设备节点,都有自己的属性,而在描述其属性时,一般都遵循以下格式:
property-name = value;
其中,value
有多种取值方式:
-
元胞数组(arrays of cells)
什么意思呢?就是用一个数组来表示value
,使用尖括号包围起来。
该数组中的每一个值都是一个32
位的数据,可以理解为值的单位就是32
位,也就是一个cell
表示一个32
位的数。当value
是一个64
位的数时,可以使用2
个cell
来表示,示例如下:interrupts = <17 0xc>; //17和0xc都是32位的数,value可以是十进制,也可以是十六进制。 clock-frequency = <0x00000001 0x00000000>; //value是64位数时,使用2个cell表示。
-
字符串
value
是一个字符串,使用双引号包围起来。示例如下:compatible = "simple-bus";
-
字节序列
value
是一个字符序列,使用中括号包围起来。示例如下:local-mac-address = [00 01 02 03 04 05];
-
组合
value
的值可以是上面三种方式的组合,用逗号隔开。示例如下:compatible = "ns16550", "ns8250"; example = <0xf00f0000 19>, "a strange property format";
🦋 设备树常用属性
- #address-cells、#size-cells
-
cell
指一个32位的数值; -
address-cells
:address
要用多少个32
位数来表示; -
sizess-cells
:size
要用多少个32
位数来表示。示例如下:
/{ #address-cells = <1>; //用一个32位数表示reg的地址,即0x80000000; #size-cells = <1>; //用一个32位数表示reg的大小, 即0x20000000; memory { reg = <0x80000000 0x20000000>; }; };
-
compatible
"compatible
"表示“兼容”。如下,对于某个LED
,内核中可能有A
、B
、C
三个驱动都支持它。led{ compatible = "A", "B", "C"; };
在内核启动时,就会为这个
LED
按这样的优先顺序为它找到驱动程序:A
、B
、C
。此外,在根节点下也有"compatible
"属性,用来选择哪一个"machine desc
":一个内核可以支持machine A
,也支持machine B
,内核启动后会根据根节点的compatible
属性找到对应的machine desc
结构体,执行其中的初始化函数。 -
model
model
属性与compatible
属性类似,但是有些差异。compatible
属性时一个字符串列表,表示硬件可以兼容A
、B
、C
等驱动;而model
用来准确定义这个硬件是什么。示例如下:{ compatible = "samsung,smdk2440", "samsung,mini2440"; model = "jz2440_v3"; };
-
status
dtsi
文件中定义了很多设备,但是在板子上某些设备是没有的,这时可以给这些设备添加一个status
属性,设置为“distabled
”,示例如下:&uart1{ status = "disabled"; };
status的常用取值如下:
value 描述 “okay” 设备正常工作 “disabled” 设备不可操作,但是后面可以恢复工作 “fail” 发生了严重错误,需修复 -
reg
reg
属性的值,是一系列"address
size
",用多少个32
位的数来表示address
和size
,由其父节点的#address-cells
、#size-cells
决定。示例如下:/ { #address-cells = <1>; #size-cells = <1>; memory { reg = <0x80000000 0x20000000>; }; };
🦋设备树常用的节点
-
根节点
dts
文件中必须有一个根节点,如下:/ { model = "SMDK24440"; compatible = "samsung,smdk2440"; #address-cells = <1>; #size-cells = <1>; };
在根节点中,必须有如下属性:
- #address-cells:在其字节点的
reg
属性中,使用多少个u32
整数来描述地址(address
)。 - size-cells:在其子节点的
reg
属性中,使用多少个u32
证书来描述大小(size
)。 - compatible:指定内核中哪个
machine_desc
可以支持本设备,即这个板子兼容哪些平台。 - model:当前是哪一个板子。
- #address-cells:在其字节点的
-
CPU节点
一般不需要我们设置,在dtsi文件中都定义好了:cpus { #address-cells = <1>; #size-cells = <0>; cpu0: cpu@0 { ....... } };
-
memory节点
芯片厂家不可能事先确定你的板子使用多大的内存,所以memory
节点需要板厂设置,比如:memory { reg = <0x80000000 0x20000000>; };
-
chosen节点
我们可以通过设备树文件给内核传入一些参数,这要在chosen
节点中设置bootargs
属性:chosen { bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200"; };
🦋编译设备树文件
我们一般情况下不会从零写dts文件,而是修改,修改之后需要将dts文件编译成二进制格式的dtb文件。如何编译呢?有两种方式:
-
在内核中直接make
首先,设置ARCH
、CROSS_COMPILE
、PATH
这三个环境变量,这三个环境变量分别表示什么意思呢?如下:ARCH
:选择编译哪一种CPU architecture
,也就是编译arch/
目录下的哪一个子目录。CROSS_COMPILE
:交叉编译器的前缀,也就是选择将代码编译成目标CPU
的指令的工具。PATH
:用于保存可以搜索的目录路径,如果待运行的程序不在当前目录,操作系统便可以去依次搜索PATH变量中记录的目录。
这三个环境变量的值如下:
ARCH=arm PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/book/100ask_imx6ull- sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
然后,在Linux源码根目录下,执行如下命令即可编译dtb文件:
make dtbs V=1 //V=1选项是为了查看编译过程中的打印信息
-
手动编译
一般情况下不推荐手动使用DTC
工具直接编译设备树文件。在Linux
内核目录下scripts/dtc
是设备树的编译工具,直接使用它的话,包含其他文件时不能使用“#include
”,而必须使用“/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
🦋内核对设备树的处理
使用设备树的目的还是为了给驱动程序提供硬件资源,那么内核是如何根据设备树文件向驱动程序提供硬件资源呢?
如上图所示,是从源代码文件dts
文件处理设备的过程:
dts
在linux
环境下被编译为dtb
文件;uboot
把dtb
文件传给内核;- 内核解析
dtb
文件,把每一个节点都转换为device_node
结构体,根节点会被保存在全局变量of_root
中,从of_root
开始可以访问到任务节点。 - 对于某些
device_node
结构体,会被转换为platform_device
结构体。
🐞哪些设备会被转换为Platform_device?
-
根节点下含有
compatile
属性的子节点;
如上所示,在根节点‘/’
下有三个子节点mytest
、i2c
、spi
,这三个子节点都有compatile
属性,所以都会转换为platform_device
结构体。📓 注意,必须是根节点的直系子节点符合该条件时才会转换。
-
含有特定
compatile
属性值的节点的子节点:compatible
= “simple-bus
”compatible
= “simple-mfd
”compatible
= “isa
”compatible
= “arm,amba-bus
”
上图的设备树文件中,节点
mytest@0
可以被转换为platform_device
,因为其父节点mytest
的compatible
属性值为simple-bus
。 -
总线
I2C
、SPI
节点下的子节点:不转换为platform_device
。
在上图所示的设备树文件中,i2c
节点会被转换为platform_device
,表示i2c
控制器;而节点at24c02
则不会被转换为platform_device
,一般是被创建为一个i2c_client
,因为其父节点是一个i2c节点。
🐞paltform_device与platform_driver配对
从设备树中转换而来的platform_device
会被注册进内核中,以后每当注册一个platform_driver
时,它们就会两两进行匹配,如果匹配成功,则会调用platform_driver
的probe
函数。匹配规则如下:
-
比较
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
中,有一个结构体:of_node
,其类型如下:
驱动程序中的platform_driver
中,有一个数组platform_driver.driver.of_match_table
,如下:
此时,使用通过如下方式来判断device
和driver
是否配对:
- 首先,如果
of_match_table
中含有compatible
值,就跟device
的compatible
属性比较,若一致则成功,否则返回失败。 - 其次,如果
of_match_table
中含有type
值,就跟dev
的device_type
属性比较,若一致则成功,否则返回失败。 - 最后,如果
of_match_table
中含有name
值,就跟dev
的name
属性比较,若一致则成功,否则返回失败。
- 首先,如果
-
比较
platform_device.name
和platform_driver.id_table[i].name
,id_table
中可能有多项。 -
比较
platform_device.name
和platform_driver.driver.name
。
platform_driver.id_table
可能为空,这时可以根据platform_driver.driver.name
来寻找同名的platform_device
。
🐞没有转换为platform_device的节点如何使用?
上面说到,所有的设备树节点都会被转换为device_node
结构体,但不是所有的device_node
都会转换为platform_device
,那么没有转换为platform_device
的节点怎么访问它们?
在linux
中提供了一些访问device_node
的接口,主要分为以下3类:
- 查找节点
/**
* @brief 根据路径找到节点
* @param path:节点路径
* @return device_node
*/
static inline struct device_node *of_find_node_by_path(const char *path);
/**
* @brief 根据名字找到节点
* @param from: 表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
* name: 节点name属性。
* @return device_node
*/
struct device_node *of_find_node_by_name(struct device_node *from,const char *name);
/**
* @brief 根据类型找到节点
* @param from: 表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
* type: 节点device_type属性。
* @return device_node
*/
struct device_node *of_find_node_by_type(struct device_node *from, const char *type);
/**
* @brief 根据compatible找到节点
* @param from: 表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
* compat: 用来指定compatible属性的值。
* type: 节点device_type属性。
* @return device_node
*/
struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compat);
/**
* @brief 根据phandle找到节点
* @param phandle : 每一个节点都有一个数字 ID,这些数字 ID 彼此不同。可以使用数字 ID 来找到 device_node。这些数字 ID 就是 phandle。
* @return device_node
*/
struct device_node *of_find_node_by_phandle(phandle handle);
/**
* @brief 找到device_node的父节点
* @param node
* @return device_node
*/
struct device_node *of_get_parent(const struct device_node *node);
/**
* @brief 找到device_node的父节点;在调用该函数后,把node节点的引用计数减少了1,这意味着不需要调用of_node_put释放node节点。
* @param node
* @return device_node
*/
struct device_node *of_get_next_parent(struct device_node *node);
/**
* @brief 取出下一个子节点;
* @param node,表示父节点;
* pre,表示上一个子节点,设为NULL时表示想找到第一个子节点。
* @return device_node
*/
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev);
/**
* @brief 取出下一个可用的子节点,有些节点的status是“disabled”,那就会跳过这些节点。
* @param node,表示父节点;
* pre,表示上一个子节点,设为NULL时表示想找到第一个子节点。
* @return device_node
*/
struct device_node *of_get_next_available_child( const struct device_node *node, struct device_node *prev);
/**
* @brief 根据名字取出子节点。
* @param node,表示父节点;
* name,表示子节点的名字。
* @return device_node
*/
struct device_node *of_get_child_by_name(const struct device_node *node, const char *name);
- 查找属性
/**
* @brief 找到节点的指定属性
* @param np,表示节点;
* name,表示要查找的属性的名字;
* lenp,用来保存这个属性值的长度。
* @return
*/
struct property *of_find_property(const struct device_node *np, const char *name, int *lenp);
- 获取属性值
/**
* @brief 根据名字找到节点的属性,并返回它的值。
* @param np,表示节点;
* name,表示要查找的属性的名字;
* lenp,用来保存这个属性值的长度。
* @return
*/
const void *of_get_property(const struct device_node *np, const char *name, int *lenp);
/**
* @brief 根据名字找到节点的属性,确定它的值有多少个元素。
* @param np,表示节点;
* propname,表示要查找的属性的名字;
* elem_size,元素的大小。
* @return
*/
int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size);
...