设备树

Last edited
Last updated July 11, 2023
Pages
Tags

1. 为什么要引入设备树

如果在驱动中直接指定好硬件的信息,当前驱动只能控制这款硬件。一旦硬件环境发生改变,驱动就无法正常工作,所以尽量不要在驱动中写死硬件信息,所以引入了设备树的概念。
notion image
notion image

2. 设备树的概念

设备树(Device Tree/DT)是linux系统中用于保存硬件设备信息的一种树形结构,设备树源码独立于内核源码存在,他存放在内核顶层目录/arch/arm/boot/dts/这个目录下。当内核启动后,内核就会解析设备树中存放的设备信息,将设备信息加载在内核中,以树形结构保存,树上每一个节点都是保存的一个硬件的信息。一个硬件设备信息的描述需要多种不同的属性来完成,这些属性在内核空间中是以一张链表的形式存在的。
/ ├── lcd ├── led ├── timer ├── GPIO ---> GPIO属性链表:[ ]->[ ]->[ ] └── uart
设备树的每一个节点都是加载一个硬件的信息,一个信息的描述可能需要多种不同的属性来完成,而描述当前设备树节点信息的各个属性在内核空间是以链表的形式存在的。

3. 设备树文件的格式

  • .dts :设备树的源码文件
  • .dtsi :头文件,源码文件的补充文件
  • dtc 编译器,使用 make dtbs 进行编译
  • .dtb :编译后的设备树二进制文件
配置内核支持设备树以及 DTC 工具的使用,在内核顶层目录下编辑 .config
CONFIG_DTC=y CONFIG_OF=y
设备树编译:
make dtbs

4. 设备树的语法格式

🌏
设备树的学习可以参考官方手册:Device Tree Usage - eLinux.org

4.1 设备树基本语法格式

设备树节点和属性是简单的树结构。属性是键值对,节点可以同时包含属性和子节点:
/dts-v1/;//声明当前设备树版本 / { //{};表示设备树的根节点,设备树中其他节点都是定义在根节点内部 node1 { //node1{};表示是根节点的子节点node1 a-string-property = "A string";//一个字符串属性键值对,用来被描述node1 a-string-list-property = "first string", "second string"; // hex is implied in byte arrays. no '0x' prefix is required a-byte-data-property = [01 23 34 56]; child-node1 { //node1子节点child-node1 first-child-property; second-child-property = <1>; a-string-property = "Hello, world"; }; child-node2 { }; }; node2 { //node2{};表示是根节点的子节点node2 an-empty-property; a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */ child-node1 { }; }; };
  1. 注释方式和 C 一样
  1. 每条语句以 ; 结尾

4.2 设备树节点的命名

设备树节点的命名格式是: <name>[@<unit-address>]
  • <name> 是一个简单的 ASCII 字符串,长度最多为 31 个字符。通常,节点是根据它所在的设备的类型命名的。如果节点使用地址描述设备,则包含单元地址。通常,单元地址是用于访问设备的主要地:
    • 例1:epioe 控制器设备树节点的命名: gpio@50006000 (由于 gpio 设备描述过程中需要有地址信息,所以 @ 后面要加地址)
    • 例2:添加 led 设备树节点,名字直接写 led 即可( led 描述符不需要描述地址信息,所以不需要添加 @ 后面的内容)

4.3 设备树节点的别名以及引用

