目录
创建小型模板设备树
添加cpus节点
添加soc节点
添加ocram节点
添加aips1、aips2和aips3这三个子节点
添加eespil、usbotg1和rngb这三个外设控制器节点
设备树在系统中的体现
根节点“/”各个属性
根节点“/”各子节点
特殊节点
aliases子节点
chosen子节点
Linux内核解析DTB文件
设备树常用OF操作函数
查找节点的OF函数
of_find_node_by_name函数
of find node_by type 函数
of_find_compatible_node函数
提取属性值的OF函数
of_find_property函数
of_property_count_elems_of_size函数
of_property_read_u8_array 函数
of_property_read_u8函数
of_n_addr_cells函数
of_n_size_cells函数
其他常用的OF函数
of_get_address函数
of_translate_address函数
of_address_to_resource函数
of_iomap函数
根据前面讲解的语法,从头到尾编写一个小型的设备树文件。当然了,这个小型设备树没有实际的意义,做这个的目的是为了掌握设备树的语法。在实际产品开发中,我们是不需要完完全全的重写一个.dts设备树文件,一般都是使用 SOC 厂商提供好的.dts文件,我们只需要在上面根据自己的实际情况做相应的修改即可。在编写设备树之前要先定义一个设备,我们就以I.MX6ULL这个SOC为例,我们需要在设备树里面描述的内容如下:
1.I.MX6ULL这个Cortex-A7 架构的32位CPU。
2.I.MX6ULL内部ocram,起始地址 0x00900000,大小为128KB(0x20000)。
3.I.MX6ULL内部aips1域下的ecspil外设控制器,寄存器起始地址为0x02008000,大小为 0x4000。
4.I.MX6ULL内部 aips2域下的 usbotg1外设控制器,寄存器起始地址为0x02184000,大小为 0x4000。
5.I.MX6ULL内部aips3域下的rngb外设控制器,寄存器起始地址为0x02284000,大小为 0x4000。
为了简单起见,我们就在设备树里面就实现这些内容即可,首先,搭建一个仅含有根节点“/”的基础的框架,新建一个名为myfirst.dts文件,在里面输入如下所示内容:
设备树框架很简单,就一个根节点"/”,根节点里面只有一个compatible属性。我们就在这个基础框架上面将上面列出的内容一点点添加进来。
首先添加CPU节点,I.MX6ULL采用Cortex-A7架构,而且只有一个CPU,因此只有一个cpu0节点,完成以后如下所示:
第4-14行,cpus节点,此节点用于描述SOC内部的所有CPU,因为I.MX6ULL只有一个CPU,因此只有一个cpu0子节点。
像 uart, iic控制器等等这些都属于SOC内部外设,因此一般会创建一个叫做soc的父节点来管理这些SOC内部外设的子节点,添加soc节点以后的myfirst.dts文件内容如下所示:
第17-22行,soc节点,soc节点设置#address-cells=<1>, #size-cells=<1>,这样soc子节点的 re 属性中起始地占用一个字长,地址空间长度也占用一个字长。
第21行, ranges属性,ranges属性为空,说明子空间和父空间地址范围相同。
根据第2点的要求,添加ocram节点, ocram是I.MX6ULL内部RAM,因此ocram节点应该是soc节点的子节点。ocram起始地址为0x00900000,大小为128KB(0x20000),添加ocram节点以后 myfirst.dts文件内容如下所示:
第24-27行, ocram节点,第24行节点名字@后面的0x00900000就是ocram的起始地址。第26行的reg属性也指明了ocram内存的起始地址为0x00900000,大小为0x20000
I.MX6ULL内部分为三个域:aips1~3,这三个域分管不同的外设控制器,aips1~3这三个域对应的内存范围如表所示:
我们先在设备树中添加这三个域对应的子节点。aips1~3这三个域都属于soc节点的子节点,完成以后的myfirst.dts文件内容如下所示:
第30~36行,aips1节点。
第39-45行,aips2节点。
第48-54行, aips3节点。
最后我们在myfirst.dts文件中加入 ecspil,usbotg1和mgb这三个外设控制器对应的节点,其中 ecspil属于 aips1的子节点,usbotg1属于aips2的子节点,rngb属于aips3的子节点。最终的myfirst.dts文件内容如下:
第38-44行,ecspil外设控制器节点。
第56-60行, usbotg1外设控制器节点。
第72~75行,rngb 外设控制器节点。
至此,myfirst.dts 这个小型的模板设备树就编写好了,基本和imx6ull.dtsi很像,可以看做是 imx6ull.dtsi的缩小版。在myfirst.dts 里面我们仅仅是编写了I.MX6ULL的外设控制器节点,像IIC 接口,SPI 接口下所连接的具体设备我们并没有写,因为具体的设备其设备树属性内容不同。
Linux内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/devicetree目录下根据节点名字创建不同文件夹,如图43.5.1所示:
图就是目录/proc/device-tree目录下的内容,/proc/device-tree目录下是根节点“/”的所有属性和子节点,依次来看一下这些属性和子节点。
在图中,根节点属性属性表现为一个个的文件(图中细字体文件),比如图中的“#address-cells"、"#size-cells"、"compatible”、"model”和"name”这5个文件,它们在设备树中就是根节点的5个属性。既然是文件那么肯定可以查看其内容,输入 cat命令来查看 model和compatible这两个文件的内容,结果如图所示:
从图可以看出,文件model的内容是"Freescale I.MX6 ULL 14x14 EVK Board",文件
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,我们接下来看一下这两个特殊的子节点。
打开imx6ull.dtsi文件, aliases节点内容如下所示:
单词aliases的意思是“别名”,因此aliases节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量的使用&label的形式来访问节点。
chosen并不是一个真实的设备, chosen节点主要是为了uboot向Linux内核传递数据,重点是bootargs参数。一般.dts文件中chosen节点通常为空或者内容很少,imx6ull-alientekemmc.dts中chosen节点内容如下所示:
从示例代码43.6.2.1中可以看出, chosen节点仅仅设置了属性"stdout-path”,表示标准输·出使用uart1。但是当我们进入到/proc/device-tree/chosen目录里面,会发现多了bootargs这个属性,如图所示:
输入cat命令查看bootargs这个文件的内容,结果如图所示:
从图可以看出, bootargs这个文件的内容为"console=ttymxc0,115200....",这个不就是在 uboot中设置的bootargs环境变量的值吗?现在有两个疑点:
1.我们并没有在设备树中设置chosen节点的bootargs属性,那么图中bootargs这个属性是怎么产生的?
2.为何bootargs文件的内容和uboot中bootargs环境变量的值一样?它们之间有什么关系?
前面uboot的时候说uboot 在启动Linux内核的时候会将bootargs的值传递给Linux内核, bootargs会作为Linux内核的命令行参数, Linux内核启动的时候会打印出命令行参数(也就是uboot传递进来的bootargs的值),如图所示:
既然chosen节点的bootargs属性不是我们在设备树里面设置的,那么只有一种可能,那就是uboot自己在chose节点里面添加了bootargs属性!并且设置bootargs属性的值为 bootargs环境变量的值。因为在启动Linux内核之前,只有uboot知道bootargs环境变量的值,并且uboot也知道.dtb设备树文件在DRAM中的位置,因此uboot的“作案”嫌疑最大。在uboot源码中全局搜索“chosen”这个字符串,看看能不能找到一些蛛丝马迹。果然不出所料,在common/fdt_support.c文件中发现了"chosen"的身影, fdt_support.c文件中有个fdt_chosen函数,此函数内容如下所示:
第288行,调用函数fdt_find_or_add_subnode从设备树(.dtb)中找到chosen节点,如果没有找到的话就会自己创建一个chosen节点。
第292行,读取 uboot 中 bootargs 环境变量的内容。
第294行,调用函数fdt_setprop向chosen节点添加bootargs属性,并且bootargs属性的值就是环境变量 bootargs的内容。
证据“实锤”了,就是uboot中的fdt_chosen函数在设备树的chosen节点中加入了bootargs属性,并且还设置了bootargs属性值。接下来我们顺着fdt_chosen函数一点点的抽丝剥茧,看看都有哪些函数调用了fdt_chosen,一直找到最终的源头。这里我就不卖关子了,直接告诉大家整个流程是怎么样的,见图:
图中框起来的部分就是函数do_bootm_linux函数的执行流程,也就是说do_bootm_linux函数会通过一系列复杂的调用,最终通过fdt_chosen函数在chosen节点中加入了bootargs属性。而我们通过bootz命令启动Linux内核的时候会运行do_bootm_linux函数,至此,真相大白,一切事情的源头都源于如下命令:
当我们输入上述命令并执行以后, do_bootz函数就会执行,然后一切就按照图中所示的流程开始运行。
Linux内核在启动的时候会解析DTB文件,然后在/proc/device-tree目录下生成相应的设备树节点文件。接下来我们简单分析一下Linux内核是如何解析DTB文件的,流程如图所示:
从图中可以看出,在start_kernel函数中完成了设备树节点解析的工作,最终实际工作的函数为unflatten_dt_node。
绑定信息文档
设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属性不同。那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在Linux内核源码中有详细的.txt文档描述了如何添加节点,这些txt文档叫做绑定文档,路径为:Linux源码目录/Documentation/devicetree/bindings,如图所示:
比如我们现在要想在I.MX6ULL这颗SOC的I2C下添加一个节点,那么就可以查看· Documentation/devicetree/bindings/i2c/i2c-imx.txt,此文档详细的描述了I.MX系列的 SoC如何在设备树中添加I2C设备节点,文档内容如下所示:
有时候使用的一些芯片在Documentation/devicetree/bindings目录下找不到对应的文档,这个时候就要咨询芯片的提供商,让他们给你提供参考的设备树文件。
设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,,我们在编写驱动的时候需要获取到这些信息。比如设备树使用reg属性描述了某个外设的寄存器地址为0X02005482,长度为0X400,我们在编写驱动的时候需要获取到reg属性的
设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux内核使用device_node结构体来描述一个节点,此结构体定义在文件 include/linux/of.h 中,定义如下:
of_find_node_by_name函数通过节点名字查找指定的节点,函数原型如下:
函数参数和返回值含义如下:
from:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。
name:要查找的节点名字。
返回值:找到的节点,如果为NULL表示查找失败。
of_find_node_by_type函数通过device type属性查找指定的节点,函数原型如下:
函数参数和返回值含义如下:
from:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。
type:要查找的节点对应的type字符串,也就是device_type属性值。
返回值:找到的节点,如果为NULL表示查找失败。
of_find_compatible_node函数根据device_type和compatible这两个属性查找指定的节点,函数原型如下:
函数参数和返回值含义如下:
from:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。
matches: of_device_id匹配表,也就是在此匹配表里面查找节点。
Match:找到的匹配的of_device_id。
返回值:找到的节点,如果为NULL表示查找失败。
节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要, Linux内核中使用结构体property表示属性,此结构体同样定义在文件include/linux/of.h中,内容如下:
of_find_property函数用于查找指定的属性,函数原型如下:
函数参数和返回值含义如下:
Np:设备节点。
name:属性名字。
Lenp:属性值的字节数
返回值:找到的属性。
of_property_count_elems_of_size函数用于获取属性中元素的数量,比如reg属性值是一个数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:
函数参数和返回值含义如下:
np:设备节点。
proname::需要统计元素数量的属性名字。
elem_size:元素长度。
返回值:得到的属性元素数量。
of_property_read_u16_array 函数
of_property_read_u32_array 函数
of_property_read_u64_array 函数
这 4 个函数分别是读取属性中 u8、ul6、u32和u64类型的数组数据,比如大多数的reg属性都是数组数据,可以使用这4个函数一次读取出reg属性中的所有数据。
这四个函数的原型如下:
函数参数和返回值含义如下:
np:设备节点。
proname:要读取的属性名字。
out_value:读取到的数组值,分别为 u8、ul6、u32 和 u64。
Sz:要读取的数组元素数量。
返回值:0,读取成功,负值,读取失败,-EINVAL表示属性不存在,-ENODATA表示没有要读取的数据,-EOVERFLOW表示属性值列表太小。
of_property_read_u16函数
of_property_read_u32函数
of_property_read_u64函数
有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用于读取 u8、u16、u32和u64类型属性值,函数原型如下:
函数参数和返回值含义如下:
np:设备节点。
proname:要读取的属性名字。
out_value:读取到的数组值。
返回值:0,读取成功,负值,读取失败,-EINVAL表示属性不存在,-ENODATA表示没有要读取的数据,-EOVERFLOW表示属性值列表太小。
of_n_addr_cells函数用于获取#address-cells属性值,函数原型如下:
函数参数和返回值含义如下:
np:设备节点。
返回值:获取到的#address-cells属性值。
of_size_cells函数用于获取#size-cells属性值,函数原型如下:
函数参数和返回值含义如下:
np:设备节点。
返回值:获取到的#size-cells属性值。
of_device_is_compatible函数
of_device_is_compatible函数用于查看节点的compatible属性是否有包含compat指定的字符串,也就是检查设备节点的兼容性,函数原型如下:
函数参数和返回值含义如下:
device:设备节点。
compat:要查看的字符串。
返回值:0,节点的compatible属性中不包含compat指定的字符串;正数,节点的compatible属性中包含compat指定的字符串。
of_get_address函数用于获取地址相关属性,主要是"reg"或者"assigned-addresses"属性值,函数原型如下:
函数参数和返回值含义如下:
dev:设备节点。
Index:要读取的地址标号。
Size:地址长度。
flags:参数,比如IORESOURCE_IO、IORESOURCE_MEM等
返回值:读取到的地址数据首地址,为NULL的话表示读取失败。
of_translate_address函数负责将从设备树读取到的地址转换为物理地址,函数原型如下:
函数参数和返回值含义如下:
Dev:设备节点。
in_addr:要转换的地址。
返回值:得到的物理地址,如果为OF_BAD_ADDR的话表示转换失败。
IIC、SPI、GPIO等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间, Linux内核使用resource结构体来描述一段内存空间,“resource”翻译出来就是“资源”,因此用 resource结构体描述的都是设备资源信息,resource结构体定义在文件 include/linux/ioport.h中,定义如下:
对于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结构体类型,函数原型如下所示:
函数参数和返回值含义如下:
dev:设备节点。
Index:地址资源标号。
r:得到的resource类型的资源值。
返回值:0,成功;负值,失败。
of_iomap函数用于直接内存映射,以前我们会通过ioremap函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过of_iomap函数来获取内存地址所对应的虚拟地址,不需要使用ioremap函数了。当然了,你也可以使用ioremap函数来完成物理地址到虚拟地址的内存映射,只是在采用设备树以后,大部分的驱动都使用of_iomap函数了。of_iomap函数本质上也是将reg属性中地址信息转换为虚拟地址,如reg属性有多段的话,可以通过index参数指定要完成内存映射的是哪一段, of_iomap函数原型如下:
函数参数和返回值含义如下:
np:设备节点。
index: reg属性中要完成内存映射的段,如果reg属性只有一段的话index就设置为0
返回值:经过内存映射后的虚拟内存首地址,如果为NULL的话表示内存映射失败。
关于设备树常用的OF函数就先讲解到这里,Linux内核中关于设备树的OF函数不仅仅只有前面讲的这几个,还有很多OF函数我们并没有讲解,这些没有讲解的OF函数要结合具体的驱动,比如获取中断号的OF函数、获取GPIO的OF函数等等,这些OF函数后面的驱动实验中再详细的讲解。