正点原子嵌入式linux驱动开发——Linux设备树

在前面系统移植的过程中多次提到“设备树”这个概念和创建自己的设备树。但是并没有在TF-A和uboot里说设备树的原理,因为相对比较复杂。

本章就来详细的谈一谈设备树。掌握设备树是Linux驱动开发人员必备的技能!因为在新版本的Linux中,ARM相关的驱动全部采用了设备树(也有支持老式驱动的,比较少),最新出CPU在系统启动的时候就支持设备树,比如MP1系列、NXP的I.MX8系列等。正点原子教程中所使用的Linux版本为5.4.31,其支持设备树,所以正点原子MP1开发板的所有Linux驱动都是基于设备树的。本章就来了解一下设备树的起源、重点学习一下设备树语法

设备树

设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做DTS(Device Tree Source),这个DTS文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如CPU数量、内存基地址、IIC接口上接了哪些设备、SPI接口上接了哪些设备等等,如下图所示:
正点原子嵌入式linux驱动开发——Linux设备树_第1张图片
上图中,树的主干就是系统总线,IIC控制器、GPIO控制器、SPI控制器等都是接到系统主线上的分支。IIC控制器有分为IIC1和IIC2两种,其中IIC1上接了FT5206和AT24C02这两个IIC设备,IIC2上只接了MPU6050这个设备。DTS文件的主要功能就是按照上图所示的结构来描述板子上的设备信息,DTS文件描述设备信息是有相应的语法规则要求的,之后有详细讲解。

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

示例代码23.1.1 mach-smdk2440.c文件代码段 
90 static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = { 
91 
92     .lcdcon5 = S3C2410_LCDCON5_FRM565 | 
93                S3C2410_LCDCON5_INVVLINE | 
94                S3C2410_LCDCON5_INVVFRAME | 
95                S3C2410_LCDCON5_PWREN | 
96                S3C2410_LCDCON5_HWSWP, 
...... 
113 }; 
114
115 static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = { 
116     .displays = &smdk2440_lcd_cfg, 
117     .num_displays = 1, 
118     .default_display = 0, 
...... 
133 }; 
134 
135 static struct platform_device *smdk2440_devices[] __initdata = { 
136     &s3c_device_ohci, 
137     &s3c_device_lcd, 
138     &s3c_device_wdt, 
139     &s3c_device_i2c0, 
140     &s3c_device_iis, 
141 }; 

上述代码中的结构体变量smdk2440_fb_info就是描述SMDK2440这个开发板上的LCD信息的,结构体指针数组smdk2440_devices描述的SMDK2440这个开发板上的所有平台相关信
息。这个仅仅是使用2440这个芯片的SMDK2440开发板下的LCD信息,SMDK2440开发板还有很多的其他外设硬件和平台硬件信息。由于太过冗长,之后ARM社区就引入了PowerPC等架构已经采用的设备树(Flattened Device Tree),将这些描述板级硬件信息的内容都从Linux内中分离开来,用一个专属的文件格式来描述,这个专属的文件就叫做设备树,文件扩展名为.dts。一个SOC可以作出很多不同的板子,这些不同的板子肯定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他的.dts文件直接引用这个通用文件即可,这个通用文件就是.dtsi文件,类似于C语言中的头文件。一般.dts描述板级信息(也就是开发板上有哪些IIC设备、SPI设备等 ),.dtsi描述SOC级信息(也就是SOC有几个CPU、主频是多少、各个外设控制器信息等)

DTS、DTB和DTC

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

示例代码23.2.1 scripts/dtc/Makefile文件代码段 
4 hostprogs-$(CONFIG_DTC) := dtc
5 always := $(hostprogs-y) 
6 
7 dtc-objs := dtc.o flattree.o fstree.o data.o livetree.o treesource.o 
8         srcpos.o checks.o util.o 
9 dtc-objs += dtc-lexer.lex.o dtc-parser.tab.o 
......

可以看出,DTC工具依赖于dtc.c、flattree.c、fstree.c等文件,最终编译并链接出DTC这个主机文件。如果要编译DTS只需进入Linux源码根目录,执行“make all”或者“make dtbs”即可。

基于ARM架构的SOC有很多种,一种SOC又可以制作出很多款板子,每个板子都有一个对应的DTS文件,以STM32MP1这款芯片对应的板子为例来看一下,打开 arch/arm/boot/dts/Makefile,有如下内容:

dtb-$(CONFIG_ARCH_STM32) += \
	stm32f429-disco.dtb \
	stm32f469-disco.dtb \
	stm32f746-disco.dtb \
	stm32f769-disco.dtb \
	stm32429i-eval.dtb \
	stm32746g-eval.dtb \
	stm32h743i-eval.dtb \
	stm32h743i-disco.dtb \
	stm32mp157a-avenger96.dtb \
	stm32mp157a-dk1.dtb \
	stm32mp157d-dk1.dtb \
	stm32mp157c-dk2.dtb \
	stm32mp157f-dk2.dtb \
	stm32mp157c-dk2-a7-examples.dtb \
	stm32mp157c-dk2-m4-examples.dtb \
	stm32mp157f-dk2-a7-examples.dtb \
	stm32mp157f-dk2-m4-examples.dtb \
	stm32mp157a-ed1.dtb \
	stm32mp157c-ed1.dtb \
	stm32mp157d-ed1.dtb \
	stm32mp157f-ed1.dtb \
	stm32mp157a-ev1.dtb \
	stm32mp157c-ev1.dtb \
	stm32mp157d-ev1.dtb \
	stm32mp157f-ev1.dtb \
	stm32mp157c-ev1-a7-examples.dtb \
	stm32mp157c-ev1-m4-examples.dtb \
	stm32mp157f-ev1-a7-examples.dtb \
	stm32mp157f-ev1-m4-examples.dtb \
	stm32mp157d-atk.dtb

可以看出,当选中STM32MP1这个SOC以后(CONFIG_ARCH_STM32=y),所有使用到STM32MP1这个SOC的板子对应的.dts文件都会被编译为.dtb。如果使用STM32MP1新做了一个板子,只需要新建一个此板子对应的.dts文件,然后将对应的.dtb文件名添加到dtb-$(CONFIG_ARCH_STM32)下,这样在编译设备树的时候就会将对应的.dts编译为二进制
的.dtb文件

uboot中使用bootz或者bootm命令向Linux内核传递二进制设备树文件(.dtb)。

DTS语法

虽然基本上不会从头到尾重写一个.dts文件,大多时候是直接在SOC厂商提供的.dts文件上进行修改。但是DTS文件语法还是需要详细的学习一遍,因为后续工作中肯定需要修改.dts文件。DTS语法是一种ASCII文本文件,不管是阅读还是修改都很方便。本节就以stm32mp157d-atk.dts这个文件为例来讲解一下DTS语法。

.dtsi头文件

设备树头文件扩展名为.dtsi,在stm32mp157d-atk.dts中,具有对stm32mp15*.dtsi引用头文件。

设备树种除了用“#include”来引用.dtsi文件,也可以引用.h头文件,例如:

#include 

还可以引用.dts文件:

#include "stm32mp157c-ed1.dts"

一般.dtsi文件用于描述SOC的内部外设信息,比如CPU架构、主频、外设寄存器地址范围,比如UART、IIC等等。如果一个系列里有多个SOC就会把相同内部外设信息提炼到一个.dtsi文件里,这样为了减少代码的冗余。目前STM32MP1系列里有stm32mp151、stm32mp153和stm32mp157这三款SOC,其中151是外设最少的,153和157的外设是在151的基础上逐渐增加的。因此151就相当于“基类”,153和157是在151基础上得到的“派生类”。因此ST就把最基本的外设资源都写在stm32mp151.dtsi文件里。 stm32mp151.dtsi就是描述151、153和157共有的外设信息的。

设备节点

设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键值对。以下文件是结合ST官方的设备树缩减出来设备树文件内容:
正点原子嵌入式linux驱动开发——Linux设备树_第2张图片
第1行,“/”是根节点,每个设备树文件只有一个根节点。在内核移植设备树的时候,新建的stm32mp157d-atk.dts和stm32mp157d-atk.dtsi这两个文件都有一个“/”根节点,这两个“/”根节点的内容会合并成一个根节点。

第5、9和26行,aliases、cpus和soc是三个子节点,33行sram是soc的子节点。在设备树中节点命名格式如下

node-name@unit-address

其中“node-name”是节点名字,为ASCII字符串,“unit-address”一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”、“sram@10000000”、 “soc”。

上述代码中,采用了另一种格式,使用“:”分成两部分,“:”之前是节点标签(label),后面是节点名字:

