ZYNQ学习笔记-设备树

设备树是描述Linux开发板硬件的一种数据结构,是为了将硬件信息与驱动剥离而防止内核代码中出现大量板级外设描述代码。防止板级设备发送变动(比如LED引脚更换)就需要重新写驱动代码。使用设备树后,驱动代码可以通过函数获取运行驱动所需的信息(如寄存器地址)。

设备树文件

  • dts

设备树的源文件后缀就是.dts,每一款硬件平台都可以单独写一份dts文件。

  • dtsi

类似于c语言的头文件,可以在dts中include一个dtsi文件。

  • dtc

设备树的编译器。

  • dtb

设备树编译后的二进制数据。

设备树语法

设备树的结构

设备树顾名思义就是一颗树,每个设备就是一个节点,一棵树有一个根,根节点(root node)。除了根节点,每个节点都有一个父节点(parent node),每个节点包含了一些键值对描述该节点。

一种设备树的示意结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/{ // 根节点
node1{ // node1 节点
property1=value1; // node1 节点的属性 property1
property2=value2; // node1 节点的属性 property2
...
};

node2{ // node2 节点
property3=value3; // node2 节点的属性 property3
...
node3{ // node2 的子节点 node3
property4=value4; // node3 节点的属性 property4
...
};
};
};

比如node2可以是IIC1总线设备,然后node3可以是一个EEPROM。

设备树节点与属性

设备树中一个节点可以这样表示:

1
2
3
4
[label:]node-name[@unit-address] {
[properties definitions]
[child nodes]
};

“[]”表示内容可选。

label用于在其他节点通过&label格式引用。

node-name是节点名称。如uart1

unit-address是设备的地址,如果没有可以不要(如cpu@0)。

child nodes即子节点。

设备树的属性有多种类型:

  • 字符串
1
compatible="arm,cortex-a9";

字符串使用双引号括起来,例如上面的这个 compatible 属性的值是” arm,cortex-a9”
字符串。

  • 32位无符号整形
1
2
clock-latency = <1000>;
reg = <0x00000000 0x00500000>;

32 位无符号整形数据使用尖括号括起来,例如属性 clock-latency 的值是一个 32 位无符号整形数据 1000,而 reg 属性有两个数据,使用空格隔开,那么这个就可以认为是一个数组 。

  • 二进制数据
1
local-mac-address = [00 0a 35 00 1e 53];

二进制数据使用方括号括起来 。

  • 字符串数组
1
compatible = "n25q512a","micron,m25p80";

属性值也可以使用字符串列表,字符串之间使用逗号分割。

  • 混合值
1
mixed-property = "a string", [0x01 0x23 0x45 0x67], <0x12345678>;
  • 节点引用
1
clocks = <&clkc3>;

使用尖括号。

注释与宏定义

注释的方法和 C 语言当中是一样的,可以使用” // ”进行单行注释,也可以使用” /* */ ” 进行多行注释 。

设备树中可以使用”#include” 包含 dtsi、 dts 以及 C 语言的头文件 ,用以使用宏定义。

标准属性

节点内容由一堆属性组成,不同设备属性不同,但是有一些标准属性,很多驱动都会使用。

  1. compatible属性

一般该字符串使用” <制造商>,<型号>” 这样的形式进行命名 :

1
compatible = "xlnx,xuartps", "cdns,uart-r1p8";

例子当中的 xlnx 和 cdns 就表示制造商,而后面的 xuartps 和 uart-r1p8 就表示具体设备的型号。该设备首先使用第一个兼容值( xlnx,xuartps)在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值( cdns,uart-r1p8)查找,直到找到或者查找完整个 Linux 内核也没有找到对应的驱动。

驱动中有一个表格,其中储存了一些compatible值,一旦匹配上就可以使用这个驱动。

根节点的 compatible 属性可以知道我们所使用的处理器型号, Linux 内核会通过根
节点的 compoatible 属性查看是否支持此该处理器, 因为内核在启动初期会进行校验,必须要支持才会启动 Linux 内核。

  1. model属性

一个字符串描述信息, 它指定制造商的设备型号, model 属性一般定义在根节点下,一般就是对板子的描述信息,没啥实质性的作用, 内核在解析设备树的时候会把这个属性对应的字符串信息打印出来。

  1. status属性

status 标识了设备的状态,使用 status 可以去禁止设备或者启用设备,看下设备树规范中的 status 可选值:

