目录
④#address-cells 和#size-cells 属性
一、设备树
DTS即Device Tree Source 设备树源码, Device Tree是一种描述硬件的数据结构,它起源于 OpenFirmware (OF)。设备树(Device Tree)是描述计算机的特定硬件设备信息的数据结构,以便于操作系统的内核可以管理和使用这些硬件,包括CPU或CPU,内存,总线和其他一些外设。
设备树是通过OpenFirmware (OF)项目从基于SPARC的工作站和服务器派生而来的。当前的Devicetree一般针对嵌入式系统,但仍然与某些服务器级系统一起使用。一般x86架构的个人计算机通常不使用设备树,而是依靠各种自动配置协议来识别硬件。使用设备树的系统通常将静态设备树传递给操作系统,但也可以在引导的早期阶段生成设备树。U-Boot可以在启动新操作系统时传递设备树。一些系统使用的引导加载程序可能不支持设备树,但是可以与操作系统一起安装静态设备树,Linux内核支持这种方法。
Device Tree规范目前由名为devicetree.org
的社区管理,该社区与Linaro和Arm等相关联。
优势:Linux内核从3.x版本之后开始支持使用设备树,可以实现驱动代码与设备的硬件信息相互的隔离,减少了代码中的耦合性,在此之前,一些与硬件设备相关的具体信息都要写在驱动代码中,如果外设发生相应的变化,那么驱动代码就需要改动。但是在引入了设备树之后,这种情况找到了解决的办法,通过设备树对硬件信息的抽象,驱动代码只要负责处理逻辑,而关于设备的具体信息存放到设备树文件中。如果只是硬件接口信息的变化而没有驱动逻辑的变化,开发者只需要修改设备树文件信息,不需要改写驱动代码。
二、DTS、DTB和DTC
1.DTS
硬件的相应信息都会写在.dts为后缀的文件中,每一款硬件可以单独写一份xxxx.dts
,一般在Linux
源码中存在大量的dts
文件,对于arm架构可以在arch/arm/boot/dts
找到相应的dts.
2.DTSI
对于一些相同的dts
配置可以抽象到dtsi
文件中,然后可以用include的方式
到dts
文件中,对于同一个节点的设置情况,dts
中的配置会覆盖dtsi
中的配置。具体如下图所示:
3.DTC
dtc
是编译dts
的工具,可以在Ubuntu
系统上通过指令apt-get install device-tree-compiler
安装dtc
工具。但是,一般在内核源码scripts/dtc
路径下已经包含了dtc
工具。DTS 是设备树源码文件, DTB 是将DTS 编译以后得到的二进制文件。将.c 文件编译为.o 需要用到 gcc 编译器,那么将.dts 编译为.dtb需要用到 DTC 工具。
scripts/dtc/Makefile 文件内容:
hostprogs-y := dtc
always := $(hostprogs-y)
dtc-objs:= dtc.o flattree.o fstree.o data.o livetree.o treesource.o \
srcpos.o checks.o util.o
dtc-objs += dtc-lexer.lex.o dtc-parser.tab.o
......
DTC 工具依赖于 dtc.c、 flattree.c、 fstree.c 等文件,最终编译并链接出 DTC 这个主机文件。如果要编译 DTS 文件的话只需要进入到 Linux 源码根目录下,然后执行如下命令:
make all 或者 make dtbs
make all命令是编译 Linux 源码中的所有东西,包括 zImage, .ko 驱动模块以及设备树,如果只是编译设备树的话建议使用make dtbs命令。
4.DTB
dtb(Device Tree Blob)
,dts
经过dtc
编译之后会得到dtb
文件,dtb
通过Bootloader
引导程序加载到内核。所以Bootloader
需要支持设备树才行,Kernel也需要加入设备树的支持。
如果要使用Device Tree,要了解硬件配置和系统运行参数,并把这些信息组织成Device Tree source file。 通过DTC(Device Tree Compiler),可以将Device Tree source file变成适合机器处理的 Device Tree binary file(DTB,device tree blob)。
在系统启动的时候,bootloader可以将保存在flash中的DTB 拷贝到内存并把DTB的起始地址传递给kernel,bootloader或者其他特殊功能的程序。从而使DTB文件会被保存到ROM
中,最终通过bootbolader
被加载到内核,内核就可以通过解析设备树来让驱动去控制硬件。
三、设备树基本框架
设备树用树状结构描述设备信息,它有以下几种特性:
1.根节点:\
2.设备节点:nodex
①节点名称:node
②节点地址:node@0, @后面即为地址
3.属性:属性名称(Property name)和属性值(Property value)
4.标签
上述.dts文件并没有什么真实的用途,但它基本表征了一个Device Tree源文件的结构。
注:“/”是根节点,每个设备树文件只有一个根节点。在设备树文件中会发现有的文件下也有“/”根节点,这两个“/”根节点的内容会合并成一个根节点。
Linux 内核启动的时会解析设备树中各个节点的信息,并且在根文件系统的/proc/devicetree 目录下根据节点名字创建不同文件夹。
/proc/device-tree 目录下是根节点“/”的所有属性和子节点。根节点属性属性表现为一个文件。“#address-cells”、“#size-cells”、“compatible”、“model”和“name”这 5 个文件,它们在设备树中就是根节点的 5个属性。可以通过cat查看内容刚好和设备树属性值相对应。
以目录存在的就是根节点下的子节点。
四、DTS语法
1. .dtsi头文件
与C语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。在.dts 设备树文件中,还可以通过“#include”来引用.h、 .dtsi 和.dts 文件。
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"
imx6ull.dtsi 文件描述了 I.MX6ULL 这颗 SOC 内部外设情况信息。比如 CPU 架构、主频、外设寄存器地址范围。
2.设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。
节点命名格式:
label: node-name@unit-address
label:节点标签,方便访问节点:通过&label访问节点,追加节点信息
node-name:节点名字,为字符串,描述节点功能
unit-address:设备的地址或寄存器首地址,若某个节点没有地址或者寄存器,可以省略
每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。
/ {
aliases {
can0 = &flexcan1;
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 { /*cpu0是cpus的子节点*/
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
};
intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
}
设备树源码中常用的几种数据形式
1.字符串: compatible = "arm,cortex-a7";设置 compatible 属性的值为字符串“arm,cortex-a7”
2.32位无符号整数:reg = <0>; 设置reg属性的值为0
3.字符串列表:字符串和字符串之间采用“,”隔开
compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";设置属性 compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。
3.数据类型
①字符串:compatible = "arm,cortex-a7";
②32位无符号整形:reg = <0>; 在设备树中数值基本上的u32类型的
③字符串列表:属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开
compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
//设置属性 compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”
4.属性
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性, Linux 下的很多外设驱动都会使用这些标准属性。
①compatible属性
compatible 属性也叫做兼容性属性,compatible 属性的值是一个字符串列表, compatible 属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序, compatible 属性的值格式如下所示:
"manufacturer,model"
manufacturer:厂商名称model:模块对应的驱动名字
imx6ull-alientekemmc.dts 中 sound 节点是 I.MX6U-ALPHA 开发板的音频设备节点, I.MX6U-ALPHA 开发板上的音频芯片采用的欧胜(WOLFSON)出品的 WM8960, sound 节点的 compatible 属性值如下:
compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";
属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960”,其中“fsl”表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。 sound这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查。
一般驱动程序文件会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。
注意:在根节点来说,Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。如果不支持的话那么这个设备就没法启动 Linux 内核。
②model属性
model 属性值是一个字符串,一般 model 属性描述设备模块信息。
③status属性
status 属性和设备状态有关的, status 属性值是字符串,描述设备的状态信息。
值 | 描述 |
"okay" | 表明设备是可操作的 |
"disabled" | 表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后 |
"fail" | 表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作 |
"fail-sss" | 含义和“fail”相同, sss 部分是检测到的错误内容。 |
④#address-cells 和#size-cells 属性
这两个属性的值都是无符号 32 位整形, #address-cells 和#size-cells 这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。 #address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位), #size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。
注意:子节点的地址信息描述来自于父节点的#address-cells 和#size-cells的值,而不是该节点本身的值。是用于描述子节点的地址信息。
#address-cells 和#size-cells 表明了子节点应该如何编写 reg 属性值,一般 reg 属性都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度, reg 属性的格式一为:
reg = <address1 length1 address2 length2 address3 length3……>
每个“address length”组合表示一个地址范围,其中 address 是起始地址, length 是地址长度, #address-cells 表明 address 这个数据所占用的字长, #size-cells 表明 length 这个数据所占用的字长.
spi4 {
compatible = "spi-gpio";
#address-cells = <1>;
#size-cells = <0>;
gpio_spi: gpio_spi@0 {
compatible = "fairchild,74hc595";
reg = <0>;
};
};
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>;
};
};
节点 spi4 的#address-cells = <1>, #size-cells = <0>,说明 spi4 的子节点 reg 属性中起始地址所占用的字长为 1,地址长度所占用的字长为 0。子节点 gpio_spi: gpio_spi@0 的 reg 属性值为<0>,因为父节点设置了#addresscells = <1>, #size-cells = <0>,因此 addres=0,没有 length 的值,相当于设置了起始地址,而没有设置地址长度。
设置 aips3: aips-bus@02200000 节点#address-cells = <1>, #size-cells = <1>,说明 aips3的子节点dcp起始地址长度所占用的字长为 1,地址长度所占用的字长也为 1。
⑤reg属性
reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息, reg 属性的值一般是(address, length)对.
uart1: serial@02020000 {
compatible = "fsl,imx6ul-uart",
"fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x02020000 0x4000>;
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_UART1_IPG>,
<&clks IMX6UL_CLK_UART1_SERIAL>;
clock-names = "ipg", "per";
status = "disabled";
};
uart1 的父节点 aips1: aips-bus@02000000 设置了#address-cells = <1>、 #sizecells = <1>,因此 reg 属性中 address=0x02020000, length=0x4000。UART1 寄存器首地址为 0x02020000,但是 UART1 的地址长度(范围)并没有 0x4000 这么多。
⑥ranges属性
ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字
矩阵, ranges 是一个地址映射/转换表, ranges 属性每个项目由子地址、父地址和地址空间长度
这三部分组成。
child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址
所占用的字长
parent-bus-address: 父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长。
length: 子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长
如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。
5.特殊节点
在根节点“/”中有两个特殊的子节点: aliases 和 chosen。
①aliases
在 imx6ull.dtsi 文件中, aliases 节点内容如下所示
aliases {
can0 = &flexcan1;
can1 = &flexcan2;
...
usbphy0 = &usbphy1;
usbphy1 = &usbphy2;
};
aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。但是,一般会在节点命名的时候会加上 label,然后通过&label来访问节点。
②chosen
chosen 不是一个真实的设备, chosen 节点主要是为了 uboot 向 Linux 内核传递数据(bootargs 参数)。前面了解到uboot 在启动 Linux 内核的时候会将 bootargs 的值传递给 Linux内核, bootargs 会作为 Linux 内核的命令行参数。中间就是通过chosen来完成的。
6.绑定信息文档
在Linux 内核源码中有详细的.txt 文档描述了如何添加节点,这些.txt 文档叫做绑定文档,路径为:Linux 源码目录/Documentation/devicetree/bindings。
五、OF 操作函数
设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,但是在编写驱动时如何获取到这些信息呢?
Linux 内核提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_” (称为OF 函数)。这些 OF 函数原型都定义在 include/linux/of.h 文件中。
1.查找节点
设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。 Linux 内核使用 device_node 结构体来描述一个节点:
struct device_node {
const char *name; /* 节点名字 */
const char *type; /* 设备类型 */
phandle phandle;
const char *full_name; /* 节点全名 */
struct fwnode_handle fwnode;
struct property *properties; /* 属性 */
struct property *deadprops; /* removed 属性 */
struct device_node *parent; /* 父节点 */
struct device_node *child; /* 子节点
...
}
①通过节点名字查找指定的节点:of_find_node_by_name
struct device_node *of_find_node_by_name(struct device_node *from,
const char *name);
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name:要查找的节点名字。
返回值: 找到的节点,如果为 NULL 表示查找失败。
②通过 device_type 属性查找指定的节点:of_find_node_by_type
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串, device_type 属性值。
返回值: 找到的节点,如果为 NULL 表示查找失败
③通过device_type 和 compatible两个属性查找指定的节点:of_find_compatible_node
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type,
const char *compatible)
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,device_type 属性值,可以为 NULL
compatible: 要查找的节点所对应的 compatible 属性列表。
返回值: 找到的节点,如果为 NULL 表示查找失败
④通过 of_device_id 匹配表来查找指定的节点:of_find_matching_node_and_match
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
inline struct device_node *of_find_node_by_path(const char *path)
path:设备树节点中绝对路径的节点名,可以使用节点的别名
返回值: 找到的节点,如果为 NULL 表示查找失败
2.获取属性值
节点的属性信息里面保存了驱动所需要的内容,Linux 内核中使用结构体 property 表示属性。
struct property {
char *name; /* 属性名字 */
int length; /* 属性长度 */
void *value; /* 属性值 */
struct property *next; /* 下一个属性 */
unsigned long _flags;
unsigned int unique_id;
struct bin_attribute attr;
}
①查找指定的属性:of_find_property
property *of_find_property(const struct device_node *np,
const char *name,
int *lenp)
np:设备节点。
name: 属性名字。
lenp:属性值的字节数,一般为NULL
返回值: 找到的属性。
② 获取属性中元素的数量(数组):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:设备节点。
proname: 需要统计元素数量的属性名字。
elem_size:元素长度。
返回值: 得到的属性元素数量
③从属性中获取指定标号的 u32 类型数据值:of_property_read_u32_index
int of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index,
u32 *out_value)
np:设备节点。
proname: 要读取的属性名字。
index:要读取的值标号。
out_value:读取到的值
返回值: 0 读取成功;负值: 读取失败,
-EINVAL 表示属性不存在
-ENODATA 表示没有要读取的数据,
-EOVERFLOW 表示属性值列表太小
④读取属性中 u8、 u16、 u32 和 u64 类型的数组数据
of_property_read_u8_array
of_property_read_u16_array
of_property_read_u32_array
of_property_read_u64_array
int of_property_read_u8_array(const struct device_node *np,
const char *propname,
u8 *out_values,
size_t sz)
大多数的 reg 属性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据。
np:设备节点。proname: 要读取的属性名字。
out_value:读取到的数组值,分别为 u8、 u16、 u32 和 u64。
sz: 要读取的数组元素数量。
返回值: 0:读取成功;负值: 读取失败
-EINVAL 表示属性不存在
-ENODATA 表示没有要读取的数据
-EOVERFLOW 表示属性值列表太小
⑤读取一个整形值的属性
of_property_read_u8
of_property_read_u16
of_property_read_u32
of_property_read_u64
int of_property_read_u32(const struct device_node *np,
const char *propname,
u32 *out_value)
返回值和上面一样的。
⑥读取属性中字符串值:of_property_read_string
int of_property_read_string(struct device_node *np,
const char *propname,
const char **out_string)
np:设备节点。proname: 要读取的属性名字。
out_string:读取到的字符串值。
返回值: 0,读取成功,负值,读取失败
⑦获取#address-cells 属性值:of_n_addr_cells
int of_n_addr_cells(struct device_node *np)
np:设备节点。
返回值: 获取到的#address-cells 属性值。
⑧获取#size-cells 属性值:of_size_cells
int of_n_size_cells(struct device_node *np)
np:设备节点。
返回值: 获取到的#size-cells 属性值。
⑨内存映射
of_iomap 函数用于直接内存映射,前面通过 ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址。这样就不用再去先获取reg属性值,再用属性值映射内存。
of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段, of_iomap 函数原型如下:
void __iomem *of_iomap(struct device_node *np, int index)
np:设备节点。
index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
返回值: 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。
#if 1
/* 1、寄存器地址映射 */
IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);
SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);
SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);
GPIO1_DR = ioremap(regdata[6], regdata[7]);
GPIO1_GDIR = ioremap(regdata[8], regdata[9]);
#else //第一对:起始地址+大小 -->映射 这样就不用获取reg的值
IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0);
SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);
SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);
GPIO1_DR = of_iomap(dtsled.nd, 3);
GPIO1_GDIR = of_iomap(dtsled.nd, 4);
#endif