label:node-name@unit-address

引入label的目的就是为了方便访问节点,可以直接通过&label来访问这个节点

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

1、字符串

compatible = "arm,cortex-a7";

2、32位无符号整数

reg = <0>;

3、字符串列表

属性值可以为字符串列表,字符串之间采用“,”隔开:

compatible = "st,stm32mp157d-atk", "st,stm32mp157";

标准属性

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

compatible属性

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

"manufacturer,model"

其中manufacturer表示厂商,model一般是模块对应的驱动名字。比如stm32mp15xx-dkx.dtsi中有一个音频设备节点,这个节点的音频芯片采用的Cirrus Logic公司出品的cs42l51,compatible属性值如下:

compatible = "cirrus,cs42l51"

属性值为“cirrus,cs42l51”,其中’cirrus’表示厂商是Cirrus Logic,“cs42l51”表示驱动模块名字。compatible也可以多个属性值。比如:

compatible = "cirrus,my_cs42l51","cirrus,cs42l51";

这样设备就有两个属性值,这个设备首先使用第一个兼容值在Linux内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查,以此类推,直到查找完compatible属性中的所有值

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

model属性

model属性值也是一个字符串,一般model属性描述开发板的名字或者设备模块信息。

status属性

status属性值也是字符串,字符串是设备的状态信息,可选的状态如下图所示:
正点原子嵌入式linux驱动开发——Linux设备树_第3张图片

#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 =

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

reg属性

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

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属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,对于使用的stm32mp157来说,子地址空间和父地址空间完全相同,因此会在stm32mp151.dtsi中找到大量的值为空的ranges属性。

name属性

name属性值为字符串,name属性用于记录节点名字, name属性已经被弃用。

device_type属性

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

根节点compatible属性

在自己新建的stm32mp157d-atk.dts中,根节点的compatible属性内容如下:
正点原子嵌入式linux驱动开发——Linux设备树_第4张图片
通过根节点的compatible属性可以知道所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是“stm32mp157d-atk”这个设备,第二个值描述了设备所使用的SOC,比如这里使用的是“stm32mp157”这颗SOC。Linux内核会通过根节点的compoatible属性查看是否支持此设备,如果支持的话设备就会启动Linux内核

使用设备树之前设备匹配方法

在没有使用设备树以前,uboot会向Linux内核传递一个叫做machine id的值,machine id也就是设备ID,告诉Linux内核自己是个什么设备,看看Linux内核是否支持。Linux内核是支持很多设备的,针对每一个设备Linux内核都用MACHINE_START和MACHINE_END来定义一个machine_desc结构体来描述这个设备,比如在文件arch/arm/mach-imx/mach-mx35_3ds.c中定义了“Freescale MX35PDK”这个设备,而MACHINE_START和MACHINE_END定义在/arch/arm/include/asm/mach/arch.h中,展开后可以获得“MACH_TYPE_MS35_3DS”这个machine id,这是定义在/include/generated/mach-types.h中的,其中有大量machine id。

那么经过以上替换查找,最终找到了machine id之后,uboot会把他传给Linux内核,内核就会检查这个machine id,如果找到相等的那就表明Linux内核支持这个设备可以启动。

使用设备树以后的设备匹配方法

引入设备树后就不使用MACHINE_START了,换位DT_MACHINE_START,他也定义在/arch/arm/include/asm/mach/arch.h中,DT_MACHINE_START里面直接将.nr设置为“~0”。

打开arch/arm/mach-stm32//board-dt.c,有如下内容:
正点原子嵌入式linux驱动开发——Linux设备树_第5张图片
machine_desc结构体中有个.dt_compat成员变量,此成员变量保存着本设备兼容属性,上述示例代码中设置.dt_compat为stm32_compat,此表里面含有Linux内核所支持的soc兼容值。stm32mp157d-atk.dts中根节点的compatible属性值如下:

compatible = "st,stm32mp157d-atk", "st,stm32mp157";

其中“st,stm32mp157”与stm32_compat中的“st,stm32mp157”匹配,因此正点原子STM32MP157开发板可以正常启动Linux内核。

接下来简单看一下Linux内核是如何根据设备树根节点的compatible属性来匹配出对应的machine_desc,Linux内核调用start_kernel函数来启动内核,start_kernel函数会调用setup_arch函数来匹配machine_desc,setup_arch函数定义在文件arch/arm/kernel/setup.c中,函数内容如下(有缩减):
正点原子嵌入式linux驱动开发——Linux设备树_第6张图片
第1081行,调用setup_machine_fdt函数来获取匹配machine_desc,参数就是atags的首地址,也就是uboot传递给Linux内核的dtb文件首地址,setup_machine_fdt函数的返回值就是
找到的最匹配的machine_desc。

函数setup_machine_fdt定义在文件 arch/arm/kernel/devtree.c中,内容如下(有缩减):
正点原子嵌入式linux驱动开发——Linux设备树_第7张图片
第227行,调用函数of_flat_dt_match_machine来获取匹配的machine_desc,参数mdesc_best是默认的machine_desc,参数arch_get_next_mach是个函数,此函数定义在定义在arch/arm/kernel/devtree.c文件中。找到匹配的machine_desc的过程就是用设备树根节点的compatible属性值和Linux内核中保存的所有machine_desc结构的.dt_compat中的值比较,看
看哪个相等,如果相等的话就表示找到匹配的machine_desc,arch_get_next_mach函数的工作就是获取Linux内核中下一个machine_desc结构体。

最后再来看一下of_flat_dt_match_machine函数, 此函数定义在文件drivers/of/fdt.c中,内容如下(有缩减):
正点原子嵌入式linux驱动开发——Linux设备树_第8张图片
第824行,通过函数of_get_flat_dt_root获取设备树根节点。

第825-831行,此循环就是查找匹配的machine_desc过程,第826行的of_flat_dt_match函数会将根节点compatible属性的值和每个machine_desc结构体中.dt_compat的值进行比较,直至找到匹配的那个machine_desc。

总结一下,Linux内核通过根节点compatible属性找到对应的设备的函数调用过程,如下图所示:
正点原子嵌入式linux驱动开发——Linux设备树_第9张图片

向节点追加或修改内容

产品开发过程中可能面临着频繁的需求更改,比如第一版硬件上有一个IIC接口的六轴芯片MPU6050,第二版硬件又要把这个MPU6050更换为MPU9250等。一旦硬件修改了,就要同步的修改设备树文件,毕竟设备树是描述板子硬件信息的文件。假设现在有个六轴芯片fxls8471,fxls8471要接到STM32MP157D-ATK开发板的I2C1接口上,那么相当于需要在i2c1这个节点上添加一个fxls8471子节点

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

但是这样会有个问题!i2c1节点是定义在stm32mp151.dtsi文件中的,而stm32mp151.dtsi是共有的设备树头文件,其他所有使用到stm32mp1这颗SOC的板子都会引用stm32mp151.dtsi这个文件。直接在i2c1节点中添
加fxls8471就相当于在其他的所有板子上都添加了fxls8471这个设备。

STM32MP157D-ATK开发板使用的设备树文件为stm32mp157d-atk.dts和stm32mp157d-atk.dtsi,因此需要在stm32mp157d-atk.dts文件中完成数据追加的内容,方式如下:

示例代码23.3.5.3 节点追加数据方法 
1 &i2c1 { 
2 /* 要追加或修改的内容 */ 
3 };

打开stm32mp157d-atk.dts,在根节点后添加如下内容:
正点原子嵌入式linux驱动开发——Linux设备树_第10张图片
示例代码23.3.5.4就是向i2c1节点添加/修改数据,比如第5行将status属性的值由原来的disabled改为okay。第6行的属性“clock-frequency”表示i2c1时钟为100KHz。“clock-frequency”就是新添加的属性。

第8-14行,i2c1子节点fxls8471,表示I2C1上连接的fxls8471,“fxls8471”子节点里面描述了fxls8471这颗芯片的相关信息。

这个就是向节点追加或修改内容,重点就是通过&label来访问节点,然后直接在里面编写要追加或者修改的内容。

创建小型模板设备树

上一节已经对DTS的语法做了比较详细的讲解,本节就根据前面讲解的语法,从头到尾编写一个小型的设备树文件。在实际产品开发中,是不需要完完全全的重写一个.dts设备树文件,一般都是使用SOC厂商提供好的.dts文件,只需要在上面根据自己的实际情况做相应的修改即可。在编写设备树之前要先定义一个设备,就以STM32MP157这个SOC为例,需要在设备树里面描述的内容如下:

  1. 这个芯片、这个芯片是由两个Cortex-A7架构的32位CPU和Cortex-M4组成。
  2. STM32MP157内部sram,起始地址为0x10000000,大小为384KB(0x60000)。
  3. STM32MP157内部timers6,起始地址为0x40004000,大小为25.6KB(0x400)。
  4. TM32MP157内部spi2,起始地址为0x4000b000,大小为25.6KB(0x400)。
  5. STM32MP157内部usart2,起始地址为0x4000e000,大小为25.6KB(0x400)。
  6. STM32MP157内部i2c1,起始地址为0x40012000,大小为25.6KB(0x400)。

为了简单起见,在设备树里面就实现这些内容即可,具体的实现可以看正点原子的文档教程,搭建完后和现有的stm32mp151.dtsi很像,可以看做是stm32mp151.dtsi的缩小版。

设备树在系统中的体现

Linux内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-tree目录下根据节点名字创建不同文件夹,如下图所示:
正点原子嵌入式linux驱动开发——Linux设备树_第11张图片
上图就是目录/proc/device-tree目录下的内容,/proc/device-tree目录下是根节点“/”的所有属性和子节点。

根节点“/”各个属性

根节点属性属性表现为一个个的文件,比如上图中的“#address-cells”、“#size-cells”、“compatible”、“model”和“name”这5个文件,它们在设备树中就是根节点的5个属性。既然是文件那么肯定可以查看其内容,输入cat命令来查看model和compatible这两个文件的内容,结果如下图所示:
model和compatible文件内容
可以看出,文件model的内容是“STMicroelectronics STM32MP157D eval daughter”,文件compatible的内容为“st stm32mp157d-ed1st,stm32mp157”。打开文件stm32mp157d-atk.dts查看一下,就是model和compatible的属性值

根节点“/”各子节点

上图中各个文件夹就是根节点“/”的各个子节点,比如“aliases”、“reboot”、“chosen”和“cpus”等等。

/proc/device-tree目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进入/proc/device-tree/soc目录中就可以看到soc节点的所有子节点,如下图所示:
正点原子嵌入式linux驱动开发——Linux设备树_第12张图片
和根节点“/”一样,上图中的所有文件分别为soc节点的属性文件和子节点文件夹。

特殊节点

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

aliases子节点

在stm32mp157d-atk.dts文件中,aliases节点内容如下:
aliases子节点
单词aliases的意思是“别名”,因此aliases节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过一般会在节点命名的时候会加上label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量的使用&label的形式来访问节点。

chosen子节点

chosen并不是一个真实的设备,chosen节点主要是为了uboot向Linux内核传递数据,重点是bootargs参数。一般.dts文件中chosen节点通常为空或者内容很少,stm32mp157d-atk.dts中chosen节点内容如下所示:
chosen子节点
从示例代码 23.6.2.1中可以看出,chosen节点仅仅设置了属性“stdout-path”,表示标准输出使用serial0,而 aliases已经设置了serial0为uart4,所以开发板启动以后使用UART4作为默认终端。但是进入到/proc/device-tree/chosen目录里面,会发现多了bootargs这个属性,通过cat命令查看bootargs文件内容,如下图所示:
bootargs文件内容
可以看到,这就是uboot中设置的bootargs环境变量。前面讲解uboot的时候说过,uboot在启动Linux内核的时候会将bootargs的值传递给Linux内核,bootargs会作为Linux内核的命令行参数,Linux内核启动的时候会打印出命令行参数(也
就是uboot传递进来的bootargs的值),如下图所示:
命令行参数
那只能是uboot自己在chosen节点里面添加了bootargs属性!并且设置bootargs属性的值为bootargs环境变量的值。在uboot源码中,找到common/fdt_support.c文件中发现了“chosen”的身影,fdt_support.c文件中有个fdt_chosen函数,此函数内容如下所示:
正点原子嵌入式linux驱动开发——Linux设备树_第13张图片
第288行,调用函数fdt_find_or_add_subnode从设备树(.dtb)中找到chosen节点,如果没有找到的话就会自己创建一个chosen节点。

第292行,读取uboot中bootargs环境变量的内容。

第294行,调用函数fdt_setprop向chosen节点添加bootargs属性,并且bootargs属性的值就是环境变量bootargs的内容。

