设备树
linux3.0以后版本才引进了设备树,在此之前的linux内核代码中描述硬件平台信息的代码散乱的分布在arch/arm/plat-XXX 和 arch/arm/mach-XXX等目录中。所以为了改善这一情况,设备树孕育于出,它将这些硬件描述信息从内核代码中剥离出来,使用一种树形结构来描述硬件平台的组成和配置信息。
具体关于设备树的介绍可以参考:https://www.devicetree.org/
1、设备树简介
1、什么是设备树?(Device Tree)
在linux内核早期,平台硬件的信息(例如内存地址、外设配置等)是直接编码在内核代码中的。这意味着一旦硬件平台发生改变,就要重新编写代码。这种方式难以维护和移植。
为了解决这一问题,linux内核在3.X版本之后开时引用了设备树机制。简单的来说,设备树就是一个描述硬件平台的信息的文本文件,内核通过解析这个文件来了解硬件平台的配置。
2、设备树的作用
设备的主要作用:
- 描述硬件的拓扑结构:使用树形结构可以清晰的描述硬件平台上的各种设备以及相互的关系。例如哪个USART链接着哪些GOIO控制器,哪个中断控制器控制着那个外设的中断。
- 传递硬件配置信息:将硬件设备的地址、中断号、时钟频率等配置信息传递给内核。
- 实现硬件描述与驱动代码的分离:使得驱动程序更见通用,时许更具设备树中的信息进行适当的调整,而无需修改驱动代码。
3、设备树的基本概念
设备树有节点(node)和属性(property)组成,形成一个树状结构。
节点(node):代表硬件设备,例如内存、外设(USART、SPI、IIC等)。每个节点都有唯一的名称。并且包含多个属性。节点之间存在着父子关系。根节点可以用‘/’表示。
属性(property):描述节点的特性,例如设备的地址(reg)、中断号(interrupts)、时钟频率(clock-frequency)等。属性有键值成对组成,键是属性的名称,值属性的数据。
举一个简单的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14/ { // 根节点
compatible = "my-company,my-platform"; // 兼容性字符串,用于驱动匹配
model = "My Platform v1.0"; // 平台型号
memory { // 内存节点
reg = <0x40000000 0x10000000>; // 内存起始地址和大小
};
uart0: uart@1000 { // UART 节点,命名为 uart0,地址为 0x1000
compatible = "ns16550a"; // UART 控制器类型
reg = <0x1000 0x100>; // UART 寄存器地址和大小
interrupts = ; // UART 中断号
};
};
4、设备树的表示形式:DTS、DTB、DTC
DTS(Device Tree Source):设备树的源文件,以.dts为结尾的文件。在使用时可以像C语言中引用头文件一样使用
#include
指令。DTC(Device Tree Compiler):设备树的编译器。是一个工具程序,类似于gcc,可以将DTS文件编一个二进制的DTB文件。
DTB(Device Tree Blob):编译后的二进制文件,内核在启动时会加载DTB文件,从其中获取硬件信息。
使用dtc命令可以将DTS文件编译成DTB文件。
1
dtc -I dts -O dtb -o my_device.dtb my_device.dts
- dtc:命令
- -I dts:指定输入文件格式为DTS
- -O dtb:指定输出文件格式为DTB
- -o my_device.dtb:指定输出DTB的名称
- my_device.dts:指定输入的DTS的名称
2、设备树的框架
1、设备的组织结构:树形结构
设备树采用树形结构来描述硬件平台的组成和连接关系。
根节点(root node):用
/
表示,是设备树的顶层节点,描述着整个硬件平台的基本信息。子节点(child node):根节点下的节点,描述平台生的各个设备,例如CPU、内存、总线、外设等。子节点也可以有自己的子节点。
父节点(parent node)和子节点(child node):节点之间通过父子关系链接起来,表示设备之间的层次关系和链接关系。
示例:
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
28
29
30
31/ { // 根节点
compatible = "my-company,my-platform";
model = "My Platform v1.0";
cpus { // CPU 节点
cpu@0 { // 第一个 CPU 节点
compatible = "arm,cortex-a9";
reg = <0x0>;
};
};
memory { // 内存节点
reg = <0x40000000 0x10000000>;
};
uart0: uart@1000 { // UART 节点
compatible = "ns16550a";
reg = <0x1000 0x100>;
interrupts = ;
};
i2c0: i2c@2000 { // I2C 控制器节点
compatible = "i2c-designware";
reg = <0x2000 0x100>;
eeprom@50 { // 连接在 i2c0 上的 EEPROM 芯片节点
compatible = "atmel,24c32";
reg = <0x50>;
};
};
};
2、设备树的语法规则
设备树使用Device Tree Source(DTS)的文本格式进行描述,类似于C语言的结构体。
节点定义:
1
2
3
4[label:] node-name@unit-address {
properties;
child-nodes;
};- label:节点标签,用于在其他地方使用该节点时,可选。例如uart0、i2c0
- node-name:节点名称,描述设备的类型。例如CPU、memory、uart。
- unit-address:节点地址,用于区分同一类型的设备,可选。例如@0200.
- properties:节点属性,描述设备的具体属性。
- child-nodes:节点的子节点。
属性定义:
1
property-name = value;
* property-name:属性名称,是一个字符串。例如compatible、reg、interrups。 * value:属性的值。
包含头文件
- 使用
#include
指令可以包含其他的DTS文件或者DTSI文件。
1
#include "header.dtsi"
- 使用
3、设备树的重要概念
compatible 属性:它时设备树中最重要的属性之一,它表示一个字符串列表,用于驱动程序和设备进行匹配。驱动程序会查找设备树中compatible属性包含其支持的字符串节点,如果匹配成功,则驱动程序就负责管理该设备。
例如:
1
compatible = "ns16550a", "uart16550";
表示该设备兼容ns16550a和uart16550两种类型的驱动程序。
model属性:用于指定设备的制造商和型号。
例如:
1
model = "EmbedFire LubanCat2 HDMI+MIPI";
status属性:用于指示设备的操作状态,通过status去禁止设备或者启用设备。
1
2
3
4/* External sound card */
sound: sound {
status = "disabled";
};| 状态值 | 描述 |
| ——– | ————————————- |
| okay | 使能设备 |
| disabled | 禁用设备 |
| fail | 表示设备不可运行, |
| fail-sss | 表示设备不可运行,sss与具体的设备相关 |
reg属性:用于描述地址空间资源,通常包含设备的起始地址和大小。
例如:
1
reg = <0x1000 0x100>;
表示的意思是设备的其实地址是0x1000,大小为0x100字节。
interrupt属性:用于描述设备的中断资源,通产包含中断号和中断触发方式。
追加/修改节点内容
1
2
3&cpu0 {
cpu-supply = <&vdd_cpu>;
};这些代码并不包含在根节点下,他们不是一个新的节点,而是像原有的节点追加内容。在上面的例子中,&cpu0表示像节点标签cpu0的节点追加数据,这个节点可能定义在泵文件也可以定义在迸溅所包含的设备树文件中。
特殊节点
aliases子节点
aliases子节点的作用就是为其他节点起一个别名,示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14aliases {
csi2dphy0 = &csi2_dphy0;
csi2dphy1 = &csi2_dphy1;
csi2dphy2 = &csi2_dphy2;
/*----------- 省略------------*/
mmc0 = &sdhci;
mmc1 = &sdmmc0;
mmc2 = &sdmmc1;
mmc3 = &sdmmc2;
serial0 = &uart0;
serial1 = &uart1;
serial2 = &uart2;
/*----------- 以下省略------------*/
}以serual0 = &uart0为例,serial0是一个节点的名字,设置别名后我门可以使用serial0来指代uart0节点。
chsen子节点
chosen子节点位于根节点下,示例:
1
2
3chosen {
bootargs = "earlycon=uart8250,mmio32,0xfe660000 console=ttyFIQ0 root=PARTUUID=614e0000-0000 rw rootwait";
};chosen子节点不代表实际硬件,它主要用于内核传递参数。此外这个节点还可以用作uboot像linux内核传递配置参数的通道,我梦在uboot设置好参数,就是通过这个节点向内核中传递的。
3、如何获取设备树节点信息
在设备树中‘’节点‘’对应着实际硬件中的设备,我梦在设备树中田间一个led节点,正常情况下我们可以从这个节点获取编写led驱动所用的所有信息。
在内核中提供了一组函数用于从设备节点中获取资源的函数,这些函数以of_开头,称为OF操作。
1、查找节点函数
根据节点路径寻找节点函数
1
struct device_node *of_find_node_by_path(const char *path)
参数:
* path:指定节点在设备树中的路径
返回值:
device_node:结构体指针,失败返回为NULL;成功返回一个指针,该指针中保存着设备节点的信息。
device_node结构体如下,它定义在
<linux/of.h>
头文件中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22struct device_node {
const char *name; // 节点的名称,例如 "uart@1000"
phandle phandle; // 节点的句柄,用于在设备树中唯一标识一个节点
const char *full_name; // 节点的完整路径名,例如 "/soc/uart@1000"
struct fwnode_handle fwnode; // 用于统一访问不同类型的固件节点,如设备树、ACPI等
struct property *properties; // 指向节点属性链表的指针
struct property *deadprops; // 指向已删除属性链表的指针(用于调试)
struct device_node *parent; // 指向父节点的指针
struct device_node *child; // 指向第一个子节点的指针
struct device_node *sibling; // 指向下一个兄弟节点的指针
#if defined(CONFIG_OF_KOBJ)
struct kobject kobj; // 用于将设备节点注册到 sysfs 文件系统中
#endif
unsigned long _flags; // 节点标志
void *data; // 用于存储驱动程序特定的数据
#if defined(CONFIG_SPARC)
unsigned int unique_id; // SPARC 平台特有的唯一 ID
struct of_irq_controller *irq_trans; // SPARC 平台特有的中断转换
#endif
};name:节点中属性为name的值
phandle:节点中为device-type的值
full-name:节点的名字,在device_node结构体后面放一个字符串,full-name指向它
properties:链表,连接该节点的所有属性
1
2
3
4
5
6
7struct property {
char *name; // 属性的名称,例如 "compatible"、"reg"
int length; // 属性值的长度(以字节为单位)
void *value; // 指向属性值的指针
struct property *next; // 指向下一个属性的指针,构成链表
unsigned long _flags;
};parent:指向父节点
child:指向子节点
sibling:指向兄弟节点
根据系欸但名字寻找节点函数
1
struct device_node *of_find_node_by_name(struct device_node *from,const char *name);
参数:
- from:致电给从哪个节点开始查找,它本身不在查找行列中,之查找它之后的节点。
- name:要寻找的节点名。
返回值:
* device—node:节点结构体指针,上面的一样。
根据节点类型寻找节点函数
1
struct device_node *of_find_node_by_type(struct device_node *from,const char *type)
参数:
- from:指定从哪个节点开始找。
- type:要寻找的节点的类型。
返回值:
- device_node: device_node类型的结构体指针。
根据节点类型和compeatible属性寻找节点函数
1
struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible)
相对于of_find_node_by_name,多了一个compatible(属性)的参数。
compatible:要寻找的节点的兼容性属性。
根据匹配表寻找节点函数
1
static inline 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)
相对于前面的几种,多了matches、match参数。
matches:指向一个of_device_id数组的指针。这个数组包含了要匹配的设备ID列表。
match:指向of_device_id指针的指针。如果找到匹配的节点,则将匹配的结构体的地址存储在这个指针指向的位置。
struct of_device_id结构体:
1
2
3
4
5
6
7
8
9/*
* Struct used for matching a device
*/
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};- name:设备的名称。
- type:设备的类型。
- compatible:设备的兼容性字符串。用于驱动和设备的匹配。
- data:指向驱动私有数据的指针。
寻找父节点函数
1
struct device_node *of_get_parent(const struct device_node *node)
参数:
- node:指定要查找父节点的节点。
返回值:
- node:是一个指向device_node类型的结构体指针。
寻找子节点函数
1
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)
- node:指定谁要查找它的子节点。
- prev:前一个子节点。这是一个迭代的过程。就像找孙子,你要先把哪个儿子的节点找到,才能找到该儿子下的孙子节点。如果为NULL,则直接找儿子节点。
这里介绍的七个寻找节点函数,共同点是返回值类型都相同。只要找到节点就可以返回节点对应的device-node结构体,通过这个结构体就可以获取到设备节点中的属性信息、父子节点等。
第一个函数of_find_node_by_path与其他六个不同,它是通过节点路径来寻找节点的,节点路径是从设备树源文件(.dts)中找到的。中间四个函数根据节点属性在某一节点之后查找相符合的要求的设备节点。最后两个根据父子系节点进行查找。
2、提取属性值的of函数
上面我们看了七个查找节点的函数,他们都是找到一个设备节点然后返回相对应的结构体指针device-node。下面我们讲解从结构体中获取我们想要的设备节点属性信息。
查找节点属性函数
1
struct property *of_find_property(const struct device_node *np,const char *name,int *lenp)
参数:
- np:指定要获取的设备节点的属性信息。
- name:属性名
- lenp:获取等到的属性值的大小。这个指针作为输出参数,这个参数带回的值是实际获取得到的属性大小
返回值:
property:获取的属性。而struct property *,我们称为系欸但属性结构体。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct 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
};- name:属性名
- length:属性长度
- value:属性值
- next:下个属性
读取整形属性函数
读取属性函数是一组函数,分别为8、16、32、64位数据。
1
2
3
4
5
6
7
8
9
10
11//8位整数读取函数
int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz)
//16位整数读取函数
int of_property_read_u16_array(const struct device_node *np, const char *propname, u16 *out_values, size_t sz)
//32位整数读取函数
int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz)
//64位整数读取函数
int of_property_read_u64_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz)参数:
- np:指定要读取那个设备节点结构体。
- propname:指定要获取设备几点的那个属性
- out-value:这是一个输出参数,是函数的返回值,保存读取等到的数据
- sz:这是一个输入参数,用于设置读取的长度。
返回值:
- 成功返回0,错误返回错误状态吗。-EINVAL(属性不存在)、-ENODATA(没有要读取的数据)、-EOVERFLOW(属性值列表太小)。
读取字符串属性函数
1
int of_property_read_string(const struct device_node *np,const char *propname,const char **out_string)
参数:
- np:要读取那个设备节点的属性
- propname:属性名
- out-string:获取等待字符串的指针。
返回值:
- 成功返回0,失败返回错误状态吗
推荐使用下面的这个函数
1
int of_property_read_string_index(const struct device_node *np,const char *propname, int index,const char **out_string)
相对于前面增加了index,用于指定读取属性值中的第几个字符串,index从零开始计数。
读取布尔型属性函数
1
static inline bool of_property_read_bool(const struct device_node *np, const char *propname);
参数:
- np:要读取的那个设备节点的属性信息
- propname:属性名
返回值:
- 这个函数不是读取某个布尔型属性的值,仅仅读取这个属性存在或者不存在,如果想要获取值,还是建议使用“全能”函数查找节点of_find_property。
3、内存映射相关的of函数
在设备树的节点中大多包含一些捏村想过的属性,比如常用的reg属性。通常情况下,等到寄存器地址之后我们还要通过ioremap函数将物理地址转化为虚拟地址。现在内核提供了一些of函数,自动完成物理地址到虚拟地址的转换。
1 | void __iomem *of_iomap(struct device_node *np, int index) |
参数:
- np:要获取的那个设备节点的属性信息
- index:通常情况下reg属性包含多段,index用于指定映射哪一段,从零开始
返回值:
- 成功返回转换的地址。失败返回NULL。
内核也提供看常规获取地址的of函数,这些函数得到的值就是我们在设备树中设置的地址值,
1 | int of_address_to_resource(struct device_node *dev, int index, struct resource *r) |
参数:
- dev:指定要获取的那个设备节点的属性信息
- index:通常情况下reg包含多端。
- r:这是要给resource结构体,是输出桉树用于返回得到的地址信息。
返回值:
- 成功返回0,失败返回错误状态吗
1 | struct resource { |
- start:起始地址
- end:结束地址
- name:属性的名称
4、使用设备树驱动led示例
1、像设备树中添加节点:
在linux内核源码中找到设备树文件。(/home/h/h/linux-5.4.31/arch/arm/boot/dts)
打开设备树文件,在其中添加led节点。(stm32mp157a-fsmpla.dts)
1
2
3
4
5
6
7
8
9
10led_test@0x54004000{
compatible = "h,led1";
reg = <0x54004000 0x400>;
led_name = "led01";
led_minor = <11>;
moder_clear = <0x3f>;
moder_data = <0x15>;
odr = <0x7>;
shift = <5>;
};参数:
* led_test:设备节点名称。 * @0x54004000:设备的起始地址。当属性中有reg时,可以在这里添加0x54004000,当然有没有并没有什么影响。 * compatible:用于匹配驱动程序。 * reg:设备的寄存器空间地址。包括其实地址和空间大小。 * (下面就是自定义属性) * led_name:设备名称。 * moder_clear:清理GPIO引脚的输入输出端口。 * moder_data:配置GPIO引脚。 * odr:输出数据寄存器的值。决定了led的初始状态。 * shift:寄存器的位移量。
添加好设备节点后,保存退出。
进入linux内核目录下进行编译,重新传入板子中。
1
2
3h@h-virtual-machine:~/h/linux-5.4.31$ source /opt/stm32_sdk/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi
h@h-virtual-machine:~/h/linux-5.4.31$ make -j4 ARCH=arm dtbs LOADADDR=0xC2000040
cp arch/arm/boot/dts/stm32mp157a-fsmpla.dtb /tftpboot/重新启动单片机,linux内核就会重新加载我们新写好的设备树文件。
2、编写驱动代码(led_pdrv.c)
编写驱动框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16static int __init led_pdev_init(void)
{
printk("--------- %s ---------\n", __FUNCTION__);
// 2、注册platform-driver对象
return platform_driver_register(&led_pdrv);
}
static void __exit led_pdrv_exit(void)
{
printk("--------- %s ---------\n", __FUNCTION__);
platform_driver_unregister(&led_pdrv);
}
module_init(led_pdev_init);
module_exit(led_pdrv_exit);
MODULE_LICENSE("GPL");创建platform-driver对象,并且通过led_match_table中的compatible与驱动进行匹配。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 与pdev中的name匹配
const struct platform_device_id led_id_table[] = {
{ "mp157_led", 0xa },
};
// 与设备树中的compatible匹配
const struct of_device_id led_match_table[] = {
{ .compatible = "h,led1" },
{ /* sentinel */ }
};
struct platform_driver led_pdrv = {
.probe = led_pdrv_probe,
.remove = led_pdrv_remove,
.driver = {
.name = "fsmp1_led",
.of_match_table = led_match_table,
},
.id_table = led_id_table,
};编写led_pdrv_probe、led_pdrv_remove函数,如果驱动匹配成功后,会调用led_pdrv_probe函数执行,在卸载时调用led_pdrv_remove函数。
int led_pdrv_probe(struct platform_device *pdev)
分配全局设备对象空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 设计led的结构体
struct mp157_led_t {
struct miscdevice misc;
unsigned int *moder;
unsigned int *odr;
int m_clear;
int m_data;
int o_data;
int shift;
};
// 1、分配全局设备对象空间
led_dev = kzalloc(sizeof(*led_dev), GFP_KERNEL);
if (!led_dev) {
printk("kzalloc failed\n");
return -ENOMEM;
}获取设备节点和数据
1
2
3
4
5
6
7
8// 获取设备树节点对象
np = pdev->dev.of_node; //这里的np是直接获取pdev的设备树节点,并不是寻找到设备树节点
of_property_read_string(np, "led_name", (const char **)&name);
of_property_read_u32(np, "led_minor", &minor);
of_property_read_u32(np, "led_clear", &led_dev->m_clear);
of_property_read_u32(np, "moder_data", &led_dev->o_data);
of_property_read_u32(np, "odr", &led_dev->o_data); // 修正:读取odr属性,赋值给o_data
of_property_read_u32(np, "shift", &led_dev->shift);初始化杂项设备
1
2
3led_dev->misc.name = name;
led_dev->misc.minor = minor;
led_dev->misc.fops = &led_fops;注册杂项设备
1
2
3
4
5ret = misc_register(&led_dev->misc);
if (ret) {
printk("misc register failed\n");
goto err_kfree;
}硬件初始化
1
2
3
4
5
6
7led_dev->moder = ioremap(res1->start, res1->end - res1->start + 1);
if (!led_dev->moder) {
printk("ioremap failed\n");
ret = -EINVAL;
goto err_misc_deregister;
}
led_dev->odr = led_dev->moder + 5;
int led_pdrv_remove(struct platform_device *pdev)
平台资源的释放和注销(先注册后释放)
1
2
3
4printk("--------- %s ---------\n", __FUNCTION__);
iounmap(led_dev->moder);
misc_deregister(&led_dev->misc);
kfree(led_dev);
编写文件操作函数open、write、release。
首先定义结构体
1
2
3
4
5
6const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_pdrv_open,
.write = led_pdrv_write,
.release = led_pdrv_close,
};int led_pdrv_open(struct inode *inode, struct file *filp)
1
2
3// 将GPIOZ——5、6、7设置成输出模式
*led_dev->moder &= ~(led_dev->m_clear << (led_dev->shift * 2));
*led_dev->moder |= led_dev->m_data << (led_dev->shift * 2);ssize_t led_pdrv_write(struct file *filp, const char __user *buf, size_t size, loff_t *flags)
将应用数据转换为内核数据
1
2
3
4
5ret = copy_from_user(&value, buf, size);
if (ret) {
printk("copy from user error\n");
return -EAGAIN;
}根据传输的值进行开关灯
1
2
3
4
5if (value) { // 开灯
*led_dev->odr |= led_dev->o_data << led_dev->shift;
} else { // 关灯
*led_dev->odr &= ~(led_dev->o_data << led_dev->shift);
}
int led_pdrv_close(struct inode *inode, struct file *filp)
1
*led_dev->odr &= ~(led_dev->o_data << led_dev->shift);