一、什么是设备树
- 1、新版本的 linux 中,ARM 相关的驱动全部采用了设备树,最新出的 CPU 的驱动开发基本都是基于设备树的
- 2、uboot 启动内核用到
zImage
、imx6ull-alientek-emmc.dtb
。bootz 80800000 - 83000000
- 3、
设备树
就是设备
和树
,描述设备树的文件叫做 DTS,DTS 采用树形结构描述板级设备信息。
主干是系统总线,主干的分支是控制器,分支的分支上接的是具体的设备 - 4、
在单片机驱动
里面,spi flash芯片:w25q64 的速度属性都是在.c
文件中写死的。
若板级信息都写到.c
里面会产生大量的.c
文件。把不同的板子信息以这样的形式都写到内核里显然不现实。
因此将板子信息做成独立的格式,文件后缀为.dts
。一个平台会者机器对应一个.dts
。
二、DTS、DTB 和 DTC 的关系
.dts
:相当于.c
文件,将板级信息从linux内核中分离出来,用此种格式来描述,就是设备树源码文件.dtsi
:相当于.h
文件,一款SOC可以做出很多种板子,这些不同的板子必然有共同的信息,将这些信息提取出来做为有一个通用的文件,其它的.dts
文件直接引用这个.dtsi
文件即可(一般.dts
描述板级信息( 也就是开发板上有哪些 IIC 设备、 SPI 设备等 ),.dtsi
描述 SOC 级信息(比如 CPU 架构、主频、外设寄存器地址范围,比如 UART、 IIC 等等。比如 imx6ull.dtsi 就是描述 I.MX6ULL 这颗 SOC 内部外设情况信息的 )- DTC 工具就相当于gcc编译器,将
.dts
编译成.dtb
文件 .dtb
文件相当于 bin文件 或者 可执行文件,可由 DTC 工具将.dts
编译成.dtb
文件, DTC 源码在 Linux 内核的 scripts/dtc 目录下- 描述板级硬件信息的内容都从 Linux 内中分离开来,用一个专属的文件格式来描述,这个专属的文件就叫做设备树,文件扩展名为.dts。 一个 SOC 可以作出很多不同的板子,这些不同的板子肯定是有共同的信息, 将这些共同的信息提取出来作为一个通用的文件,其他的.dts 文件直接引用这个通用文件即可,这个通用文件就是.dtsi 文件,类似于 C 语言中的头文件。一般.dts 描述板级信息(也就是开发板上有哪些 IIC 设备、 SPI 设备等), .dtsi 描述 SOC 级信息(也就是 SOC 有几个 CPU、主频是多少、各个外设控制器信息等)
- 通过
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
编译所有的dts
文件,若要编译指定的.dts
三、DTS基本语法
- 1、设备树文件也就是
dts
文件 也有头文件,扩展名为.dtsi
。可以将一款SOC的其它所有设备 / 平台的共有的信息提出来,作为一个通用的.dtsi
文件,.dtsi
文件一般是用来描述cpu内部外设的属性的。(.dts
还可以包含 c 的头文件) - 2、DTS也是以
/
开始。
dts文件的注释方法同c。
/dts-v1/;
#include <dt-bindings/input/input.h> // 可以包含c的头文件
#include "imx6ull.dtsi" // 设备树头文件
/ { // 斜杠后面这个括号表示根结点
// skeleton.dtsi文件(3者的)的根结点合并到一起
#address-cells = <1>;
#size-cells = <1>;
chosen { //处理同memory
stdout-path = &uart1;
};
aliases {
// skeleton.dtsi文件中有此一级子节点但是空的,imx6ull.dtsi也有此节点,将节点内的内容移动到此处,如下
can0 = &flexcan1;
...
};
memory {
device_type = "memory";
//reg = <0 0>; 下面也有个memory一级子节点,此种情况下的处理方法是合并同类项,同样的reg,保留后面的。所以欲修改某个属性值可以在后面再赋值一次即可
reg = <0x80000000 0x20000000>; //(起始地址和长度)
};
cpus {
};
intc: interrupt-controller@00a01000 {
};
// 描述 6ull 芯片内部内存映射,内部外设信息
clocks {
};
soc {
...
aips2: aips-bus@02100000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02100000 0x100000>;
ranges;
usbotg1: usb@02184000 {
};
usbotg2: usb@02184200 {
};
usbmisc: usbmisc@02184800 {
};
fec1: ethernet@02188000 {
};
...
i2c1: i2c@021a0000 {
// imx6ull.dtsi 里的 i2c1 属性信息只有这么多,通用信息或属性,半导体厂商做好的
#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"; 默认是 disable,在下面改为 okay,此处屏蔽
// imx6ull-alientek-emmc.dts 追加的部分如下
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
// 具体的 i2c 设备,磁力计,0e表示设备地址
mag3110@0e {
compatible = "fsl,mag3110";
reg = <0x0e>;
position = <2>;
};
// 具体的 i2c 设备,6轴传感器
fxls8471@1e {
compatible = "fsl,fxls8471";
reg = <0x1e>;
position = <0>;
interrupt-parent = <&gpio5>;
interrupts = <0 8>;
};
}; // i2c1
...
}; // aips2
...
}; // soc
model = "string"; // (属性名 = 属性值)
compatible = "string"
chosen { //(一级子节点,他自己没有子节点了,若有其就是二级子节点)
stdout-path = &uart1;
};
memory { //(一级子节点)
reg = <0x80000000 0x20000000>; //(起始地址和长度)
};
reserved-memory {
};
backlight {
};
pxp_v4l2 {
};
regulators {
};
sound {
};
spi4 {
};
};
- 3、从根节点开始描述设备信息
- 4、在 根节点外有一些取址符号如
&cpu0
这样的语句是追加 - 5、节点名字的完整要求:
node_name@unit_address
有时也常常会遇到这种格式label: node-name@unit-address
,label 是节点标签,后面的才是节点名字,引入 label 的目的就是为了方便访问节点,可以直接通过&label
来访问这个节点,比如通过&cpu0
就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。.dts
文件中有使用此方法来访问.dtsi
文件中定义的节点的&lcdif
,注意只有标签后面才能接冒号:
,节点名称后面直接空格,大括号
。而且追加访问只能在根结点同一级处里面追加,不能在一级子节点、二级子节点中追加访问
unit_address 一般都是外设寄存器的起始地址(不绝对),有时候是 I2C 的设备地址或者其它含义,具体节点具体分析
例如:
i2c4: i2c@021f8000 { // 021f8000是单元起始地址
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021f8000 0x4000>;
interrupts = <GIC_SPI 35 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_I2C4>;
status = "disabled";
};
uart6: serial@021fc000 {
compatible = "fsl,imx6ul-uart",
"fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x021fc000 0x4000>;
interrupts = <GIC_SPI 17 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_UART6_IPG>,
<&clks IMX6UL_CLK_UART6_SERIAL>;
clock-names = "ipg", "per";
dmas = <&sdma 0 4 0>, <&sdma 47 4 0>;
dma-names = "rx", "tx";
status = "disabled";
};
五、设备树在系统中的体现
- 使用指令
ls /proc/device-tree
- 设备启动以后可以在根文件系统中看到设备树的节点信息。
在/proc/device-tree
目录下存放的是设备树中根节点的各个一级子节点
,这些一级子节点
是以目录
的形式给出的
分别进入这些代表着一级子节点的目录
,里面的文件表示这些一级子节点的各个属性
/sys/firmware/devicetree/base # ls
#address-cells memory
#size-cells model
aliases name
backlight pxp_v4l2
chosen regulators
clocks reserved-memory
compatible soc
cpus sound
interrupt-controller@00a01000 spi4
/sys/firmware/devicetree/base #
/sys/firmware/devicetree/base #
/sys/firmware/devicetree/base #
/sys/firmware/devicetree/base #
- 内核启动的时候会解析设备树,然后在
/procdevice-tree
目录下呈现出来。解析过程本次不做介绍。
六、特殊节点
-
1、aliases
-
2、chosen:主要是了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数作为命令行参数。
uboot里面的 bootargs 值为bootargs=console=ttymxc0,115200 rw root=/dev/nfs nfsroot=192.168.1.77:/home/jl/linux/nfs/rootfs ip=192.168.1.66:192.168.1.77:192.168.1.1:255.255.255.0::eth0:off
linux kernel cmdline 为Kernel command line: console=ttymxc0,115200 rw root=/dev/nfs nfsroot=192.168.1.77:/home/jl/linux/nfs/rootfs ip=192.168.1.66:192.168.1.77:192.168.1.1:255.255.255.0::eth0:off
一般 .dts 文件中 chosen 节点通常为空或者内容很少 -
uboot 是如何向 kernel 传递 bootargs 的?
cd /proc/device-tree
该目录下有三个文件:bootargs name stdout-path
bootargs
属性值和 uboot 里面的环境变量 一样
但是在设备树文件imx6ull-alientek-emmc.dts
文件中,chosen
节点内只有stdout-path
这一个
uboot 接触过 dtb,最终通过bootz 80800000 - 83000000
来启动内核的,经过分析判断, uboot 拥有bootargs
环境变量和dtb
文件,可能是 uboot 修改了dtb
,最终发现在fdt_support.c
中的fdt_chosen
函数中添加了这个属性值。
七、特殊属性
- 脱离一个具体的器件来看设备树中结点的属性是没有意义的,此处只介绍一些约定俗成的属性(page 1082)
- 1、compatible
兼容性属性
值是一个字符串列表
用于将设备和驱动绑定起来
pxp_v4l2 {
compatible = "fsl,imx6ul-pxp-v4l2", "fsl,imx6sx-pxp-v4l2", "fsl,imx6sl-pxp-v4l2";
status = "okay";
};
...
sound {
compatible = "fsl,imx6ul-evk-wm8960",
"fsl,imx-audio-wm8960"; // 支持这两个设备
model = "wm8960-audio";
cpu-dai = <&sai2>;
...
}
// 内核源码 imx-wm8960.c
static const struct of_device_id imx_wm8960_dt_ids[] = {
{ .compatible = "fsl,imx-audio-wm8960", },
{ /* sentinel */ }
};
- 2、model
model 属性值也是一个字符串,一般 model 属性描述设备模块信息,比如名字什么的 - 3、status
是和设备状态有关的, status 属性值也是字符串,字符串是设备的状态信息,上两个值常用
- 4、#address-cells 和#size-cells
#address-cells 和#size-cells 这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息
设备树中#address-cells和#size-cells作用
// 父节点的决定子节点的
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";
};
-
根结点的compatible属性,值是字符串
-
根结点下的用于内核查找,判断是否支持这个平台
内核启动时会检查是否支持此平台或者机器(教程 1086) -
不使用设备树时,通过 machine id 来判断内核是否支持
Linux内核都用MACHINE_START
和MACHINE_END
来定义一个machine_desc
结构体来描述这个设备
// arch.h
/*
* Set of macros to define architecture features. This is built into
* a table by the linker.
*/
#define MACHINE_START(_type,_name) \
static const struct machine_desc __mach_desc_##_type \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_##_type, \
.name = _name,
#define MACHINE_END \
};
展开示例见教程 1087,1088
- 使用设备树的话,就不需要使用机器ID,而是使用根节点的 compatible 属性值 (教程1088 末尾开始)
八、linux内核的 OF 操作函数
- 1、驱动如何获取到设备书中节点信息,在驱动中使用 OF函数 来获取设备树属性内容(教程 1106)
- 2、驱动要想获取到设备树节点内容,首先要找到节点
九、源码
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/init.h>
#include <linux/fs.h>
#include<linux/slab.h>
#include<linux/io.h>
#include<linux/uaccess.h>
#include<linux/cdev.h>
#include<linux/device.h>
#include<linux/of.h>
#include<linux/of_address.h>
#include<linux/of_irq.h>
#if 0
backlight {
compatible = "pwm-backlight";
pwms = <&pwm1 0 5000000>;
brightness-levels = <0 4 8 16 32 64 128 255>;
default-brightness-level = <6>;
status = "okay";
};
#endif
// 驱动入口函数
static int __init dtsof_init(void)
{
// 在调用 modprobe 时完成获取设备树
int ret = 0;
struct device_node *bl_nd;
struct property *comppro;
const char *str;
u32 def_value;
u32 *brival;
u8 cnt = 0;
u8 i = 0;
// 1. 寻找节点,node:backlight
// 参数为 节点的路径
bl_nd = of_find_node_by_path("/backlight");
if(bl_nd == NULL) // 寻找失败
{
printk("Fail\r\n");
ret = -1;
goto fail_findnd;
}
// 获取 backlight 结点下的 compatible 属性
// 1
comppro = of_find_property(bl_nd, "compatible", NULL);
if(comppro == NULL)
{
printk("Fail\r\n");
ret = -1;
goto failfindpro;
}
else
{
printk("compatilble = \"%s\"\r\n", (char *)comppro->value);
}
// 2
// 获取 backlight 结点下的 status 属性
ret = of_property_read_string(bl_nd, "status", &str);
if(ret)
{
goto fail_getstatus;
}
else
{
printk("status = \"%s\"\r\n", str);
}
//3
// 获取 backlight 结点下的 default-brightness-level 属性
ret = of_property_read_u32(bl_nd, "default-brightness-level", &def_value);
if(ret < 0)
{
goto fail_readu32;
}
else
{
printk("default-brightness-level = %u\r\n", def_value);
}
// 4
// 获取 backlight 结点下的 brightness-levels 属性
ret = of_property_count_elems_of_size(bl_nd, "brightness-levels", sizeof(u32));
if(ret < 0)
{
goto fail_readele;
}
else
{
cnt = ret;
printk("ele_size = %d\r\n", cnt);
}
brival = kmalloc(ret*sizeof(u32), GFP_KERNEL);
if(!brival)
{
ret = -1;
goto fail_kmalloc;
}
ret = of_property_read_u32_array(bl_nd, "brightness-levels", brival,cnt);
if(ret < 0)
{
goto fail_readarray;
}
else
{
printk("\"brightness-levels\" = < ");
for(i=0; i<cnt; i++)
{
printk("%d ", brival[i]);
}
printk(" >\r\n");
kfree(brival);
}
return 0;
fail_readarray:
kfree(brival);
fail_kmalloc:
fail_readele:
fail_readu32:
fail_getstatus:
failfindpro:
fail_findnd:
return ret;
}
// 出口函数
static void __exit dtsof_exit(void)
{
;
}
// register module entrance and exit
module_init(dtsof_init);
module_exit(dtsof_exit);
MODULE_LICENSE("GPL");
十、测试
- 1、第一次加载
dtsof.ko
这个驱动,先试用指令depmod
- 2、
modprobe dtsof.ko