STM32MP157驱动开发——Linux设备树


一、设备树相关知识

1.设备树是什么

  设备树(Device Tree),可以理解为按照树形结构描述板级设备。描述设备树的文件为DTS(Device Tree Source),也就是开发板上的设备信息,比如CPU数量,内存基地址,IIC接口及连接的设备等。使用DTC工具(支持Makefile-gcc编译)将DTS文件进行编译,就得到了设备树镜像xxxx.dtb文件。
  如下图所示,树的主干就是系统总线, IIC 控制器、 GPIO 控制器、 SPI 控制器等都是接到系统主线上的分支。IIC 控制器有分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02这两个 IIC 设备, IIC2 上只接了 MPU6050 这个设备。 DTS 文件的主要功能就是按照下面的结构来描述板子上的设备信息。
在这里插入图片描述

2.设备树的由来

  早期的Linux内核是没有设备树的,那时候的Linux板级支持包(BSP)会将所有的板载设备信息编译进内核中,只需要一次烧写对应的开发板镜像,即可支持所有的板载设备。在2011年,Linux之父Linus认为这种内核组成方式违背了Linux设计之初的简洁、高效理念,所以要求开发人员将设备信息这部分从内核中摘除,独立编译成相关的设备树文件。这样大大精简了内核部分的相关代码,同时也提高了Linux Kernel部分的通用性,只需要分别移植内核和相关的板级设备树文件,即可完成适配,而所需要付出的代价仅仅是多烧写一个设备树镜像而已,如果使用相关的烧写工具或烧写脚本,则可以忽略不计。所以一些厂商后来设计的开发板,没有了历史包袱,只支持设备树类型的系统镜像。

3.DTS相关语法

  以.dtsi结尾的文件为DTS的头文件,dtsi文件与dts文件的语法和内容相同,只不过一般存放的是一些通用的设备描述,便于移植。在dts文件中可以使用#include xxxx.dtsi来引用头文件。dts文件一般是特定的板级设备的适配,且dts文件也可以使用include进行包含(一般不推荐),除此之外,还可以包含C语言的.h文件。
例:

#include <dt-bindings/interrupt-controller/arm-gic.h>
#include <dt-bindings/clock/stm32mp1-clks.h>
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/reset/stm32mp1-resets.h>
#include <dt-bindings/thermal/thermal.h>

/ {
	#address-cells = <1>;
	#size-cells = <1>;
	
	cpus {
		#address-cells = <1>;
		#size-cells = <0>;
	
		cpu0: cpu@0 {
			compatible = "arm,cortex-a7";
			......
			nvmem-cell-names = "part_number";
			#cooling-cells = <2>;
		};
	};
	cpu0_opp_table: cpu0-opp-table {
		compatible = "operating-points-v2";
		opp-shared;
	};

...
	soc {
		compatible = "simple-bus";
		#address-cells = <1>;
		#size-cells = <1>;
		interrupt-parent = <&intc>;
		ranges;
		
		sram: sram@10000000 {
			compatible = "mmio-sram";
			reg = <0x10000000 0x60000>;
			#address-cells = <1>;
			#size-cells = <1>;
			ranges = <0 0x10000000 0x60000>;
		};
	};
};

描述了板载的CPU和外设信息。/表示的是根节点,一个dts文件只会有一个根节点,不同的dts合并时会将根节点下的设备进行合并。在设备树中节点命名格式为node-name@unit-address,“node-name”是节点名字,可以用来描述节点的功能。“unit-address”一般表示设备的地址或寄存器首地址,如果没有地址或者寄存器可以不要。还可以使用标签来更方便的访问节点,如label: node-name@unit-address可以使用&label来访问节点。

4.标准属性

  节点是具体的设备,由一堆属性组成,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性, Linux 下的很多外设驱动都会使用这些标准属性。
