从0开始详细了解Linux设备树

举报
lesmaths 发表于 2023/09/18 12:10:23 2023/09/18
【摘要】 1、什么是设备树? 设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做 DTS(Device Tree Source),这个DTS文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如CPU数量、内存基地址、IIC接口上接了哪些设备、SPI接口上接了哪些设备。 树的主干就是系统总线,IIC控制器、GPIO控制器、SPI 控制器等都是接到系统主...

1、什么是设备树?

设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做 DTS(Device Tree Source),这个DTS文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如CPU数量、内存基地址、IIC接口上接了哪些设备、SPI接口上接了哪些设备。

树的主干就是系统总线,IIC控制器、GPIO控制器、SPI 控制器等都是接到系统主线上的分支。IIC控制器有分为IIC1和IIC2两种,其中IIC1上接了FT5206和AT24C02这两个IIC设备,IIC2上只接了MPU6050这个设备。DTS文件主要功能就是按照结构来描述板子上的设备信息,DTS文件描述设备信息是有相应的语法规则要求的,稍后会讲解DTS语法规则。

在以前的Linux内核中ARM架构并没有采用设备树,在没有设备树的时候Linux是如何描述ARM架构的板级信息的呢?在Linux内核源码中大量的arch/arm/mach-xxx和arch/arm/plat-xxx文件夹,这些文件夹里面的文件就是独赢平台下的板级信息。而随着智能手机的发展,每年新出的ARM架构芯片少说都在数十、数百款,Linux内核下板级信息将会成指数级增长!这些板级信息文件都是.c或.h文件。都会被硬编码进Linux内核中,导致Linux内核“虚胖”……从此以后ARM社区引入了PowerPC等架构已经采用的设备树,将这些描述板级硬件信息的内容从Linux内核中分离出来,用一个专属的文件格式来描述,这个专属的文件就叫设备树,扩展名为.dts。

一个SOC可以作出很多不同的板子,这些不同的板子肯定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他的.dts文件直接引用这个通用文件即可,这个通用文件就是.dtsi文件,类似于C语言中的头文件。一般.dts描述板级信息(也就是开发板上有哪些IIC设备、SPI设备等),.dtsi描述SOC级信息(SOC有几个CPU、主频是多少、各个外设控制等)。

这个就是设备树的由来,简而言之,Linux内核中ARM架构有太多冗余的垃圾板级信息文件,然后ARM社区引入了设备树。

2、DTS/DTB和DTC

设备树源文件扩展名为.dts,但是我们前面移植Linux的时候却移植在使用.dtb文件,那么DTS和DTB两个文件是什么关系呢?

DTS是设备树源码文件,DTB是将DTS编译以后得到的二进制文件。将.c文件编译为.o需要用到gcc编译器,将.dts编译为.dtb文件则需要用到DTC工具。DTC工具源码就在Linux内核的scripts/dtc目录下,其顶层Makefile文件内容如下:

hostprogs-y := dtc
always :=$(hostprogs-y)

dtc-objs:= dtc.o flattree.o fstree.o data.o livetree.o treesource.o \
		srcpos.o checks.o util.o

dtc-objs += dtc-lexer.lex.o dtc-parser.tab.o
......

可以看出,DTC工具依赖于dtc.c、flattree.c、fstree.c等文件,最终编译并链接出DTC这个主机文件。如果要编译DTS文件的话只需要进入到Linux源码根目录下,然后执行如下命令

make all		//或者make dtbs

make all命令是编译Linux源码中所有东西,包括zImage,.ko驱动模块以及设备树,如果只是编译设备树的话建议使用make dtbs命令。

基于ARM架构的SOC有很多,一种SOC又可以制作出很多款板子,每个板子都有一个u敌营的DTS文件,那么如何确定编译哪一个DTS文件呢?我们就以I.MX6ULL这个芯片对应的板子为例看一看,打开arch/arm/boot/dts/Makefile可以看到:

381 dtb-$(CONFIG_SOC_IMX6UL) += \
382 imx6ul-14x14-ddr3-arm2.dtb \
383 imx6ul-14x14-ddr3-arm2-emmc.dtb \
......
400 dtb-$(CONFIG_SOC_IMX6ULL) += \
401 imx6ull-14x14-ddr3-arm2.dtb \
402 imx6ull-14x14-ddr3-arm2-adc.dtb \
...
422 imx6ull-alientek-emmc.dtb \
423 imx6ull-alientek-nand.dtb \
424 imx6ull-9x9-evk.dtb \
425 imx6ull-9x9-evk-btwifi.dtb \
426 imx6ull-9x9-evk-ldo.dtb
427 dtb-$(CONFIG_SOC_IMX6SLL) += \
428 imx6sll-lpddr2-arm2.dtb \
429 imx6sll-lpddr3-arm2.dtb \
......

可以看到422和423行是我们的设备树,向选中I.MX6ULL这个SOC以后,所有使用到这个SOC的板子对应的.dts文件都会被编译为.dtb。如果我们使用I.MX6ULL新做了一个板子,只需要新建一个你的板子对应的.dts文件,然后将对应的.dtb文件名添加到dtb-$(CONFIG_*SOC_*IMX6ULL)下,这样在编译设备树的时候就会将对应的.dts编译为二进制的.dtb文件。

422和423行就是我们之前对开发板移植Linux系统的时候添加的设备树。关于.dtb文件怎么使用前面讲解uboot移植、Linux内核移植的时候已经多次介绍,不记得可以复习复习。(uboot中使用bootz或bootm命令向Linux内核传递二进制设备树文件.dtb文件)

3、DTS语法

虽然我们基本上不会从头到尾重写一个.dts文件,大多时候是在SOC厂商提供的.dts文件上进行修改。但是DTS文件语法我们还是需要详细的学习一遍,因为我们需要修改.dts文件。DTS语法非常人性化,是一种ASCII文本文件,不管是阅读还是修改都很方便,所以不要觉得很麻烦哦~

本节我们就以imx6ull-alientek-emmc.dts这个文件为例讲解一下DTS语法。

1.dtsi头文件

与C语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi前面也介绍了。设备树.dts文件可以通过#include来引用.h、.dtsi和.dts文件。只是我们在编写设备树头文件的时候最好选择.dtsi后缀。

一般.dtsi文件用于描述SOC的内部外设信息,比如CPU架构、主频、外设寄存器地址范围,比如UART、IIC等。

2.设备节点

设备树采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息, 属性就是键值对。从imx6ull.dtsi文件中缩减出来的设备树文件内容:

1 / {
2 	aliases {
3 		can0 = &flexcan1;
4 	};
5 
6	cpus {
7 		#address-cells = <1>;
8 		#size-cells = <0>;
9
10 		cpu0: cpu@0 {
11 			compatible = "arm,cortex-a7";
12 			device_type = "cpu";
13 			reg = <0>;
14 		};
15 	};
16
17 	intc: interrupt-controller@00a01000 {
18 		compatible = "arm,cortex-a7-gic";
19 		#interrupt-cells = <3>;
20 		interrupt-controller;
21 		reg = <0x00a01000 0x1000>,
22 				<0x00a02000 0x100>;
23	};
24 }

第 1 行,“/”是根节点,每个设备树文件只有一个根节点。细心的同学应该会发现, imx6ull.dtsi 和imx6ull-alientek-emmc.dts 这两个文件都有一个“/”根节点,这样不会出错吗?不会的,因为这两个“/”根节点内容会合并成一个根节点。

第2、6和17行,aliases、cpus和ints是3个字节点,在设备树中节点命名格式如下node

node-name@unit-address

其中node-name就是节点名字,为ASCII字符串,unit-address表示设备的地址或寄存器首地址,如果节点没有地址或寄存器的话可以不要,比如 “cpu@0” 、“interrupt-controller@00a01000”。

但我们在上面示例代码中看到节点命名却为:cpu0:cpu@0。这里冒号前面的是节点标签(label),后面的才是节点名字。引入label的目的就是为了方便访问节点,可以直接通过&label来访问这个节点,比如通过 &cpu0 就可以访问 “cpu@0” 这个节点,而不需要输入完整的节点名字。

