【正点原子I.MX6U-MINI驱动篇】4、Linux设备树详解

掌握设备树是Linux驱动开发人员必备的技能!因为在3.x 版本以前的 Linux 内核中ARM架构并没有采用设备树,在新版本的Linux中,ARM相关的驱动全部采用了设备树,最新出的CPU其驱动开发也基本都是基于设备树的,比如ST新出的STM32MP157、NXP的I.MX8系列等。我所使用的Linux版本为4.1.15,其支持设备树,所以正点原子I.MX6U开发板的所有Linux驱动都是基于设备树的

一、什么是设备树?

设备树(Device Tree),将这个词分开就是设备,描述设备树的文件叫做 DTS(Device
Tree Source),这个DTS文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如
CPU 数量、 内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等。用一种新的文件描述板子的结构信息,这个描述结构是树型结构的,我们称之为设备树。信息如图所示:


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

在3.x 版本以前的 Linux 内核中ARM架构并没有采用设备树。在没有设备树的时候Linux是如何描述ARM架构中的板级信息呢?在Linux内核源码中大量的arch/arm/mach-xxxarch/arm/plat-xxx文件夹,这些文件夹里面的文件就是对应平台下的板级信息。比如在arch/arm/mach-s3c24xx中有如下内容(有缩减):

/* LCD driver info */

static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = {

	.lcdcon5	= S3C2410_LCDCON5_FRM565 |
			  S3C2410_LCDCON5_INVVLINE |
			  S3C2410_LCDCON5_INVVFRAME |
			  S3C2410_LCDCON5_PWREN |
			  S3C2410_LCDCON5_HWSWP,

	.type		= S3C2410_LCDCON1_TFT,

	.width		= 240,
	.height		= 320,

	.pixclock	= 166667, /* HCLK 60 MHz, divisor 10 */
	.xres		= 240,
	.yres		= 320,
	.bpp		= 16,
	.left_margin	= 20,
	.right_margin	= 8,
	.hsync_len	= 4,
	.upper_margin	= 8,
	.lower_margin	= 7,
	.vsync_len	= 4,
};

static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = {
	.displays	= &smdk2440_lcd_cfg,
	.num_displays	= 1,
	.default_display = 0,

#if 0
	/* currently setup by downloader */
	.gpccon		= 0xaa940659,
	.gpccon_mask	= 0xffffffff,
	.gpcup		= 0x0000ffff,
	.gpcup_mask	= 0xffffffff,
	.gpdcon		= 0xaa84aaa0,
	.gpdcon_mask	= 0xffffffff,
	.gpdup		= 0x0000faff,
	.gpdup_mask	= 0xffffffff,
#endif

	.lpcsel		= ((0xCE6) & ~7) | 1<<4,
};

上述代码中的结构体变量s3c2410fb_mach_info 就是描述 SMDK2440 这个开发板上的 LCD 信息的,结构体指针数组 smdk2440_devices 描述的 SMDK2440 这个开发板上的所有平台相关信息。这个仅仅是使用2440这个芯片的SMDK2440开发板下的LCD信息,SMDK2440开发板还有很多的其他外设硬件和平台硬件信息使用 2440 这个芯片的板子有很多,每个板子都有描述相应板级信息的文件,这导致Linux内核中有大量的无用的板极信息,这仅仅只是一个 2440。随着智能手机的发展,每年新出的 ARM 架构芯片少说都在数十、数百款,Linux 内核下板级信息文件将会成指数级增长!这些板级信息文件都是.c 或.h 文件,都会被硬编码进Linux内核中,导致Linux内核虚胖

就好比你喜欢吃自助餐,然后花了 100 多到一家宣传看着很不错的自助餐厅,结果你想吃的牛排、海鲜、烤肉基本没多少,全都是一些凉菜、炒面、西瓜、饮料等小吃,相信你此时肯定会脱口而出一句“FUCK!”、“骗子!”。