key 描述
“okay” 表明设备是可操作的。 启动设备
“disable” 表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后。至于 disabled 的具体含义还要看设备的绑定文档。
“fail” 表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作。
“fail-sss” 含义和“ fail”相同,后面的 sss 部分是检测到的错误内容。

如果没有添加,则属性的默认值为okey。

  1. #address-cells和size-cells

这两个属性的值都是无符号 32 位整形, #address-cells 和#size-cells 这两个属性可以
用在任何拥有子节点的设备节点中,用于描述子节点的地址信息。
#address-cells,用来描述子节点”reg”属性的address字段占用字长;
#size-cells,用来描述子节点”reg”属性的length字段占用字长;

一字为32bit。

  1. reg属性

reg 属性的值一般是(address, length)对。 reg 属性一般用于描述设备地址空间资源信息,一般都是描述某个外设的寄存器地址范围信息、 flash 设备的分区信息等。

1
reg = <address1 length1 address2 length2 address3 length3……>
  1. ranges属性

ranges 是地址转换表,其中的每个项目是一个子地址、父地址以及在子地址空间的大小的映射。 ranges 属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵。

child-bus-address : 子 总 线 地 址 空 间 的 物 理 地 址 , 由 ranges 属 性 所 在 节 点 的#address-cells 属性确定此物理地址占用的字长。

parent-bus-address: 父总线地址空间的物理地址, 由 ranges 属性所在节点的父节点的#address-cells 属性确定此物理地址所占用的字长。
length: 子地址空间的长度, 由 ranges 属性所在节点的#address-cells 属性确定此地址长度所占用的字长。

如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换 。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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>;
};
};

ranges 属性,值为<0x0 0xe0000000 0x00100000>,此属性值指定了一个 1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为 0x0,父地址空间的物理起始地址为 0xe0000000。

serial 是串口设备节点, reg 属性定义了 serial 设备寄存器的起始地址为 0x4600,
寄存器长度为 0x100。经过地址转换, serial 设备可以从 0xe0004600 开始进行读写操作,0xe0004600=0x4600+0xe0000000。

  1. device-type属性

device_type 属性值为字符串, 表示节点的类型;此属性在设备树当中用的比较少,一般用于 cpu 节点或者 memory 节点。

节点追加或修改内容

假设有一个i2c控制器,现状需要在其下添加一个OLED,如果直接在i2c的节点下追加会很不方便(假设过一段时间又要加一个),此时可以通过以下格式在其他位置添加:

1
2
3
&i2c0 {
/* 要追加或修改的内容 */
};

第 1 行, &i2c0 表示要引用到 i2c0 这个 label 所对应的节点,也就是 zynq-7000.dtsi 文件中的“ i2c0: i2c@e0004000”。
第 2 行,花括号内就是要向 i2c0 这个节点添加的内容,包括修改某些属性的值。

如果追加的属性值已经,会修改原属性值。

例子:

1
2
3
4
5
6
7
&i2c0 {
24c64@50{
compatible = "atmel,24c64";
reg = <0x50>;
pagesize = <32>;
}
};

特殊的节点

  1. aliases节点
1
2
3
4
5
6
7
8
9
aliases {
ethernet0 = &gem0;
i2c0 = &i2c_2;
i2c1 = &i2c0;
i2c2 = &i2c1;
serial0 = &uart0;
serial1 = &uart1;
spi0 = &qspi;
};

用来取别名,这样在内核中就可以使用ethernet0来访问gem0节点。

  1. chosen节点
1
2
3
4
chosen {
bootargs = "console=ttyPS0,115200 earlyprintk root=/dev/mmcblk0p2 rw rootwait";
stdout-path = "serial0:115200n8";
};

属性 stdout-path = “ serial0:115200n8” ,表示标准输出设备使用串口 serial0,在 system-top.dts 文件当中, serial0 其实是一个别名,指向的就是 uart0;“ 115200”则表示串口的波特率为 115200,“ n”表示无校验位,“ 8”则表示有 8 位数据位 。

bootargs为传递给内核的变量。U-boot的传递的变量会添加在这后面。

  1. memory节点
1
2
3
4
memory {
device_type = "memory";
reg = <0x0 0x20000000>;
};

描述内存大小和起始位置。

Linux中的设备树