一、设备树节点添加别名标签
gpioe: gpio@50006000 { gpio-controller; #gpio-cells = <2>; interrupt-controller; #interrupt-cells = <2>; reg = <0x4000 0x400>; clocks = <&rcc GPIOE>; st,bank-name = "GPIOE"; status = "disabled"; };
这个gpio设备树节点的名字是gpio@50006000,gpioe是给当前节点添加的一个标签,在使用这个节点时直接使用gpioe就可以代表这个节点,换句话说,gpioe是gpio@50006000节点的别名
二、设置树的别名节点
aliases { serial0 = &uart4; serial5 = &usart3; };
当前aliases是一个别名节点,节点内的属性都是用于给一个节点起别名,比如serial0就是给uart4起别名
三、关于节点的引用
1.serial0 = &uart4; //这里&uart4表示获取uart4节点在设备树在的路径,并且赋值给serial0这个键 2. 1.dts / { node1{ a="aaa"; b="bbb"; c="ccc"; }; }; &node1{ a="rrr"; d="ddd"; }; //编译节点时整合成以下形式编译: /{ node1{ a="rrr"; b="bbb"; c="ccc"; d="ddd"; }; };
在同一个路径下如果存在相同名字的节点,这两个节点在编译时会合并,如果两个节点内部有同名的属性,属性的值后一次的会覆盖前一次的,如果属性名不一样,会合并在一起

4.4 设备树中键值对的值的格式

属性是简单的键值对,其中值可以为空或包含任意字节流。虽然数据类型未编码到数据结构中,但可以在设备树源文件中表示一些基本数据表示形式
// 文本字符串(以 null 结尾)用双引号表示: string-property = "a string"; // "cell"是 32 位无符号整数,由尖括号分隔: cell-property = <0xbeef 123 0xabcd1234>; // 单字节数据用方括号分隔: binary-property = [0x01 0x23 0x45 0x67];
// 不同表示形式的数据可以使用逗号连接在一起: mixed-property = "a string", [0x01 0x23 0x45 0x67], <0x12345678>; // 逗号也用于创建字符串列表: string-list = "red fish", "blue fish";
设备树中标准化的键值对(有特定含义的键值对):
//这个键值对是一个空属性键值对,起到标识作用,标识当前节点是一个gpio控制器 1. gpio-controller; //这个键值对也是一个空属性键值对,起到标识作用, //标识这个设备树节点对应的设备是用来接收中断信号的设备 2. interrupt-controller //这个属性是用来描述当前设备的寻址内存首地址以及内存大小 3.reg=<地址 地址对应的内存大小> //当当前节点每个子节点内存连续时可以使用这种方式描述当前地址 4.rangs<索引号 父地址 内存大小> ex:node1{ rangs=<0 1 0x400>; child_node1{ reg=<0 0x4>;//当前节点的地址是1+0 }; child_node2{ reg=<4 0x4>;//当前节点的地址是4+1 }; }; //指定当前节点子节点中reg属性有n个无符号32位整型用来描述地址 5.#address-cells = <n> //指定当前节点子节点中reg属性有n个无符号32位整型用来描述地址内存大小 6.#size-cells = <n> //这个属性是用来描述当前节点对应的设备的厂商信息 7.compatible="厂商名,设备名"

5. 添加一个自己编写的设备树节点并被内核解析

5.1 添加设备树节点

编辑: vim arch/arm/boot/dts/stm32mp157a-fsmp1a.dts
// 在根节点中添加如下子节点: mynode@0x12345678{ compatible = "hqyj,mynode"; astring="hello 23031"; uint =<0xaabbccdd 0x11223344>; binarry=[00 0c 29 7b f9 be]; mixed ="hello",[11 22],<0x12345678>; };

5.2 重新编译设备树

make dtbs

5.3 复制设备树镜像到 tftp 目录下

cp arch/arm/boot/dts/stm32mp157a-fsmp1a.dtb ~/tftpboot

5.4 重启开发板,查看内核是否解析当前节点

[root@fsmp1a ]# ls /proc/device-tree/mynode@0x12345678/ astring binarry compatible mixed name uint

6. 在驱动程序中获取设备树的设备信息

6.1 概述

当内核解析设备完毕,每一个设备树节点的信息都存在内核的一个 struct device_node 类型的空间中,而每一个节点中的属性是存放在一个属性链表中,链表的每一个节点的类型都为 struct property 类型。

6.2 device_node 结构体

这个结构体的作用是保存一个设备树节点的信息
#include<linux/of.h> struct device_node { const char *name;// 设备树节点的名字 mynode const char *full_name;// mynode@0x12345678 struct property *properties;// 描述当前设备信息的属性链表第一个节点地址 struct property *deadprops; /* removed properties */ struct device_node *parent;// 当前节点的父节点首地址 struct device_node *child;// 当前节点的子节点首地址 struct device_node *sibling;// 当前节点的兄弟节点首地址 };

6.3 property 结构体

设备树中每一个属性信息都保存在内核的一个 struct property 结构体空间中
struct property { char *name;//键名 int length; //数值的大小 void *value; //数据保存的空间首地址 struct property *next;//下一个节点首地址 };

6.4 解析设备树节点的相关 API

struct device_node *of_find_node_by_name(struct device_node *from, const char *name)
功能:通过设备树节点的名字解析设备树节点信息
参数:
  • from:要寻找的设备树节点所在的子树节点首地址,填写 NULL 则从根节点开始查找
  • name:要查找的设备树节点的名字
返回值:成功返回找到节点结构体的指针,失败返回 NULL
 
struct device_node *of_find_node_by_path(const char *path)
功能:通过设备树节点在设备树中的路径查找节点
参数:
  • path:路径
返回值:成功返回当前节点结构体的指针,失败返回 NULL
 
struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)
功能:通过节点中的 compatible 属性的值解析出指定的设备树节点
参数:
  • from:要寻找的设备树节点所在的子树节点首地址,填写 NULL 则从根节点开始查找
  • type:设备类型,这个属性很少用,所以一般填 NULL
  • compatible:要查找的 compatible 的值
返回值:成功返回当前节点的结构体指针,失败返回 NULL
 
struct property *of_find_property(const struct device_node *np, const char *name, int *lenp)
参数:
  • np:要操作的节点结构指针
  • name:要找到的属性键值对的键名
  • lenp:找到的属性键值对的值的长度
返回值:成功返回目标属性结构体指针,失败返回NUll

6.5 获取设备树属性的实例

需要安装第 5 节的设备树文件
#include <linux/init.h> #include <linux/module.h> #include <linux/of.h> /* mynode@0x12345678{ compatible = "hqyj,mynode"; astring="hello 23031"; uint =<0xaabbccdd 0x11223344>; binarry=[00 0c 29 7b f9 be]; mixed ="hello",[11 22],<0x12345678>; };*/ struct device_node *dnode; //用来指向目标结构体首地址 struct property *pro; int length; static int __init mycdev_init(void) { int i; //解析设备树节点信息 dnode = of_find_node_by_name(NULL, "mynode"); if (dnode == NULL) { // 解析失败 printk("解析设备树节点失败\n"); return -ENXIO; } printk("解析设备树节点成功\n"); //根据节点和指定键名找到目标信息 pro = of_find_property(dnode, "uint", &length); if (pro == NULL) { printk("解析设备树指定的属性失败\n"); return -ENXIO; } printk("name=%s value=%x %x\n", pro->name, __be32_to_cpup((unsigned int *)pro->value), __be32_to_cpup((unsigned int *)pro->value + 1)); pro = of_find_property(dnode, "binarry", &length); printk("name=%s ", pro->name); for (i = 0; i < length; i++) { printk("value=%x\n", *((char *)pro->value + i)); } return 0; } static void __exit mycdev_exit(void) { } module_init(mycdev_init); module_exit(mycdev_exit); MODULE_LICENSE("GPL");
运行结果:
[root@fsmp1a ]# insmod mychrdev.ko [ 9347.477720] 解析设备树节点成功 [ 9347.480440] name=uint value=aabbccdd 11223344 [ 9347.484503] name=binarry [ 9347.484510] value=0 [ 9347.489246] value=c [ 9347.491281] value=29 [ 9347.493416] value=7b [ 9347.495650] value=f9 [ 9347.497898] value=be

6.6 根据键名解析出指定数值的API

int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value)
功能:获取u32类型的值
参数:
  • np:节点指针
  • propname:键名
  • index:值中32位数的索引号,第一个值索引号为0
  • out_value:输出的数值
