i.MX6ULL驱动开发 | 04-Linux设备树基本语法与实例解析

Posted Mculover666

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了i.MX6ULL驱动开发 | 04-Linux设备树基本语法与实例解析相关的知识,希望对你有一定的参考价值。

文章目录

一、设备树简介

1. 设备树在ARM架构的引入

在之前使用S3C2440开发板移植Linux 3.4.2内核时,修改了很多关于c文件去适配开发板,和开发板相关的文件放在arch/arm/mxch-xxx目录下,因此linux内核arm架构下添加了很多开发板的适配文件:

这些c文件仅仅用来适配某款开发板,对于Linux内核来说并没有提交什么新功能,但是每适配一款新的开发板就需要一堆文件,导致Linux内核越来越臃肿:

终于Linus忍不住天天merge这些鬼东西,向arm社区发出了一封邮件,第一句话就足矣表现不满:“This whole ARM thing is a f*cking pain in the ass”。

因此,Arm社区开始引入之前powerPC架构就采用的设备树,将描述这些板级信息的文件与Linux内核代码分离,Linux 4.x版本几乎都支持设备树,所有开发板的设备树文件统一放在arch/arm/boot/dts目录中

2. 什么是设备树

设备树全称Device Tree,是一种数据结构,用来描述板级设备信息,比如CPU数量、外设基地址、总线设备等,如图:

3. DTS、DTSI、DTB

(1)DTS:设备树描述文件为.dts格式,这个也是我们重点需要掌握编写的。
(2)DTSI

为了减少冗余,设备树头文件格式为.dtsi文件,可以被不同的.dts文件引用。

比如imx6ull有野火、正点原子、米尔、百问网等很多款开发板,这些开发板肯定需要一个dts文件来描述,但是关于imx6ull芯片级别的描述,就不需要每个文件都去描述一下,而是大家都引用NXP官方提供的.dtsi描述文件即可。

这样既最大化的降低了设备描述文件的冗余程序,也极大的降低了开发者适配新开发板的工作量

(3)DTC

编写.dtc文件使用设备树语法,则需要一个特定的编译器来编译,称为dtc工具,源码在Linux内核的scripts/dtc目录下。

(4)DTB:设备树源码.dts.dtsi文件最终经过dtc编译器,会生成.dtb文件。

4. 设备树编译

(1)简单粗暴,编译内核

make

(2)编译全部设备树文件

make dtbs

(3)编译指定的设备树文件

make <xxx.dtb>

二、设备树语法

1. 设备树版本

/dts-v1/

2. 设备树节点

设备树是由一个个节点组成的,每个节点相当于树上的一片叶子,节点的结构和约定如下。

(1)节点名称

node-name@unit-address

node-name指明了节点的名称,长度应该为1-31个字符,命名应该以小写或者大写字母开头,支持的字符如下表:

节点名称的unit-address表示设备地址或者寄存器首地址(具体节点具体分析,不一定是绝对地址),必须与节点reg属性中指定的首地址匹配

eg. imx6ull.dtsi中描述的uart1控制器节点:

该节点label为uart1,节点名称为serial,设备地址(寄存器首地址)为02020000,正是imx6ull uart1外设寄存器的首地址

(2)路径名称

通过指定从根节点到所有子节点到所需节点的完整路径,可以唯一地标识设备树中的节点。

指定设备路径的约定如下:

/node-name-1/node-name-2/node-name-N

(3)属性

设备树中的每个节点都有用来描述节点信息的属性。属性由名称和值两部分组成,属性名称的可用字符如下表:

属性值是一个由零个或多个字节组成的数组,其中包含与属性相关联的信息,支持的数据类型如下:


(4)节点标签

节点标签用隔开,为了方便访问节点,可用直接通过&node-lable来访问节点,示例如下:

node-label: node-name@unit-address 

;

3. 设备树节点标准属性

DTSpec为设备节点指定一组标准属性,如下。

(1)compatible

compatible属性值由string list组成,定义了设备的兼容性,推荐格式为manufacturer,model,manufacturer描述了生产商,model描述了型号。

compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

开发板上的音频芯片采用的欧胜WM8960,sound节点的compatible属性值如下:

compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960"

使用的时候,sound这个设备首先使用第一个兼容值在Linux内核中查找,看看能不能找到对应的驱动文件;如果没有找到的话,就使用第二个兼容值查找。

一般驱动程序文件都会有一个OF匹配表,此OF匹配表保存着一些compatible值,如果设备节点的compatible属性值核OF匹配表中的任何一个值相等,那么就表示这个设备可以使用这个驱动。

比如在文件imx-wm8960.c文件中:

static const struct of_device_id imx_wm8960_dt_ids[] = 
	 .compatible = "fsl,imx-audio-wm8960", ,
	 /* sentinel */ 
;

这个数组就是imx-wm8960.c这个驱动文件的匹配表。

(2) model

model属性值是一个string,指明了设备的厂商和型号,推荐格式为manufacturer,model

model = "Freescale i.MX6 ULL 14x14 EVK Board";

(3) phandle

phandle属性值是一个u32,为设备树中唯一的节点指定一个数字标识符,用于其它节点指明关系。

(4) status

status属性值是一个string,表示设备的运行状态,可用值如下表:

(5)#address-cells 和 #size-cells

#address-cells and #size-cells属性值是一个u32,可以用在任何拥有子节点的设备中,并描述子设备节点应该如何寻址

#address-cells属性定义子节点reg属性中地址字段所占用的字长,也就是占用u32 单元格的数量。

#size-cells属性定义子节点reg属性值的长度所占用的 u32 单元格的数量。

(6)reg

reg属性值是一个 prop-encoded-array,用来描述设备地址空间资源信息,一般是某个外设的寄存器地址范围信息,包括起始地址和地址长度。

reg = <address1 length1 address2 length2 address3 length3……>

其中address是起始地址,length是地址长度。#address-cells表明address这个数据所占用的字长,#size-cells表示length这个数据所占用的字长。

比如:

spi4 
		compatible = "spi-gpio";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_spi4>;
		status = "okay";
		gpio-sck = <&gpio5 11 0>;
		gpio-mosi = <&gpio5 10 0>;
		num-chipselects = <1>;
		#address-cells = <1>;
		#size-cells = <0>;

		gpio_spi: gpio_spi@0 
			compatible = "fairchild,74hc595";
			gpio-controller;
			#gpio-cells = <2>;
			reg = <0>;
			registers-number = <1>;
			registers-default = /bits/ 8 <0x57>;
			spi-max-frequency = <100000>;
		;
	;

#address-cells = <1>表示子节点中reg属性的address占用1个u32数据,#size-cells表示子节点中reg属性的length不占用空间,没有。

在子节点gpio_spi中:reg属性值设置为<0>,相当于设置了起始地址,而没有设置地址长度。

(7)virtual-reg

(8)ranges

(9)dma-ranges

4. 特殊节点

(1)根节点

树是由树根开始的,在设备树中称之为根节点,路径为/,根节点不需要节点名称,所有子节点都是挂在根节点上的,可以看到最简单的根节点如下:

/ 

;

根节点的属性有:

(2)aliases

aliases节点用来定义别名,为了内核方便访问节点。

(3)chosen

chosen节点是为了uboot向Linux内核传递数据,重点是bootargs参数,一般.dts文件中chosen节点通常为空或者内容很少。

此处关于uboot如何通过设备树传参给kernel,可以单独写篇文章,待补充…

5. 向节点追加内容

(1)向根节点追加内容

/ 
	//要补充的内容
;

(2)向子节点追加内容

&node-label 
	//追加内容
;

三、设备树实例

i.MX6ULL内部框图

如何寻找开发板对应的设备树文件

直接查看arch/arm/boot/dts/Makefile文件,在该文件中查找即可,比如imx6ull相关部分如下:

dtb-$(CONFIG_SOC_IMX6ULL) += \\
        imx6ull-14x14-ddr3-arm2.dtb \\
        imx6ull-14x14-ddr3-arm2-adc.dtb \\
        imx6ull-14x14-ddr3-arm2-cs42888.dtb \\
        imx6ull-14x14-ddr3-arm2-ecspi.dtb \\
        imx6ull-14x14-ddr3-arm2-emmc.dtb \\
        imx6ull-14x14-ddr3-arm2-epdc.dtb \\
        imx6ull-14x14-ddr3-arm2-flexcan2.dtb \\
        imx6ull-14x14-ddr3-arm2-gpmi-weim.dtb \\
        imx6ull-14x14-ddr3-arm2-lcdif.dtb \\
        imx6ull-14x14-ddr3-arm2-ldo.dtb \\
        imx6ull-14x14-ddr3-arm2-qspi.dtb \\
        imx6ull-14x14-ddr3-arm2-qspi-all.dtb \\
        imx6ull-14x14-ddr3-arm2-tsc.dtb \\
        imx6ull-14x14-ddr3-arm2-uart2.dtb \\
        imx6ull-14x14-ddr3-arm2-usb.dtb \\
        imx6ull-14x14-ddr3-arm2-wm8958.dtb \\
        imx6ull-14x14-evk.dtb \\
        imx6ull-14x14-evk-btwifi.dtb \\
        imx6ull-14x14-evk-emmc.dtb \\
        imx6ull-14x14-evk-gpmi-weim.dtb \\
        imx6ull-14x14-evk-usb-certi.dtb \\
        imx6ull-9x9-evk.dtb \\
        imx6ull-atk-emmc.dtb \\
        imx6ull-9x9-evk-btwifi.dtb \\
        imx6ull-9x9-evk-ldo.dtb

可以看到所有imx6ull的开发板,其中我们移植的开发板为imx6ull-atk-emmc.dtb,本文就以该设备树文件为例,讲述设备树语法。

1. skeleton描述文件

查看文件arch/arm/boot/dts/skeleton.dtsi,内容非常简洁,只定义了根节点:

2. imx6ull芯片级描述文件(通用)

不同的imx6ull开发板都是使用imx6ull这颗处理器芯片,而imx6ull soc芯片级的描述是固定的,通常这个也是由芯片厂商提供

查看imx6ull.dtsi文件,整体框架如下:

接下来我们逐个分析。

2.1. 根节点的补充

该文件引用的skeleton.dtsi文件中,已经定义了根节点,如果再次定义根节点,其中的内容将作为对根节点的补充

在该描述文件中,挂在根节点上的子节点有:aliases、cpus、intc、clocks、soc。

(1)aliases节点

aliases节点用来定义一个或多个别名属性,按照约定,该节点应该在根节点上。

(2)cpus节点
所有的设备树都需要cpus节点,用来描述系统的CPU信息。

i.MX6ULL是单核处理器,因此只有一个/cpus/cpu*子节点,用来表示某一个具体CPU核的信息,其中有以下属性:

  • compatible:
  • device_type:描述设备类型
  • reg
  • clock-latency
  • operating-points
  • fsl,soc-operating-points
  • fsl,low-power-run
  • clocks
  • clock-names


(3)intc节点

(4)clocks节点

(5)soc节点

soc节点中,描述了i.MX6ULL片上的总线和全部外设:

2.2. aips2总线节点分析

aips2: aips-bus@02100000 
			compatible = "fsl,aips-bus", "simple-bus";
			#address-cells = <1>;
			#size-cells = <1>;
			reg = <0x02100000 0x100000>;
			ranges;

			//一堆外设子节点,省略...
;

aips2节点的属性有:

  • compatible:兼容性
  • #address-cells:子节点reg属性中地址字段所占用的单元格数量,占用1个u32
  • size-cells:子节点reg属性值的长度所占用的单元格的数量,占用1个u32
  • reg:寄存器起始地址0x02100000,长度0x100000
  • ranges:空

2.3. i2c控制器节点分析

i2c控制器是挂在aips2总线上的,对应到设备树中,i2c控制器节点挂在aips2节点上,描述代码如下:

以i2c1节点为例,标签是i2c1,节点名称是i2c,寄存器起始地址是0x021a0000,有如下属性:

  • #address-cells:子节点reg属性中地址字段所占用的单元格数量,占用1个u32
  • size-cells:子节点reg属性值的长度所占用的单元格的数量,占用0个u32
  • compatible:兼容性,fsl,imx6ul-i2c和fsl,imx21-i2c
  • reg:寄存器,起始地址是0x021a0000,长度是0x4000
  • interrupts:中断,不了解A7的中断控制器,看不懂
  • clocks:时钟源,clks节点的IMX6UL_CLK_I2C1这个时钟
  • status:节点状态,禁用

3. imx6ull ATK开发板描述文件

imx6ull-atk-emmc.dts这个文件的大概框架如下。

3.1. 版本

/dts-v1/;

3.2. 根节点的补充

框架如下:

其中 chosen 节点是uboot用来向内核传递参数,内容如下:

3.3. 子节点的补充

根节点之后,使用引用符&来对imx6ull.dtsi文件中定义的子节点进行补充,用来描述开发板的具体配置,这个也是主要需要适配修改的文件。

3.4. 磁力计mag3110节点分析

在NXP官方开发板上,磁力计mag3110是接在i2c1总线控制器上的,对应到设备树中,磁力计节点挂在i2c1控制器节点上,如下。

可以看到,i2c1节点的补充描述中,就描述了i2c1控制器上所连接的设备。

i2c1节点的补充属性有:

  • clock-frequency:i2c控制器时钟频率,100khz
  • pinctrl-names:
  • pinctrl-0:
  • status:设备状态,就绪

i2c控制器上接了两个设备,一个是mag3110磁力计,一个是fxls8471加速度计。注意,在描述节点时,@后面的地址变为了i2c总线的设备地址,mag3110的i2c从机地址是0e,fxls8471的i2c从机地址是1e

至此,imx6ull设备树分析完成,完整的思维导图文档在这里:【腾讯文档】imx6ull设备树

四、设备树在系统中的体现

Linux内核启动的时候会解析设备树dtb文件,所以启动以后可以在根文件系统中看到设备树的节点信息,在/proc/device-tree目录中:

这里 device-tree目录是一个软链接,实际指向/sys/firmware/devicetree/base目录。

在device-tree目录中,首先可以看到设备树根节点下的所有一级子节点。

(1)属性是以文件的方式给出,可以直接查看。

比如查看根节点的model属性:

(2)节点以目录的方式给出

比如soc子节点的内容如下:

五、设备树绑定信息文档

在设备树中添加一个新的节点时,添加的格式在Linux内核源码中有详细的.txt文档描述,这些txt文档就称为绑定文档。

绑定文档在/Documentation/devicetree/bindings路径中:

比如我们在开发板的i2c上新添加了一个设备,需要在设备树的i2c节点下新添加一个节点,就可以查看i2c/i2c-imx.txt文档:

六、Linux内核的OF操作函数

Linux内核提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀of_,所以也称为OF函数,声明在文件include/linux/of.h文件中。

1. 内核对于设备树节点的描述

Linux内核使用device_node结构体来描述一个设备树节点,定义在文件include/linux/of.h文件中。

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 properties */
	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
;

2. 查找节点

(1)通过节点名字查找节点

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

参数意义如下:

  • from:开始查找的节点,NULL表示根节点
  • name:要查找的节点名称

返回值为找到的节点,NULL为查找失败。

(2)通过节点类型查找节点

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

type参数指定要查看节点对应的type字符串,也就是device_type属性值。

(3)通过device_type和 compatible查找节点

extern struct device_node *of_find_compatible_node(struct device_node *from,
	const char *type, const char *compat);

(4)通过of_device_id匹配表来查找节点

extern 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);

(5)通过路径来查找节点

static inline struct device_node *of_find_node_by_path(const char *path)

	return of_find_node_opts_by_path(path, NULL);

这里的path必须要是绝对路径。

3. 获取父子节点

(1)获取父节点