①compatible属性
“兼容性”属性,值为一个字符串列表,用于绑定设备和驱动。例如:compatible = "cirrus,my_cs42l51","cirrus,cs42l51";其中cirrus为厂商,第二个值表示驱动模块名,属性值可以是多个。Linux 内核首先使用第一个兼容值进行查找,如果没有找到就使用第二个,直到查找完 compatible 属性中的所有值。一般驱动文件会使用OF匹配表保存可使用的 compatible 属性值,如果设备节点的值与其中某一个相等,则可以使用这个驱动。
②model属性
model 属性值也是一个字符串,一般用于描述开发板名称或设备模块信息。
③status属性
用来描述设备的状态信息:

描述
okey可操作
disable当前不可操作,但可以根据情况改变
fail不可操作,且不大可能变为可操作
fail-sss与fail相同,sss为检测到的错误内容

④#address-cells 和#size-cells 属性
值为无符号32位整形,可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。#address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位), #size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位),即描述起始地址和地址长度的属性。比如reg = <address1 length1 address2 length2 address3 length3……>,address为起始地址,length为地址长度,#address-cells则表明address数据的字长,#size-cells 表明 length 这个数据所占用的字长。
⑤reg属性
一般是(address, length)对,用于描述设备地址空间资源信息或者设备地址信息。
⑥ranges属性
可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵。ranges是一个地址映射/转换表,每个属性由子地址、父地址和地址空间长度三部分组成。
例:

soc {
	compatible = "simple-bus";
	#address-cells = <1>;
	#size-cells = <1>;
	interrupt-parent = <&intc>;
	ranges = <0 0x10000000 0x100000>;
	
	sram: sram@10000000 {
		compatible = "mmio-sram";
		reg = <0x0 0x60000>;
		#address-cells = <1>;
		#size-cells = <1>;
		ranges = <0 0x10000000 0x60000>;
	};
};

⑦name属性
用于记录节点名称,新版本中已经弃用,不推荐使用。
⑧device_type属性
多数已弃用,有些CPU或Memory节点可能会使用一下

二、创建自定义设备树

1.创建小型模板设备树

板载设备:
① 这个芯片是由两个 Cortex-A7 架构的 32 位 CPU 和 Cortex-M4 组成。
② STM32MP157 内部 sram,起始地址为 0x10000000,大小为 384KB(0x60000)。
③STM32MP157 内部 timers6,起始地址为 0x40004000,大小为 25.6KB(0x400)。
④STM32MP157 内部 spi2,起始地址为 0x4000b000,大小为 25.6KB(0x400)。
⑤ STM32MP157 内部 usart2,起始地址为 0x4000e000,大小为 25.6KB(0x400)。
⑥ STM32MP157 内部 i2c1,起始地址为 0x40012000,大小为 25.6KB(0x400)

设备树描述文件myfirst.dts:

#include xxxx.dtsi

/{
	compatible = "st, stm32mp157d-atk", "st, stm32mp157";
	/*CPU节点*/
	cpus {
		#address-cells = <1>;
		#size-cells = <0>;
		
		/* CPU0 节点 */
		cpu0: cpu@0 {
			compatible = "arm,cortex-a7";
			device_type = "cpu";
			reg = <0>;
		};
		/* CPU1 节点 */
		cpu1: cpu@1 {
			compatible = "arm,cortex-a7";
			device_type = "cpu";
			reg = <1>;
		};
	};

	/* soc 节点 */
	soc {
		compatible = "simple-bus";
		#address-cells = <1>;
		#size-cells = <1>;
		ranges;		/*ranges 属性为空,说明子空间和父空间地址范围相同*/
		/* sram 节点 */
		sram: sram@10000000 {
			compatible = "mmio-sram";
			reg = <0x10000000 0x60000>;
			ranges = <0 0x10000000 0x60000>;
		};
		/* timers6 节点 */
		timers6: timer@40004000 {
			#address-cells = <1>;
			#size-cells = <0>;
			compatible = "st,stm32-timers";
			reg = <0x40004000 0x400>;
		};
		/* spi2 节点 */
		spi2: spi@4000b000 {
			#address-cells = <1>;
			#size-cells = <0>;
			compatible = "st,stm32h7-spi";
			reg = <0x4000b000 0x400>;
		};
		/* usart2 节点 */
		usart2: serial@4000e000 {
			compatible = "st,stm32h7-uart";
			reg = <0x4000e000 0x400>;
		};
		/* i2c1 节点 */
		i2c1: i2c@40012000 {
			compatible = "st,stm32mp15-i2c";
			reg = <0x40012000 0x400>;
		};
	};
};

