一、设备树基础
1、什么是设备树
描述设备树的文件叫做DTS文件,DTS文件采用树形结构描述板级设备(开发板上的设备信息)。
在以前的linux内核中,ARM架构没有采用设备树,在内核源码中有大量的arch/arm/mach-xxx和arch/arm/plat-xxx文件夹,这些文件夹里的文件就是对应平台下的板级信息。
如果不使用设备树来描述板级设备信息,则这些信息的.h和.c文件就要被编译进linux内核,这会造成内核有太多冗余。
2、DTS、DTB和DTC
设备树源文件扩展名为.dts,编译后的二进制文件扩展名为.dtb,编译工具为DTC工具。编译.dts文件时,进入内核源码根目录,使用make dtbs来进行编译。
设置linux内核编译时编译哪个SOC的设备树文件,修改arch/arm/boot/dts/Makefile的内容,找到自己使用的SOC,然后添加自己开发板的.dtb文件。在编译时对应的.dts文件就会被编译成二进制的.dtb文件。
二、设备树语法
1、设备树头文件
设备树支持头文件,头文件扩展名为xxx.dtsi、xxx.h 和 xxx.dts。
一般.dtsi文件用于描述SOC的内部外设信息,比如CPU架构、主频、外设寄存器地址范围(如UART、I2C等)。一般开发板使用哪颗SOC,就对调用对应的.dtsi文件。
2、设备节点
设备树是采用树形结构来 描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息。设备树模板:
/ { //根节点
aliases { //一级子节点aliases
can0 = &flexcan1;
};
cpus { //一级子节点cpus
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 { //二级子节点
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
};
intc: interrupt-controller@00a01000 { //一级子节点interrupt-controller
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
};
第1行 “/” 是根节点,每个设备树文件只有一个根节点,包含的.dtsi里面也有一个根节点,这些根节点的内容会合并成一个根节点,如果合并的节点都对同一个属性赋值,则新的值会覆盖旧的值。
第2、6、17行是一级子节点的名字,设备树中节点命名格式为:node-name@unit-address ,其中“node-name”是节点名字,第17行节点名字前可以有一个标签 lable: ,使用 &lable 可以访问节点,向节点内添加属性信息。“@unit-address” 一般表示设备的地址或寄存器首地址。如果节点没有可以不要。
第10行是二级子节点。
3、标准属性
节点内都是属性,不同设备需要的属性不同,用户也可以自定义属性,有一些属性是标准属性,linux下的很多外设驱动都会使用这些标准属性。
- compatible
compatible属性叫做“兼容性”属性,它的值是一个字符串列表,作用是将设备和驱动绑定起来,compatible格式如下:
compatible = "manufacturer,model";
其中 manufacturer 表示厂商,modle 一般是模块对应的驱动名字。比如imx6ull-alientek-emmc.dts中的sound节点是音频设备节点,sound节点的compatible属性如下:
compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";
属性值有两个,其中==“fsl”表示厂商是飞思卡尔,“imx6ul-evk-wm8960”==表示驱动模块名字。sound首先使用第一个兼容值在linux内核里面查找,如果没有找到匹配驱动文件,就使用第二个兼容值进行查找。
一般驱动文件都会有一个OF匹配表,此OF匹配表保存着一些compatible值,如果compatible的属性值和OF匹配表中的任何一个值,就表示设备可以使用这个驱动。比如imx-wm8960.c中有如下内容:
static const struct of_device_id imx_wm8960_dt_ids[] = {
{ .compatible = "fsl,imx-audio-wm8960", }, //匹配值
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx_wm8960_dt_ids);
tatic struct platform_driver imx_wm8960_driver = {
.driver = {
.name = "imx-wm8960",
.pm = &snd_soc_pm_ops,
.of_match_table = imx_wm8960_dt_ids, //设置匹配表
},
.probe = imx_wm8960_probe,
.remove = imx_wm8960_remove,
};
第1行 imx_wm8960_dt_ids 就是驱动文件的匹配表,匹配值为 “fsl,imx-audio-wm8960” ,如果节点中 compatible 的值与之相匹配,则节点会使用此驱动文件。
第11行设置驱动使用的匹配表。
- model
model属性一般描述设备模块信息,比如模块名字:
model = "wm8960-audio";
-
status
status属性用来描述设备状态,状态可选如下:
值 描述 “okay” 表明设备是可操作的。 “disable” 表明设备当前是不可操作的,但在未来可变为可操作,比如热拔插以后。具体含义要看设备的绑定文件。 “fail” 表明表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作。 “fail-sss” 含义和“fail”相同,后面的 sss部分是检测到的错误内容。 -
#address-cells 和 #size-cells
这两个属性值都是无符号32位整形,作用在当前节点的子节点,用于描述子节点的地址信息。
#address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32位),#size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32位)。reg属性一般都是和地址有关,格式如下:
reg = <address1 length1 address2 length2 address3 length3…………>;
其中 address 表示起始地址,length 表示地址范围。#address-cells 和 #size-cells 使用示例如下:
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>; //0x02280000地址占一个字,0x4000地址范围占一个字
};
- reg
reg属性一般描述某个外设寄存器地址范围信息,比如 uart1 寄存器组的起始地址为 0x02020000,地址范围为 0x4000 则 uart1 节点中 reg = <0x02280000 0x4000>; 重点是获取寄存器首地址。
- ranges
ranges属性可以为空或者为 <child-bus-address,parent-bus-address,length> ,ranges 是一个地址映射表,组成部分说明如下:
child-bus-address:子总线地址空间的物理地址,由父节点的 #address-cells 确定此物理地址所占用的字长。
parent-bus-address:父总线地址空间的物理地址,由父节点的 #address-cells 确定此物理地址所占用的字长。
length:子地址空间的长度,由父节点的 #size-cells确定此地址长度所占用的字长。
如果ranges属性值为空,说明子地址空间和父地址空间完全相同,空值定义:ranges;
ranges属性值不为空示例:
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0xe0000000 0x00100000>;
serial {
device_type = "serial";
compatible = "ns16550";
reg = <0x4600 0x100>;
clock-frequency = <0>;
interrupts = <0xA 0x8>;
interrupt-parent = <&ipic>;
};
};
第5行定义了 ranges 属性,子地址空间为0x0,父地址空间为0xe0000000,子地址空间范围为1024KB(0x00100000)。
第6行,serial 是串口设备节点,reg 属性定义了设备寄存器起始地址为0x4600,经过地址映射,起始地址变为0xe0004600。
- name
name 属性用于记录节点名字,name 属性已被弃用,老的设备树文件可能会有。
- device_type
device_type 用于描述设备的 FCode,但是设备树没有 FCode,所以此属性被弃用。
4、根节点的 compatible 属性
根节点的 compatible 描述了所使用的设备,如 imx6ull-alientek-emmc.dts 中根节点的 compatible:
/ {
model = "Freescale i.MX6 ULL 14x14 EVK Board";
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
......
};
compatible 第一个值是指使用了“imx6ull-14x14-evk”这个设备,第二个值描述了设备所使用的 SOC 是“imx6ull”。linux会根据根节点的 compatible 属性查看是否支持此设备。
- 在没有使用设备树以前的设备匹配方法
U-Boot 会向 linux 内核传递一个 machine id 的值,看 linux 是否支持。linux 内核使用 MACHINE_START 和 MACHINE_END 来定义一个 machine_desc 结构体来描述这个设备,,比如在 arch/arm/mach-imx/mach-mx35_3ds.c中,将 MACHINE_START 和 MACHINE_END 之间的内容进行展开,会得到:
static const struct machine_desc __mach_desc_MX35_3DS \
__used \
__attribute__((__section__(".arch.info.init"))) = {
.nr = MACH_TYPE_MX35_3DS,
.name = "Freescale MX35PDK",
/* Maintainer: Freescale Semiconductor, Inc */
.atag_offset = 0x100,
.map_io = mx35_map_io,
.init_early = imx35_init_early,
.init_irq = mx35_init_irq,
.init_time = mx35pdk_timer_init,
.init_machine = mx35_3ds_init,
.reserve = mx35_3ds_reserve,
.restart = mxc_restart,
}
其中第4行的 “MACH_TYPE_MX35_3DS” 就是 “Freescale MX35PDK” 这个板子的 machine id ,MACH_TYPE_MX35_3DS 定义在 include/generated/mach-types.h 中,此文件定义了大量的 machine id。
- 使用设备树后的设备匹配方法
使用设备树以后不再使用 MACHINE_START ,而是使用 DT_MACHINE_START。DT_MACHINE_START 内的 .nr = ~0;因此不再使用 machine id 来查找设备。
在 arch/arm/mach-imx/mach-imx6ul.c 有如下内容:
static const char *imx6ul_dt_compat[] __initconst = { //兼容属性数组
"fsl,imx6ul",
"fsl,imx6ull",
NULL,
};
DT_MACHINE_START(IMX6UL, "Freescale i.MX6 Ultralite (Device Tree)")
.map_io = imx6ul_map_io,
.init_irq = imx6ul_init_irq,
.init_machine = imx6ul_init_machine,
.init_late = imx6ul_init_late,
.dt_compat = imx6ul_dt_compat,
MACHINE_END
只要开发板根节点的 compatible 属性和 imx6ul_dt_compat 里面的值相匹配,表示 linux 支持该设备。linux 查找匹配设备过程如下:
5、特殊节点 aliases 和 chosen
- aliases
aliases 节点的主要功能是定义节点别名,目的是为了方便访问节点,但一般使用标签来访问节点。aliases 格式如下:
aliases {
can0 = &flexcan1; //&后的一定是节点名字,不能是标签
can1 = &flexcan2;
......
};
- chosen
chosen 节点主要是为了 U-Boot 向 linux 内核传递数据,重点是 bootargs 参数,chosen 节点内容如下:
chosen {
stdout-path = &uart1;
};
可以看出 chosen 仅仅设置了标准输出使用串口1,但是进入根文件系统 /proc/device-tree/chosen 目录里面,会发现多了一个 bootargs 属性,且属性值为在 U-Boot 中设置的 bootargs 的值。
U-Boot 就是通过 chosen 节点把 bootargs 传递给 linux 内核,U-Boot 源码中有一个 fdt_chosen 函数,这个函数就是查找或创建 chosen 节点,并把 bootargs 的值写进 chosen 节点里面。具体的调用过程如下:
6、向节点追加或修改内容
向节点添加子节点,按照节点模板添加即可,注意按照父节点的 #address-cells 和 #size-cells 来确定子节点 reg 属性的值。
向某一存在的节点添加内容,使用 &节点名字或标签{ }; 来添加属性,重复的属性新的值会覆盖旧的值。
7、设备树在系统中的体现
在根文件系统的 /proc/device-tree 中保存着设备树信息。其中文件夹是子节点,文件是属性。在设备树中创建了新的节点以后,可以在根文件系统查看是否创建成功。
三、设备树常用OF操作函数
OF函数用于驱动程序获取设备树节点属性信息,这些函数原型定义在 /include/linux/of.h 文件。
1、查找节点的OF函数
获取节点信息以前,首先要查找到节点。linux 内核使用 device_node 结构体来描述一个节点,结构体定义在 /include/linux/of.h 文件中。查找节点有5个函数:
### 1)of_find_node_by_name 函数
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函数
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:要查找的节点 device_type 的属性值。
返回值:找到的节点,如果 NULL 表示查找失败。
3)of_find_compatible_node函数
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:要查找的节点 device_type 的属性值,可以为 NULL,表示忽略掉 device_type 的值。
compatible:要查找的节点 compatible 的属性值。
返回值:找到的节点,如果 NULL 表示查找失败。
4)of_find_matching_node_and_match函数
of_find_matching_node_and_match 函数通过 of_device_id 匹配表来查找指定的节点,of_device_id 结构体在 /include/linux/mod_devicetable.h 中定义,函数原型如下:
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函数
of_find_node_by_path 函数通过路径来查找指定的节点,函数原型如下:(内联函数inline)
inline struct device_node *of_find_node_by_path(const char *path)
path:带有全路径的节点名,可以使用节点的别名,比如 “/backlight” 就是 backlight 这个节点的全路径。
返回值:找到的节点,如果 NULL 表示查找失败。
2、查找父/子节点的OF函数
Linux 内核提供了几个查找节点对应的父节点或子节点的 OF 函数。
1)of_get_parent函数
of_get_parent 函数用于获取指定节点的父节点 (如果有父节点的话 ),函数原型如下:
struct device_node *of_get_parent(const struct device_node *node)
node:要查找的父节点的子节点。
返回值:找到的父节点。
3)of_get_next_child函数
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函数
linux 内核中使用结构体 property 表示属性,结构体定义在 /include/linux/of.h 中:
struct property {
char *name; /* 属性名字 */
int length; /* 属性长度 */
void *value; /* 属性值 */
struct property *next; /* 下一个属性 */
unsigned long _flags;
unsigned int unique_id;
struct bin_attribute attr;
};
1)of_find_property函数
of_find_property 函数用于查找指定的属性,函数原型如下:
property *of_find_property(const struct device_node *np, const char *name, int *lenp)
np:设备节点。
name: 属性名字。
lenp:属性值的字节数,NULL 为不指定字节数。
返回值:找到的属性。
2)of_property_count_elems_of_size函数
of_property_count_elems_of_size 函数用于获取属性中元素的数量,函数原型如下:
int of_property_count_elems_of_size(const struct device_node *np, const char *propname,int elem_size)
np:设备节点。
propname:需要统计元素数量的属性名字。
elem_size:元素数据大小,一般是 sizeof(u32)。
返回值:得到的属性元素数量。
3)of_property_read_u32_index函数
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:设备节点。
propname:需要获取元素的属性名字。
index:要读取的值的序号(0、1、2…)。
返回值:0 读取成功,负值读取失败 ,-EINVAL表示属性不存在, -ENODATA表示没有要读取的数据,
-EOVERFLOW表示属性值列表太小。
4)of_property_read_xx_array函数 (其中xx为u8、u16、u32、u64)
这四个函数分别是读取属性中的 u8、u16、u32、u64 类型的数据数组,比如可以一次性读出 reg 属性的所有数据,函数原型如下:
int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz)
int of_property_read_u16_array(const struct device_node *np, const char *propname, u16 *out_values, size_t sz)
int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz)
int of_property_read_u64_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz)
np:设备节点。
propname:需要获取元素的属性名字。
out_value:读取到的数组值。
返回值:0 读取成功,负值读取失败 ,-EINVAL表示属性不存在, -ENODATA表示没有要读取的数据,
-EOVERFLOW表示属性值列表太小。
5)of_property_read_xx函数(其中xx为u8、u16、u32、u64)
有些属性只有一个整形值,这些函数用来读取只有一个整形值的属性,函数原型如下:
int of_property_read_u8(const struct device_node *np, const char *propname, u8 *out_value)
int of_property_read_u16(const struct device_node *np, const char *propname, u16 *out_value)
int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value)
int of_property_read_u64(const struct device_node *np, const char *propname, u64 *out_value)
np:设备节点。
propname:需要获取元素的属性名字。
out_value:读取到的数据值。
返回值:0 读取成功,负值读取失败 ,-EINVAL表示属性不存在, -ENODATA表示没有要读取的数据,
-EOVERFLOW表示属性值列表太小。
6)of_property_read_string函数
of_property_read_string 函数用于读取属性中字符串值,函数原型如下:
int of_property_read_string(struct device_node *np, const char *propname, const char **out_string)
np:设备节点。
propname:需要获取元素的属性名字。
out_string:读取到的数据值。
返回值:0 读取成功,负值读取失败。
7)of_n_addr_cells函数
of_n_addr_cells 函数用于获取 #address-cells 属性值,函数原型如下:
int of_n_addr_cells(struct device_node *np)
np:设备节点。
返回值:获取到的 #address-cells 属性值。
8)of_n_size_cells函数
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函数
of_device_is_compatible 函数用于查看节点的 compatible 属性是否有包含 compat 指定的字符串,也就是检查设备节点的兼容性,函数原型如下:
int of_device_is_compatible(const struct device_node *device, const char *compat)
device:设备节点。
compat:要查看的字符串,比如 “fsl,imx6ull”。
返回值:0 节点的 compatible 属性中包含 compat 指定的字符串;其他值,节点的 compatible 属性中不包含 compat指定的字符串。
2)of_get_address函数
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函数
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函数
linux 使用 resource 结构体来描述一段内存空间,resource 结构体定义在 /include/linux/ioport.h 中:
struct resource {
resource_size_t start; //起始地址
resource_size_t end; //结束地址
const char *name; //资源名字
unsigned long flags; //资源标志位,一般表示资源类型,可选标志定义在include/linux/ioport.h中
struct resource *parent, *sibling, *child;
};
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函数
of_iomap 函数用于获取物理地址所对应的虚拟地址,可以替代 ioremap 函数,在使用设备树的驱动文件中大多使用of_iomap 函数。
of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是那一段,函数原型如下:
void __iomem *of_iomap(struct device_node *np, int index)
np:设备节点。
index:reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
返回值:经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。
reg 的属性值转换为 resource 结构体类型,函数原型如下:
int of_address_to_resource(struct device_node *dev, int index, struct resource *r)
dev:设备节点。
index:地址资源标号。
r:得到的 resource 类型的资源值。
返回值:0,成功;负值,失败。
5)of_iomap函数
of_iomap 函数用于获取物理地址所对应的虚拟地址,可以替代 ioremap 函数,在使用设备树的驱动文件中大多使用of_iomap 函数。
of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是那一段,函数原型如下:
void __iomem *of_iomap(struct device_node *np, int index)
np:设备节点。
index:reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
返回值:经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。