第10行cpu0也是一个节点,只是cpu0是cpus的子节点。每个节点都有不同属性,不同的属性就有不同的内容,属性都是键值对,值可以为空或任意的字节流。设备树源码中常用的几种数据形式如下所示:

①、字符串:conpatible = “arm,cortex-a7”;

设置compatible属性的值为这个内容

②、32位无符号整数:reg = <0>; 或 reg = <0 0x123456 100>

设置reg属性的值为0;后者是设置为一组值。

③、字符串列表:conpatible=“fsl,imx6ull-gpmi-nand”,“fsl,imx6ull-gpmi-emmc”;

设置属性为两个字符串内容。

3.标准属性

节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux下的很多外设驱动都会使用这些标准属性,我们学习几个常用的标准属性。

①、compatible属性

compatible属性也叫做“兼容性”属性,这是一个非常重要的属性!compatible属性的值是一个字符串列表,用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,compatible属性的值格式为:“manufacturer,model”

manufacturer表示厂商,model是模块对应的驱动名字。

比如imx6ull-alientek-emmc.dts中sound节点是开发板的音频设备节点,开发板上音频芯片采用WOLFSON出品的WM8960,则sound节点的compatible属性值如下:

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

属性值有两个,其中fsl表示厂商是飞思卡尔,后面表示驱动模块名字。sound这个设备首先使用第一个兼容值在Linux内核里面查找,看看能不能找到与之匹配的驱动文件,如果没找到则使用第二个兼容值查找。

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

②、model属性

model属性也是一个字符串,一般model属性描述设备模块信息,比如名字什么的,如:model=“wm8960-audio”;

③、status属性

status属性看名字就知道是和设备状态有关的,status属性值也是字符串,字符串是设备的状态信息,可选状态有:

okay 表明设备可操作
disabled 不可操作的,但可以变为可操作的,比如热插拔设备插入以后
fail 不可操作,设备检测到一系列错误
fail-sss 和fail相同,sss部分是检测到的错误内容

④、reg属性

reg属性的值一般是(address,length)对。reg属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器范围

⑤、#address-cells和#size-cells属性

这两个属性的值都是无符号32位整形,两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。address属性值决定了子节点属性中地址所占用的字长,size决定了子节点属性中长度信息所占的字长。这两个属性表明了子节点应该如何编写reg属性值,一般reg属性值都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度,reg属性格式一般为:reg = <add1 length1 add2 length2 add3 length3…>

每个“address length”组合表示一个地址范围,其中 address 是起始地址, length 是地址长度,#address-cells表明address这个数据所占用的字长,#size-cells表明length这个数据所占用字长。

⑥、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 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。

⑦、name属性

name属性为字符串,name属性用于记录节点名字,name属性以及被弃用,不推荐使用name属性,一些老的设备树文件可能会使用此属性。

⑧、device_type属性

device_type属性值为字符串,IEEE 1275会用到此属性,用于描述设备的FCode,但是设备树没有FCode,所以此属性也被抛弃了。此属性只能用于cpu节点或者memory节点。imx6ull.dtisi的cpu0节点用到了此属性。

4.根节点compatible属性

每个节点都有compatible属性,根节点“/”也不例外,前面说过,设备节点的compatible属性值是为了匹配Linux内核中的驱动程序,那么根节点中的compatibe属性是干什么的呢?

通过根节点的compatible属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,第二个值描述了所使用的SOC。Linux内核会通过根节点的compatible属性查看是否支持此设备,如果支持的话设备就会启动Linux内核。

在没有使用设备树以前,uboot会向Linux内核传递一个叫做machine id的值也就是设备ID,告诉Linux内核自己是什么设备,看看Linux是否支持。针对每个设备Linux内核都用MACHINE*START来定义一个machine_*desc结构体来描述这个设备。起始就是将设备ID与宏对比看有没有相等的,相等就说明Linux内核支持这个设备,如果不支持的话这个设备就无法启动Linux内核。

而在使用设备树以后就换为了DT_*MACHINE_*START定义在arch/arm/include/asm/mach/arch.h里。二者其实基本相同,只是.nr设置不同,现在DT直接将.nr设置为~0。意思是引入设备树以后不会再根据machine id来检查Linux内核是否支持某个设备了。