2.设备树在系统中的体现

linux内核在启动时,会在/proc/device-tree目录下,根据节点名称创建不同的文件夹。例如,/proc/devicetree/base目录下,文件module的内容为“ STMicroelectronics STM32MP157D eval daughter”,与dts文件中的属性值相同。在xxx/base/soc下,则是soc的子节点信息。

3.特殊节点

  在根节点“/”中有两个特殊节点:aliaseschosen,aliases的功能是定义别名,类似于前面的lable: xxxx定义,然后用&lable访问。chosen不是一个真实设备,此节点主要是为了向内核传递数据,一般dts文件中的chosen节点为空或者内容很少。
例:

aliases {
	serial0 = &uart4;
};
chosen {
	stdout-path = "serial0:115200n8";
};

在源文件中,仅设置了chosen的属性stdout-path,表示默认输出使用serial0,而在aliases中又将serial0设置为uart4,所以默认的输出会通过串口4打印。在板子的/proc/device-tree/chosen目录下,会发现还会多出一个bootargs属性,这个就是在U-Boot中自定义的启动参数,即chosen将人为的设置传递给了内核。

4.绑定信息文档

  在Linux源码目录/Documentation/devicetree/bindings中,有详细的文档指导如何为设备树添加不同种类的节点,包括cpu、clock、connector等。
例:
在这里插入图片描述

5.设备树常用OF操作函数

  设备树中描述了详细的设备信息,当编写驱动时往往需要用到。例:设备树使用 reg 属性描述某个外设的寄存器地址为 0X02005482,长度为 0X400,在编写驱动的时候需要获取到 reg 属性的0X02005482 和 0X400 这两个值初始化外设。那么就可以使用Linux内核提供的"of_xxx"函数来获取这些参数,又称为OF函数,相关函数原型在"include/linux/of.h"中。
  Linux内核使用device_node结构体来描述一个节点,结构体的定义也在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
};

其中包括5个OF函数:
of_find_node_by_name
原型

struct device_node *of_find_node_by_name(struct device_node *from, const char *name);

功能:通过节点名字查找指定的节点
参数
from:开始查找的节点,如果为NULL,查找整个设备树
name:要查找的节点名称
返回值:找到的节点,NULL表示查找失败
of_find_node_by_type
原型

struct device_node *of_find_node_by_type(struct device_node *from, const char *type)

功能:通过节点的device_type属性查找节点
参数
from:开始查找的节点,如果为NULL,查找整个设备树
type:要查找的节点对应的type字符串,即device_type的值
返回值:找到的节点,NULL表示查找失败
of_find_compatible_node
原型

struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)

功能:通过节点的 device_type 和 compatible 两个属性查找节点
参数
from:开始查找的节点,如果为NULL,查找整个设备树
type:要查找的节点对应的type字符串,即device_type的值,可以为NULL,表示忽略type属性
compatible:要查找的节点所对应的 compatible 属性列表
返回值:找到的节点,NULL表示查找失败
of_find_matching_node_and_match
原型

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)

功能:通过 of_device_id 匹配表查找指定的节点
参数
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树
matches:of_device_id 匹配表,在此匹配表里面查找节点
match:找到的匹配的 of_device_id
返回值: 找到的节点,如果为 NULL 表示查找失败
of_find_node_by_path
原型

inline struct device_node *of_find_node_by_path(const char *path)

功能:通过路径来查找指定的节点
参数
path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径
返回值:找到的节点,如果为 NULL 表示查找失败

6.查找父/子节点的OF函数

of_get_parent
原型

struct device_node *of_get_parent(const struct device_node *node)

功能:获取指定节点的父节点
参数
node:要查找的父节点的节点
返回值:找到的父节点
of_get_next_child
原型

struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)

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

7.提取属性值的OF函数

  节点的属性值保存在结构体property中,此结构体的定义也在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
};

提取属性的OF函数有:
of_find_property
原型

property *of_find_property(const struct device_node *np, const char *name, int *lenp)