同样的,当Linux之父linus看到ARM社区向Linux内核添加了大量无用冗余的板级信息文件,不禁的发出了一句This whole ARM thing is a fucking pain in the ass。从此以后 ARM 社区就引入了 PowerPC 等架构已经采用的设备树(Flattened Device Tree),将这些描述板级硬件信息的内容都从 Linux 内中分离开来,用一个专属的文件格式来描述,这个专属的文件就叫做设备树,文件扩展名为.dts

一个SOC可以作出很多不同的板子,这些不同的板子肯定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他的.dts文件直接引用这个通用文件即可,这个通用文件就是.dtsi 文件,类似于 C 语言中的头文件。一般.dts 描述板级信息(也就是开发板上有哪些 IIC 设备、SPI 设备等),.dtsi 描述 SOC 级信息(也就是 SOC 有几个 CPU、主频是多少、各个外设控制器信息等)。这个就是设备树的由来,简而言之就是,Linux内核中ARM架构下有太多的冗余的垃圾板
级信息文件,导致linus震怒,然后ARM社区引入了设备树

二、DTS、DTB和DTC

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

  • .dts相当于.c,就是DTS源码文件
  • DTC工具相当于gcc编译器,将.dts编译成.dtb
  • .dtb相当于bin文件,或可执行文件

通过make dtbs编译所有的dts文件。如果要编译指定的dtbs

make imx6ull-alientek-emmc.dtb

DTS 是设备树源码文件,DTB 是将DTS 编译以后得到的二进制文件。将.c 文件编译为.o 需要用到 gcc 编译器,那么将.dts 编译为.dtb需要什么工具呢?需要用到 DTC 工具。

DTC工具源码在Linux内核的scripts/dtc目录下,scripts/dtc/Makefile 文件内容如下:

# 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

# Source files need to get at the userspace version of libfdt_env.h to compile

HOSTCFLAGS_DTC := -I$(src) -I$(src)/libfdt

HOSTCFLAGS_checks.o := $(HOSTCFLAGS_DTC)
HOSTCFLAGS_data.o := $(HOSTCFLAGS_DTC)
HOSTCFLAGS_dtc.o := $(HOSTCFLAGS_DTC)
HOSTCFLAGS_flattree.o := $(HOSTCFLAGS_DTC)
HOSTCFLAGS_fstree.o := $(HOSTCFLAGS_DTC)
HOSTCFLAGS_livetree.o := $(HOSTCFLAGS_DTC)
HOSTCFLAGS_srcpos.o := $(HOSTCFLAGS_DTC)
HOSTCFLAGS_treesource.o := $(HOSTCFLAGS_DTC)
HOSTCFLAGS_util.o := $(HOSTCFLAGS_DTC)

HOSTCFLAGS_dtc-lexer.lex.o := $(HOSTCFLAGS_DTC)
HOSTCFLAGS_dtc-parser.tab.o := $(HOSTCFLAGS_DTC)

# dependencies on generated files need to be listed explicitly
$(obj)/dtc-lexer.lex.o: $(obj)/dtc-parser.tab.h

# generated files need to be cleaned explicitly
clean-files	:= dtc-lexer.lex.c dtc-parser.tab.c dtc-parser.tab.h

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

make all

或者:

make dtbs

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


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

可以看出,当选中I.MX6ULL这个SOC以后,所有使用到I.MX6ULL这个SOC(System on Chip,意为系统级芯片)的板子对应的.dts文件都会被编译为.dtb。如果我们使用I.MX6ULL新做了一个板子,只需要新建一个此板子对应的.dts文件,然后将对应的.dtb文件名添加到dtb-$(CONFIG_SOC_IMX6ULL)下,这样在编译设备树的时候就会将对应的.dts编译为二进制的.dtb文件

三、DTS语法

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

3.1 .dtsi头文件

和C语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。在 imx6ull-alientek-emmc.dts 中有如下所示内容:

第 11 行,使用“#include”来引用“input.h”这个.h 头文件。
第 12 行,使用“#include”来引用“imx6ull.dtsi”这个.dtsi 头文件。

看到这里,大家可能会疑惑,不是说设备树的扩展名是.dtsi吗?为什么也可以直接引用C语言中的.h 头文件呢?这里并没有错,.dts文件引用C语言中的.h文件,甚至也可以引用.dts文件。因此在.dts设备树文件中,可以通过#include来引用.h、.dtsi.dts文件。只是,我们在编写设备树头文件的时候最好选择.dtsi 后缀

一般.dtsi文件用于描述SOC的内部外设信息,比如CPU架构、主频、外设寄存器地址范围,比如 UART、IIC 等等。比如imx6ull.dtsi就是描述I.MX6ULL这颗SOC内部外设情况信息的,内容如下:

这个.dtsi文件一般是芯片外设的一些信息,这些信息一般是不能修改的,而我们的板载外设信息一般放在dts文件中,使我们根据我们板子的外设自己添加的,是可以修改的,我们所说的修改设备树文件就是修改这个.dtc文件。

3.2 设备节点

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

/ {
	aliases {
		can0 = &flexcan1;
		......
	};

	cpus {
		#address-cells = <1>;
		#size-cells = <0>;

		cpu0: cpu@0 {
			compatible = "arm,cortex-a7";
			device_type = "cpu";
			reg = <0>;
		};
	};

	intc: interrupt-controller@00a01000 {
		compatible = "arm,cortex-a7-gic";
		#interrupt-cells = <3>;
		interrupt-controller;
		reg = <0x00a01000 0x1000>,
		      <0x00a02000 0x100>;
	};
	......
};
  • 第 1 行, /是根节点,每个设备树文件只有一个根节点。细心的同学应该会发现,imx6ull.dtsi和 imx6ull-alientek-emmc.dts 这两个文件都有一个/根节点,这样不会出错吗?不会的,因为这两个/根节点的内容会合并成一个根节点。

  • 第 2、6 和17行,aliasescpusintc是三个子节点,在设备树中节点命名格式如下:

node-name@unit-address

其中node-name是节点名字,为ASCII字符串,节点名字应该能够清晰的描述出节点的功能,比如uart1就表示这个节点是UART1外设。unit-address一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话unit-address可以不要,比如cpu@0interrupt-controller@00a01000。但是我们在示例代码中我们看到的节点命名却如下所示:

cpu0:cpu@0

上述命令并不是node-name@unit-address这样的格式,而是用隔开成了两部分,前面的是节点标签(label),后面的才是节点名字,格式如下所示:

label: node-name@unit-address

引入label的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点,比如通过&cpu0就可以访问cpu@0这个节点,而不需要输入完整的节点名字。再比如节点intc:interruptcontroller@00a01000,节点label是intc,而节点名字就很长了,为interrupt-controller@00a01000。很明显通过&intc 来访问interrupt-controller@00a01000这个节点要方便很多!

  • 第10行,cpu0也是一个节点,只是cpu0是cpus的子节点。

综上所述:标签:节点名字@设备的地址或寄存器首地址

3.3 标准属性

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

3.1.1 compatible 属性

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

"manufacturer,model"

