🐱作者:一只大喵咪1201
🐱专栏:《Linux驱动》
🔥格言:你只管努力,剩下的交给时间!
目录
- 🧲设备树简介
- 🏹设备树语法
- 🏹常见节点和属性
- 🏹编译设备树文件
- 🧲内核对设备树的处理
- 🏹会被转换为platform_device的节点
- 🏹匹配驱动程序
- 🏹使用没有转换的节点
- 🧲总结
🧲设备树简介
如上图所示,在总线驱动模型中,由platform_device
结构体对象来提供不同类型的硬件资源:
- 代码表某个开发板的
board_XXX.c
文件中,定义一个或多个platform_device
结构体对象来给驱动程序提供硬件资源。 - 不同开发板的硬件资源是不同的,所以多个开发板就会有多个
board_XXX.c
文件。
每一个board_XXX.c
都需要进行编译,在修改platform_device
中的某些资源后也需要重新编译,此时对Linux内核就存在两个影响:
- 操作复杂:每次修改或者增加后都需要重新编译,并且还要对驱动程序进行重新装载。
- 内核冗余:由于开发板的种类非常多,要想让内核能驱动这些开发板,内核中势必会存在大量的
board_XXX_c
文件。
对此,Linux之父Linus 大发雷霆:“this whole ARM thing is a f*cking pain in the ass”。认为这些东西都行垃圾,进行了改进,于是Linux内核开始引入了设备树。
设备树是一个配置文件,该文件是给内核里的驱动程序指定硬件的信息的,比 如 LED 驱动,在内核的驱动程序里去操作寄存器,但是操作哪一个引脚?这由设备树指定。
- 设备树的优势在于,它并不属于内核,而是位于内核之外的,而且也不参与编译。
Linux内核在运行时,会从设备树文件中读取设备节点的硬件资源信息,并提供给相应的驱动程序,它完全起到了原本board_XXX.c
中platform_device
结构体对象的作用,而且还解决了总线模型中存在的问题。
🏹设备树语法
如上图所示,之所以叫设备树,是因为这些设备节点挂载在系统总线上,形成一个树状结构。
root
:表示根节点。CPU
:这些蓝色框中的设备节点是根节点的子节点。I2C
:这些黑色框中的设备节点是子节点的子节点。
某一条支路上的设备节点可以无限挂载下去,而且一个设备节点可以拥有多个子节点,每一个节点都表示一个设备,都包含着硬件资源信息。
怎么描述设备树呢?用设备树文件dts(device tree source)
,它需要编译成为dtb(device tree blob)
文件,内核使用的就是dtb
文件。
- 我们要写的是
dts
文件,这才是设备树的根本。
如上图所示dts
文件中的代码,这就是设备树文件,它的语法规则如下:
- DTS文件布局:
/ {
[property definitions];
[child nodes];
};
和Linux文件系统一样,/
表示根节点,后面跟一对大括号,以封号结束,表示整个设备树。
括号内包含:
- property definitions(属性)
- child nodes(子节点)。
位于根节点中的属性就是用来描述根节点的。属性和子节点都在[]
内,表示可有可无,并不是必须写的,数量也并不是固定的,可以有一个,也可以有多个。
node格式:
设备树中的设备节点,被称为node(基本单元)。
[lable:] node_name[@unit-address] {
[property definitions];
[child nodes];
};
每一个设备节点的组成和根节点类似,也是由属性,子节点和大括号组成:
lable
:表示该设备节点的标号,可以省略。node_name
:表示该设备节点的名称,不能省略。@unit-addrsss
:表示该设备节点的地址,可以省略。
就拿上图dts
文件中设备节点uart
来说,它的名称是uart
,标号是uart0
,地址是@fe001000
,可以使用下面两种方式来修改uart@fe0010000
这个node:
- 在根节点之外使用
label
引用node
&uart0 {
//修改属性
};
- 在根节点之外使用绝对路径
&{/uart@fe001000} {
//修改属性
};
从上面两个例子中可以看出,lable
的好处是使用起来更方便,而且节点名称后的地址没有实际作用,可以看作是和设备名一起构造出的设备节点名称。
properties(属性)格式:
无论是根节点还是设备节点,都有属性,属性的描述也有一定的规则,简单来说就是property_name = value
,也就是属性名称= 属性值
。
但是属性值value
有多种类型:
-
interrupts = <17 0xc>
:value值用<>
括起来,17和0xc是两个32位的数据:- 可以是10进制的,也可以是16进制,重点是尖括号,表示是32位数据。
- 之间使用空格隔开,可以有多个数据。
-
clock-frequency = <0x00000001 0x00000000>
:value是一个64位的数据,需要用到0x00000001
和0x00000000
两个cell来表示。 -
compatible = "simple -bus", "A", "B"
:value有三个字符串,字符串之间用,
隔开。 -
local-mac-address = [00 00 12 34 56 78]
:value值有多个,全部都是16进制,用两个16进制数来表示一个字节,[]
内的数字必然是16进制的。 -
example = <0xf00f0000 19>, "hello"
:value值有两种类型,之间用,
隔开。
🏹常见节点和属性
根节点:
dts
文件中必须要有一个根节点:
/dts-v1/
/ {
//根节点属性
modle = "fsl,mpc8572ds";
compatible = "fsl,mpc8572ds", "smdk2410", "mini2440";
#address-cells = <1>;
#size-cells = <1>
};
model
:表示使用该设备树文件的开发板是什么型号的,fsl,mpc8572ds
表示这是飞思卡尔公司的mpc8572ds
开发板。compatible
:表示兼容性,表示该设备树兼容fsl,mpc8572ds
,smdk2410
,mini2440
三种驱动程序,- 由于这里是根节点,所以是内核驱动程序,普通设备节点就是该设备的驱动程序。
- 启动时,会按照先
fsl,mpc8572ds
再smdk2410
,最后是mini2440
的顺序去寻找驱动程序。
对于这两个属性的值,建议采样这样的形式manufacturer,model
,即厂家名,模块名
。
#address-cells
:cell
是一个32位的数值,该属性是说地址address
要用多少个32位的数来表示。#size_cells
:表示大小size
要用多少个32位的数来表示。
/ {
#address-cells = <1>;
#size-cells = <1>;
memory {
reg = <0x80000000 0x20000000>;
};
};
上例中是描述根节点下一段内存的起始地址和大小,#address-cells
大小是1,所以reg
属性中用一个数0x80000000
来表示这段内存的起始地址。#size-cells
大小是1,所以reg
中用一个数0x2000000
来表示这段内存的大小。
reg
:表示寄存器地址,在设备树里,可以用来描述一段空间,因为在ARM中,寄存器和内存是统一编址的,所以在访问寄存器和内存是方法上没有区别。reg
属性的值,是一系列address size
。
CPU节点:
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0:cpu@0 {
.....
};
};
CPU节点一般不用我们设置,在dtsi
文件中都定义好了,也就是说有人已经写好了,不用我们管。后面会介绍到dtsi
文件。
memory节点:
前面在介绍#address-cells
和#size-cells
的时候已经讲解过了,这里就不说了。
chosen节点:
chosen {
//chosen属性
bootargs = "root=/dev/sda2";
};
这是一个虚拟节点,可以通过该节点向内核中传入一些参数,传入的就是属性值。
status属性:
&uart1 {
status = "disabled";
};
上例中,可以通过status
控制uart1
是否使能,如果是disabled
的话,就不会创建uart1
设备节点。
status
常用取值:
value | 描述 |
---|---|
“okay” | 设备正常运行 |
“disabled” | 设备不可操作,但是后面可以恢复工作 |
“fail” | 发生了严重错误,需要修复 |
常用的主要是okay
和disabled
这两个值。
name属性:
name = "字符串"
,该属性是用来表示节点名字的,在匹配驱动程序时会用到,但是过时了,使用的不是很多。
device_type:
device_type = "字符串"
,该属性是用来表示节点类型的,也是在匹配驱动程序时会用到,也过时了,使用的不是很多。
🏹编译设备树文件
一般不会从头写dts
文件,而是修改,修改完毕后需要重新编译成dtb
文件,先看一下Linux-4.9.88/arch/arm/boot/dts
目录下的100ask_imx6ull-14x14.dts
设备树文件:
如上图所示,除了有很多节点外,还使用C语言的语法#include
包含了imx6ull.dtsi
头文件。
dtsi
设备树文件是别人写好的模板,我们只需要在这个基础上进行修改即可。
进入到内核目录Linux-4.9.88
中:
- 执行指令
touch arch/arm/boot/dts/100ask_imx6ull-14x14.dts
修改一下设备树文件的时间。 - 执行
make dtbs V=1
编译设备树文件。
使用V=1
选项是为了查看编译过程中的打印信息:
如上图所示编译过程中的打印信息:
- 先使用了
gcc
编译器对dts
设备树文件进行了预处理,目的就是将使用C语言#include
包含的dtsi
头文件复制到dts
文件中。 - 然后再使用
scripts/drc/dtc
处理dts
文件生成dtb
文件。
如果只使用dtb
编译工具,是不支持#include
语法的,只能使用/inculde
语法将dtsi
文件包含进去。
- 这也说明了,天下板子一大抄,我们写的
dts
文件继承了别人写好的dtsi
文件。
增加设备树节点:
如上图,在设备树文件中增加BigMiaomi_Node
节点,属性为BigMiaomi_test = "A-Big-Miaomi"
。然后使用make dtbs
指令编译dts
文件。编译好以后,将生成的dtb
文件放到网络文件系统中。然后启动开发板,并且挂载网络文件系统。
如上图,将网络文件系统中的dtb
文件拷贝到/boot
目录下,覆盖原本的dtb
文件,然后执行reboot
指令重启开发板。
如上图所示,重启后在/sys/firmware/devicetree/base/
目录下有多个文件,这里的每个文件都代表一个设备树中的节点,可以看到,我们增加的BigMiaomi_Node
也在这里。
查看新增的节点文件,可以看到里面有节点属性BigMiaomi_test
和节点名字name
两个文件。
如上图,使用cat
指令查看这两个文件,属性文件中存放的是属性值A-Big-Maiomi
,节点名字文件中存放的是节点名字BigMiaomi_Node
,和本喵在设备树文件中增加的节点相对应。
- 对于
value
是字符串的属性,使用cat指令就可以查看。- 对于
value
是数值的属性,需要使用hemdump
指令才能查看。
🧲内核对设备树的处理
设备树是给驱动程序提供资源的,起到总线驱动模型中platform_device
结构体对象的作用,那么内核是如何做到的呢?是怎么处理设备树dts
文件的呢?
如上图所示,设备树文件从dts
文件类型开始,处理流程为:
dts
在Linux服务器上被编译为dtb
文件。u-boot
把dtb
文件传给内核。- 内核解析
dtb
文件,把每一个节点都转换为device_node
结构体。 - 对于某些
device_node
结构体,内核会将其转换为platform_device
结构体。
如上图所示device_node
结构体定义,设备树中的每一个节点都会被内核解释为这样的一个结构体对象,包含成员:
name
:节点名称,来自name
属性。type
:节点类型,来自device_type
属性。porperties
:节点属性链表,包含节点中的多个属性。parent
:父节点。child
:字节点。
再看属性结构体struct property
的定义:
name
:属性名称。length
:属性值的长度(字节)。value
:属性值。
假设属性值是一个字符串,那么此时vaule
中存放的就是字符串的首地址,通过length
可以获得整个字符串。包括数字也是利用这二者求得的。
🏹会被转换为platform_device的节点
虽然所有节点都会被转换为device_node
结构体,但是并不是所有节点都会被转换为platform_device
结构体,会被转换为platform_device
结构体提供硬件资源的节点要有以下特点:
用上面设备树文件为例来说明:
- 根节点下含有
compatible
属性的节点。
上面设备树文件中的mytest
,i2c
,spi
节点都会被转换为platform_device
结构体,因为它们都包含compatible
属性。
- 必须是根节点的直系子节点符合该条件时才会转换。
- 含有特定
compatible
属性值节点的子节点,该值有四种,只要符合一个就可以。- 这四个值是
compatible = "simple-bus", "simple-mfd", "is", "arm,amba-bu"
。
- 这四个值是
上面设备树文件中mytest
的子节点mytest@0
可以转换,因为mytest
节点的compatible
属性中有"simple-bus"
属性,所以它的子节点mytest@0
会被转换。
而i2c
节点的子节点at24c02
不会被转换,因为它的父节点的compatible
属性中没有那个四个值之一,同样的spi
节点的子节点flash@0
也不会被转换。
- 这两个不会被转换的节点分别挂载在
i2c
总线和spi
总线上,它们如何处理完全由父节点i2c
控制器和spi
控制器决定。
对于at24c02
一般会被其父节点i2c
处理成i2c_client
结构体对象,对于flash@0
一般会被其父节点spi
处理成spi_device
结构体对象。
将设备树中的某些节点转换为platform_device
结构体对象后,该结构体中也有提供资源的resource
数组,该数组中的资源值来自第一步转换后的device_node
结构体中的属性链表properties
。
如上图所示,在platform_device
中有一个dev
成员,该成员也是一个结构体,里面含有device_node* of_node
成员。该成员of_node
就表示根节点。
- 内核设备树文件中的根节点也处理成一个
device_node
结构体变量。
所以节点转换为platform_device
结构后,可以从of_node
根节点中找到设备树中任意一个device_node
节点,获取它们属性链表中的任意一个属性,然后存放到platform_device
结构体中的resource
数组里。
- 节点中的
reg
属性转换为platform_device
后的资源类型是IORESOURCE_MEM
类型。- 节点中的
interrupts
属性转换为platform_device
后的资源类型是IORESOURCE_IRQ
类型。
🏹匹配驱动程序
和总线驱动模型中一样,设备树中的节点被转换成platform_device
结构体以后会插入到总线的Dev
链表中,此时就会自动去Drv
链表中匹配platform_driver
,匹配成功后调用驱动程序中的probe
函数。
上图所示是用来匹配的paltform_match
函数,之前在总线模型中,只讲解了使用1,3,4
三步来匹配,没有讲解第二步,因为这一步是在使用了设备树后才会用到的。
第二步匹配过程:
如上图,由设备树节点转换后的platform_device
结构体对象,通过dev
成员中的of_node
成员,可以找到插入Dev
链表中新节点的device_node
结构体对象,得到新节点的属性。
该结构体前面介绍过:
name
来自设备树节点中的name
属性。type
来自设备树节点中的device_type
属性。properties
中存放节点的所有属性。
如上图所示,Drv
链表中进行匹配的platform_driver
结构体,它的device_driver
成员就会包含一个数组of_match_table
:
如上图所示是该数组中存放的每个元素的类型定义,包含:
name
:所支持节点的name
属性。type
:所支持节点的device_type
属性。compatible
:所支持节点的compatible
属性。
此时Dev
链表中有新插入节点的device_node
结构体对象,Drv
链表中有驱动程序中的device_driver
结构体对象。
如上图所示,匹配顺序如下:
- 新节点中的
compatible
属性和驱动程序中的compatible
进行匹配,成功则返回,失败则进行第二步。 - 新节点的
device_type
属性和驱动程序中的type
进行匹配,成功则返回,失败则进行第三步。 - 新节点的
name
属性和驱动程序的name
进行匹配,成功则返回,失败则说明设备树无法匹配成功。
而设备树中建议不再使用device_type
和name
属性,所以基本上只使用设备节点的compatible
属性来匹配platform_driver
。
- 至此,加上总线驱动模型中的
1,3,4
步,匹配platform_device
和platform_driver
时一共有1,2,3,4
步。
🏹使用没有转换的节点
设备树中所有节点都会转换为device_node
结构体,但是并不是所有device_node
都会转为为platform_device
结构体,这些没有转换的结构体我们该怎么使用它们呢?
- 没有转换的结构体如何使用
device_node
中的属性呢?
如上图所示of_find_node_by_path
函数:
- 功能:根据节点路径获取
device_node
结构体指针。 - 形参:要获取节点的绝对路径。
- 返回值:
device_node
结构体指针。
通过该函数可以得到没有转换为platform_device
节点的device_node
,该节点中包含prorerties
属性列表。
如上图所示of_find_property
函数:
- 功能:获得指定节点
np
中名为name
的属性。 - 形参:
np
是指定节点的device_node
结构体指针,name
是要寻找属性的名称,lenp
是属性长度,即它的值长度。 - 返回值:得到是要寻找属性的
struct property
结构体指针。
虽然这样类似的函数有很多,但是使用这两个就可以使用没有转换为platform_device
的节点了,其他函数在遇到的时候再讲解。
- 根据节点路径名先获得该节点的
device_node
结构体指针。 - 再根据该指针获得指定
name
的属性。
🧲总结
要明白引入设备树的原因,以及掌握书写设备树的基本语法,知道设备树文件中常见的节点和属性,了解内核对设备树文件的大致处理流程等知识。