最终翻看源码,可以得到如下的调用过程图:
正点原子嵌入式linux驱动开发——Linux设备树_第14张图片
上图中框起来的部分就是函数do_bootm_linux函数的执行流程,也就是说do_bootm_linux函数会通过一系列复杂的调用,最终通过fdt_chosen函数在chosen节点中加入了bootargs属性。而通过bootz命令启动Linux内核的时候会运行do_bootm_linux函数。

当输入bootm命令之后,do_bootm就会执行,然后就如上图所示加入bootargs属性。

Linux内核解析DTB文件

Linux内核在启动的时候会解析DTB文件,然后在/proc/device-tree目录下生成相应的设备树节点文件:
正点原子嵌入式linux驱动开发——Linux设备树_第15张图片
从上图中可以看出,在start_kernel函数中完成了设备树节点的解析,实际工作的函数为unflatten_dt_node。

绑定信息文档

设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属性不同。在Linux内核源码中有详细的TXT文档描述了如何添加节点,这些TXT文档叫做绑定文档,路径为:Linux源码目录/Documentation/devicetree/bindings,如下图所示:
正点原子嵌入式linux驱动开发——Linux设备树_第16张图片
有时候使用的一些芯片在Documentation/devicetree/bindings目录下找不到对应的文档,这个时候就要咨询芯片的提供商。

设备树常用OF操作函数

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

查找节点的OF函数

设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux内核使用device_node结构体来描述一个节点,此结构体定义在文件include/linux/of.h中,定义如下:
正点原子嵌入式linux驱动开发——Linux设备树_第17张图片
与查找节点有关的OF函数共有5个。

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

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

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

函数参数和返回值含义如下:

  • from:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。
  • type:要查找的节点对应的type字符串,也就是device_type属性值。
  • 返回值:找到的节点,如果为NULL表示查找失败。

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:开 始查找的节点,如果为NULL表示从根节点开始查找整个设备树。
  • type:要查找的节点对应的type字符串,也就是device_type属性值,可以为NULL,表示忽略掉device_type属性。
  • compatible:要查找的节点所对应的compatible属性列表。
  • 返回值: 找到的节点,如果为NULL表示查找失败。

of_find_matching_node_and_match函数

该函数通过of_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:开始查找的节点,如果为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表示查找失败。

查找父/子节点的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,表示从第一个子节点开始。
  • 返回值: 找到的下一个子节点。

提取属性值的OF函数

节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux内核中使用结构体property表示属性,此结构体同样定义在文件include/linux/of.h中,内容如下:
正点原子嵌入式linux驱动开发——Linux设备树_第18张图片

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

该函数用于获取属性中元素的数量,比如reg属性值是一个数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:

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

函数参数和返回值含义如下:

  • np:设备节点。
  • proname:需要统计元素数量的属性名字。
  • elem_size:元素长度。
  • 返回值:得到的属性元素数量。

of_property_read_u32_index函数

该函数用于从属性中获取指定标号的u32类型数据值(无符号32位),比如某个属性有多个u32类型的值,那么就可以使用此函数来获取指定标号的数据值,此函数原型如下:

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

函数参数和返回值含义如下:

  • np:设备节点。
  • proname:要读取的属性名字。
  • index:要读取的值标号。
  • out_value:读取到的值。
  • 返回值: 0读取成功,负值,读取失败,-EINVAL表示属性不存在,-ENODATA表示没有
    要读取的数据,-EOVERFLOW表示属性值列表太小。

of_property_read_u8_array函数

此函数有一系列,分别是读取u8、u16、u32和u64类型的数组数据,比如大多数的reg属性都是数组数据,可以使用这4个函数一次读取出reg属性中的所有数据。这四个函数的原型如下:

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)

函数参数和返回值含义如下:

  • np:设备节点。
  • proname:要读取的属性名字。
  • out_value:读取到的数组值,分别为u8、u16、u32和u64。
  • sz:要读取的数组元素数量。
  • 返回值:0,读取成功,负值,读取失败,-EINVAL表示属性不存在,-ENODATA表示没有要读取的数据,-EOVERFLOW表示属性值列表太小。

of_property_read_u8函数

有些属性只有一个整形值,该系列函数就是用于读取这种只有一个整形值的属性,分别用于读取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)

函数参数和返回值含义如下:

  • 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_string:读取到的字符串值。
  • 返回值:0,读取成功,负值,读取失败。

of_n_addr_cells函数

该函数用于获取#address-cells属性值,函数原型如下:

int of_n_addr_cells(struct device_node *np)

函数参数和返回值含义如下:

  • np:设备节点。
  • 返回值: 获取到的#address-cells属性值。

of_n_size_cells函数

该函数用于获取#size-cells属性值,函数原型如下:

int of_n_size_cells(struct device_node *np)

函数参数和返回值含义如下:

  • np:设备节点。
  • 返回值:获取到的#size-cells属性值。

其他常用OF函数

of_device_is_compatible函数

该函数用于查看节点的compatible属性是否有包含compat指定的字符串,也就是检查设备节点的兼容性,函数原型如下:

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

函数参数和返回值含义如下:

  • device:设备节点。
  • compat:要查看的字符串。
  • 返回值:0,节点的compatible属性中不包含compat指定的字符串;正数,节点的compatible属性中包含compat指定的字符串。

of_get_address函数

该函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值,函数属性如下:

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

函数参数和返回值含义如下:

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

IIC、SPI、GPIO等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux内核使用resource结构体来描述一段内存空间,用resource结构体描述的都是设备资源信息,resource结构体定义在文件include/linux/ioport.h中,定义如下:
正点原子嵌入式linux驱动开发——Linux设备树_第19张图片
对于32位的SOC来说,resource_size_t是u32类型的。其中start表示开始地址,end表示结束地址,name是这个资源的名字,flags是资源标志位,一般表示资源类型,可选的资源标志定义在文件include/linux/ioport.h中。

一般最常见的资源标志就是IORESOURCE_MEM、IORESOURCE_REG和IORESOURCE_IRQ等。接下来回到 of_address_to_resource函数,此函数看名字像是从设备树里面提取资源值,但是本质上就是将reg属性值转换为resource结构体类型,函数原型如下所示:

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

函数参数和返回值含义如下:

  • dev:设备节点。
  • index:地址资源标号。
  • r:得到的resource类型的资源值。
  • 返回值:0,成功;负值,失败。

of_iomap函数

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

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

函数参数和返回值含义如下:

  • np:设备节点。
  • index:reg属性中要完成内存映射的段,如果reg属性只有一段的话index就设置为0。
  • 返回值:经过内存映射后的虚拟内存首地址,如果为NULL的话表示内存映射失败。

设备树重点需要了解的内容如下:

  1. DTS、DTB和DTC之间的区别,如何将.dts文件编译为.dtb文件。
  2. 设备树语法,这个是重点,在实际工作中是需要修改设备树的。
  3. 设备树的几个特殊子节点。
  4. 关于设备树的OF操作函数,也是重点,因为设备树最终是被驱动文件所使用的,而驱动文件必须要读取设备树中的属性信息,比如内存信息、GPIO信息、中断信息等等。要想在驱动中读取设备树的属性值,就必须使用Linux内核提供的众多的OF函数。

总结

设备树在Linux中非常重要,几乎所有外设的操作都需要用到,比如之前系统移植的时候,从TF-A(这个在I.MX6ULL没有),到通用的U-Boot和Linux内核,对于开发板的定义都要用到设备树。

DTS、DTB、DTC

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

DTS语法

这一部分了解一下就好,主要是针对例如ST这样的芯片厂商提供的example,对着他的基础,针对具体的开发板进行修改。

.dtsi就相当于是设备树的头文件;.dts就是设备树文件;设备树主要就是要创建设备节点,同时需要设置节点的属性。

根节点的属性,就需要关注compatible属性,来匹配Linux内核的驱动程序。其调用流程如下:
正点原子嵌入式linux驱动开发——Linux设备树_第20张图片
具体的添加节点的语法,其实参考之前的移植笔记,就大概知道了,一些语法属性也是这样来学习巩固。

设备树的体现

在根文件系统的/proc/device-tree目录下就可以看到,这个目录下的文件就是各个节点及其属性。

特殊节点

aliases节点,来定义节点别名用来访问节点,但是一般都会通过加上label然后通过&label来访问。

chosen节点,用来让uboot像Linux内核传递数据,尤其是bootargs参数。

解析DTB以及绑定信息文档

start_kernel()函数中,最终会调用unflatten_dt_node来完成解析。

查询硬件对应节点的属性可以在Linux源码目录下的/Documentation/devicetree/bindings来查看txt文档。

查找节点

通过各种OF函数来查询节点的各种信息。

你可能感兴趣的:(linux学习,linux,驱动开发,stm32,笔记,学习)