参考正点原子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
文件的主要功能就是按照图
43.1.1
所示的结构来描述板子上的设备信息,
DTS
文件描述设备信息是有相应的语法规则要求的,稍
后我们会详细的讲解
DTS
语法规则。
在 3.x
版本
(
具体哪个版本笔者也无从考证
)
以前的
Linux
内核中
ARM
架构并没有采用设备
树。在没有设备树的时候
Linux
是如何描述
ARM
架构中的板级信息呢?在
Linux
内核源码中
大量的
arch/arm/mach-xxx
和
arch/arm/plat-xxx
文件夹,这些文件夹里面的文件就是对应平台下
的板级信息。比如在
arch/arm/mach-smdk2440.c
中有如下内容
(
有缩减
):
static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = {
.lcdcon5 = S3C2410_LCDCON5_FRM565 |
S3C2410_LCDCON5_INVVLINE |
S3C2410_LCDCON5_INVVFRAME |
S3C2410_LCDCON5_PWREN |
S3C2410_LCDCON5_HWSWP,
......};
static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = {
.displays = &smdk2440_lcd_cfg,
.num_displays = 1,
.default_display = 0,
......};
static struct platform_device *smdk2440_devices[] __initdata = {
&s3c_device_ohci,
&s3c_device_lcd,
&s3c_device_wdt,
&s3c_device_i2c0,
&s3c_device_iis,
};
上述代码中的结构体变量 smdk2440_fb_info
就是描述
SMDK2440
这个开发板上的
LCD
信
息的,结构体指针数组
smdk2440_devices
描述的
SMDK2440
这个开发板上的所有平台相关信
息。这个仅仅是使用
2440
这个芯片的
SMDK2440
开发板下的
LCD
信息,
SMDK2440
开发板
还有很多的其他外设硬件和平台硬件信息。使用
2440
这个芯片的板子有很多,每个板子都有描
述相应板级信息的文件,这仅仅只是一个
2440
。随着智能手机的发展,每年新出的
ARM
架构
芯片少说都在数十、数百款,
Linux
内核下板级信息文件将会成指数级增长!这些板级信息文件
都是
.c
或
.h
文件,都会被硬编码进
Linux
内核中,导致
Linux
内核“虚胖”。就好比你喜欢吃自
助餐,然后花了
100
多到一家宣传看着很不错的自助餐厅,结果你想吃的牛排、海鲜、烤肉基
本没多少,全都是一些凉菜、炒面、西瓜、饮料等小吃,相信你此时肯定会脱口而出一句“
F*k!
”、
“骗子!”。同样的,当
Linux
之父
linus
看到
ARM
社区向
Linux
内核添加了大量“无用”、冗余
的板级信息文件,不禁的发出了一句“
This whole ARM thing is a f*cking 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
是设备树源码文件,
DTB
是将
DTS
编译以后得到的二进制文件。将
.c
文件编译为
.o
需要用到
gcc
编译器,那么将
.dts
编译为
.dtb
需要什么工具呢?需要用到
DTC
工具!
DTC
工具源码在
Linux
内核的
scripts/dtc
目录下,
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
......
可以看出,DTC
工具依赖于
dtc.c
、
flattree.c
、
fstree.c
等文件,最终编译并链接出
DTC
这
个主机文件。如果要编译
DTS
文件的话只需要进入到
Linux
源码根目录下,然后执行如下命
令:
make dtbs
基于 ARM
架构的
SOC
有很多种,一种
SOC
又可以制作出很多款板子,每个板子都有一
个对应的
DTS
文件,那么如何确定编译哪一个
DTS
文件呢?我们就以
I.MX6ULL
这款芯片对
应的板子为例来看一下,打开
arch/arm/boot/dts/Makefile
,有如下内容:
可以看出,当选中 I.MX6ULL
这个
SOC
以后
(CONFIG_SOC_IMX6ULL=y)
,所有使用到
I.MX6ULL
这个
SOC
的板子对应的
.dts
文件都会被编译为
.dtb
。如果我们使用
I.MX6ULL
新做
了一个板子,只需要新建一个此板子对应的
.dts
文件,然后将对应的
.dtb
文件名添加到
dtb-
$(CONFIG_SOC_IMX6ULL)
下,这样在编译设备树的时候就会将对应的
.dts
编译为二进制的
.dtb 文件。 比如上图中笔者在
Linux移植教程——基于 I.MX6ULL 开发板中添加的dtb文件。
三、dts 语法
虽然我们基本上不会从头到尾重写一个.dts
文件,大多时候是直接在
SOC
厂商提供的
.dts
文件上进行修改。但是
DTS
文件语法我们还是需要详细的学习一遍,因为我们肯定需要修改
.dts
文件。大家不要看到要学习新的语法就觉得会很复杂,
DTS
语法非常的人性化,是一种
ASCII
文本文件,不管是阅读还是修改都很方便。
3.1 dtsi 头文件
和 C
语言一样,设备树也支持头文件,设备树的头文件扩展名为
.dtsi
。在
imx6ull-lyh-
emmc.dts
中有如下所示内容:
在.dts
设备树文件中,可以通过
“
#include
”来引用
.h
、
.dtsi
和
.dts
文件。只是,我们在编写设备树头文件的时候最好选择
.dtsi
后
缀。
一般.dtsi
文件用于描述
SOC
的内部外设信息,比如
CPU
架构、主频、外设寄存器地址范围,比如
UART
、
IIC
等等。比如
imx6ull.dtsi
就是描述
I.MX6ULL
这颗
SOC
内部外设情况信息
的,部分内容如下:
cpu0 这个设备节点信息,这个节点信息描述了
I.MX6ULL
这颗
SOC
所使用的
CPU
信息,比如架构是
cortex-A7
,频率支持
996MHz
、
792MHz
、
528MHz
、
396MHz
和
198MHz
等等。在
imx6ull.dtsi
文件中不仅仅描述了
cpu0
这一个节点信息,
I.MX6ULL
这颗
SOC
所有的外设都描述的清清楚楚,比如
ecspi1~4
、
uart1~8
、
usbphy1~2
、
i2c1~4
等等。
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-lyh-emmc.dts
这两个文件都有一个“
/
”根节点,这样不会出错吗?不会的,因为
这两个“
/
”根节点的内容会合并成一个根节点。
第 2
、
6
和
17
行,
aliases
、
cpus
和
intc
是三个子节点,在设备树中节点命名格式如下:
node-name@unit-address
其中“node-name
”是节点名字,为
ASCII
字符串,节点名字应该能够清晰的描述出节点的
功能,比如“
uart1
”就表示这个节点是
UART1
外设。“
unit-address
”一般表示设备的地址或寄
存器首地址,如果某个节点没有地址或者寄存器的话“
unit-address
”可以不要,比如“
cpu@0
”、
“
interrupt-controller@00a01000
”。
但是我们在示例代码
中我们看到的节点命名却如下所示:
cpu0:cpu@0
上述命令并不是“node-name@unit-address
”这样的格式,而是用“:”隔开成了两部分,“:”
前面的是节点标签
(label),“:”后面的才是节点名字,格式如下所示:
label: node-name@unit-address
引入 label
的目的就是为了方便访问节点,可以直接通过
&label
来访问这个节点,比如通过
&cpu0
就可以访问“
cpu@0
”这个节点,而不需要输入完整的节点名字。再比如节点 “
intc:
interrupt-controller@00a01000
”,节点
label
是
intc
,而节点名字就很长了,为“
interrupt-
controller@00a01000
”。很明显通过
&intc
来访问“
interrupt-controller@00a01000
”这个节点要方
便很多!
每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任
意的字节流。设备树源码中常用的几种数据形式如下所示:
①、字符串
compatible = "arm,cortex-a7";
上述代码设置
compatible
属性的值为字符串“
arm,cortex-a7
”。
②、32 位无符号整数
reg = <0>;
上述代码设置
reg
属性的值为
0
,
reg
的值也可以设置为一组值,比如:
reg = <0 0x123456 100>;
③、字符串列表
属性值也可以为字符串列表,字符串和字符串之间采用“
,
”隔开,如下所示:
compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
上述代码设置属性
compatible
的值为“
fsl,imx6ull-gpmi-nand
”和“
fsl, imx6ul-gpmi-nand
”。
3.3 标准属性
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以
自定义属性。除了用户自定义属性,有很多属性是标准属性,
Linux
下的很多外设驱动都会使用
这些标准属性,本节我们就来学习一下几个常用的标准属性。
1
、
compatible
属性
compatible 属性也叫做“兼容性”属性,这是非常重要的一个属性!
compatible
属性的值是
一个字符串列表,
compatible
属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要
使用的驱动程序,
compatible
属性的值格式如下所示:
"manufacturer,model"
其中 manufacturer
表示厂商,
model
一般是模块对应的驱动名字。比如
imx6ull-lyh
-emmc.dts
中
sound
节点是
I.MX6U-ALPHA
开发板的音频设备节点,
I.MX6U-ALPHA
开发板上
的音频芯片采用的欧胜
(WOLFSON)
出品的
WM8960
,
sound
节点的
compatible
属性值如下:
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
中有如下内容:
static const struct of_device_id imx_wm8960_dt_ids[] = {
{
.compatible = "fsl,imx-audio-wm8960",
},
{/* sentinel */}};
MODULE_DEVICE_TABLE(of, imx_wm8960_dt_ids);
static struct platform_driver imx_wm8960_driver = {
.driver = {
.name = "imx-wm8960",
.pm = &snd_soc_pm_ops,
.of_match_table = imx_wm8960_dt_ids,
},
.probe = imx_wm8960_probe,
.remove = imx_wm8960_remove,
};
数组 imx_wm8960_dt_ids
就是
imx-wm8960.c
这个驱动文件的匹配表,此
匹配表只有一个匹配值“
fsl,imx-audio-wm8960
”。如果在设备树中有哪个节点的
compatible
属
性值与此相等,那么这个节点就会使用此驱动文件。
wm8960 采用了
platform_driver
驱动模式,关于
platform_driver
驱动后面会讲
解。此行设置
.of_match_table
为
imx_wm8960_dt_ids
,也就是设置这个
platform_driver
所使用的
OF
匹配表。
2
、
model
属性
model
属性值也是一个字符串,一般
model
属性描述设备模块信息,比如名字什么的,比如:
model = "wm8960-audio";
3
、
status
属性
status 属性看名字就知道是和设备状态有关的,
status
属性值也是字符串,字符串是设备的
状态信息,可选的状态如表
43.3.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
这个数据所占用
的字长,比如
:
spi4
{
compatible = "spi-gpio";
#address - cells = <1>;
#size - cells = <0>;
gpio_spi:gpio_spi @0
{
compatible = "fairchild,74hc595";
reg = <0>;
};
};
aips3 : aips - bus @02200000
{
compatible = "fsl,aips-bus", "simple-bus";
#address - cells = <1>;
#size - cells = <1>;
dcp:dcp @02280000
{
compatible = "fsl,imx6sl-dcp";
reg = <0x02280000 0x4000>;
};
};
5
、
reg
属性
reg 属性前面已经提到过了,
reg
属性的值一般是
(address
,
length)
对。
reg
属性一般用于描
述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息,比如在
imx6ull.dtsi
中有
如下内容:
uart1: serial@02020000 {
compatible = "fsl,imx6ul-uart",
"fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x02020000 0x4000>;
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_UART1_IPG>,
<&clks IMX6UL_CLK_UART1_SERIAL>;
clock-names = "ipg", "per";
status = "disabled";
};
上述代码是节点
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
寄存器首地址。
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
属性,如下所示:
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0xe0000000 0x00100000>;
serial {
device_type = "serial";
compatible = "ns16550";
reg = <0x4600 0x100>;
clock-frequency = <0>;
interrupts = <0xA 0x8>;
interrupt-parent = <&ipic>;
};
};
第 5
行,节点
soc
定义的
ranges
属性,值为
<0x0 0xe0000000 0x00100000>
,此属性值指定
了一个
1024KB(0x00100000)
的地址范围,子地址空间的物理起始地址为
0x0
,父地址空间的物
理起始地址为
0xe0000000
。
第 10
行,
serial
是串口设备节点,
reg
属性定义了
serial
设备寄存器的起始地址为
0x4600
,
寄存器长度为
0x100
。经过地址转换,
serial
设备可以从
0xe0004600
开始进行读写操作,
0xe0004600=0x4600+0xe0000000
。
7
、
name
属性
name 属性值为字符串,
name
属性用于记录节点名字,
name
属性已经被弃用,不推荐使用
name
属性,一些老的设备树文件可能会使用此属性。
8
、
device_type
属性
device_type 属性值为字符串,
IEEE 1275
会用到此属性,用于描述设备的
FCode
,但是设
备树没有
FCode
,所以此属性也被抛弃了。此属性只能用于
cpu
节点或者
memory
节点。
imx6ull.dtsi
的
cpu0
节点用到了此属性,内容如下所示:
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
......
};
关于标准属性就讲解这么多,其他的比如中断、IIC
、
SPI
等使用的标准属性等到具体的例
程再讲解。
3.4 根节点compatible属性
每个节点都有 compatible
属性,根节点“
/
”也不例外,
imx6ull-alientek-emmc.dts
文件中根
节点的
compatible
属性内容如下所示:
/ {
model = "Freescale i.MX6 ULL 14x14 EVK Board";
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
......
可以看出,compatible
有两个值:“
fsl,imx6ull-14x14-evk
”和“
fsl,imx6ull
”。前面我们说了,
设备节点的
compatible
属性值是为了匹配
Linux
内核中的驱动程序,那么根节点中的
compatible
属性是为了做什么工作的? 通过根节点的
compatible
属性可以知道我们所使用的设备,一般第
一个值描述了所使用的硬件设备名字,比如这里使用的是“
imx6ull-14x14-evk
”这个设备,第二
个值描述了设备所使用的
SOC
,比如这里使用的是“
imx6ull
”这颗
SOC
。
Linux
内核会通过根
节点的
compoatible
属性查看是否支持此设备,如果支持的话设备就会启动
Linux
内核。接下来
我们就来学习一下
Linux
内核在使用设备树前后是如何判断是否支持某款设备的。
1
、使用设备树之前设备匹配方法
在没有使用设备树以前,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
中有如下定义:
MACHINE_START(MX35_3DS, "Freescale MX35PDK")
/* Maintainer: Freescale Semiconductor, Inc */
.atag_offset = 0x100,
.map_io = mx35_map_io,
.init_early = imx35_init_early,
.init_irq = mx35_init_irq,
.init_time = mx35pdk_timer_init,
.init_machine = mx35_3ds_init,
.reserve = mx35_3ds_reserve,
.restart = mxc_restart,
MACHINE_END
根据 MACHINE_START
和
MACHINE_END
的宏定义,将示例代码
43.3.4.2
展开后如下所
示:
static const struct machine_desc __mach_desc_MX35_3DS \
__used \
__attribute__((__section__(".arch.info.init"))) = {
.nr = MACH_TYPE_MX35_3DS,
.name = "Freescale MX35PDK",
/* Maintainer: Freescale Semiconductor, Inc */
.atag_offset = 0x100,
.map_io = mx35_map_io,
.init_early = imx35_init_early,
.init_irq = mx35_init_irq,
.init_time = mx35pdk_timer_init,
.init_machine = mx35_3ds_init,
.reserve = mx35_3ds_reserve,
.restart = mxc_restart,
};
这里定义了一个 machine_desc
类型的结构体变量
__mach_desc_MX35_3DS
, 这 个 变 量 存 储 在 “
.arch.info.init
” 段 中 。 第
4
行 的
MACH_TYPE_MX35_3DS
就 是 “
Freescale MX35PDK
” 这 个 板 子 的
machine id
。
MACH_TYPE_MX35_3DS
定义在文件
include/generated/mach-types.h
中,此文件定义了大量的
machine id
,内容如下所示:
15 #define MACH_TYPE_EBSA110 0
16 #define MACH_TYPE_RISCPC 1
17 #define MACH_TYPE_EBSA285 4
18 #define MACH_TYPE_NETWINDER 5
19 #define MACH_TYPE_CATS 6
20 #define MACH_TYPE_SHARK 15
21 #define MACH_TYPE_BRUTUS 16
22 #define MACH_TYPE_PERSONAL_SERVER 17
......
287 #define MACH_TYPE_MX35_3DS 1645
......
1000 #define MACH_TYPE_PFLA03 4575
第 287
行就是
MACH_TYPE_MX35_3DS
的值,为
1645
。
前面说了,
uboot
会给
Linux
内核传递
machine id
这个参数,
Linux
内核会检查这个
machine
id
,其实就是将
machine id
与示例代码
43.3.4.3
中的这些
MACH_TYPE_XXX
宏进行对比,看
看有没有相等的,如果相等的话就表示
Linux
内核支持这个设备,如果不支持的话那么这个设
备就没法启动
Linux
内核。
2
、使用设备树以后的设备匹配方法
当 Linux
内 核 引 入 设 备 树 以 后 就 不 再 使 用
MACHINE_START
了 , 而 是 换 为 了
DT_MACHINE_START
。
DT_MACHINE_START
也定义在文件
arch/arm/include/asm/mach/arch.h
里面,定义如下:
#define DT_MACHINE_START(_name, _namestr) \
static const struct machine_desc __mach_desc_##_name \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = ~0, \
.name = _namestr,
在 DT_MACHINE_START
里面直接将
.nr
设置为
~0
。说明引入设备树以后不会再根据
machine
id
来检查
Linux
内核是否支持某个设备了。
打开文件 arch/arm/mach-imx/mach-imx6ul.c
,有如下所示内容:
static const char *imx6ul_dt_compat[] __initconst = {
"fsl,imx6ul",
"fsl,imx6ull",
NULL,
};
DT_MACHINE_START(IMX6UL, "Freescale i.MX6 Ultralite (Device Tree)")
.map_io = imx6ul_map_io,
.init_irq = imx6ul_init_irq,
.init_machine = imx6ul_init_machine,
.init_late = imx6ul_init_late,
.dt_compat = imx6ul_dt_compat,
MACHINE_END
machine_desc 结构体中有个
.dt_compat
成员变量,此成员变量保存着本设备兼容属性,示
例代码
中设置
.dt_compat = imx6ul_dt_compat
,
imx6ul_dt_compat
表里面有
"fsl,imx6ul"
和
"fsl,imx6ull"
这两个兼容值。只要某个设备
(
板子
)
根节点“
/
”的
compatible
属性值与
imx6ul_dt_compat
表中的任何一个值相等,那么就表示
Linux
内核支持此设备。
compatible = "fsl,imx6ull-14x14-evk", "
fsl,imx6ull
";
其中“fsl,imx6ull
”与
imx6ul_dt_compat
中的“
fsl,imx6ull
”匹配,因此
I.MX6U-ALPHA
开
发板可以正常启动
Linux
内核。
3.5 向节点追加或修改内容
示例代码
就是
I.MX6ULL
的
I2C1
节点,现在要在
i2c1
节点下创建一个子节点,
这个子节点就是
fxls8471
,最简单的方法就是在
i2c1
下直接添加一个名为
fxls8471
的子节点,
如下所示:
i2c1 : i2c @021a0000{
#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";
//fxls8471 子节点
fxls8471 @1e
{
compatible = "fsl,fxls8471";
reg = <0x1e>;
};
};
但是这样会有个问题!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-lyh-emmc.dts
文件中完成数据追加的内容,方式如下:
&i2c1{
clock - frequency = <100000>;
pinctrl - names = "default";
pinctrl - 0 = <&pinctrl_i2c1>;
status = "okay";
mag3110 @0e
{
compatible = "fsl,mag3110";
reg = <0x0e>;
position = <2>;
};
fxls8471 @1e
{
compatible = "fsl,fxls8471";
reg = <0x1e>;
position = <0>;
interrupt - parent = <&gpio5>;
interrupts = <0 8>;
};
};
因为示例代码
中的内容是
imx6ull-lyh-emmc.dts
这个文件内的,所以不会对
使用
I.MX6ULL
这颗
SOC
的其他板子造成任何影响。这个就是向节点追加或修改内容,重点
就是通过
&label
来访问节点,然后直接在里面编写要追加或者修改的内容。
四、设备树在系统中的体现
Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的
/proc/device-
tree
目录下根据节点名字创建不同文件夹,如图
所示:
1
、根节点“
/
”各个属性
在上图
中,根节点属性属性表现为一个个的文件
(
图中细字体文件
)
,比如图
中
的“
#address-cells
”、“
#size-cells
”、“
compatible
”、“
model
”和“
name
”这
5
个文件,它们在设
备树中就是根节点的
5
个属性。既然是文件那么肯定可以查看其内容,输入
cat
命令来查看
model
和
compatible
这两个文件的内容,结果如图
所示:
2
、根节点“
/
”各子节点
图
中各个文件夹
(
图中粗字体文件夹
)
就是根节点“
/
”的各个子节点,比如“
aliases
”、
“
backlight
”、“
chosen
”和“
clocks
”等等。大家可以查看一下
imx6ull-lyh-emmc.dts
和
imx6ull.dtsi
这两个文件,看看根节点的子节点都有哪些,看看是否和图
中的一致。
五、特殊节点
在根节点“/
”中有两个特殊的子节点:
aliases
和
chosen
,我们接下来看一下这两个特殊的
子节点。
4.1 aliases
打开
imx6ull.dtsi
文件,
aliases
节点内容如下所示:
aliases {
can0 = &flexcan1;
can1 = &flexcan2;
ethernet0 = &fec1;
ethernet1 = &fec2;
gpio0 = &gpio1;
gpio1 = &gpio2;
......
spi0 = &ecspi1;
spi1 = &ecspi2;
spi2 = &ecspi3;
spi3 = &ecspi4;
usbphy0 = &usbphy1;
usbphy1 = &usbphy2;
};
单词 aliases
的意思是“别名”,因此
aliases
节点的主要功能就是定义别名,定义别名的目
的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上
label
,然后通过
&label
来访问节点,这样也很方便,而且设备树里面大量的使用
&label
的形式来访问节点。
4.2 chosen
chosen 并不是一个真实的设备,
chosen
节点主要是为了
uboot
向
Linux
内核传递数据,重
点是
bootargs
参数。一般
.dts
文件中
chosen
节点通常为空或者内容很少,
imx6ull-lyh-
emmc.dts
中
chosen
节点内容如下所示:
chosen {
stdout-path = &uart1;
};
chosen 节点仅仅设置了属性“
stdout-path
”,表示标准输
出使用
uart1
。但是当我们进入到
/proc/device-tree/chosen
目录里面,会发现多了
bootargs
这个
属性,如图
43.6.2.1
所示:
uboot 中的
fdt_chosen
函数在设备树的
chosen
节点中加入了
bootargs
属性,并且还设置了
bootargs
属性值。接下来我们顺着
fdt_chosen
函数一点点的抽丝剥茧,看
看都有哪些函数调用了
fdt_chosen
,一直找到最终的源头。
大致流程:
六、Linux内核解析DTB
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
设备节点。
八、of操作函数
设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,
我们在编写驱动的时候需要获取到这些信息。比如设备树使用
reg
属性描述了某个外设的寄存
器地址为
0X02005482
,长度为
0X400
,我们在编写驱动的时候需要获取到
reg 属性的
0X02005482
和
0X400
这两个值,然后初始化外设。
Linux
内核给我们提供了一系列的函数来获
取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“
of_
”,所以在很多资
料里面也被叫做
OF
函数。这些
OF
函数原型都定义在
include/linux/of.h
文件中。
Linux 内核使用
device_node
结构体来描述一个节点,此结构体定
义在文件
include/linux/of.h
中,定义如下:
struct device_node {
const char *name; /* 节点名字 */
const char *type; /* 设备类型 */
phandle phandle;
const char *full_name; /* 节点全名 */
struct fwnode_handle fwnode;
struct property *properties; /* 属性 */
struct property *deadprops; /* removed 属性 */
struct device_node *parent; /* 父节点 */
struct device_node *child; /* 子节点 */
struct device_node *sibling;
struct kobject kobj;
unsigned long _flags;
void *data;
#if defined(CONFIG_SPARC)
const char *path_component_name;
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
8.1 查找节点
1
、
of_find_node_by_name
函数
of_find_node_by_name
函数通过节点名字查找指定的节点,函数原型如下:
struct device_node *of_find_node_by_name(struct device_node *from,
const char
*name);
函数参数和返回值含义如下:
from
:开始查找的节点,如果为
NULL
表示从根节点开始查找整个设备树。
name
:要查找的节点名字。
返回值:
找到的节点,如果为
NULL
表示查找失败。
2
、
of_find_node_by_type
函数
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
表示查找失败。
3
、
of_find_compatible_node
函数
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
表示查找失败
4
、
of_find_matching_node_and_match
函数
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
表示查找失败
5
、
of_find_node_by_path
函数
of_find_node_by_path
函数通过路径来查找指定的节点,函数原型如下:
inline struct device_node *of_find_node_by_path(const char *path)
函数参数和返回值含义如下:
path
:带有全路径的节点名,可以使用节点的别名,比如“
/backlight
”就是
backlight
这个
节点的全路径。
返回值:
找到的节点,如果为
NULL
表示查找失败
8.2 查找父子节点
1
、
of_get_parent
函数
of_get_parent
函数用于获取指定节点的父节点
(
如果有父节点的话
)
,函数原型如下:
struct device_node *of_get_parent(const struct device_node *node)
函数参数和返回值含义如下:
node
:要查找的父节点的节点。
返回值:
找到的父节点。
2
、
of_get_next_child
函数
of_get_next_child
函数用迭代的方式查找子节点,函数原型如下:
struct device_node *of_get_next_child(const struct device_node *node,
struct device_node
*prev)
函数参数和返回值含义如下:
node
:父节点。
prev
:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为
NULL
,表示从第一个子节点开始。
返回值:
找到的下一个子节点。
8.3 提取属性
节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux
内
核中使用结构体
property
表示属性,此结构体同样定义在文件
include/linux/of.h
中,内容如下:
struct property {
char *name; /* 属性名字 */
int length; /* 属性长度 */
void *value; /* 属性值 */
struct property *next; /* 下一个属性 */
unsigned long _flags;
unsigned int unique_id;
struct bin_attribute attr;
};
Linux
内核也提供了提取属性值的
OF
函数,我们依次来看一下。
1
、
of_find_property
函数
of_find_property
函数用于查找指定的属性,函数原型如下:
property *of_find_property(const struct device_node *np, const char
*name,
int *lenp)
函数参数和返回值含义如下:
np
:设备节点。
name
: 属性名字。
lenp
:属性值的字节数
返回值:
找到的属性。
2
、
of_property_count_elems_of_size
函数
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
:元素长度。
返回值:
得到的属性元素数量。
3
、
of_property_read_u32_index
函数
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
表示属性值列表太小。
4
、
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
属性中的所有数据。这四个函数的原型
如下:
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
表示属性值列表太小。
5
、
of_property_read_u8
函数
of_property_read_u16
函数
of_property_read_u32
函数
of_property_read_u64
函数
有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用
于读取
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
表示属性值列表太小。
6
、
of_property_read_string
函数
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
,读取成功,负值,读取失败。
7
、
of_n_addr_cells
函数
of_n_addr_cells
函数用于获取
#address-cells
属性值,函数原型如下:
int of_n_addr_cells(struct device_node *np)
函数参数和返回值含义如下:
np
:设备节点。
返回值:
获取到的
#address-cells
属性值。
8
、
of_n_size_cells
函数
of_size_cells
函数用于获取
#size-cells
属性值,函数原型如下:
int of_n_size_cells(struct device_node *np)
函数参数和返回值含义如下:
np
:设备节点。
返回值:
获取到的
#size-cells
属性值。
8.4 其他常用函数
1
、
of_device_is_compatible
函数
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
指定的字符串。
2
、
of_get_address
函数
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
的话表示读取失败。
3
、
of_translate_address
函数
of_translate_address
函数负责将从设备树读取到的地址转换为物理地址,函数原型如下:
u64 of_translate_address(struct device_node *dev, const __be32 *in_addr)
函数参数和返回值含义如下:
dev
:设备节点。
in_addr
:要转换的地址。
返回值:
得到的物理地址,如果为
OF_BAD_ADDR
的话表示转换失败。
4
、
of_address_to_resource
函数
IIC、
SPI
、
GPIO
等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,
Linux 内核使用
resource
结构体来描述一段内存空间,“
resource
”翻译出来就是“资源”,因此用
resource
结构体描述的都是设备资源信息,
resource
结构体定义在文件
include/linux/ioport.h
中,定义如
下:
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
struct resource *parent, *sibling, *child;
};
对于 32
位的
SOC
来说,
resource_size_t
是
u32
类型的。其中
start
表示开始地址,
end
表示
结束地址,
name
是这个资源的名字,
flags
是资源标志位,一般表示资源类型,可选的资源标志
定义在文件
include/linux/ioport.h
中,如下所示:
#define IORESOURCE_BITS 0x000000ff
#define IORESOURCE_TYPE_BITS 0x00001f00
#define IORESOURCE_IO 0x00000100
#define IORESOURCE_MEM 0x00000200
#define IORESOURCE_REG 0x00000300
#define IORESOURCE_IRQ 0x00000400
#define IORESOURCE_DMA 0x00000800
#define IORESOURCE_BUS 0x00001000
#define IORESOURCE_PREFETCH 0x00002000
#define IORESOURCE_READONLY 0x00004000
#define IORESOURCE_CACHEABLE 0x00008000
#define IORESOURCE_RANGELENGTH 0x00010000
#define IORESOURCE_SHADOWABLE 0x00020000
#define IORESOURCE_SIZEALIGN 0x00040000
#define IORESOURCE_STARTALIGN 0x00080000
#define IORESOURCE_MEM_64 0x00100000
#define IORESOURCE_WINDOW 0x00200000
......
大 家 一 般 最 常 见 的 资 源 标 志 就 是 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
,成功;负值,失败。
5
、
of_iomap
函数
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
的话表示内存映射失败。
关于设备树常用的 OF
函数就先讲解到这里,
Linux
内核中关于设备树的
OF
函数不仅仅只
有前面讲的这几个,还有很多
OF
函数我们并没有讲解,这些没有讲解的
OF
函数要结合具体
的驱动,比如获取中断号的
OF
函数、获取
GPIO
的
OF
函数等等,这些
OF
函数我们在后面的
驱动实验中再详细的讲解。
关于设备树就讲解到这里,关于设备树我们重点要了解一下几点内容:
①、
DTS
、
DTB
和
DTC
之间的区别,如何将
.dts
文件编译为
.dtb
文件。
②、设备树语法,这个是重点,因为在实际工作中我们是需要修改设备树的。
③、设备树的几个特殊子节点。
④、关于设备树的
OF
操作函数,也是重点,因为设备树最终是被驱动文件所使用的,而驱动文件必须要读取设备树中的属性信息,比如内存信息、
GPIO
信息、中断信息等等。要想在
驱动中读取设备树的属性值,那么就必须使用
Linux
内核提供的众多的
OF
函数。