extern struct device_node *of_get_parent(const struct device_node *node);

(2)迭代查找子节点

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

prev参数是前一个子节点,如果为NULL表示从第一个子节点开始。

4. 提取属性值

在节点描述类型device_node中,有这样一项用来描述属性值:

struct	property *properties;

property结构体类型定义如下:

struct property 
	char	*name;
	int	length;
	void	*value;
	struct property *next;
	unsigned long _flags;
	unsigned int unique_id;
	struct bin_attribute attr;
;

(1)查找指定节点的属性

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

参数name指属性名字,lenp指属性值的字节数。

(2)获取属性中元素的数量

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

参数propname是需要统计元素数量的属性名字,参数elem_size是元素的长度。

返回值是获取到的属性元素数量。

eg. reg属性的值通常是一个数组,使用此函数可以获取的数组的大小。

(3)从属性中获取指定索引的u32类型数据值

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

参数out_value用来返回获取到的值。

返回值用来表示是否获取成功。

(4)从属性中获取数组值

extern int of_property_read_u8_array(const struct device_node *np,
			const char *propname, u8 *out_values, size_t sz);
extern int of_property_read_u16_array(const struct device_node *np,
			const char *propname, u16 *out_values, size_t sz);
extern int of_property_read_u32_array(const struct device_node *np,
				      const char *propname,
				      u32 *out_values,
				      size_t sz);
extern int of_property_read_u64_array(const struct device_node *np,
				      const char *propname,
				      u64 *out_values,
				      size_t sz);

eg. reg属性的值通常是一个数组,使用这个函数可以一次读取出一个数组,也就是reg属性的全部值。

(5)从属性中获取布尔值/整形值

/**
 * of_property_read_bool - Findfrom a property
 * @np:		device node from which the property value is to be read.
 * @propname:	name of the property to be searched.
 *
 * Search for a property in a device node.
 * Returns true if the property exist false otherwise.
 */
static inline bool of_property_read_bool(const struct device_node *np,
					 const char *propname)

	struct property *prop = of_find_property(np, propname, NULL);

	return prop ? true : false;


static inline int of_property_read_u8(const struct device_node *np,
				       const char *propname,
				       u8 *out_value)

	return of_property_read_u8_array(np, propname, out_value, 1);


static inline int of_property_read_u16(const struct device_node *np,
				       const char *propname,
				       u16 *out_value)

	return of_property_read_u16_array(np, propname, out_value, 1);


static inline int of_property_read_u32(const struct device_node *np,
				       const char *propname,
				       u32 *out_value)

	return of_property_read_u32_array(np, propname, out_value, 1);


static inline int of_property_read_s32(const struct device_node *np,
				       const char *propname,
				       s32 *out_value)

	return of_property_read_u32(np, propname, (u32*) out_value);

(6)从属性中获取字符串

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

(7)获取#address-cells和#size-cells属性值

extern int of_n_addr_cells(struct device_node *np);
extern int of_n_size_cells(struct device_node *np);

6. 地址相关操作

地址相关操作的函数定义在include/linux/of_address.h文件中。

(1)获取地址相关属性

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

参数index是要读取的地址标号,size是地址长度,flag是是参数,比如 IORESOURCE_IO、IORESOURCE_MEM等。

返回值是读取到的数据首地址,为NULL则表示读取失败。

(2)将从设备树读取到的地址转换为物理地址

extern u64 of_translate_address(struct device_node *np, const __be32 *addr);

返回值为转换得到的地址,如果为 OF_BAD_ADDR 的话表示转换失败。

(3)将地址转换为resources资源

GPIO、IIC、SPI这些外设都有对应的寄存器,这些寄存器就是一段内存空间,Linux内核使用 resource 结构体来描述一段内存空间,定义在文件include/linux/ioport.h中。

/*
 * Resources are tree-like, allowing
 * nesting etc..
 */
struct resource 
	resource_size_t start;
	resource_size_t end;
	const char *name;
	unsigned long flags;
	struct resource *parent, *sibling, *child;
;

其中: