什么是设备树?
设备树文件(Device Tree),描述设备树的文件叫做DTS((Device Tree Source),这个DTS文件采用树形结构描述板级设备,也就是开发板上的设备信息。
设备树结构示意图:
DTS、DTB和DTC
DTS是设备树源码文件,扩展名为.dts。
DTB是将DTS文件编译以后得到的二进制文件,将.dts文件编译成为.dtb需要用到DTC工具。
DTC工具源码在Linux内而过的scripts/dtc目录下。
.dtsi是设备树的头文件扩展名。
编译DTS文件需要进入Linux源码根目录下,执行:
make dtbs
即可编译dtb文件,那么如何确定编译哪一个DTS文件内,以STM32MP1这款SOC对应的开发板为例,打开arch/arm/boot/dts/Makefile:
dtb-$(CONFIG_ARCH_STM32) += \
stm32mp157c-ev1-a7-examples.dtb \
stm32mp157c-ev1-m4-examples.dtb \
...
将CONFIG_ARCH_STM32=y配置上时,所有使用到STM32MP1这个SOC的板子对应的.dts文件都会编译成.dtb,如果有新增设备树文件的需求在下面添加上设备树的文件名即可编译出对应的二进制设备树文件(.dtb)。
DTS语法
dtsi头文件
一般dtsi文件用于描述SOC内部外设信息,比如CPU架构,主频,外设寄存器地址范围,比如UART,IIC等。一般一个系列里有多个SOC就会把相同内部外设信息提炼到一个.dtsi文件里,减少代码冗余。一般基于soc的开发板设备树文件都会#include对应的soc的dtsi的设备树文件。
设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键值对。
/ :表示根节点,每个设备树文件只有一个根节点,多数平台不只有一个设备树文件,所有也可能会有多个"/"根节点,这种情况下两个"/"根节点的内容会合并成一个根节点。
节点命名
设备树种节点命名格式1:
node-name@unit-address
node-name : 节点名字
unit-address:设备的地址或寄存器首地址
设备树种节点命名格式2:
label:node-name@unit-address
label:节点标签
node-name : 节点名字
unit-address:设备的地址或寄存器首地址
引入label的目的是为了方便访问节点,可以直接通过&label来访问这个节点,比如:
&sram来访问sram::sram@10000000
标准属性
节点是由一堆属性组成,节点都是具体的设备,不同的设备的属性不同,很多属性是标准属性,但是用户也可以自定义属性。Linux常用的标准属性如下:
1.compatible属性
compatible的属性的值是一个字符串列表,用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,compatible属性格式如下:
"manufacturer,model"
manufacturer:表示厂商
model:一般是模块对应驱动的名字
compatible也可以有多个属性值:
compatible = "cirrus,my_cs42l51","cirrus,cs42l51";
这种设备首先使用第一个兼容值在Linux内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到就用第二个值查找,直到查找完compatible中的所有值。
一般驱动程序文件都会有一个OF匹配表,在OF匹配表中保存着一些compatible值,如果设备节点的compatible属性值与OF匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。
static const struct of_device_id adnp_of_match[] = {
{ .compatible = "ad,gpio-adnp", },
{ },
};
2.model属性
model属性值也是一个字符串,一般model属性描述开发板的名字或者设备模块信息,比如:
model = "snps,axs101";
3.status属性
status属性与设备状态有关,也是字符串,表示设备的状态信息
值 | 描述 |
---|---|
"okay" | 表示设备是可操作的 |
"disabled" | 表明设备当前是不可操作的,但是未来可以变为可操作的,比如热插拔设备插入以后 |
"fail" | 表明设备当前是不可操作的,但是在未来可以变为可操作的 |
“fail-sss” | 含义与"fail"相同,后面的sss部分是检测到的错误的内容 |
4.reg属性
reg属性的值一般是(address,length)对。reg属性一般用于描述设备地址空间资源信息或者设备地址信息,比如某个外设的寄存器地址范围信息:
reg = <0x4000e000 0x400>; /* 设备寄存器首地址为0x4000e000,地址长度范围为0x400(一般芯片手册中可以找到)*/
5.#address-cells
和#size-cells
属性
这两个属性的值都是无符号32位整型,可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。
#addresss-cells
:决定了子节点reg属性中地址信息所占用的字长(32位)
#size-cells
:决定了子节点reg属性中长度信息所占的字长(32位)
cpus {
#address-cells = <1>; /* 说明起始地址占用字长为1 */
#size-cells = <0>; /* 说明地址长度所占用的字长为0 */
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>; /* address=0,因为#size-cells为0,没有length值,相当于只设置起始地址,
而没有设置地址长度 */
clocks = <&scmi0_clk CK_SCMI0_MPU>;
clock-names = "cpu";
operating-points-v2 = <&cpu0_opp_table>;
nvmem-cells = <&part_number_otp>;
nvmem-cell-names = "part_number";
#cooling-cells = <2>;
};
};
scmi_sram: sram@2ffff000 {
compatible = "mmio-sram";
reg = <0x2ffff000 0x1000>;
#address-cells = <1>; /* 起始地址占用字节为1*/
#size-cells = <1>; /* 地址长度占用字节为1*/
ranges = <0 0x2ffff000 0x1000>;
scmi0_shm: scmi_shm@0 {
reg = <0 0x80>; /* 起始地址为0x0,地址长度为0x80 */
};
scmi1_shm: scmi_shm@200 {
reg = <0x200 0x80>;
};
};
6.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属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行转换,比如:
soc
{
compatible = "simple-bus";
...
ranges;
};
ranges = <0 0x10000000 0x100000> /*指定了一个1024kb(0x100000)的地址范围,子地址空间的物理起始地址为0,父地址空间的物理起始地址为0x10000000。*/
sram: sram@10000000 {
compatible = "mmio-sram";
reg = <0x0 0x60000>; /* 定义了sram设备寄存器的起始地址为0,寄存器长度0x60000*/
#address-cells = <1>;
#size-cells = <1>;
ranges = <0 0x10000000 0x60000>; /* 经过地址转换,sram设备可以从0x10000000开始进行读写操作,0x10000000 = 0x0+0x10000000
}; */
7.根节点compatible属性
每个节点都有compatible属性,根节点"/"也有,一般设备节点的compatible属性值是为了匹配Linux内核中的驱动程序,根节点的compatible属性一般写法是(硬件设备名,SOC名),比如:
/{
model = "STMicroelectronics STM32MP157C-DK2 Discovery Board";
compatible = "st,stm32mp157d-evm","st,stm32mp157";
}
硬件设备名:stm32mp157d-evm
SOC:stm32mp157
Linux内核会通过根节点的compatible的属性查看是否支持此设备,如果支持的话设备就会启动Linux内核。
(1)使用设备树之前的设备匹配方法:
在没有使用设备树以前,uboot会向Linux内核传递一个叫做machine id的值,machine id就是设备id,告诉Linux内核自己是个什么设备,Linux会检查machine id与mach-types.h中的MACH_TYPE_XXX宏进行对比,如果不支持就无法启动Linux内核。如今的BSP基本都是使用设备树来做设备匹配。
(2)使用设备树之后的设备匹配方法:
在Linux内核引入设备树后,使用DT_MACHINE_START和DT_MACHINE_END来定义一个machine_desc结构体来描述这个设备, 这里宏定义在文件arch/arm/include/asm/mach/arch.h里面,定义如下:
#define DT_MACHINE_START(_name, _namestr) \
static const struct machine_desc __mach_desc_##_name \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = ~0, \
.name = _namestr,
#endif
.nr:表示不会再根据machine id来检查Linux内核是否支持设备
以arch/arm/mach-stm32/board-dt.c为例:
static const char *const stm32_compat[] __initconst = {
"st,stm32f429",
"st,stm32f469",
"st,stm32f746",
"st,stm32f769",
"st,stm32h743",
"st,stm32mp151",
"st,stm32mp153",
"st,stm32mp157",
NULL
};
DT_MACHINE_START(STM32DT, "STM32 (Device Tree Support)")
.dt_compat = stm32_compat,
#ifdef CONFIG_ARM_SINGLE_ARMV7M
.restart = armv7m_restart,
#endif
MACHINE_END
machine_desc结构体中有个.dt_compat成员变量,此成员变量保存着本设备兼容属性。只要某个设备(板子)根节点"/"的compatible属性值与stm32_compat表中的任何一个值相等,就表示Linux内核支持此设备。
向节点追加或修改内容
比如需要向i2c1节点增加一个名为fxls8471的子节点,这时需要在板子(不是soc的设备树)的设备树上添加,方式如下:
&i2c1{
/*要追加或修改的内容*/
};
&i2c1:表示要访问i2c1这个label所对应的节点,也就是stm32mp151.dtsi中的"i2c1:i2c@40012000"。
打开板子的设备树文件,在根节点后添加数据
&i2c1 {
pinctrl-names = "default", "sleep";
pinctrl-0 = <&i2c1_pins_b>;
pinctrl-1 = <&i2c1_pins_sleep_b>;
status = "okay"; /*i2c1使能*/
clock-frequency = <100000>; /*i2c1时钟为100Khz*/
fxls8471@1e { /*fxls8471设备节点的相关信息*/
compatible = "fsl,fxls8471";
reg = <0x1e>;
position = <0>;
interrupt-parent = <&gpioh>;
interrupts = <6 IRQ_TYPE_EDGE_FALLING>;
};
};
设备树在系统中的体现
Linux内核启动的时候会解析设备树中的各个节点信息,并且在根文件系统的/proc/device-tree/base目录下根据节点的名字创建不同的文件夹。可以通过cat命令查看根节点"/"的各个属性。
在/proc/device-tree/base下的各个文件夹就是根节点"/"的各个子节点,比如"aliases"、"reboot"、"chosen"和"cpus"等等。
/proc/device-tree目录就是设备树在根目录系统中的体现,同样是按照树形结构组织的,进入/proc/device-tree/soc目录中就可以看到soc节点的所有子节点。
特殊节点
aliases子节点
aliases的意思是"别名",因此aliases的节点的主要功能就是定义别名,类似于C语言中的typedef,重定义别名方便访问节点。一般在命名节点的时候会加上label,然后通过&label来访问节点,设备书中会大量的使用&label的形式来访问节点。
aliases{
serial0 = &uart4;
};
chosen子节点
chosen并不是一个真实的设备,chosen节点主要是为了uboot向Linux内核传递数据,重点是bootargs参数。一般.dts文件中chose节点通常为空或者内容很少,比如:
chosen{
stdout-path = "serial0::115200n8";
};
chosen节点仅仅设置了属性"stdout-path",表示标准输出使用serial0,而aliases已经设置了serial0为uart4,所以开发板使用UART4作为默认中断。
Linux内核解析DTB文件
Linux内核在启动的时候会解析DTB文件,然后在/proc/device-tree目录下生成相应的设备树节点文件。Linux解析流程如下:
start_kernel()->setup_arch()->unflatten_devivce_tree()->__unflatten_device_tree()->unflatten_dt_node()->解析出DTB文件中的各个节点
在start_kernel函数中完成了设备树节点解析的工作,最终实际工作的函数为unflatten_dt_node。
绑定信息文档
当需要在设备中添加一个硬件对应的节点时,可以在Linux源码的/Documentation/devicetree/bindings目录下查找平台的绑定文件,比如我们需要在STM32MP157这个SOC的I2C下添加一个节点,就可以查看/Documentation/devicetree/bindings/i2c/i2c-stm32.txt,这个文档详细的描述了STM32MP1系列的SOC如何在设备树中添加I2C设备节点:
...
Example:
i2c@40005400 {
compatible = "st,stm32f4-i2c";
#address-cells = <1>;
#size-cells = <0>;
reg = <0x40005400 0x400>;
interrupts = <31>,
<32>;
resets = <&rcc 277>;
clocks = <&rcc 0 149>;
pinctrl-0 = <&i2c1_sda_pin>, <&i2c1_scl_pin>;
pinctrl-names = "default";
};
i2c@40005400 {
compatible = "st,stm32f7-i2c";
#address-cells = <1>;
#size-cells = <0>;
reg = <0x40005400 0x400>;
interrupts = <31>,
<32>;
resets = <&rcc STM32F7_APB1_RESET(I2C1)>;
clocks = <&rcc 1 CLK_I2C1>;
pinctrl-0 = <&i2c1_sda_pin>, <&i2c1_scl_pin>;
pinctrl-1 = <&i2c1_sda_pin_sleep>, <&i2c1_scl_pin_sleep>;
pinctrl-names = "default", "sleep";
st,syscfg-fmp = <&syscfg 0x4 0x1>;
st,syscfg-fmp-clr = <&syscfg 0x44 0x1>;
};
如果该路径下找不到对应的文档,就需要咨询芯片厂商,让他们提供参考的设备树文件。
设备树常用OF函数
在编写驱动的过程中,需要解析设备树中描述设备的详细信息,这个时候就需要使用OF系列函数来解析设备树的各个属性值了。这些OF函数原型都定义在include/linux/of.h文件中。
查找节点的OF函数
设备都是一节点的形式"挂"在设备树上的,所以想要获取这个设备的其他属性信息,必须现货区到这个设备的节点。Linux内核使用device_node结构体来描述一个节点,结构体定义在include/linux/of.h中。
struct device_node {
const char *name; /*节点名字*/
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;
#if defined(CONFIG_OF_KOBJ)
struct kobject kobj;
#endif
unsigned long _flags;
void *data;
#if defined(CONFIG_SPARC)
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
查找节点相关的OF函数有以下5个:
1.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函数
此函数通过device_type属性查找指定的节点,函数原型:
struct device_node *of_find_node_by_type(struct device_node *from,
const char *type)
from:开始查找的节点,如果为NULL表示从根节点开始查找着整个设备树
type:要查找的节点对应的type字符串,也就是device_type属性值
返回值:找到的节点,如果是NULL表示查找失败
3.of_find_compatible_node函数
此函数通过device_type和compatible这两个属性查找指定的节点,函数原型:
struct device_node *of_find_compatible_node(
struct device_node *from,
const char *type,
const char *compat)
from:开始查找的节点,如果为NULL表示从根节点开始查找着整个设备树
type:要查找的节点对应的type字符串,也就是device_type属性值,可以为NULL,表示忽略掉device_type属性
compat:要查找的节点所对应的compatible属性列表
返回值:找到的节点,如果为NULL表示查找失败
4.of_find_matching_node_and_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)
from:开始查找的节点,如果为NULL表示从根节点开始查找着整个设备树
matches:of_device_id匹配表,也就是在匹配表中查找节点
match:找到的匹配的of_device_of
返回值:找到的节点,如果为NULL表示查找失败
5.of_find_node_by_path函数
此函数通过路径来查找指定的节点,函数原型:
inline struct device_node *of_find_node_by_path(const char *path)
path:带有全路径的节点名,可以使用节点的别名,比如"/backlight"就是backlight这个节点的全路径。
返回值:找到的节点,如果为NULL表示查找失败
查找父/子节点的OF函数
Linux内核提供了几个查找节点对应的父节点或者子节点的OF函数
1.of_get_parent函数
此函数用于获取指定节点的父节点,函数原型如下:
struct device_node *of_get_parent(const struct device_node *node)
node:要查找的父节点的节点
返回值:找到的父节点。
2.of_get_next_child函数
此函数用于迭代的查找子节点,函数原型:
inline struct device_node *of_get_next_child(
const struct device_node *node, struct device_node *prev)
node:父节点
prev:从前一个子节点,也就是从哪一个子节点开始迭代的查找一下个子节点。可以设置为NULL,表示从第一个子节点开始。
返回值:找到下一个子节点
提取属性值的OF函数
节点的属性信息中保存了驱动中所需要的内容,因此对于属性值的提取非常重要,Linux内核中使用结构体property表示属性,结构体定义在文件include/linux/of.h中。
struct property {
char *name; /*属性名字*/
int length; /*属性长度*/
void *value; /*属性值*/
struct property *next; /*下一个属性*/
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
unsigned long _flags;
#endif
#if defined(CONFIG_OF_PROMTREE)
unsigned int unique_id;
#endif
#if defined(CONFIG_OF_KOBJ)
struct bin_attribute attr;
#endif
};
1.of_find_property函数
此函数用于查找指定的属性,函数原型:
struct property *of_find_property(const struct device_node *np,
const char *name,
int *lenp);
np:设备节点
name:属性名字
lenp:属性值的字节数
返回值:找到的属性
2.of_property_count_elems_of_size函数
此函数用于获取属性中元素的数量,比如reg属性值是一个数组,那么可以使用此函数获取这个数组的大小,函数原型:
int of_property_count_elems_of_size(const struct device_node *np,
const char *propname, int elem_size);
np:设备节点
proname:需要统计元素数量的属性名字
elem_size:元素长度
返回值:得到的属性元素数量
3.of_property_read_u32_index函数
此函数用于从属性中获取指定标号的u32类型数据值,比如某个属性有多个u32类型的值,那么就可以使用此函数来获取指定标号的数据值,函数原型:
int of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index, u32 *out_value);
np:设备节点
propname:要读取的属性名字
out_value:读取到的值
返回值:0读取成功,负值,读取失败,-EINVAL表示属性不存在,-ENODATA表示没有要读取的数据,-EOVERFLOW表示属性值列表太小
4.of_property_read_u8_array,of_property_read_u16_array,of_property_read_u32_array,of_property_read_u64_array函数
这4个函数分别是读取属性中u8,u16,u32和u64类型的数组数据,比如大多数的reg属性都是数组数据,可以使用这4个函数一次读取出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:节点
proname:要读取的属性名字
out_value:读取到数组值
返回值:0读取成功,负值,读取失败,-EINVAL表示属性不存在,-ENODATA表示没有要读取的数据,-EOVERFLOW表示属性值列表太小
5.of_property_read_u8,of_property_read_u16,of_property_read_u32,of_property_read_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:设备节点
proname:要读取的属性名字
out_value:读取到的数值
返回值:0读取成功,负值,读取失败,-EINVAL表示属性不存在,-ENODATA表示没有要读取的数据,-EOVERFLOW表示属性值列表太小
6.of_property_read_string函数
此函数用于读取属性中字符串值,函数原型:
int of_property_read_string(const struct device_node *np,
const char *propname,
const char **out_string)
np:设备节点
proname:要读取的属性名字
out_string:读取到的字符串值
返回值:0,读取成功,负值,读取失败
7.of_n_addr_cells函数
此函数用于获取#address-cells属性值,函数原型:
int of_n_addr_cells(struct device_node *np)
np:设备节点
返回值:获取到的#address-cells属性值
8.of_n_size_cells函数
此函数用于获取#size-cells属性值,函数原型:
int of_n_size_cells(struct device_node *np)
np:设备节点
返回值:获取到的#size-cells属性值
其他常用的OF函数
1.of_device_is_compatible函数
此函数用于查看节点的compatible属性是否有包含compat指定的字符串,也就是检查设备节点的兼容性,函数原型:
int of_device_is_compatible(const struct device_node *device,
const char *compat)
device:设备节点
compat:要查看的字符串
返回值:0,节点的compatible属性中不包括compat指定的字符串;正数,节点的compatible属性中包含compat指定的字符串。
2.of_get_address函数
此函数,用于获取地址相关属性,主要是"reg"或者"assigned-address"属性值,函数原型:
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函数
该函数负责将从设备树读取到的地址转换为物理地址,函数原型:
u64 of_translate_address(struct device_node *dev, const __be32 *addr)
dev:设备节点
addr:要转换的地址
返回值:得到的物理地址,如果为OF_BAD_ADDR的话表示转换失败。
4.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,成功;负值,失败
resource结构体被用来描述一段内存空间,外设比如IIC,SPI等都有对应的寄存器,这些寄存器就是一组内存空间,因此用resource描述设备资源信息,resource定义在include/linux/ioport.h中:
struct resource {
resource_size_t start; /*开始地址*/
resource_size_t end; /*结束地址*/
const char *name; /*资源的名字*/
unsigned long flags; /*资源标志位*/
unsigned long desc;
struct resource *parent, *sibling, *child;
};
资源标志相关宏定义在include/linux/ioport.h中,常见的资源标志就是IORESOURCE_MEM、IORESOURCE_REG和IORESOURCE_IRQ等。
5.of_iomap函数
此函数用于直接内存映射,可以直接获取内存地址所对应的虚拟地址,本质是将reg属性中的地址信息转换为虚拟地址,如果reg属性有多段的话,可以通过index参数指定要完成内存映射的是哪一段。当然也可以使用ioremap函数来完成物理地址到虚拟地址的内存映射。of_iomap函数原型:
void __iomem *of_iomap(struct device_node *np,
int index)
np:设备节点
index:reg属性中要完成内存映射的段,如果reg属性只有一段的话index就设置为0
返回值:经过内存映射后虚拟内存首地址,如果为NULL的话表示内存映射失败
总结
Linux内核中关于设备树的OF函数不仅仅只有以上这些,还有针对驱动的接口,比如获取中断号的OF函数,获取GPIO的OF函数等等。
参考文章:
【正点原子】STM32MP1嵌入式Linux驱动开发指南V2.0 - 第2章 Linux设备树