其中manufacturer表示厂商,model一般是模块对应的驱动名字。比如imx6ull-alientek-emmc.dts中 sound`节点是 I.MX6U-MINI开发板的音频设备节点,I.MX6U-MINI开发板上的音频芯片采用的欧胜(WOLFSON)出品的WM8960,sound节点的compatible属性值如下:

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

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

3.1.2 model 属性

model 属性值也是一个字符串,一般 model 属性描述设备模块信息,比如名字什么的,比如:

model = "wm8960-audio";

3.1.3 status 属性

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

status 属性值表

3.1.4 #address-cells和#size-cells属性

这两个属性的值都是无符号32位整形,#address-cells和#size-cells 这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。

  • #address-cells属性值决定了子节点reg属性中地址信息所占用的字长(32 位)
  • #size-cells属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)

#address-cells 和 #size-cells 表明了子节点应该如何编写 reg 属性值,一般 reg 属性都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度,reg 属性的格式一为:

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

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

第142,143行,节点 spi4 的 #address-cells = <1>,#size-cells = <0>,说明 spi4的子节点 reg 属性中起始地址所占用的字长为 1地址长度所占用的字长为 0

第 8 行,子节点 gpio_spi: gpio_spi@0 的 reg 属性值为 <0>,因为父节点设置了 #address-cells = <1>,#size-cells = <0>,因此 addres=0,没有length 的值,相当于设置了起始地址,而没有设置地址长度。

3.1.5 reg 属性

reg 属性前面已经提到过了,reg 属性的值一般是(address,length)对。reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息,比如在 imx6ull.dtsi 中有如下内容:

上述代码是节点uart1,uart1节点描述了 I.MX6ULL 的UART1相关信息,重点是第 328行的reg属性。其中uart1的父节点 aips1: aips-bus@02000000设置了#address-cells = <1>、#size-cells = <1>,因此 reg 属性中address=0x02020000,length=0x4000。查阅《I.MX6ULL 参考手册》可知,I.MX6ULL的UART1寄存器首地址为0x02020000,但是UART1的地址长度(范围)并没有0x4000这么多,这里我们重点是获取UART1寄存器首地址。

3.1.6 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 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,对于我们所使用的 I.MX6ULL 来说,子地址空间和父地址空间完全相同,因此会在 imx6ull.dtsi中找到大量的值为空的 ranges 属性,如下所示:

第 144 行定义了 ranges 属性,但是 ranges 属性值为空。

3.1. 7 name 属性

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

3.1.8 device_type 属性

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

3.4 根节点 compatible 属性

每个节点都有 compatible 属性,根节点/也不例外,imx6ull-alientek-emmc.dts 文件中根节点的 compatible 属性内容如下所示:

/ {
	model = "Freescale i.MX6 ULL 14x14 EVK Board";
	compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

	chosen {
		stdout-path = &uart1;
	};
	......
};

可以看出,compatible 有两个值:fsl,imx6ull-14x14-evkfsl,imx6ull。前面我们说了,设备节点的 compatible 属性值是为了匹配 Linux 内核中的驱动程序,那么根节点中的 compatible属性是为了做什么工作的? 通过根节点的 compatible 属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是imx6ull-14x14-evk这个设备,第二个值描述了设备所使用的 SOC,比如这里使用的是imx6ull这颗 SOC。Linux内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。

3.5 向节点追加或修改内容

产品开发过程中可能面临着频繁的需求更改,比如第一版硬件上有一个 IIC 接口的六轴芯片MPU6050,第二版硬件又要把这个 MPU6050 更换为MPU9250等。一旦硬件修改了,我们就要同步的修改设备树文件,毕竟设备树是描述板子硬件信息的文件。假设现在有个六轴芯片fxls8471,fxls8471要接到 I.MX6U-MINI开发板的 i2c1 接口上,那么相当于需要在 i2c1 这个节点上添加一个 fxls8471 子节点。先看一下 I2C1 接口对应的节点,打开文件 imx6ull.dtsi 文件,找到如下所示内容:

现在要在 i2c1 节点下创建一个子节点,这个子节点就是 fxls8471,最简单的方法就是在 i2c1 下直接添加一个名为 fxls8471 的子节点,如下所示:

//fxls8471 子节点
fxls8471@1e {
	compatible = "fsl,fxls8471";
	reg = <0x1e>;
};

第 947~951 行就是添加的 fxls8471 这个芯片对应的子节点。但是这样会有个问题!i2c1 节点是定义在 imx6ull.dtsi 文件中的,而 imx6ull.dtsi 是设备树头文件,其他所有使用到 I.MX6ULL这颗 SOC 的板子都会引用 imx6ull.dtsi 这个文件。直接在 i2c1 节点中添加 fxls8471 就相当于在其他的所有板子上都添加了 fxls8471 这个设备,但是其他的板子并没有这个设备啊!因此,这样写肯定是不行的。

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

&i2c1 {
/* 要追加或修改的内容 */
};
  • 第 1 行,&i2c1 表示要访问 i2c1 这个 label 所对应的节点,也就是 imx6ull.dtsi 中的i2c1:i2c@021a0000
  • 第 2 行,花括号内就是要向 i2c1 这个节点添加的内容,包括修改某些属性的值。打开 imx6ull-alientek-emmc.dts,找到如下所示内容
&i2c1 {
	clock-frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c1>;
	status = "okay";

	ap3216c@1e {
		compatible = "alientek,ap3216c";
		reg = <0x1e>;
	};

	fxls8471@1e {
		compatible = "fsl,fxls8471";
		reg = <0x1e>;
		position = <0>;
		interrupt-parent = <&gpio5>;
		interrupts = <0 8>;
	};
};

  • 第 316 行的属性clock-frequency就表示 i2c1 时钟为 100KHz。clock-frequency就是新添加的属性。
  • 第 319 行,将 status 属性的值由原来的 disabled 改为 okay。
  • 第 321~324 行,i2c1 子节点 ap3216c,因为正点原子的 I.MX6U-ALPHA 开发板在 I2C1 上接了一个三合一整合型光感测器。
  • 第 326~332 行,i2c1 子节点 fxls8471,就是我们需要追加的I2C设备 fxls8471这颗六轴芯片。

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

四、创建自己的模板设备树mydts.dts

/dts-v1/;

#include xxx.h
#include xxx.dtsi

/ {
  /* skeletion.dtsi文件*/
	#address-cells = <1>;
	#size-cells = <1>;
	
	chosen { 
		stdout-path = &uart1;
	};
	
	aliases { 
		can0 = &flexcan1;
		......
	};
	
	memory { 
		device_type = "memory"; 
		reg = <0 0>; 
		reg = <0x80000000 0x20000000>;
	};

  /* imx6ull.dtsi文件*/
	cpus {
	
	};
	
	intc: interrupt-controller@00a01000 {
	
	};
	
	clocks {
	
	};
	
	soc {
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "simple-bus";
		interrupt-parent = <&gpc>;
		ranges;
		busfreq {
		
		};
		pmu {
		
		};
		ocrams: sram@00900000 {
		
		};
		ocrams_ddr: sram@00904000 {
		
		};
		ocram: sram@00905000 {
		
		};
		dma_apbh: dma-apbh@01804000 {
		
		};
		gpmi: gpmi-nand@01806000{
		
		};
		aips1: aips-bus@02000000 {
		
		};
		aips2: aips-bus@02100000 {
			compatible = "fsl,aips-bus", "simple-bus";
			#address-cells = <1>;
			#size-cells = <1>;
			reg = <0x02100000 0x100000>;
			ranges;
			usbotg1: usb@02184000 {
			
			};
			usbotg1: usb@02184000 {
			
			};
			usbmisc: usbmisc@02184800 {
			
			};
			......
			adc1: adc@02198000 {
			
			};
			i2c1: i2c@021a0000 {
			  /* imx6ull.dtsi里面的iic属性信息*/
				#address-cells = <1>;
				#size-cells = <0>;
				compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
				reg = <0x021a0000 0x4000>;
				interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
				clocks = <&clks IMX6UL_CLK_I2C1>;
				//status = "disabled";

 			  /* imx6ull-alientek-emmc.dts追加的内容*/
				clock-frequency = <100000>;
				pinctrl-names = "default";
				pinctrl-0 = <&pinctrl_i2c1>;
				status = "okay";
				
				/* 具体的iic设备1*/
				ap3216c@1e {
						compatible = "alientek,ap3216c";
						reg = <0x1e>;
				};
			};
			i2c2: i2c@021a4000 {
			
			};
			i2c3: i2c@021a8000 {
			};
......
		};
		aips3: aips-bus@02200000 {
		
		};
	};

  /* imx6ull-alientek-emmc.dts文件*/
	model = "Freescale i.MX6 ULL 14x14 EVK Board";//属性
	compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";//属性

	chosen {//一级子节点

	};

	memory {//一级子节点

	};
	
	reserved-memory {//一级子节点
	
	};
	
	backlight {//一级子节点

	};
	
	pxp_v4l2 {//一级子节点

	};

	regulators{//一级子节点

	};

	sound{//一级子节点

	};
	
	spi4{//一级子节点
	
	};
	 /* 果果自定义添加的节点*/
};

//在/根节点外有一些&cpu0这样的语句是"追加"
&cpu0 {

};

&clks {

};

五、从网络启动Linux系统

从网络启动linux系统的唯一目的就是为了调试!不管是为了调试linux系统还是linux下的驱动。每次修改linux系统文件或者linux下的某个驱动以后都要将其烧写到EMMC中去测试,这样太麻烦了。我们可以设置linux从网络启动,也就是将 linux 镜像文件和根文件系统都放到Ubuntu下某个指定的文件夹中,这样每次重新编译 linux 内核或者某个 linux 驱动以后只需要使用 cp 命令将其拷贝到这个指定的文件夹中即可,这样就不用需要频繁的烧写 EMMC,这样就加快了开发速度。我这里通过 tftp 从Ubuntu中下载zImage 和设备树文件,前提是要将zImage和设备树文件放到Ubuntu下的tftp目录中。

如何得到zImage 和设备树文件呢?

只要我们编译好内核就能得到这两个文件,在linux源码目录下中执行./mx6ull_alientek_emmc.sh编译内核,编译完成以后就会在arch/arm/boot这个目录下生成一个叫做zImage的文件,zImage就是我们要用的Linux镜像文件。另外也会在arch/arm/boo/dts下生成很多.dtb文件,这些.dtb 就是设备树文件。

zImage

设备树

之后复位开发板,进入boot设置环境变量,没有设置之前默认是从emmc启动

如果想从网络启动Linux系统,就必须设置bootargs和bootcmd这两个环境变量,设置如下:

setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'
setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000'
saveenv

  • 1 、console
    console用来设置 linux 终端(或者叫控制台),也就是通过什么设备来和Linux进行交互,是串口还是 LCD 屏幕?如果是串口的话应该是串口几等等。一般设置串口作为Linux 终端,这样我们就可以在电脑上通过Mobaxterm来和 linux 交互了。这里设置console为ttymxc0,因为linux启动以后I.MX6ULL 的串口1在linux下的设备文件就是/dev/ttymxc0,在Linux下,一切皆文件。ttymxc0 后面有个“,115200”,这是设置串口的波特率,console=ttymxc0,115200 综合起来就是设置ttymxc0(也就是串口 1)作为 Linux 的终端,并且串口波特率设置为115200。

  • 2 、root
    root用来设置根文件系统的位置,root=/dev/mmcblk1p2 用于指明根文件系统存放在mmcblk1 设备的分区 2 中。EMMC 版本的核心板启动 linux 以后会存在/dev/mmcblk0、/dev/mmcblk1、/dev/mmcblk0p1、/dev/mmcblk0p2、/dev/mmcblk1p1 和/dev/mmcblk1p2 这样的文件,其中/dev/mmcblkx(x=0 ~ n)表示 mmc 设备,而/dev/mmcblkxpy(x=0~ n,y=1~ n)表示 mmc 设备x 的分区 y。在 I.MX6U-MINI开发板中/dev/mmcblk1 表示 EMMC,而/dev/mmcblk1p2 表示EMMC 的分区 2。
    root 后面有rootwait rwrootwait表示等待 mmc 设备初始化完成以后再挂载,否则的话mmc 设备还没初始化完成就挂载根文件系统会出错的。rw表示根文件系统是可以读写的,不加rw的话可能无法在根文件系统中进行写操作,只能进行读操作。

这样每一次重启开发板就会通过TFTP从Ubuntu中下载zImage 和设备树文件。这样就大大的方便了我们调试。这样就是从网络启动内核和设备树,而不需要我们一遍一遍的通过工具烧写系统了。

后面如果启动开发板不想从从Ubuntu中下载zImage 和设备树文件,时候可以在进入boot是执行下面命令,就恢复了从emmc中启动内核。

setenv bootargs
setenv bootcmd 'run findfdt;mmc dev ${mmcdev};mmc dev ${mmcdev}; if mmc rescan; then if run loadbootscript; then run bootscript; else if run loadimage; then run mmcboot; else run netboot; fi; fi; else run netboot; fi'
saveenv

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

Linux内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device- tree目录下根据节点名字创建不同文件夹,如图所示:

cd /proc/device-tree
ls -l

上图就是目录/proc/device-tree 目录下的内容,/proc/device-tree目录下是根节点“/”的所有属性和子节点,我们依次来看一下这些属性和子节点。

6.1 根节点“/”各个属性

根节点属性属性表现为一个个的文件(图中细字体文件),比如的#address-cells#size-cellscompatiblemodelname这 5 个文件,它们在设备树中就是根节点的5个属性。既然是文件那么肯定可以查看其内容,输入cat命令来查看modelcompatible这两个文件的内容

cat model
cat compatible

可以看出,文件 model 的内容是Freescale i.MX6 ULL 14x14 EVK Board,文件 compatible 的内容为fsl,imx6ull-14x14-evkfsl,imx6ull。打开文件imx6ull-alientek-emmc.dts查看一下,这不正是根节点/的 model 和 compatible 属性值吗!

6.2 根节点“/”各子节点

根节点“/”的各个子节点,比如aliasesbacklightchosenclocks等等。大家可以查看一下 imx6ull-alientek-emmc.dtsimx6ull.dtsi 这两个文件,看看根节点的子节点都有哪些。/proc/device-tree目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进入/proc/device-tree/soc目录中就可以看到 soc 节点的所有子节点

和根节点“/”一样,图中的所有文件分别为 soc 节点的属性文件和子节点文件夹。这些属性文件的内容和 imx6ull.dtsi 中 soc 节点的属性值相同,也可以进入“busfreq”这样的文件夹里面查看 soc 节点的子节点信息。

七、特殊节点

在根节点“/”中有两个特殊的子节点:aliases 和 chosen。

7.1 aliases 子节点

打开 imx6ull.dtsi 文件,aliases 节点内容如下所示:

/ {
	aliases {
		can0 = &flexcan1;
		can1 = &flexcan2;
		ethernet0 = &fec1;
		ethernet1 = &fec2;
		gpio0 = &gpio1;
		gpio1 = &gpio2;
		gpio2 = &gpio3;
		gpio3 = &gpio4;
		gpio4 = &gpio5;
		i2c0 = &i2c1;
		i2c1 = &i2c2;
		i2c2 = &i2c3;
		i2c3 = &i2c4;
		mmc0 = &usdhc1;
		mmc1 = &usdhc2;
		serial0 = &uart1;
		serial1 = &uart2;
		serial2 = &uart3;
		serial3 = &uart4;
		serial4 = &uart5;
		serial5 = &uart6;
		serial6 = &uart7;
		serial7 = &uart8;
		spi0 = &ecspi1;
		spi1 = &ecspi2;
		spi2 = &ecspi3;
		spi3 = &ecspi4;
		usbphy0 = &usbphy1;
		usbphy1 = &usbphy2;
	};
};

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

7.2 chosen 子节点

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这个属性

chosen 节点目录

输入 cat 命令查看 bootargs 这个文件的内容

cat bootargs 

bootargs 文件内容

从图可以看出,bootargs 这个文件的内容为console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw,这个不就是我们在 uboot 中设置的 bootargs 环境变量的值。

uboot在启动Linux内核的时候会将 bootargs 的值传递给Linux内核,bootargs会作为Linux内核的命令行参数。也就是说,虽然我们没有在内核中设置这个参数,但是我们在uboot在启动Linux内核的时候会将 bootargs 的值传递给Linux内核。这个参数就是我们uboot传递个内核的。

在前面我们说了如果想从网络启动Linux系统,就必须设置bootargs和bootcmd这两个环境变量,设置如下:

setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'
setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000'
saveenv

在传递完参数后,uboot还执行了三条命令,

tftp 80800000 zImage
tftp 83000000 imx6ull-alientek-emmc.dtb
bootz 80800000 - 83000000

其实就算是不从网络启动linux系统,从emmc中启动也会执行bootz 命令。

fdt_chosen 函数调用流程

真相大白,一切事情的源头都源于如下命令:

bootz 8080000083000000

当我们输入上述命令并执行以后,do_bootz函数就会执行,然后一切就按照上图所示的流程开始运行,将参数传递给我们的linux内核。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

果果小师弟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值