功能:查找指定的属性
参数
np:设备节点
name:属性名字
lenp:属性值的字节数
返回值:找到的属性

of_property_count_elems_of_size
原型

int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size)

功能
获取属性中元素的数量,例:reg 属性值是一个数组,使用此函数可以获取到这个数组的大小
参数
np:设备节点
proname:需要统计元素数量的属性名字
elem_size:元素长度。
返回值:得到的属性元素数量

of_property_read_u32_index
原型

int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value)

功能
用于从属性中获取指定标号的无符号32位类型数据值(u32),如果某个属性有多个 u32 类型的值,可以使用此函数来获取指定标号的数据值
参数
np:设备节点
proname:要读取的属性名字
index:要读取的值标号
out_value:读取到的值
返回值
0:读取成功
负值:读取失败
-EINVAL:属性不存在
-ENODATA:没有要读取的数据
-EOVERFLOW:属性值列表太小

of_property_read_u8(u16 u32 u64)_array

原型

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)

功能
读取属性中 u8、u16、u32 和 u64 类型的数组数据,比如大多数的 reg 属性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据
参数
np:设备节点
proname:要读取的属性名字
out_value:读取到的数组值,分别为 u8、u16、u32 和 u64。
返回值
0:读取成功
负值:读取失败
-EINVAL:属性不存在
-ENODATA:没有要读取的数据
-EOVERFLOW:属性值列表太小

of_property_read_u8(u16 u32 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)

功能
有些属性只有一个整型值,这四个函数就是用于读取这种只有一个整形值的属性,分别用于读取 u8、u16、u32 和 u64 类型属性值
参数
np:设备节点
proname:要读取的属性名字
out_value:读取到的属性值
返回值
0:读取成功
负值:读取失败
-EINVAL:属性不存在
-ENODATA:没有要读取的数据
-EOVERFLOW:属性值列表太小

of_property_read_string

原型

int of_property_read_string(struct device_node *np, const char *propname, const char **out_string)

功能
用于读取属性中字符串值
参数
np:设备节点
proname:要读取的属性名字
out_value:获取到的字符串值
返回值
0:读取成功
负值:读取失败

of_n_addr_cells

原型

int of_n_addr_cells(struct device_node *np)

功能
用于获取 #address-cells 属性值
参数
np:设备节点
返回值
获取到的 #address-cells 属性值

of_n_size_cells

原型

int of_n_size_cells(struct device_node *np)

功能
用于获取 #size-cells 属性值
参数
np:设备节点
返回值
获取到的 #size-cells 属性值

8.其他常用的OF函数

of_device_is_compatible

原型

int of_device_is_compatible(const struct device_node *device, const char *compat)

功能
用于查看节点的 compatible 属性是否有包含 compat 指定的字符串,即检查设备节点的兼容性
参数
device:设备节点
compat:要查看的字符串
返回值
0:节点的 compatible 属性中不包含 compat 指定的字符串
正数:节点的 compatible属性中包含 compat 指定的字符串。

of_get_address

原型

const __be32 *of_get_address(struct device_node *dev, int index, u64 *size, unsigned int *flags)

功能
用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值
参数
dev:设备节点
index:要读取的地址标号
size:地址长度
flags:参数,比如 IORESOURCE_IO、 IORESOURCE_MEM 等
返回值
读取到的地址数据首地址,为 NULL 的话表示读取失败

of_translate_address

原型

u64 of_translate_address(struct device_node *dev, const __be32 *addr)

功能
将从设备树读取到的地址转换为物理地址
参数
dev:设备节点
in_addr:要转换的地址
返回值
得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败

of_address_to_resource

原型

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

功能
将 reg 属性值转换为 resource 结构体类型,resource结构体描述的都是设备资源信息,如IIC、SPI外设中的寄存器地址或资源
参数
dev:设备节点。
index:地址资源标号
r:得到的 resource 类型的资源值
返回值
0:成功
负值:失败

of_iomap

原型

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

功能
用于直接内存映射,采用设备树后可以使用of_iomap()替换原先使用的ioremap()函数,直接获取内存地址到虚拟地址的映射。很常用!
参数
dev:设备节点
index: reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0
返回值
经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值