代码学习资料来源于:
第6.1讲 Linux设备树详解-什么是设备树?_哔哩哔哩_bilibili
仅用于个人学习/复习,侵联删
在linux内核3.x版本之后,linux内核开始使用设备树,设备树描述开发板上的硬件信息。
如上图所示,树的主干就是系统总线,IIC控制器,GPIO控制器,SPI控制器等都是接在系统主线上的分支,IIC控制器又分为IIC1和IIC2两种,DTS文件描述设备信息是有相应的语法规则的。
描述板级硬件信息的内容都从linux内中分离开来,用一个专属的文件格式来描述,这个专属的文件就称为“设备树”,文件扩展名为.dts。一个SOC可以做出很多不同的板子,这些不同的板子肯定是有共同信息的,将这些共同的信息提取出来作为一个通用的文件,其他的.dts文件直接引用这个这个通用文件即可,这个通用文件就是.dtsi,类似于c语言中的头文件。一般.dts描述板级信息(开发板上有哪些IIC设备、SPI设备),.dtsi描述SOC级信息(也就是SOC有几个CPU,主频是多少,各个外设控制器信息等)。
.dts相当于.c,也就是DTS源码文件。
DTS工具相当于设备树源码文件,DTB是将DTS编译以后得到的二进制文件,DTC则是将DTS->DTB的编译工具。
DTC工具源码在在srcipts/dtc目录下,scripts/dtc/Makefile文件内容如下:
# SPDX-License-Identifier: GPL-2.0
# scripts/dtc makefile
hostprogs-y := dtc
ifeq ($(DTC_EXT),)
always := $(hostprogs-y)
endif
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
# Source files need to get at the userspace version of libfdt_env.h to compile
HOST_EXTRACFLAGS := -I$(src)/libfdt
# Generated files need one more search path to include headers in source tree
HOSTCFLAGS_dtc-lexer.lex.o := -I$(src)
HOSTCFLAGS_dtc-parser.tab.o := -I$(src)
# dependencies on generated files need to be listed explicitly
$(obj)/dtc-lexer.lex.o: $(obj)/dtc-parser.tab.h
可以看出,DTC工具依赖于dtc.c、flattree.c、fstree.c等文件,最终编译并链接出DTC这个主机文件。如果要编译DTS文件的话只需要到linux源码根目录,然后执行如下命令:make all或者make dtbs
如何确认使用哪一个DTS文件呢?查看arch/arm/boot/dts/Makefile,
和c语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi,在imx6ul-isiot.dtsi文件中有:
#include
#include
#include "imx6ul.dtsi"
include/dt-bindings/input/input.h 文件如下:
#ifndef _DT_BINDINGS_INPUT_INPUT_H
#define _DT_BINDINGS_INPUT_INPUT_H
#include "linux-event-codes.h"
#define MATRIX_KEY(row, col, code) \
((((row) & 0xFF) << 24) | (((col) & 0xFF) << 16) | ((code) & 0xFFFF))
#endif /* _DT_BINDINGS_INPUT_INPUT_H */
同样的名称在dts里面相同的引用是追加的意思。根节点外有一些像 & 这样的语句称作“追加”,追加进来的将前面一个给替换掉了。
/ { // 根节点
model = "Engicam Is.IoT MX6UL eMMC Starter kit"; // 属性及属性值,开机的时候会打印该句
compatible = "engicam,imx6ul-isiot", "fsl,imx6ul"; // 根级的compatible匹配
memory@80000000 {
reg = <0x80000000 0x20000000>; // 起始地址和长度
};
aliases {
ethernet0 = &fec1;
ethernet1 = &fec2;
... ...
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
}
}
intc: interrupt-controller@a01000 { // 一般是外设的起始地址,intc是节点标签。
}
timer {
}
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&gpc>;
ranges;
gpmi: gpmi-nand@1806000 {
}
aips1: aips-bus@2000000 {
}
pwm1: pwm@2080000 {
}
}
aips2: aips-bus@2100000 {
i2c1: i2c@21a0000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>; // i2c1外设寄存器及其地址范围
interrupts = ; // 中断号,共享中断
clocks = <&clks IMX6UL_CLK_I2C1>;
status = "disabled";
};
}
... ...
}
imx6ul-14x14-evk.dtsi文件下有:
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
// 具体的i2c设备
mag3110@e { // 描述i2c,i2c地址,而不是imx6ul外设寄存器的首地址(具体问题具体分析)
compatible = "fsl,mag3110";
reg = <0x0e>;
};
};
node-name@unit-address // 单元地址
label : node-name@unit-address // 语法规则
// cpu0 : cpu@0 举例
引入标签的目的是为了方便访问节点,可以直接通过&label这种方式来直接访问这个节点,比如通过&cpu0就可以访问cpu@0这个节点,不需要输入完整的节点名字。
系统启动时以后可以在根文件系统里面看到设备树的节点信息,在/proc/device-tree目录下存放着设备树信息
内核启动时会解析设备树,然后在/proc/device-tree目录下呈现出来
aliases节点主要用来定义别名,定义别名的目的就是为了方便地访问节点。不过我们一般会在节点命名的时候会加上label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量地使用&label的形式来访问节点。
aliases {
ethernet0 = &fec1;
ethernet1 = &fec2;
... ...
};
chosen并不是一个真实的设备,chosen节点主要为了uboot向linux内核传递数据,重点是bootargs参数。一般.dts文件中chosen节点通常为空或者内容很少,例如如下的chosen节点:
// imx6ul-tx6ul.dtsi
chosen {
stdout-path = &uart1; // 属性stdout-path,表示标准输出使用uart1
};
用法1、如下,可以看到在leds设备节点下有compatible = "gpio-leds",compatible属性的值是一个字符串列表,compatible属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,可以对应多个字符串值。
leds {
compatible = "gpio-leds";
user_led: user {
label = "Heartbeat";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
gpios = <&gpio5 9 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
用法2、根节点 / 下面的compatible,内核启动的时候会检查是否支持此平台,或设备。
model属性值也是一个字符串,一般module属性描述设备模块信息,比如名字:
model = "Engicam Is.IoT MX6UL eMMC Starter kit";
status属性是和设备状态有关的,status属性值也是字符串,字符串是设备的状态信息,可选的状态如下:
"okay" // 表示设备是可操作的
"disabled" // 表示设备是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后
"fail" // 表示设备不可操作,设备检测到了一系列错误,而且设备也不大可能变得操作
"fail-sss" // 含义与fail相同,后面的sss部分是检测到的错误内容
这两个属性的值都是无符号32位整型,#address-cells和#size-cells这两个属性可以用在任何拥有子节点的设备,用于描述子节点的地址信息,address-cells属性值决定了子节点reg属性中地址信息所占有的字长(32位),size-cells属性值决定了子节点reg属性值中长度信息所占的字长(32位),#address-cells和#size-cells表明子节点应该如何编写reg属性值,一般reg属性都是与地址有关的内容,和地址相关的信息有两种:起始地址和地址长度,reg属性的格式一般为:
reg =
例如:
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&gpc>;
ranges;
ocram: sram@900000 {
compatible = "mmio-sram";
reg = <0x00900000 0x20000>; // 0x00900000是外设首地址,0x20000表示地址段长度
};
一个节点的reg是由父节点的address-cells和size-cells决定的。
大部分情况下reg用来描述内存的,一般是
对组,在i2c上就代表i2c外设的器件地址,举例如下:sgtl5000: codec@a {
compatible = "fsl,sgtl5000";
reg = <0x0a>; // 器件地址
#sound-dai-cells = <0>;
clocks = <&clks IMX6UL_CLK_OSC>;
clock-names = "mclk";
VDDA-supply = <®_3p3v>;
VDDIO-supply = <®_3p3v>;
VDDD-supply = <®_1p8v>;
};
ranges属性可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵,ranges是一个地址映射/转化表,ranges属性每个项目由子地址、父地址和地址空间长度这三部分组成:
child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells确定此物理地址所占的字长
parent-bus-address:父总线地址空间的物理地址,同样由父节点的#address-cells确定此物理地址所占的字长
length:子地址空间的长度,由父节点的#size-cells确定此地址长度所占的字长
如果ranges属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换
vendor: vendor {
#address-cells = <1>;
#size-cells = <1>;
ranges = <0 0 0 0xffffffff>;
compatible = "simple-bus";
};
表示设备类型,用的很少
memory { device_type = "memory"; reg = <0 0 0 0>; };
驱动如何获取到设备树中结点信息呢?
例如:
backlight {
compatible = "pwm-backlight";
pwms = <&pwm8 0 100000>;
brightness-levels = < 0 1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29
30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49
50 51 52 53 54 55 56 57 58 59
60 61 62 63 64 65 66 67 68 69
70 71 72 73 74 75 76 77 78 79
80 81 82 83 84 85 86 87 88 89
90 91 92 93 94 95 96 97 98 99
100>;
default-brightness-level = <100>;
};
backlight结点表示display的背光芯片,背光芯片有亮度等级,有默认等级,有用的哪一个pwm等等信息。
相关结构体:
struct property {
char *name;
int length;
void *value;
struct property *next;
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
unsigned long _flags;
#endif
#if defined(CONFIG_OF_PROMTREE)
unsigned int unique_id;
#endif
#if defined(CONFIG_OF_KOBJ)
struct bin_attribute attr;
#endif
};
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 properties */
struct device_node *parent;
struct device_node *child;
struct device_node *sibling;
#if defined(CONFIG_OF_KOBJ)
struct kobject kobj;
#endif
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
};
在驱动中使用OF函数获取设备树中属性的内容:
与查找节点相关的OF函数有如下5个:
include/linux/of.h
of_find_node_by_name 函数 // 通过名字查找节点
of_find_node_by_type 函数 // 通过类型查找节点
of_find_compatible_node 函数 // 通过兼容性查找
of_find_node_by_path 函数 // 通过路径查找节点
of_find_matching_node_and_match // 通过of_device_id查找节点
关于更详细的Linux内核of函数可以参考下面的博客:
linux内核驱动学习--设备树查找节点的 OF 函数_of_get_address___小小酥__的博客-CSDN博客
linux内核OF函数使用demo:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* 解析的节点 backlight
/ {
backlight {
compatible = "pwm-backlight";
pwms = <&pwm8 0 100000>;
brightness-levels = <0, 1, 2, 3, 4, 5, 6>;
default-brightness-level = <6>; // 32位
status = "okay";
};
}
*/
// struct device_node *of_find_node_by_path(const char *path)
static int __init dtsof_init(void)
{
int ret = 0;
/* 路径 /backlight */
struct device_node *nd;
nd = of_find_node_by_path("/backlight");
if (nd == NULL) {
printk("No /backlight node \n");
return 0;
}
/* 获取属性 */
struct property *prop;
prop = of_find_property(nd, "compatible", NULL);
if (!prop) {
return;
} else {
printk("compatible = %s", (char*)prop->value);
}
const char *st = NULL;
ret = of_property_read_string(nd, "status", &st);
if (ret) {
printk("of_property_read_string status error \n");
} else {
printk("status = %s", st);
}
/* 数字属性 */
int def_value;
ret = of_property_read_u32(nd, "default-brightness-level", def_value); // ARRAY_SIZE 数组包含的最大个数
if (ret < 0) {
printk("Failed to parse \""default-brightness-level"\" property\n");
return -EINVAL;
} else {
printk("default-brightness-level = %d \n", def_value);
}
// u32 level[7];
/* 数组类型的读取 */
int elemnum = of_property_count_elems_of_size(nd, "brightness-levels", sizeof(u32));
if (elemnum < 0) {
printk("Read \""level"\" property failed \n");
return -EINVAL;
} else {
printk("level size = %d \n", ret);
}
u32 *brival;
brival = kmalloc(elemnum * sizeof(u32), GFP_KERNEL);
if (!brival)
return -1;
/* 获取数组 */
ret = of_property_read_u32_array(nd, "brightness-levels", brival, elemnum);
if (ret < 0) {
printk("read bright_levels property failed \n");
kfree(brival);
return -1;
} else {
for (int i = 0; i < elemnum; i++) {
printk("brightness-levels[%d] = %d \n", i, *(brival + i));
}
}
kfree(brival);
return ret;
}
static void __exit dtsof_exit(void) { }
module_init(dtsof_init);
module_exit(dtsof_exit);