Linux设备树
掌握设备树是Linux驱动开发人员必备的技能,因为在新版本的Linux内核中,ARM相关的驱动全部采用的设备树,最新出的CPU及其驱动开发也基本基于设备树。比如ST出的STM32MP157、NXP的I.MX8系列等。我们所使用的Linux版本为4.1.15,其支持设备树。所以正点原子I.MX6U-ALPHA开发板的所有Linux驱动都是基于设备树的。
设备树的概念
什么是设备树?
设备树(Device Tree),将这个词分开就是设备和树,描述设备树的文件叫做DTS(device tree source)
,这个dts文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如CPU数量、内存基地址、IIC接口上接了哪些设备、SPI接口上接了哪些设备等等。如下图所示:
在图中,树的主干就是系统总线,IIC控制器、GPIO控制器、SPI控制器等都是接到系统主线上的分支。
IIC控制器有分IIC1和IIC2两种,其中IIC1上接了FT5236和AT24C02两个IIC设备,IIC2只接了MPU6050这个设备。
DTS文件的主要功能就是按照图所示的结构来描述板子上的设备信息,DTS文件描述设备信息是有相应的语法规则要求的。
在Linux内核源码中大量的arch/arm/mach-xxx和arch/arm/plat-xxx文件夹,这些文件夹里面的文件就是对应平台下的板级信息。比如在arch/arm/mach-smdk2440.c中有如下内容(有缩减):
1 | 90 static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = { |
上述代码中的结构体变量smdk2440_fb_info就是描述SMDK2440这个开发板上的LCD信息的,结构体指针数组smdk2440_devices描述的SMDK2440这个开发板上的所有平台相关信息。
一个SOC可以作出很多不同的板子,这些不同的板子肯定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他的.dts文件直接引用这个通用文件即可,这个通用文件就是.dtsi文件,类似于C语言中的头文件。一般.dts描述板级信息(也就是开发板上有哪些IIC设备、SPI设备等),.dtsi描述SOC级信息(也就是SOC有几个CPU、主频是多少、各个外设控制器信息等)。
这个就是设备树的由来,简而言之就是,Linux内核中ARM架构下有太多的冗余的垃圾板级信息文件,导致linus震怒,然后ARM社区引入了设备树。
DTS、DTB和DTC
DTS是设备树源码文件,DTB是将DTS编译以后得到的二进制文件。将.c文件编译为.o需要用到gcc编译器,那么将.dts编译为.dtb需要什么工具呢?需要用到DTC具!DTC工具源码在Linux内核的scripts/dtc目录下。
DTS语法
虽然我们基本上不会重新写一个dts文件,大多时候是直接在SOC厂商提供的dts文件上进行修改。但是DTS文件语法我们还是需要详细的学习一遍,因为我们肯定需要修改dts文件。DTS的语法非常人性化,是一种ASCII文本文件,不管是阅读还是修改都很方便。
dtsi头文件
和C语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。在imx6ull-alientek-emmc.dts中有如下所示内容:
1 | 12 |
第12行,使用“#include”来引用“input.h”这个头文件。
第13行,使用“#include”来引用“imx6ull.dtsi”这个dtsi头文件。在.dts设备树文件中,可以通过“#include”来引用.h、.dtsi和.dts文件。只是,我们在编写设备树头文件的时候最好选择.dtsi后缀。
一般dtsi文件用于描述SOC的内部外设信息,比如CPU架构、主频、外设寄存器地址范围,比如UART、IIC等等。比如imx6ull.dtsi就是描述I.MX6ULL这颗SOC内部外设情况信息的,内容如下:
1 | 10 |
以上代码中第54-89行就是cpu0这个设备节点信息,这个节点信息描述了I.MX6ULL这颗SOC所使用的CPU信息,比如架构是cortex-A7,频率支持996MHz、792MHz、528MHz、396MHz和198MHz等等。在imx6ull.dtsi文件中不仅仅描述了cpu0这一个节点信息,I.MX6ULL这颗SOC所有的外设都描述的清清楚楚,比如ecspi14、uart18、usbphy12、i2c14等等。
设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键值对。以下是从imx6ull.dtsi文件中缩减出来的设备树文件内容:
1 | 1 / { |
第一行,“/”是根节点,每个设备树文件只有一个根节点
。imx6ull.dtsi和imx6ull-alientek-emmc.dts这两个文件都有一个“/”根节点,这两个“/”根节点的内容会合并成一个根节点。
第2、6和17行,aliases、cpus和intc是三个子节点,在设备树中节点命名格式如下:
1 | node-name@unit-address |
其中“node-name”是节点名字,为ASCII字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是UART1外设。“unit-address”一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”、“interrupt-controller@00a01000”。
但是我们在以上代码中我们看到的节点命名却如下所示:
1 | cpu0:cpu@0 |
上述命令并不是“node-name@unit-address”这样的格式,而是用“:”隔开成了两部分,“:”前面的是节点标签(label),“:”后面的才是节点名字,格式如下所示:
1 | label: node-name@unit-address |
引入label的目的就是为了方便访问节点,可以直接通过&label来访问这个节点,
比如通过&cpu0就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。再比如节点“intc:interrupt-controller@00a01000”,节点label是intc,而节点名字就很长了,为“interrupt-controller@00a01000”。很明显通过&intc来访问“interrupt-controller@00a01000”这个节点要方便很多!
第10行,cpu0也是一个节点,只是cpu0是cpus的子节点。每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。设备树源码中常用的几种数据形式如下所示:
- 字符串
1
compatible = "arm,cortex-a7";
- 32位无符号整数上述代码设置reg属性的值为0,reg的值也可以设置为一组值,比如:
1
reg = <0>;
1
reg = <0 0x123456 100>;
- 字符串列表
属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开,如下所示:上述代码设置属性compatible的值为“fsl,imx6ull-gpmi-nand”和“fsl,imx6ul-gpmi-nand”。1
compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
标准属性
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux下的很多外设驱动都会使用这些标准属性,本节我们就来学习一下几个常用的标准属性。
compatible属性
compatible属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible属性的值是一个字符串列表,compatible属性用于将设备和驱动绑定起来
。字符串列表用于选择设备所要使用的驱动程序,compatible属性的值格式如下所示:
1 | "manufacturer,model" |
其中manufacturer表示厂商,model一般是模块对应的驱动名字。比如imx6ull-alientek-emmc.dts中sound节点是I.MX6U-ALPHA开发板的音频设备节点,I.MX6U-ALPHA开发板上的音频芯片采用的欧胜(WOLFSON)出品的WM8960,sound节点的compatible属性值如下:
1 | compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960"; |
属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960”,其中“fsl”表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。sound这个设备首先使用第一个兼容值在Linux内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查。
一般驱动程序文件都会有一个OF匹配表,此OF匹配表保存着一些compatible值,如果设备节点的compatible属性值和OF匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。比如在文件imx-wm8960.c中有如下内容:
1 | 632 static const struct of_device_id imx_wm8960_dt_ids[] = { |
第632~635行的数组imx_wm8960_dt_ids就是imx-wm8960.c这个驱动文件的匹配表,此匹配表只有一个匹配值“fsl,imx-audio-wm8960”。如果在设备树中有哪个节点的compatible属性值与此相等,那么这个节点就会使用此驱动文件。第642行,wm8960采用了platform_driver驱动模式,关于platform_driver驱动后面会讲解。此行设置.of_match_table为imx_wm8960_dt_ids,也就是设置这个platform_driver所使用的OF匹配表。
model属性
model属性值也是一个字符串,一般model属性描述设备模块信息,比如名字什么的,比如:
1 | model = "wm8960-audio"; |
status属性
status属性看名字就知道是和设备状态有关的,status属性值也是字符串,字符串是设备的状态信息,可选的状态如图所示:
#address-cells、#size-cells
这两个属性的值都是无符号32位整形,#address-cells和#size-cells这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。#address-cells属性值决定了子节点reg属性中地址信息所占用的字长(32位),#size-cells属性值决定了子节点reg属性中长度信息所占的字长(32位)。#address-cells和#size-cells表明了子节点应该如何编写reg属性值
,一般reg属性都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度,reg属性的格式为:
1 | reg = <address1 length1 address2 length2 address3 length3……> |
每个“address length”组合表示一个地址范围,其中address是起始地址,length是地址长度,#address-cells表明address这个数据所占用的字长,size-cells表明length这个数据所占用的字长,比如:
1 | 1 spi4 { |
第3,4行,节点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的值,相当于设置了起始地址,而没有设置地址长度。
第14,15行,设置aips3:aips-bus@02200000节点#address-cells=<1>,#size-cells=<1>,说明aips3:aips-bus@02200000节点起始地址长度所占用的字长为1,地址长度所占用的字长也为1。第19行,子节点dcp:dcp@02280000的reg属性值为<0x022800000x4000>,因为父节点设置了#address-cells=<1>,#size-cells=<1>,address=0x02280000,length=0x4000,相当于设置了起始地址为0x02280000,地址长度为0x4000。
reg属性
reg属性前面已经提到过了,reg属性的值一般是(address,length)对。reg属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息
,比如在imx6ull.dtsi中有如下内容:
1 | 323 uart1: serial@02020000 { |
上述代码是节点uart1,uart1节点描述了I.MX6ULL的UART1相关信息,重点是第326行的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寄存器首地址。
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属性,如下所示:
1 | 137 soc { |
第142行定义了ranges属性,但是ranges属性值为空。
ranges属性不为空的示例代码如下所示:
1 | 1 soc { |
第5行,节点soc定义的ranges属性,值为<0x0 0xe0000000 0x00100000>,此属性值指定了一个1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为0x0,父地址空间的物理起始地址为0xe0000000。
第10行,serial是串口设备节点,reg属性定义了serial设备寄存器的起始地址为0x4600,寄存器长度为0x100。经过地址转换,serial设备可以从0xe0004600开始进行读写操作,0xe0004600=0x4600+0xe0000000。
name属性
name属性值为字符串,name属性用于记录节点名字,name属性已经被弃用,不推荐使用name属性,一些老的设备树文件可能会使用此属性。
device_type属性
device_type属性值为字符串,IEEE1275会用到此属性,用于描述设备的FCode,但是设备树没有FCode,所以此属性也被抛弃了。此属性只能用于cpu节点或者memory节点。imx6ull.dtsi的cpu0节点用到了此属性,内容如下所示:
1 | 54 cpu0: cpu@0 { |
根节点compatible属性
每个节点都有compatible属性,根节点“/”也不例外,imx6ull-alientek-emmc.dts文件中根节点的compatible属性内容如下所示:
1 | 14 / { |
可以看出,compatible有两个值:“fsl,imx6ull-14x14-evk”和“fsl,imx6ull”。前面我们说了,设备节点的compatible属性值是为了匹配Linux内核中的驱动程序,那么根节点中的compatible属性是为了做什么工作的?通过根节点的compatible属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是“imx6ull-14x14-evk”这个设备,第二个值描述了设备所使用的SOC,比如这里使用的是“imx6ull”这颗SOC。Linux内核会通过根节点的compoatible属性查看是否支持此设备,如果支持的话设备就会启动Linux内核。
向节点追加或修改内容
产品开发过程中可能面临着频繁的需求更改,比如第一版硬件上有一个IIC接口的六轴芯片MPU6050,第二版硬件又要把这个MPU6050更换为MPU9250等。一旦硬件修改了,我们就要同步的修改设备树文件,毕竟设备树是描述板子硬件信息的文件。假设现在有个六轴芯片fxls8471,fxls8471要接到I.MX6U-ALPHA开发板的I2C1接口上,那么相当于需要在i2c1这个节点上添加一个fxls8471子节点。先看一下I2C1接口对应的节点,打开文件imx6ull.dtsi文件,找到如下所示内容:
1 | 937 i2c1: i2c@021a0000 { |
以上代码就是I.MX6ULL的I2C1节点,现在要在i2c1节点下创建一个子节点,这个子节点就是fxls8471,最简单的方法就是在i2c1下直接添加一个名为fxls8471的子节点,如下所示:
1 | 937 i2c1: i2c@021a0000 { |
第947~950行就是添加的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文件中完成数据追加的内容,方式如下:
1 | 1 &i2c1 { |
第1行,&i2c1表示要访问i2c1这个label所对应的节点,也就是imx6ull.dtsi中的“i2c1:i2c@021a0000”。
第2行,花括号内就是要向i2c1这个节点添加的内容,包括修改某些属性的值。打开imx6ull-alientek-emmc.dts,找到如下所示内容:
1 | 224 &i2c1 { |
以上代码就是向i2c1节点添加/修改数据,比如第225行的属性“clock-frequency”就表示i2c1时钟为100KHz。“clock-frequency”就是新添加的属性。
第228行,将status属性的值由原来的disabled改为okay。
第230234行,i2c1子节点mag3110,因为NXP官方开发板在I2C1上接了一个磁力计芯片mag3110,正点原子的I.MX6U-ALPHA开发板并没有使用mag3110。242行,i2c1子节点fxls8471,同样是因为NXP官方开发板在I2C1上接了fxls8471这颗六轴芯片。
第236
因为以上代码中的内容是imx6ull-alientek-emmc.dts这个文件内的,所以不会对使用I.MX6ULL这颗SOC的其他板子造成任何影响。这个就是向节点追加或修改内容,重点就是通过&label来访问节点,然后直接在里面编写要追加或者修改的内容。
创建小型模板设备树
上一节已经对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文件,在里面输入如下所
示内容:
1 | 1 / { |
设备树框架很简单,就一个根节点“/”,根节点里面只有一个compatible属性。我们就在这个基础框架上面将上面列出的内容一点点添加进来。
添加cpus节点
首先添加CPU节点,I.MX6ULL采用Cortex-A7架构,而且只有一个CPU,因此只有一个cpu0节点,完成以后如下所示:
1 | 1 / { |
第4~14行,cpus节点,此节点用于描述SOC内部的所有CPU,因为I.MX6ULL只有一个CPU,因此只有一个cpu0子节点。
添加soc节点
像uart,iic控制器等等这些都属于SOC内部外设,因此一般会创建一个叫做soc的父节点来管理这些SOC内部外设的子节点,添加soc节点以后的myfirst.dts文件内容如下所示:
1 | 1 / { |
第17~22行,soc节点,soc节点设置#address-cells=<1>,#size-cells=<1>,这样soc子节点的reg属性中起始地占用一个字长,地址空间长度也占用一个字长。
添加ocram节点
根据第2点的要求,添加ocram节点,ocram是I.MX6ULL内部RAM,因此ocram节点应该是soc节点的子节点。ocram起始地址为0x00900000,大小为128KB(0x20000),添加ocram节点以后myfirst.dts文件内容如下所示:
1 | 1 / { |
第24~27行,ocram节点,第24行节点名字@后面的0x00900000就是ocram的起始地址。第26行的reg属性也指明了ocram内存的起始地址为0x00900000,大小为0x20000。
添加aips1、aips2和和aips3这三个子节点
I.MX6ULL内部分为三个域:aips13,这三个域分管不同的外设控制器,aips13这三个域对应的内存范围如图所示:
我们先在设备树中添加这三个域对应的子节点。aips1~3这三个域都属于soc节点的子节点,完成以后的myfirst.dts文件内容如下所示:
1 | 1 / { |
第3036行,aips1节点。第3945行,aips2节点。第48~54行,aips3节点
添加ecspi1、usbotg1和和rngb这三个外设控制器节点
最后我们在myfirst.dts文件中加入ecspi1,usbotg1和rngb这三个外设控制器对应的节点,其中ecspi1属于aips1的子节点,usbotg1属于aips2的子节点,rngb属于aips3的子节点。最终的myfirst.dts文件内容如下:
1 | 1 / { |
第3844行,ecspi1外设控制器节点。第5660行,usbotg1外设控制器节点。第72~75行,rngb外设控制器节点。
至此,myfirst.dts这个小型的模板设备树就编写好了,基本和imx6ull.dtsi很像,可以看做是imx6ull.dtsi的缩小版。在myfirst.dts里面我们仅仅是编写了I.MX6ULL的外设控制器节点,像IIC接口,SPI接口下所连接的具体设备我们并没有写,因为具体的设备其设备树属性内容不同。
常用OF操作函数
设备树在系统中的体现
Linux内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-tree目录下根据节点名字创建不同文件夹
,如图所示:
我们依次来看一下这些属性和子节点。
根节点“/”各个属性
在图中,根节点属性属性表现为一个个的文件(图中细字体文件),比如图1中的“#address-cells”、“#size-cells”、“compatible”、“model”和“name”这5个文件,它们在设备树中就是根节点的5个属性。既然是文件那么肯定可以查看其内容,输入cat命令来查看model和compatible这两个文件的内容,结果如图所示:
从图可以看出,文件model的内容是“Freescalei.MX6ULL14x14EVKBoard”,文件compatible的内容为“fsl,imx6ull-14x14-evkfsl,imx6ull”。打开文件imx6ull-alientek-emmc.dts查看一下,这不正是根节点“/”的model和compatible属性值吗!
根节点“/”各子节点
图中各个文件夹(图中粗字体文件夹)就是根节点“/”的各个子节点,比如“aliases”、“backlight”、“chosen”和“clocks”等等。大家可以查看一下imx6ull-alientek-emmc.dts和imx6ull.dtsi这两个文件,看看根节点的子节点都有哪些,看看是否和图中的一致。
/proc/device-tree目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进入/proc/device-tree/soc目录中就可以看到soc节点的所有子节点,如图所示:
和根节点“/”一样,图中的所有文件分别为soc节点的属性文件和子节点文件夹。大家可以自行查看一下这些属性文件的内容是否和imx6ull.dtsi中soc节点的属性值相同,也可以进入“busfreq”这样的文件夹里面查看soc节点的子节点信息。
特殊节点
在根节点“/”中有两个特殊的子节点:aliases和chosen,我们接下来看一下这两个特殊的子节点。
aliases子节点
打开imx6ull.dtsi文件,aliases节点内容如下所示:
1 | 18 aliases { |
单词aliases的意思是“别名”,因此aliases节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量的使用&label的形式来访问节点。
chosen子节点
chosen并不是一个真实的设备,chosen节点主要是为了uboot向Linux内核传递数据,重点是bootargs参数。一般.dts文件中alientek-emmc.dts中chosen节点内容如下所示:
1 | 18 chosen { |
从以上代码中可以看出,chosen节点仅仅设置了属性“stdout-path”,表示标准输出使用uart1。但是当我们进入到/proc/device-tree/chosen目录里面,会发现多了bootargs这个属性,如图所示
输入cat命令查看bootargs这个文件的内容,结果如图所示:
设备树常用OF操作函数
设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。比如设备树使用reg属性描述了某个外设的寄存器地址为0X02005482,长度为0X400,我们在编写驱动的时候需要获取到reg属性的0X02005482和0X400这两个值,然后初始化外设。
Linux内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做OF函数。这些OF函数原型都定义在include/linux/of.h文件中。
查找节点的OF函数
设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux内核使用device_node结构体来描述一个节点,此结构体定义在文件include/linux/of.h中,定义如下:
1 | 49 struct device_node { |
与查找节点有关的OF函数有5个,我们依次来看一下。
of_find_node_by_name函数
of_find_node_by_name函数通过节点名字查找指定的节点,函数原型如下:
1 | struct device_node *of_find_node_by_name(struct device_node *from, const char *name); |
函数参数和返回值含义如下:from
:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。name
:要查找的节点名字。返回值
:找到的节点,如果为NULL表示查找失败。
of_find_node_by_type函数
of_find_node_by_type函数通过type属性查找指定的节点,函数原型如下:
1 | 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函数
of_find_compatible_node函数根据type和compatible这两个属性查找指定的节点,函数原型如下:
1 | struct device_node *of_find_compatible_node(struct device_node *from, const char *type, |
函数参数和返回值含义如下:from
:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。type
:要查找的节点对应的type字符串,也就是device_type属性值,可以为NULL,表示忽略掉device_type属性。compatible
:要查找的节点所对应的compatible属性列表。返回值
:找到的节点,如果为NULL表示查找失败
of_find_matching_node_and_match函数
of_find_matching_node_and_match函数通过of_device_id匹配表来查找指定的节点,函数原型如下:
1 | struct device_node *of_find_matching_node_and_match(struct device_node *from, |
函数参数和返回值含义如下:from
:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。matches
:of_device_id匹配表,也就是在此匹配表里面查找节点。match
:找到的匹配的of_device_id。返回值
:找到的节点,如果为NULL表示查找失败。
of_find_node_by_path函数
of_find_node_by_path函数通过路径来查找指定的节点,函数原型如下:
1 | inline struct device_node *of_find_node_by_path(const char *path) |
函数参数和返回值含义如下:path
:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是backlight这个节点的全路径。返回值
:找到的节点,如果为NULL表示查找失败
查找父/子节点的OF函数
Linux内核提供了几个查找节点对应的父节点或子节点的OF函数,我们依次来看一下。
of_get_parent函数
of_get_parent函数用于获取指定节点的父节点(如果有父节点的话),函数原型如下:
1 | struct device_node *of_get_parent(const struct device_node *node) |
函数参数和返回值含义如下:node
:要查找父节点的节点。返回值
:找到的父节点。
提取属性值的OF函数
节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux内核中使用结构体property表示属性
,此结构体同样定义在文件include/linux/of.h中,内容如下:
1 | 35 struct property { |
Linux内核也提供了提取属性值的OF函数,我们依次来看一下。
of_find_property函数
of_find_property函数用于查找指定的属性,函数原型如下:
1 | struct property *of_find_property(const struct device_node *np, |
函数参数和返回值含义如下:np
:设备节点。name
:属性名字。lenp
:属性值的字节数返回值
:找到的属性。
of_property_count_elems_of_size函数
of_property_count_elems_of_size函数用于获取属性中元素的数量,比如reg属性值是一个数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:
1 | int of_property_count_elems_of_size(const struct device_node *np, |
函数参数和返回值含义如下:np
:设备节点。proname
:需要统计元素数量的属性名字。elem_size
:元素长度。返回值
:得到的属性元素数量。
of_property_read_u32_index函数
of_property_read_u32_index函数用于从属性中获取指定标号的u32类型数据值(无符号32位),比如某个属性有多个u32类型的值,那么就可以使用此函数来获取指定标号的数据值,此函数原型如下:
1 | int of_property_read_u32_index(const struct device_node *np, |
函数参数和返回值含义如下:np
:设备节点。proname
:要读取的属性名字。index
:要读取的值标号。out_value
:读取到的值返回值
:0读取成功,负值,读取失败,-EINVAL表示属性不存在,-ENODATA表示没有要读取的数据,-EOVERFLOW表示属性值列表太小。
of_property_read_u8_array函数、of_property_read_u16_array函数、of_property_read_u32_array函数、of_property_read_u64_array函数
这4个函数分别是读取属性中u8、u16、u32和u64类型的数组数据,比如大多数的reg属性都是数组数据,可以使用这4个函数一次读取出reg属性中的所有数据。这四个函数的原型如下:
1 | int of_property_read_u8_array(const struct device_node *np, |
函数参数和返回值含义如下:np
:设备节点。proname
:要读取的属性名字。out_value
:读取到的数组值,分别为u8、u16、u32和u64。sz
:要读取的数组元素数量。返回值
:0,读取成功,负值,读取失败,-EINVAL表示属性不存在,-ENODATA表示没有要读取的数据,-EOVERFLOW表示属性值列表太小。
of_property_read_u8函数、of_property_read_u16函数、of_property_read_u32函数、of_property_read_u64函数
有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用于读取u8、u16、u32和u64类型属性值,函数原型如下:
1 | int of_property_read_u8(const struct device_node *np, |
函数参数和返回值含义如下:np
:设备节点。proname
:要读取的属性名字。out_value
:读取到的数组值。返回值
:0,读取成功,负值,读取失败,-EINVAL表示属性不存在,-ENODATA表示没有要读取的数据,-EOVERFLOW表示属性值列表太小。
of_property_read_string函数
of_property_read_string函数用于读取属性中字符串值,函数原型如下:
1 | 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函数
of_n_addr_cells函数用于获取#address-cells属性值,函数原型如下:
1 | int of_n_addr_cells(struct device_node *np) |
函数参数和返回值含义如下:np
:设备节点。返回值
:获取到的#address-cells属性值。
of_n_size_cells函数
of_size_cells函数用于获取#size-cells属性值,函数原型如下:
1 | int of_n_size_cells(struct device_node *np) |
函数参数和返回值含义如下:np
:设备节点。返回值
:获取到的#size-cells属性值。
其他常用的OF函数
of_device_is_compatible函数
of_device_is_compatible函数用于查看节点的compatible属性是否有包含compat指定的字符串,也就是检查设备节点的兼容性,函数原型如下:
1 | int of_device_is_compatible(const struct device_node *device, const char *compat) |
函数参数和返回值含义如下:device
:设备节点。compat
:要查看的字符串。返回值
:0,节点的compatible属性中不包含compat指定的字符串;正数,节点的compatible属性中包含compat指定的字符串。
of_get_address函数
of_get_address函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值,函数属性如下:
1 | const __be32 *of_get_address(struct device_node *dev, |
函数参数和返回值含义如下:dev
:设备节点。index
:要读取的地址标号。size
:地址长度。flags
:参数,比如IORESOURCE_IO、IORESOURCE_MEM等返回值:读取到的地址数据首地址,为NULL的话表示读取失败。
of_translate_address函数
of_translate_address函数负责将从设备树读取到的地址转换为物理地址,函数原型如下:
1 | u64 of_translate_address(struct device_node *dev, const __be32 *in_addr) |
函数参数和返回值含义如下:dev
:设备节点。in_addr
:要转换的地址。返回值
:得到的物理地址,如果为OF_BAD_ADDR的话表示转换失败。
of_address_to_resource函数
IIC、SPI、GPIO等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux内核使用resource结构体来描述一段内存空间,“resource”翻译出来就是“资源”,因此用resource结构体描述的都是设备资源信息,resource结构体定义在文件include/linux/ioport.h中,定义如下:
1 | 18 struct resource { |
对于32位的SOC来说,resource_size_t是u32类型的。其中start表示开始地址,end表示结束地址,name是这个资源的名字,flags是资源标志位,一般表示资源类型,可选的资源标志定义在文件include/linux/ioport.h中,如下所示:
1 | 1 |
大家一般最常见的资源标志就是IORESOURCE_MEM、IORESOURCE_REG和IORESOURCE_IRQ等。接下来我们回到of_address_to_resource函数,此函数看名字像是从设备树里面提取资源值,但是本质上就是将reg属性值,然后将其转换为resource结构体类型,函数原型如下所示:
1 | int of_address_to_resource(struct device_node *dev, int index, struct resource *r) |
函数参数和返回值含义如下:dev
:设备节点。index
:地址资源标号。r
:得到的resource类型的资源值。返回值
:0,成功;负值,失败。
of_iomap函数
of_iomap函数用于直接内存映射,以前我们会通过ioremap函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过of_iomap函数来获取内存地址所对应的虚拟地址,不需要使用ioremap函数了。
当然了,你也可以使用ioremap函数来完成物理地址到虚拟地址的内存映射,只是在采用设备树以后,大部分的驱动都使用of_iomap函数了。of_iomap函数本质上也是将reg属性中地址信息转换为虚拟地址,如果reg属性有多段的话,可以通过index参数指定要完成内存映射的是哪一段,of_iomap函数原型如下:
1 | void __iomem *of_iomap(struct device_node *np, int index) |
函数参数和返回值含义如下:np
:设备节点。index
:reg属性中要完成内存映射的段,如果reg属性只有一段的话index就设置为0。返回值
:经过内存映射后的虚拟内存首地址,如果为NULL的话表示内存映射失败。