Linux 内 核 启 动 的 时 候 会 解 析 设 备 树 中 各 个 节 点 的 信 息 , 并 且 在 根 文 件 系 统 的/proc/device-tree 目录下根据节点名字创建不同文件夹 。

关于如何添加设备,在内核源码的/Documentation/devicetree/bindings 中有相应文档。比如如何添加一个IIC节点:Documentation/devicetree/bindings/i2c/i2c-cadence.txt ( 文 件 的 名 字 一 般 都 是 以i2c-xxx.txt 命名的, xxx 一般是制造商)。

驱动如何使用设备树

Linux内核为驱动提供了一些函数用来获取设备的属性信息,比如拿到reg地址进行初始化。这一系列函数都有一个of_前缀,因此被称为OF函数。

使用时需要引用include/linux/of.h文件。

查找节点的OF函数

一个节点在c中描述的结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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; /* 子节点 */
struct device_node *sibling;
struct kobject kobj;
unsigned long _flags;
void *data;
#if defined(CONFIG_SPARC)
const char *path_component_name;
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
  • of_find_node_by_name通过节点名
1
2
struct device_node *of_find_node_by_name(struct device_node *from,
const char *name);

from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name:要查找的节点名字。
返回值: 找到的节点,如果为 NULL 表示查找失败。

  • of_find_node_by_type通过device_type
1
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)

from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值。
返回值: 找到的节点,如果为 NULL 表示查找失败。

  • of_find_compatibe_node通过device_typecompatible
1
struct device_node *of_find_compatible_node(struct device_node *from,const char *type,const char *compatible)

from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值,可以为 NULL,表示忽略掉 device_type 属性。
compatible: 要查找的节点所对应的 compatible 属性列表。
返回值: 找到的节点,如果为 NULL 表示查找失败

  • of_find_matching_node_and_match通过of_device_id匹配表找
1
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 表示查找失败

  • of_find_node_by_path通过节点路径
1
inline struct device_node *of_find_node_by_path(const char *path)

path:带有全路径的节点名,可以使用节点的别名(用 aliens 节点中定义的别名)。
返回值: 找到的节点,如果为 NULL 表示查找失败

查找父子节点的OF函数

  • of_get_parent获取父节点
1
struct device_node *of_get_parent(const struct device_node *node)

node:要查找的父节点的节点。
返回值: 找到的父节点。

  • of_get_next_child获取子节点
1
struct device_node *of_get_next_child(const struct device_node *node,struct device_node *prev)

node:父节点。
prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL,表示从第一个子节点开始。
返回值: 找到的下一个子节点

提取属性的OF函数

属性的描述结构体:

1
2
3
4
5
6
7
8
9
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获取指定属性
1
property *of_find_property(const struct device_node *np,const char *name,int *lenp)

np:设备节点。

name: 属性名字。
lenp:属性值的字节数
返回值: 找到的属性。

  • of_property_count_elems_of_size获取属性中元素的数量(数组大小)
1
2
int of_property_count_elems_of_size(const struct device_node *np,
const char *propname,int elem_size)

np:设备节点。
proname: 需要统计元素数量的属性名字。
elem_size:元素长度。
返回值: 得到的属性元素数量。

  • of_property_read_u32_index从属性中获取指定下标的数据值(u32数组情况)
1
2
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 表示属性值列表太小。

  • of_property_read_u8_array读取属性中的u8类型数组数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值,分别为 u8、 u16、 u32 和 u64。
sz: 要读取的数组元素数量。
返回值: 0,读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小。

  • of_property_read_u8读取一个属性值
1
2
3
4
5
6
7
8
9
10
11
12
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:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值。
返回值: 0,读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小。

  • of_property_read_string读取属性中字符串值
1
2
3
int of_property_read_string(struct device_node *np,
const char *propname,
const char **out_string)

np:设备节点。
proname: 要读取的属性名字。
out_string:读取到的字符串值。
返回值: 0,读取成功,负值,读取失败。

  • of_n_addr_cells获取#address-cells属性
1
int of_n_addr_cells(struct device_node *np)
  • of_n_size_cells函数获取#size-cells属性
1
int of_n_size_cells(struct device_node *np)

其他OF函数

  • of_device_is_compatible 查看compatible中有没有包含指定字符串
1
2
int of_device_is_compatible(const struct device_node *device,
const char *compat)

device:设备节点。
compat:要查看的字符串。
返回值: 0,节点的 compatible 属性中不包含 compat 指定的字符串;正数,节点的
compatible 属性中包含 compat 指定的字符串。

  • of_get_address获取地址相关属性如“ reg”或者“ assigned-addresses”
    属性值
1
2
3
4
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 的话表示读取失败。

  • of_translate_address将从设备树读到地址转换为物理地址
1
2
u64 of_translate_address(struct device_node *dev,
const __be32 *in_addr)

dev:设备节点。
in_addr:要转换的地址。
返回值: 得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。

  • of_address_to_resource 获取设备寄存器的内存空间

IIC、 SPI、 GPIO 等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间, Linux内核使用 resource 结构体来描述一段内存空间,“ resource”翻译出来就是“资源”,因此用 resource 结 构 体 描 述 的 都 是 设 备 资 源 信 息 , resource 结 构 体 定 义 在 文 件include/linux/ioport.h 中,定义如下:

1
2
3
4
5
6
7
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
struct resource *parent, *sibling, *child;
};

对于 32 位的 SOC 来说, resource_size_t 是 u32 类型的。其中 start 表示开始地址, end表示结束地址, name 是这个资源的名字, flags 是资源标志位,一般表示资源类型,可选的资源标志定义在文件 include/linux/ioport.h 中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define IORESOURCE_BITS 0x000000ff
#define IORESOURCE_TYPE_BITS 0x00001f00
#define IORESOURCE_IO 0x00000100
#define IORESOURCE_MEM 0x00000200
#define IORESOURCE_REG 0x00000300
#define IORESOURCE_IRQ 0x00000400
#define IORESOURCE_DMA 0x00000800
#define IORESOURCE_BUS 0x00001000
#define IORESOURCE_PREFETCH 0x00002000
#define IORESOURCE_READONLY 0x00004000
#define IORESOURCE_CACHEABLE 0x00008000
#define IORESOURCE_RANGELENGTH 0x00010000
#define IORESOURCE_SHADOWABLE 0x00020000
#define IORESOURCE_SIZEALIGN 0x00040000
#define IORESOURCE_STARTALIGN 0x00080000
#define IORESOURCE_MEM_64 0x00100000
#define IORESOURCE_WINDOW 0x00200000
#define IORESOURCE_MUXED 0x00400000
#define IORESOURCE_EXCLUSIVE 0x08000000
#define IORESOURCE_DISABLED 0x10000000
#define IORESOURCE_UNSET 0x20000000
#define IORESOURCE_AUTO 0x40000000
#define IORESOURCE_BUSY 0x80000000

of_address_to_resource 函数 将 reg 属性值,然后将其转换为 resource 结构体类型 。

1
2
3
int of_address_to_resource(struct device_node *dev,
int index,
struct resource *r)

dev:设备节点。
index:地址资源标号。
r:得到的 resource 类型的资源值。
返回值: 0,成功;负值,失败。

  • of_iomap获取内存地址所对应的虚拟地址

reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过index 参数指定要完成内存映射的是哪一段 。

1
2
void __iomem *of_iomap(struct device_node *np,
int index)

np:设备节点。
index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
返回值: 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。

Petalinux中的设备树

Petalinux可以通过读取hdf文件自动生成设备树,在编译后,可以在components/plnx_workspace/device-tree/device-tree中找到:

  • system-top.dts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
* CAUTION: This file is automatically generated by Xilinx.
* Version:
* Today is: Sat Mar 16 08:49:42 2024
*/


/dts-v1/;
#include "zynq-7000.dtsi"
#include "pcw.dtsi"
/ {
chosen {
bootargs = "earlycon";
stdout-path = "serial0:115200n8";
};
aliases {
ethernet0 = &gem0;
serial0 = &uart1;
spi0 = &qspi;
};
memory {
device_type = "memory";
reg = <0x0 0x20000000>;
};
};
#include "system-user.dtsi"

这份文件引用了一些其他的dtsi文件:

  • zynq-7000.dtsi

zynq-7000 系列处理器相同的硬件外设配置信息( PS 端的) 。

  • pcw.dtsi

表示在 vivado 当中已经使能的 PS 外设 。

  • system-user.dtsi

用户的文件,可以在project-spec/meta-user/recipes-bsp/device-tree/files/中找到,可以放一些hdf读不到的信息,比如在IIC外设下的EEPROM。

一般如果我们想添加自己的外设,都会在system-user.dtsi中写追加设备树。