返回值:成功返回0,失败返回错误码
 
int of_property_read_variable_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz_min, size_t sz_max)
功能:获取u32类型的数组的值
参数:
  • np:节点指针
  • propname:键名
  • out_values:存放获取到的值的空间的首地址
  • min:期待读取的数据的最小个数
  • max:期待读取的数据的最大个数
返回值:成功返回 0,失败返回错误码
int of_property_read_string(const struct device_node *np, const char *propname, const char **out_string)
功能:获取字符串类型的值
参数:
  • np:节点指针
  • propname:键名
  • out_string :字符串首地址
返回值:成功返回0,失败返回错误码
int of_property_read_variable_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz_min, size_t sz_max)
功能:获取u8类型的数组的值
参数:
  • np:节点指针
  • propname:键名
  • out_values:存放获取到的值的空间的首地址
  • min:期待读取的数据的最小个数
  • max:期待读取的数据的最大个数
返回值:成功返回0,失败返回错误码
static inline int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz)
功能:获取u8类型的数组的值 参数:
  • np:节点指针
  • propname:键名
  • out_values:存放获取到的值的空间的首地址
  • sz:数组成员个数
返回值:成功返回0,失败返回错误码

6.7 示例代码

#include <linux/init.h> #include <linux/module.h> #include <linux/of.h> /*mynode@0x12345678{ compatible = "hqyj,mynode"; astring="hello 22061"; uint =<0xaabbccdd 0x11223344>; binarry=[00 0c 29 7b f9 be]; mixed ="hello",[11 22],<0x12345678>; };*/ //定义一个指向设备节点的指针 struct device_node *node; struct property *pr; //属性结构体指针 int len; u32 a; u32 b[2]; const char *str; u8 c[6]; static int __init mycdev_init(void) { int i; //根据设备节点路径获取设备节点信息 node = of_find_node_by_path("/mynode@0x12345678"); if (node == NULL) { printk("获取节点信息失败\n"); return ENODATA; } printk("获取节点信息成功\n"); //获取u32数值 of_property_read_u32_index(node, "uint", 0, &a); printk("%#x\n", a); of_property_read_u32_index(node, "uint", 1, &a); printk("%#x\n", a); printk("u32\n"); //获取u32数组 of_property_read_variable_u32_array(node, "uint", b, 2, 2); printk("%#x,%#x\n", b[0], b[1]); printk("u32数组\n"); //获取字符串类型的值 of_property_read_string(node, "astring", &str); printk("%s\n", str); printk("字符串\n"); //获取u8类型值 of_property_read_u8_array(node, "binarry", c, 6); for (i = 0; i < 6; i++) { printk("%#x\n", c[i]); } printk("u8\n"); return 0; } static void __exit mycdev_exit(void) { } module_init(mycdev_init); module_exit(mycdev_exit); MODULE_LICENSE("GPL");
运行结果:
[root@fsmp1a ]# insmod mychrdev2.ko [10248.314665] 获取节点信息成功 [10248.316935] 0xaabbccdd [10248.319176] 0x11223344 [10248.321511] u32 [10248.323243] 0xaabbccdd,0x11223344 [10248.326587] u32数组 [10248.328958] hello 23031 [10248.331300] 字符串 [10248.333539] 0x0 [10248.335267] 0xc [10248.337038] 0x29 [10248.338870] 0x7b [10248.340701] 0xf9 [10248.342531] 0xbe [10248.344360] u8