总结:Linux内核通过根节点compatible属性找到对应的设备的函数调用过程:

start_kernel()
		->setup_arch()
				->setup_machine_fdt()
						->of_flat_dt_math_machine()

5.向节点追加或修改内容(重点应用咯)

产品开发过程中一定会面临着频繁的需求更改,比如第一版硬件上有一个IIC接口的芯片MPU6050,第二版硬件又把这个MPU6050更换为MPU9250。一旦硬件修改了,我们就要同步修改设备树文件,毕竟设备树是描述板子硬件信息的文件。

假设现在有个六轴芯片fxls8471,要接到我们的I.MX6ULL开发板的I2C1接口上,那么相当于需要在I2C1这个节点上添加一个fxls8471子节点。先看一下I2C1接口对应的节点,打开文件imx6ull.dtsi:

							/*示例代码I2C1节点*/
937 i2c1: i2c@021a0000 {
938 	#address-cells = <1>;
939 	#size-cells = <0>;
940 	compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
941 	reg = <0x021a0000 0x4000>;
942	 	interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
943 	clocks = <&clks IMX6UL_CLK_I2C1>;
944 	status = "disabled";
945 };

现在要在I2C1节点下创建一个子节点fxls8471,最简单的方法就是在I2C1节点下直接添加一个名为fxls8471的子节点。

i2c1: i2c@021a0000 {
	...
	fxls9471@1e{
		compatible = "fsl,fxls8471";
		reg = <0x1e>;
	};
};

这就是添加这个芯片对应的子节点最简单的方法。但是这样会有个问题!

I2C1节点是定义在imx6ull.dtsi文件中的,而emx6ull.dtsi是设备树头文件,其他所有用到I.MX6ULL这个SOC的板子都会引用imx6ull.dtsi这个文件。直接在I2C1节点中添加fxls8471就相当于在其他所有的板子上都添加了fxls8471这个设备,但是其他板子并没有这个设备,因此简单这样添加设备是不行的!

这里就要引入标题所说的内容,如何向节点追加数据,我们现在要解决的就是如何向I2C1节点追加一个名为fxls8471的子节点,而且不能影响到其他使用I.MX6ULL的板子。我们开发板使用的设备树文件为imx6ull-alientek-emmc.dts,因此我们需要在这个文件中完成数据追加的内容,方式如下:

224 &i2c1{	/*&i2c1表示要访问这个label对应的节点,也就是imx6ull.dtsi中的i2c1*/			
225 	clock-frequency = <100000>;		/*↓要追加或修改的内容,包括修改某些属性的值*/
226 	pinctrl-names = "default";
227 	pinctrl-0 = <&pinctrl_i2c1>;
228 	status = "okay";
229
230 	mag3110@0e {
231 		compatible = "fsl,mag3110";
232 		reg = <0x0e>;
233 		position = <2>;
234 	};
235
236 	fxls8471@1e {
237 		compatible = "fsl,fxls8471";
238 		reg = <0x1e>;
239 		position = <0>;
240 		interrupt-parent = <&gpio5>;
241 		interrupts = <0 8>;
242		};

示例代码就是向i2c1节点添加/修改数据,225行属性"clock…"就表示i2c1时钟为100kHz。这就是新添加的属性;228行,将status属性值改为了okay;230行和236行分别意思是开发板在I2C1上接了磁力计芯片mag3110和六轴芯片fxls8471,两个子节点。

因为这个代码内容是imx6ull-alientek-emmc.dts这个文件内的,所以不会对使用I.MX6ULL这颗SOC的其他板子造成任何影响。这就是向节点追加或修改内容的意思,重点就是通过&label来访问节点,然后直接在里面编写要追加或者修改的内容。

4、创建小型模板设备树

第3节介绍了DTS的语法,本节我们根据DTS语法从头到尾编写一个小型的设备树文件。当然这个小型设备树没有实际的意义,只是为了掌握设备树的语法。在实际产品开发中,我们不需要完完全全重写一些.dts设备树文件,一般都是使用SOC厂商提供好的.dts文件按,只需要根据实际需求做相应的修改即可。

在编写设备树之前要定义一个设备,我们就以I.MX6ULL这个SOC为例,需要在设备树里面描述的内容如下:

①、I.MX6ULL这个Cortex-A7架构的32位CPU。

②、I.MX6ULL内部ocram,起始地址0x00900000,大小为128KB(0x20000)

③、I.MX6ULL内部aips1域下的ecspi1外设控制器,寄存器起始地址0x02008000,大小0x4000。

④、I.MX6ULL内部aips2域下的usbotg1外设控制器,起始地址0x02184000,大小0x4000。

⑤、I.MX6ULL内部aips3域下的rngb外设控制器,起始地址0x02284000,大小0x4000。

为了简单起见,我们就在设备树里面就实现这些内容即可,首先搭建一个仅含有根节点的“/”的基础框架,新建一个名为myfirst.dts文件,设备树框架很简单就一个根节点“/”,根节点里面只有一个compatible属性。我们就在这个基础框架把上面介绍过的内容一点点添加进来:

/{
	compatible = "fls,imx6ull-alientek-evk", "fsl,imx6ull";
}

首先添加cpus节点,I.MX6ULL采用Cortex-A7架构,而且只有一个CPU,因此只有一个cpu0节点;

然后添加soc节点,像uart,iic控制器等等都属于SOC内部外设,因此一般会创建一个叫做soc的父节点来管理这些SOC内部外设的子节点;添加ocram节点;添加aips1、aips2和aips3三个子节点;添加ecspi1、usbotg1和rngb三个外设控制器节点。

/{
	compatible = "fls,imx6ull-alientek-evk", "fsl,imx6ull";

	cpus{
		#address-cells = <1>;
		#size-cells = <0>;
		//CPU0节点
		cpu0:cpu@0{
			compatible = "arm,cortex-a7";
			device_type = "cpu";
			reg = <0>;				/*I.MX6ULL只有一个CPU所以只有一个cpu0子节点*/
		};
	};

	//soc节点
	/*设置#address-cells和#size-cells都为1,这样soc子节点的reg属性中起始地址占用1个字长,地址空间长度也占用一个字长。*/
	soc{
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "simple-bus";
		ranges;	//ranges属性为空说明子空间和父空间地址范围相同

		//ocram节点
		ocram:sram@00900000{		//@后面就是ocram起始地址
			compatible = "fsl,lpm-sram";
			reg = <0x00900000 0x2000>;		//reg属性指明了ocram地址和大小
		};
	
		//aips1节点
		aips1: aips-bus@02000000 {
			compatible = "fsl,aips-bus", "simple-bus";
			#address-cells = <1>;
			#size-cells = <1>;
			reg = <0x02000000 0x100000>;
			ranges;
			
			//ecspi1节点
			ecspi1: ecspi@02008000 {
				#address-cells = <1>;
				#size-cells = <0>;
				compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
				reg = <0x02008000 0x4000>;
				status = "disabled";
			};
		};
		//aips2 节点
		aips2: aips-bus@02100000 {
 			compatible = "fsl,aips-bus", "simple-bus";
 			#address-cells = <1>;
 			#size-cells = <1>;
 			reg = <0x02100000 0x100000>;
	 		ranges;
	
			//usbotg1节点
			usbotg1: usb@02184000{
				compatible = "fsl,imx6ul-usb","fsl,imx27-usb";
				reg = <0x02184000 0x4000>
				status = "disabled";
			};
		};
		//aips3 节点
		aips3: aips-bus@02200000 {
			compatible = "fsl,aips-bus", "simple-bus";
			#address-cells = <1>;
			#size-cells = <1>;
			reg = <0x02200000 0x100000>;
			ranges;
			
			//rngb节点
			rngb: rngb@02284000 {
				compatible = "fsl,imx6ul-rng","fsl,imx-rng","imx-rng";
			};
		};
	};
}

至此,myfirst.dts这个小型的模板设备树就写好了,基本和imx6ull.dtsi很想,可以看作是其缩小版。在myfirst.dts里面我们仅仅编写了I.MX6ULL的外设控制器节点,像IIC接口,SPI接口下所连接的设备我们都没写,因为具体设备其设备树属性内容不同,这个等到后面具体讲解。

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

Linux内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-tree目录下根据节点名字创建不同文件夹。/proc/device-tree目录下是根节点“/”的所有属性和子节点,我们依次来看一下这些属性和子节点。

1.根节点“/”各个属性。根节点属性表现为一个个的文件,"#address-cells"、"compatible"和"model"等文件在设备树中就是根节点的几个属性。

2.根节点“/”各子节点。比如"aliases"、“backlight”、"chosen"和"clock"等等。

/proc/device-tree目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进入/proc/device-tree/soc目录中就可以看到soc节点的所有子节点,和根节点“/”一样,所有文件分别为soc节点的属性文件和子节点文件夹。

**6、特殊节点 **

在根节点“/”有俩特殊的子节点:aliases和chosen。打开imx6ull.dtsi文件:

18 	aliases {
19 		can0 = &flexcan1;
20 		can1 = &flexcan2;
21 		ethernet0 = &fec1;
22 		ethernet1 = &fec2;
23 		gpio0 = &gpio1;
24 		gpio1 = &gpio2;
......
42 		spi0 = &ecspi1;
43 		spi1 = &ecspi2;
44 		spi2 = &ecspi3;
45 		spi3 = &ecspi4;
46 		usbphy0 = &usbphy1;
47 		usbphy1 = &usbphy2;
48 };

单词aliases意思是“别名”,因此这个子节点的主要共呢个就是定义别名,定义别名的目的就是为了方便访问节点。不过我们一般会在节点命名的时候加上label然后通过&label来访问节点,这样也很方便,而且设备树里面大量使用&label形式来访问节点。

chosen并不是一个真实的设备,chosen节点主要是为了uboot向Linux内核传递数据,重点是bootargs参数。一般.dts文件中chosen节点为空或者内容很少。打开imx6ull-alientek-emmc.dts中chosen节点内容如下:

chosen{
	stdout-path = &uart1;
};

可以看出chosen节点仅仅设置了属性“stdout-path”,表示标准输出使用uart1。但是当我们进入到/proc/device-tree/chosen目录里面,会发现多了个bootargs这个属性。uboot在启动Linux内核的时候会将bootargs的值传递给Linux内核,bootargs会作为Linux内核的命令行参数,Linux内核启动的时候会打印出命令行参数(也就是uboot传递进来的bootargs值)。

既然chosen节点的bootargs属性不是我们在设备树里设置的,那么只有一种可能就是,uboot自己在chosen节点里面添加了bootargs属性!并且设置bootargs属性的值为bootargs环境变量的值。因为在启动Linux内核之前,只有uboot知道bootargs环境变量的值,并且uboot也知道.dtb设备树在DRAM中的位置。在uboot源码中全局搜索“chosen”可以发现:

就是uboot中的fdtchosen函数在设备树的chosen节点中加入了bootargs属性,并且还设置了bootargs属性值。当我们输入bootz 80800000 - 83000000命令执行以后,doboots函数就会执行。而通过ubootz命令启动Linux内核的时候会运行do_bootm_linux函数。

7、Linux内核解析DTB文件

Linux内核在启动的时候会解析DTB文件,然后在/proc/device-tree目录下生成相应的设备树节点文件。接下来我们简单分析一下Linux内核是如何解析DTB文件的

start_kernel()
	->setup_arch()
		->unflatten_device_tree()
			->__unflatten_device_tree()
				->unflatten_dt_node()
					->解析DTB文件中各个节点

在 start_kernel 函数中完成了设备树节点解析的工作,最终实际工作的函数为 unflatten_dt_node。

8、绑定信息文件

设备树是用来描述板子上的设备信息的,不同的设备其信息是不同的,反映到设备树中就是属性不同。那么我们在设备树添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?

在Linux内核源码中有详细的.txt文档描述了如何添加字节,这些.txt文档叫做绑定文件,路径为:Linux源码目录/Documentation/devicetree/binding。

比如我们现在要在I.MX6ULL这颗SOC的I2C下添加一个节点,那么就可以查看Documentation/devicetree/bindings/i2c/i2c-imx.txt,此文档详细的描述了I.MX系列的SOC如何在设备树中添加I2C设备节点,有时候使用的一些芯片在 Documentation/devicetree/bindings 目录下找不到对应的文档,这个时候就要咨询芯片的提供商,让他们给你提供参考的设备树文件。

9、设备树常用OF操作函数

设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。比如设备树使用reg属性描述了某个外设的寄存器地址为0x02005482,长度为0x400,我们在编写驱动的时候需要获取到reg属性的两个值,然后初始化外设。Linux内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个同一个的前缀“of_”,所以在很多资料里面也被叫做OF函数。这些OF函数原型都定义在include/linux/of.h文件中。

1.查找节点的OF函数

设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取这个设备的节点。Linux内核使用device_node结构体来描述一个节点,此结构体定义在include/linux/of.h中:

49 	struct device_node {
50 		const char *name; /* 节点名字 */
51 		const char *type; /* 设备类型 */
52 		phandle phandle;
53 		const char *full_name; /* 节点全名 */
54 		struct fwnode_handle fwnode;
55
56 		struct property *properties; /* 属性 */
57 		struct property *deadprops; /* removed 属性 */
58 		struct device_node *parent; /* 父节点 */
59 		struct device_node *child; /* 子节点 */
60 		struct device_node *sibling;
61 		struct kobject kobj;
62 		unsigned long _flags;
63 		void *data;
64 	#if defined(CONFIG_SPARC)
65 		const char *path_component_name;
66 		unsigned int unique_id;
67 		struct of_irq_controller *irq_trans;
68 	#endif
69 	};

与查找节点有关的OF函数有5个,依次来看一下:

1.of_find_node_by_name 函数

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 函数

of_find_node_by_type 函数通过 device_type 属性查找指定的节点,函数原型如下:

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

from:开始查找的节点,同上

type:要查找的节点对应的type字符串,也就是device_type属性值。

返回值:找到的节点,同上

3.of_find_compatible_node 函数

of_find_compatible_node 函数根据 device_type 和 compatible 这两个属性查找指定的节点,函数原型如下:

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

from同上;type:同上,但这里可以为NULL,表示忽略device_type属性

compatible:要查找的节点对应的compatible属性列表。

返回值:找到的节点

4.of_find_matching_node_and_match 函数

通过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:开始查找的节点。

matches:of_device_id 匹配表,也就是在此匹配表里面查找节点。

match:找到的匹配的of device id。

返回值:找到的节点。

5.of_find_node_by_path 函数

通过路径来查找指定的节点,函数原型如下:

inline struct device_node *of_find_node_by_path(const char *path)

path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是backlight这个节点的全路径。

返回值:找到的节点。

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

1.struct device_node *of_get_parent(const struct device_node *node)

用于获取指定节点的父节点。

node:要查找的父节点的节点。

返回值:找到的父节点。

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

用迭代的方式查找子节点

node:父节点

prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个。可以设置为NULL表示从第一个子节点开始。

返回值:找到的下一个子节点。

3.提取属性值的OF函数

节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux内核中使用结构体property表示属性,此结构体同样在文件include/linux/of.h中:

35 	struct property {
36 		char *name; /* 属性名字 */
37 		int length; /* 属性长度 */
38 		void *value; /* 属性值 */
39 		struct property *next; /* 下一个属性 */
40 		unsigned long _flags;
41		unsigned int unique_id;
42 		struct bin_attribute attr;
43 	};

Linux内核也提供了提取属性值的OF函数:

1.property ***of_**find_property(const struct device_node *np, const char *name, int *lenp)

用于查找指定的属性。

np:设备节点

name:设备名字

lenp:属性值的字节数

返回值:找到的属性

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

用于获取属性中元素的数量,比如reg属性值是一个数组,那么使用此函数可以获得数组的大小。

np:设备节点

proname:需要统计元素数量的属性名字

elem_size:元素长度

返回值:得到的属性元素数量

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

用于从属性中获取指定标号的u32类型数据值,比如某个属性有多个u32类型的值,那么就可以使用此函数来获取指定标号的数据值。

np:设备节点

proname:要读取的属性名字

index:要读取的值标号

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 函数

分别是读取属性中u8 u16 u32 u64类型的数组数据,比如大多数reg属性都是数组数据,可以使用这4个函数依次读取出reg属性中所有的值。

np:设备节点

proname:要读取的属性名字。

out_value:读取到的数组值。

sz:要读取的数组元素数量

返回值:0,读取成功;负,读取失败;-EINVAL表示属性不存在,-ENODATA表示没有要读取的值,-EOVERFLOW表示属性值列表太小。

5.of_property_read_u8 函数
of_property_read_u16 函数
of_property_read_u32 函数
of_property_read_u64 函数

有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整型值的属性,参数只比上面那个函数少了个sz。

6.of_property_read_string

用于读取属性字符串值

np;proname;out_string:读取到的字符串值。

返回值:0读取成功,负值读取失败。

7.of_n_addr_cells

用于获取#address-cells属性值

np:设备节点

返回值:获取到的#address-cells属性值

8.of_n_size_cells

用于获取#size-cells属性值

np:设备节点

返回值:获取到的#size-cells属性值

4.其他常用的OF函数

1.of_device_is_compatible

用于查看节点的compatible属性是否包含compat指定的字符串,也就是检查设备节点的兼容性

device:设备节点

compat:要查看的字符串

返回值:0,节点的compatible属性不包含compat指定的字符串;正数,包含。

2.of_get_address

用于获取地址相关的属性,主要是“reg”或者“assigned-addresses”属性值

dev:设备节点

index:要读取的地址标号

size:地址长度

flags:参数,比如IORESOURCEIO、IORESOURCEMEM等

返回值:读取到的地址数据首地址,NULL读取失败

3.of_translate_address

用于将从设备树读取到的地址转换为物理地址

dev:设备节点

in_addr:要转换的地址

返回值:得到的物理地址,OF_*BAD_*ADDR表示转换失败

4.of_address_to_resource

IIC、SPI、GPIO等这些外设都有对应的寄存器,这些寄存器起始就是一组内存空间,Linux内核使用resource结构体来描述一段内存空间,“resource”翻译就是资源,因此用resource结构体描述的都是设备资源信息,这个函数本质就是将reg属性值,将其转换为resource结构体类型

dev:设备节点

index:地址资源标号

r:得到的resource类型的资源值

返回值:0成功;负值失败。

5.of_iomap

用于直接内存映射,以前我们回通过ioremap函数来完成地址映射,采用设备树以后就可以通过of_iomap函数来获取内存地址所对应的虚拟地址,不需要使用ioremap函数了。采用设备树以后,大部分的驱动都是用of_iomap函数了。of_iomap 函数本质上也是将reg属性中地址信息转换为虚拟地址,如果reg属性有多段的话,可以通过index参数指定要完成内存映射的是哪一段。

np:设备节点

index:reg属性中要完成内存映射的段,如果reg属性只有一段的话index就设置为0.

返回值:经过内存映射后的虚拟内存首地址,如果为NULL的话表示内存映射失败。

————————

关于设备树常用的OF函数就先讲解到这里,Linux内核中关于设备树的OF函数不仅仅只有这些,还有很多OF函数没有讲解,没有讲解的OF函数要结合具体的驱动,比如获取中断号的OF函数、获取GPIO的OF函数等等,这些我们在后面的驱动实验再详细讲解。

关于设备树我们重点要了解以下内容:

①、DTS、DTB和DTC之间的区别,如何将.dts文件编译为.dtb文件。

②、设备树语法,这个是重点,因为在实际工作中我们是需要修改设备树的。

③、设备树的几个特殊子节点。

④、关于设备树的OF操作函数,也是重点,因为设备树最终是被驱动文件所使用的,而且驱动文件必须要读取设备树中的属性信息,比如内存、GPIO信息、中断信息等。要想在驱动中读取设备树的属性值,就必须使用Linux内核提供的众多OF函数。

以后就开始采用设备树,从最基本的点灯,到复杂的音频、网络或块设备等驱动。由简入深,深度剖析设备树,最终掌握基于设备树的驱动开发!

本文是我学习正点原子的教学边学边做的笔记,希望能传播给更多想要学习Linux的人了解,个人感觉正点原子的教学非常的详细。

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。