设备树
驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中。许多硬件设备信息可以直
接通过它传递给 Linux,而不需要在内核中堆积大量的冗余代码。
设备树,将这个词分开就是“设备”和“树”,描述设备树的文件叫做 DTS(Device Tree Source),这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的硬件设备信息,比如CPU 数量、内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等。
设备树文件的扩展名为 .dts,一个 .dts(device tree source)就文件对应一个开发板。该文件放置在内核的"arch/arm/boot/dts/"目录下,zynq开发板的板级设备树文件就是arch/arm/boot/dts/system-top.dts。上述文件是使用hsi命令生成的,除了system-top.dts文件还生成了另外三个文件pl.dtsi,pcw.dtsi,zynq-7000.dtsi。
pl.dtsi 在 vivado 当中添加的 pl 端外设对应的配置信息
pcw.dtsi 在 vivado 当中已经使能的 PS 外设
zynq- 7000.dtsi zynq-7000 系列处理器相同的硬件外设配置信息(PS 端的)
部分文件后缀讲解
dts
设备树的源文件的后缀名就是.dts,每一款硬件平台可以单独写一份该后缀的文件。
dtsi
值得一提的是,对于一些相同的 dts 配置可以抽象到 dtsi 文件中,这个 dtsi 文件其实就类似于 C 语言当中的.h 头文件,可以通过 C 语言中使用 include 来包含一个.dtsi 文件。
dtc
dtc 其实就是 device-tree-compiler,那就是设备文件.dts 的编译器。
xilinx_zynq_defconfig(arch/arm/configs/xilinx_zynq_defconfig)文件就是我们 zynq平台对应的 defconfig 配置文件。
dtb
该文件是将dts文件编译后生成的二进制文件。
设备树结构
设备树用树状结构描述设备信息,组成设备树的基本单元是 node(设备节点),这些
node 被组织成树状结构,有如下一些特征:
➢ 一个 device tree 文件中只有一个 root node(根节点);
➢ 除了 root node,每个 node 都只有一个 parent node(父节点);
➢ 一般来说,开发板上的每一个设备都能够对应到设备树中的一个 node;
➢ 每个 node 中包含了若干的 property-value(键-值对,当然也可以没有 value)来描述该 node 的一些
特性;
➢ 每个 node 都有自己的 node name(节点名字);
➢ node 之间可以是平行关系,也可以嵌套成父子关系,这样就可以很方便的描述设备间的关系;
/{ // 根节点
node1{ // node1 节点
property1=value1; // node1 节点的属性 property1
property2=value2; // node1 节点的属性 property2
...
};
node2{ // node2 节点
property3=value3; // node2 节点的属性 property3
...
node3{ // node2 的子节点 node3
property4=value4; // node3 节点的属性 property4
...
};
};
}
节点与属性
[label:]node-name[@unit-address] {
[properties definitions]
[child nodes]
};
label 是方便在dts文件中被其他节点引用;
node-name 为节点名称;
unit-address一般表示设备的地址或寄存器基地址;
设备树中值的属性
1)字符串
compatible = "arm,cortex-a9";
字符串使用双引号括起来,例如上面的这个 compatible 属性的值是” arm,cortex-a9” 字符串。
2)32 位无符号整形数据
clock-latency = <1000>;
reg = <0x00000000 0x00500000>;
32 位无符号整形数据使用尖括号括起来,例如属性 cock-latency 的值是一个 32 位无符 号整形数据 1000,而 reg 属性有两个数据,使用空格隔开,那么这个就可以认为是一个数组
3)二进制数据
local-mac-address = [00 0a 35 00 1e 53];
二进制数据使用方括号括起来,例如上面这个就是一个二进制数据组成的数组。
4)字符串数组
compatible = "n25q512a","micron,m25p80";
属性值也可以使用字符串列表,例如上面的这个属性,它的值是一个字符串列表,字符串之间使用逗号分割;
5)混合值
mixed-property = "a string", [0x01 0x23 0x45 0x67], <0x12345678>;
除此之外不同的数据类型还可以混合在一起,以逗号分隔。
6)节点引用
clocks = <&clkc 3>;
这其实就是我们上面说到的引用节点的一种形式,”&clkc”就表示引用”clkc”这个节点,而 clkc 就是前面提到的”label”,引用节点也是使用尖括号来表示,关于节点之间的引用,我们后面还会再讲,这里先告一段落。
标准属性
compatible属性
compatible 属性也叫做“兼容性”属性,一般该字符串使用“<制造商>,<型号>”这样的形式进行命名。compatible属性用于将设备和驱动绑定起来,然后会按照每个兼容值在Linux内核里查找。
补充:一般驱动程序文件都会有一个OF匹配表,此OF匹配表保存着一些compatible值,如果设备树中的节点的compatible属性值和OF匹配表中的任何一个值相等,则表明设备可以使用该驱动。
model属性
model属性值也是一个字符串描述信息,它指定制造商的设备型号,该属性一般定义在根节点下,一般就是对板子的描述信息,没啥实质性的作用。
status属性
status属性标志了设备的状态,使用status属性可以禁止设备或者启用设备,status可选值规范:
值 | 描述 |
---|---|
okay | 表明设备是可操作的。启动设备 |
disabled | 表明设备当前是不可操作的,但在未来可以变为可操作的, |
fail | 设备不可操作,设备检测到了一系列的错误 |
fail-sss | 含义与“fail”相同,后面的sss部分是检测到的错误内容 |
#address-cells 和 #size-cells 属性
这两个属性的值都是无符号32位整形,address-cells和size-cells这两个属性可以用在任何拥有子节点的设备节点中,用于描述子节点的地址信息。
➢ #address-cells,用来描述子节点"reg"属性的地址表中用来描述首地址的 cell 的数量;
➢ #size-cells,用来描述子节点"reg"属性的地址表中用来描述地址长度的 cell 的数量。
#address-cells 和#size-cells 表明了子节点应该如何编写 reg 属性值,reg属性都是和地址有关的内容,reg属性的格式一为:
reg = <address1 length1 address2 length2 address3 length3……>
每个“address length”组合表示一个地址范围,其中 address 是起始地址,length 是地址长度,#address-cells 表明 address 字段占用的字长,#size-cells 表明 length 这个字段所占用的字长
reg属性
reg 属性一般用于描述设备地址空间资源信息,一般都是描述某个外设的寄存器地址范围信息、flash 设备
的分区信息等。
ranges属性
ranges 属性值可以为空或者按照(child-bus-address,parent-bus address,length)格式编写的数字矩阵
fpga_full: fpga-full {
compatible = "fpga-region";
fpga-mgr = <&devcfg>;
#address-cells = <1>;
#size-cells = <1>;
ranges;
};
//ranges的数值为空,则表示fpga_full:fpga-full节点的空间与父节点空间相同
soc{
compatible = "simple-bus"; 3 #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>;
};
};
//第 4行,节点 soc 定义的 ranges 属性,值为<0x0 0xe0000000 0x00100000>,此属性值指定了一个 1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为 0x0,父地址空间的物理起始地址为 0xe0000000。
//第 9行,serial 是串口设备节点,reg 属性定义了 serial 设备寄存器的起始地址为0x4600,寄存器长度为 0x100。经过地址转换,serial 设备可以从 0xe0004600 开始进行读写操作,0xe0004600=0x4600+0xe0000000。
device_type 属性
device_type属性值为字符串,表示节点的类型;
特殊节点
根目录下的几个特殊节点
aliases节点
aliases {
ethernet0 = &gem0;
i2c0 = &i2c_2;
i2c1 = &i2c0;
i2c2 = &i2c1;
serial0 = &uart0;
serial1 = &uart1;
spi0 = &qspi;
};
aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。但是需要注意的是,这里说的方便访问节点并不是在设备树中访问节点,例如前面说到的使用“&label”的方式访问设备树中的节点,而是内核当中
方便定位节点
chosen节点
chosen 节点一般会有两个属性,“bootargs”和“stdout-path”。打开 system top.dts 文件,找到 chosen 节点,内容如下所示
绑定文档
绑定文档主要就是提供不同控制器或者硬件设备树属性描述的参考。如果在绑定文档中没有的外设芯片则需要向开发商索要设备树的参考属性,若开发商没有则不选择该型号芯片。部分外设的设备树属性参考可以在(/Documentation/devicetree/bindings)中找到。
根目录compatible属性
在设备树没有设备树之前,uboot会向Linux内核传递一个叫做machine id的值,告诉内核自己是谁,Linux内核会查看是否支持。
Linux内核引入设备树以后就不再使用machine id了,而是换为了dt machine start。设备树根节点的compatible,其中的参数会在imx6ul_dt_compat 表里面去寻找是否有匹配的参数,如果有表示内核可以在此芯片上运行,没有则无法运行。
OF操作函数
设备都是以节点的形式“挂”到设备上,因此当需要获取设备树中的某个节点具体的值的时候,需要先找到该节点。
查找节点的OF函数
/*
@description:通过节点名字查找指定的节点
@pararm-from:开始查找的节点,如果从根目录开始可以用NULL表示
@pararm-name:要查找的节点名称
*/
struct device_node *of_find_node_by_name(struct device_node *from,const char *name);
/*
@description:函数通过 device_type 属性查找指定的节点
@pararm-from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树
@pararm-type:要查找的节点对应的 type 字符串,也就是 device_type 属性值
*/
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
/*
@description:函数根据 device_type 和 compatible 这两个属性查找指定的节点
@pararm-from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树
@pararm-type:要查找的节点对应的 type 字符串,也就是 device_type 属性值,可以为 NULL,表示忽略掉 device_type 属性
@pararm-compatible:要查找的节点所对应的 compatible 属性列表
*/
struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible)
/*
@description:通过 of_device_id 匹配表来查找指定的节点
@pararm-from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树
@pararm-matches:of_device_id 匹配表,也就是在此匹配表里面查找节点
@pararm-match:找到的匹配的 of_device_id
*/
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)
/*
@description:通过路径来查找指定的节点
@pararm-path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径
*/
inline struct device_node *of_find_node_by_path(const char *path)
查找父/子节点的OF函数
/*
@description:用于获取指定节点的父节点(如果有父节点的话)
@pararm-node:要查找的父节点的节点
*/
struct device_node *of_get_parent(const struct device_node *node)
/*
@description:用迭代的方式查找子节点
@pararm-node:父节点
@pararm-prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL,表示从第一个子节点开始
*/
struct device_node *of_get_next_child(const struct device_node *node,struct device_node *prev)
提取属性值的OF函数
节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux内核中使用结构体property表示属性,此结构体同样定义在文件Include/linux/of.h中。
/*
@description:用于查找指定的属性
@pararm-np:设备节点
@pararm-name: 属性名字
@pararm-lenp:属性值的字节数
*/
property *of_find_property(const struct device_node *np,const char *name,int *lenp)
/*
@description:用于获取属性中元素的数量
@pararm-np:设备节点
@pararm-proname: 需要统计元素数量的属性名字
@pararm-elem_size:元素长度
return:得到的属性元素数量
*/
int of_property_count_elems_of_size(const struct device_node *np,const char *propname,int elem_size)
/*
@description:数用于从属性中获取指定标号的 u32 类型数据值(无符号 32 位)
@pararm-np:设备节点
@pararm-proname: 要读取的属性名字
@pararm-index:要读取的值标号
@pararm-out_value:读取到的值
return:0 读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小
*/
int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index, u32 *out_value)
/*
@description:这4个函数分别是读取属性中u8、u16、u32 和 u64类型的数组数据,比如大多数的reg属性都是数组数据,可以使用这 4个函数一次读取出reg属性中的所有数据
@pararm-np:设备节点
@pararm-proname: 要读取的属性名字
@pararm-out_value:读取到的数组值,分别为 u8、u16、u32 和 u64
@pararm-out_sz:要读取的数组元素数量
return:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小
*/
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)
其他OF函数
/*
@description:of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段。
@pararm-np:设备节点
@pararm-index:reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0
return:经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败
*/
void __iomem *of_iomap(struct device_node *np, int index)
设备树在系统中的体现
系统启动以后可以在跟文件系统里面看到设备树的节点信息。在/proc/device-tree/目录下存放着设备树信息;内核启动的时候会解析设备树,然后在/proc/device-tree/目录下呈现出来。
思考
1、设备子节点的属性是从哪儿得来的,谁规定了具体设备的属性?例如ov5640摄像头模块,该模块下面的如此多的属性从哪儿得知需要填入的?
ov5640: ov5640@3c {
compatible = "ovti,ov5640";//在内核中匹配驱动
reg = <0x3c>;//IIC设备地址
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_csi1>;
clocks = <&clks IMX6UL_CLK_CSI>;
clock-names = "csi_mclk";
pwn-gpios = <&gpio_spi 6 1>;
rst-gpios = <&gpio_spi 5 0>;
csi_id = <0>;
mclk = <24000000>;
mclk_source = <0>;
status = "okay";
port {
ov5640_ep: endpoint {
remote-endpoint = <&csi1_ep>;
};
};
};
答:可以通过Linux内核自带的绑定文档中查询(/Documentation/devicetree/bindings),或者向制造商索要参考信息即可。
参考文件
《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.6》
《领航者ZYNQ之嵌入式Linux开发指南V2.0》