1、设备树
1.1、概念
在裸机驱动程序里,比如W25QXX、SPI,速度都是在.c文件中写死的,板级信息都写到.c文件中,导致linux内核臃肿。因此 将板子信息做成独立的格式,文件扩展名为.dts。一个平台或者机器对应一个.dts。
一般.dts 描述板级信息(也就是开发板上有哪些 IIC 设备、SPI 设备等),.dtsi 描述 SOC 级信息(也就是 SOC 有几个 CPU、主频是多少、各个外设控制器信息等)。
1.2、DTS、DTB 和 DTC
.dts相当于.c,就是DTS源码文件。
DTC工具相当于gcc编译器,将.dts编译成.dtb。
.dtb相当于bin文件,或可执行文件。
进入Ubuntu中
cd linux/IMX6ULL/linux/linux-imx-rel-imx_4.1.15_2.1.0_ga_alientek/ // 进入linux内核目录里
make dtbs//编译所有的dts文件
cd arch/arm/boot/dts -l //查看所有的.dtb文件
ls imx6ull-alientek-emmc.dts -l
cd linux/IMX6ULL/linux/linux-imx-rel-imx_4.1.15_2.1.0_ga_alientek/ // 进入linux内核目录里
make imx6ull-alientek-emmc.dtb//编译一个dtb文件
基于 ARM 架构的 SOC 有很多种,一种 SOC 又可以制作出很多款板子,每个板子都有一个对应的 DTS 文件,怎么确定编译的是哪一个文件呢?打开arch/arm/boot/dts/Makefile,里面有imx6ull-alientek-emmc.dtb、imx6ull-alientek-nand.dtb文件。这两个文件是给正点原子的 I.MX6U-ALPHA 开发板移植Linux 系统的时候添加的设备树。
2、DTS语法
2.1、分析
在linux源码中找到imx6ull-alientek-emmc.dts文件并打开,
12 #include <dt-bindings/input/input.h>
13 #include "imx6ull.dtsi"
第 12 行,使用“#include”来引用“input.h”这个.h 头文件。
第 13 行,使用“#include”来引用“imx6ull.dtsi”这个.dtsi 头文件。
打开imx6ull.dtsi文件,描述 I.MX6ULL 这颗 SOC 的内部外设信息,共有的信息,有SOC、CPU等,SOC中又有busfreq、aips1、aips2等,aips2包括IIC1、IIC2等,但是I2C1中又没有我们想要的具体的设备,这个时候可以去imx6ull-alientek-emmc.dts中找到&i2c1,这样就找到我们所需要的了,比如:mag3110。mag3110这个设备也有一些属性。
在imx6ull-alientek-emmc.dts文件中,斜杠表示根目录,根目录里面是子节点,里面放哪些子节点呢?这些事情由半导体厂商决定的,基本上都在.dtsi中。.dtsi包含了skeleton.dtsi文件。说明:这三个文件都有”/“,不是3个根节点,而是一个根节点。
//imx6ull-alientek-emmc.dts
/dts-v1/;
#include .h
#include imx6ull.dtsi
/{ //斜杠表示根目录,根目录里面是子节点
model = "Freescale i.MX6 ULL 14x14 EVK Board";
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
chosen{
};
memory{
};
reserved-memory{
};
backlight{
};
pxp_v412{
};
regulators{
};
sound{
};
spi4{
};
};
在skeleton.dtsi中:
/ {
#address-cells = <1>;
#size-cell = <1>;
chosen{};
aliases{};
memory {device_type = "memory"; reg = <0 0>;};
};
在imx6ull-alientek-emmc.dts中也有一个memory,很明显imx6ull-alientek-emmc.dts这个里面的值把0 0替换掉了。
在imx6ull.dtsi中的根目录下有一个aliases节点,将里面的内容添加到imx6ull-alientek-emmc.dts中。
此时的文件如下:
/dts-v1/;
#include .h
#include imx6ull.dtsi
/{
/* skeleton.dtsi */
#address-cells = <1>;
#size-cell = <1>;
chosen{
stdout-path = &uart1;
};
aliases{
can0 = &flexcan1;
...
};
memory {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
cpus {
};
intc: interrupt-controller@00a01000 {};
//斜杠表示根目录,根目录里面是子节点
model = "Freescale i.MX6 ULL 14x14 EVK Board";
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
chosen{
};
memory{
};
reserved-memory{
};
backlight{
};
pxp_v412{
};
regulators{
};
sound{
};
spi4{
};
};
总结:
(1)从根节点开始描述设备信息,根节点外有一些&cpu0这样的语句是”追加“。
(2)节点名字,小写。node-name@unit-address
i2c@地址 这个地址一般是外设寄存器的起始地址。
intc: interrupt-controller@00a01000
:前面是标签label, intc,后面才是名字,完整的名字是interrupt-controller@00a01000。很明显通过&intc 来访问“interrupt-controller@00a01000”这个节点要方便很多!
2.2、节点在系统中的体现
系统启动以后可以在根文件系统里面看到设备树的节点信息。在/proc/device-tree/目录下存放着设备树信息。
内核启动的时候会解析设备树,然后在/proc/device-tree/目录下呈现出来。
/ # cd /proc/device.tree/
/sys/firmware/devicetree/base #
/sys/firmware/devicetree/base # ls
此目录下的内容就是我们刚才分析的一级子目录。将属性和节点以文件夹的形式显示出来。
分析soc这个节点:
打开imx6ull.dtsi文件,
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&gpc>;
ranges;
busfreq { };
pmu { };
ocrams: sram@00900000 {};
ocrams_ddr: sram@00904000 {};
ocram: sram@00905000 {};
dma_apbh: dma-apbh@01804000 {};
gpmi: gpmi-nand@01806000{};
aips1: aips-bus@02000000 {};
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 {};
usdhc1: usdhc@02190000 {};
usdhc2: usdhc@02194000 {};
adc1: adc@02198000 {};
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";
/* imx6ull-alientek-emmc.dts追加的内容 */
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
/* 具体的IIC设备 */
mag3110@0e {
compatible = "fsl,mag3110";
reg = <0x0e>;
position = <2>;
};
ap3216c@1e {
compatible = "ap3216c";
reg = <0x1e>;
};
};
i2c2: i2c@021a4000 {};
i2c3: i2c@021a8000 {};
romcp@021ac000 {};
mmdc: mmdc@021b0000 {};
weim: weim@021b8000 {};
ocotp: ocotp-ctrl@021bc000 {};
};
aips3: aips-bus@02200000 {};
};
2.3、自己添加一个子节点
在imx6ull-alientek-emmc.dts中添加子节点。在Ubuntu中的VSCode中添加,打开源码目录,找到imx6ull-alientek-emmc.dts这个文件。在根节点的一级子目录的最底下添加。
/* 自定义节点 */
mytestnode {
};
&intc{
mytestnode{};
};
打开终端,输入:make dtbs
cp arch/arm/boot/dts/imx6ull-alientek-emmc.dts /home/yang/linux/tftpboot/ -f
2.4、特殊节点chosen、aliases
2.4.1、aliases 子节点
打开 imx6ull.dtsi 文件,单词 aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上 label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点。
2.4.2、chosen子节点
chosen 节点主要是为了 uboot 向 Linux 内核传递数据。
.dts 文件中chosen 节点仅仅设置了属性“stdout-path”,表示标准输出使用 uart1。但是当我们进入到/proc/device-tree/chosen 目录里面,会发现多了 bootargs 这个属性。输入 cat 命令查看 bootargs 这个文件的内容:“console=ttymxc0,115200……”
uboot 在启动 Linux 内核的时候会将 bootargs 的值传递给 Linux内核,bootargs 会作为 Linux 内核的命令行参数,Linux 内核启动的时候会打印出命令行参数(也就是 uboot 传递进来的 bootargs 的值)。
既然 chosen 节点的 bootargs 属性不是我们在设备树里面设置的,那么只有一种可能,那就是 uboot 自己在 chosen 节点里面添加了 bootargs 属性!并且设置 bootargs 属性的值为 bootargs环境变量的值。
在common/fdt_support.c 文件中发现了“chosen”的身影,fdt_support.c 文件中有个 fdt_chosen 函数。 uboot 中的 fdt_chosen 函数在设备树的 chosen 节点中加入了 bootargs属性,并且还设置了 bootargs 属性值。
2.4.3、Linux 内核解析 DTB 文件
Linux 内核在启动的时候会解析 DTB 文件,然后在/proc/device-tree 目录下生成相应的设备树节点文件。
2.5、设备树的特殊属性
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。
2.5.1、compatible属性
compatible 属性也叫做“兼容性”属性,!compatible 属性的值是一个字符串列表,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 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。
设备下面的compatible是用来找驱动的,根下面的compatible是用来查找内核支持不支持这个设备。 Linux 内核在使用设备树前后是如何判断是否支持某款设备的?
在没有使用设备树以前,uboot 会向 Linux 内核传递一个叫做 machine id 的值,machine id也就是设备 ID,告诉 Linux 内核自己是个什么设备,看看 Linux 内核是否支持。
引入设备树以后不会再根据 machine id 来检查 Linux 内核是否支持某个设备了。使用定义在文件 arch/arm/include/asm/mach/arch.h里面的DT_MACHINE_START,使用arch/arm/mach-imx/mach-imx6ul.c中的machine_desc 结构体。machine_desc 结构体中有个.dt_compat 成员变量,此成员变量保存着本设备兼容属性,示例代码中设置.dt_compat = imx6ul_dt_compat,imx6ul_dt_compat 表里有"fsl,imx6ul"和"fsl,imx6ull"这两个兼容值。只要某个设备(板子)根节点“/”的 compatible 属性值与imx6ul_dt_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。
Linux 内核通过根节点 compatible 属性找到对应的设备的函数调用过程如下:
2.5.2、model 属性
model = "wm8960-audio";
2.5.3、#address-cells 和#size-cells 属性
#address-cells 和#size-cells 这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。
#address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),#size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。
2.5.4、reg 属性
reg 属性的值一般是(address,length)对。reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息。
3、Linux内核的OF操作函数
Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 函数。这些 OF 函数原型都定义在 include/linux/of.h 文件中。
3.1、查找节点的OF函数
Linux 内核使用 device_node 结构体来描述一个节点,此结构体定义在文件 include/linux/of.h 中。
与查找节点有关的 OF 函数有 5 个:
(1)of_find_node_by_name 函数:
struct device_node *of_find_node_by_name(struct device_node *from,const char *name);
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name:要查找的节点名字。
(2)of_find_node_by_type 函数:通过 device_type 属性查找指定的节点。
(3)of_find_compatible_node 函数:根据 device_type 和 compatible 这两个属性查找指定的节点。
(4)of_find_matching_node_and_match 函数:通过 of_device_id 匹配表来查找指定的节点。
(5)of_find_node_by_path 函数:通过路径来查找指定的节点。
3.2、提取属性值的 OF 函数
Linux 内核中使用结构体 property 表示属性,此结构体同样定义在文件 include/linux/of.h 中。
(1)of_find_property 函数用于查找指定的属性。
(2)of_property_count_elems_of_size 函数用于获取属性中元素的数量。比如 reg 属性值是一个数组,那么使用此函数可以获取到这个数组的大小.。
(3)of_property_read_u32_index 函数用于从属性中获取指定标号的 u32 类型数据值(无符号 32位),比如某个属性有多个 u32 类型的值,那么就可以使用此函数来获取指定标号的数据值。
(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 属性中的所有数据。
(5)of_property_read_u8 函数
of_property_read_u16 函数
of_property_read_u32 函数
of_property_read_u64 函数
有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用
于读取 u8、u16、u32 和 u64 类型属性值,
(6)of_property_read_string 函数用于读取属性中字符串值.。
(7)of_n_addr_cells 函数用于获取#address-cells 属性值。
(8)of_size_cells 函数用于获取#size-cells 属性值。
4、如何使用函数获取属性值
在Ubuntu中:
cd IMX6ULL/Linux_Driver/
mkdir 4_dtsof
cp 3_newchrled/ * 4_dtsof/ -rf
cp 3_newchrled/.vscode 4_dtsof/ -rf
cd 4_dtsof/
ls
rm newchrled.code-workspace
rm ledAPP ledAPP.c //通过挂载驱动模块,在驱动模块中获取属性内容
mv newchrled.c dtsof.c
使用vscode打开4_dtsof,修改Makefile文件,将其修改为:dtsof.o。
打开dtsof.c文件,只留取头文件内容,把剩余的都删掉。
4.1、编写驱动框架
不用写注册字符设备驱动,在加载驱动的时候,dtsof_init()就会执行,直接在此函数中完成设备树属性的获取。
想要使用OF函数,需要把头文件添加过来。打开of.h文件,把of开头的头文件全部添加过来。
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
static int __init dtsof_init(void)
{
int ret = 0;
/* 找到backlight节点 */
return ret;
}
static void __exit dtsof_exit(void)
{
}
/* 注册模块入口 */
module_init(dtsof_init);
module_exit(dtsof_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yang");
我们要获取imx6ull-alientek-emmc.dts里的backlight节点的属性。
backlight {
compatible = "pwm-backlight";
pwms = <&pwm1 0 5000000>;
brightness-levels = <0 4 8 16 32 64 128 255>;
default-brightness-level = <7>;
status = "okay";
};
通过路径查找节点。函数原型如下:
struct device_node *of_find_node_by_path(const char *path);
返回值是device_node所以要定义一个device_node结构体指针:struct device_node *bl_nd;。
struct device_node *bl_nd = NULL;
/* 1、找到backlight节点:路径是:/backlight */
bl_nd = of_find_node_by_path("/backlight");
if(bl_nd == NULL){
ret = -EINVAL;
goto fail_findnd;
}
/*2、 获取属性 */
return 0;
fail_findnd;
return ret;
获取属性,第一条属性值是一个字符串,我们使用函数of_find_property();函数原型如下:
struct property *of_find_property(const struct device_node *np, const char *name, int *lenp);
返回值是property,所以要定义一个property结构体指针:struct property *comppro;。
struct property *comppro = NULL;
/* 2、获取属性值 */
comppro = of_find_property(bl_nd, "pwm-backlight", NULL);
if(comppro == NULL){
ret = -EINVAL;
goto fail_findpro;
} else {
printk("compatible=%s\r\n", (char*)comppro->value);
return 0;
fail_findpro;
fail_findnd;
return ret;
4.2、验证
make
sudo cp dtsof.ko /home/yang/linux/nfs/rootfs/lib/modules/4.1.15 -f
启动开发板:
/ # cd lib/modules/4.1.15
/lib/modules/4.1.15 # ls
/lib/modules/4.1.15 # depmod //第一次加载
/lib/modules/4.1.15 # modprobe dtsof.ko
/lib/modules/4.1.15 # rmmmod dtsof.ko
4.3、其它属性的获取
接着获取其它属性,第二个属性比较特殊,暂时不获取。
const char *str;
u32 def_value = 0;//保存读取的值
u32 elemsize;
u32 *brival;//申请一段内存保存读取到的数组的值
u8 i = 0;//循环变量,用于打印数组里的值
ret = of_property_read_string(bl_nd, "status", &str);
if(ret < 0){
goto fail_rs;
}else{
printk("status = %s\r\n");
}
/* 3、读取数字属性值 */
ret = of_property_read_u32(bl_nd, "default-brightness-level",&def_value);
if(ret < 0)
{
goto fail_read32;
}else{
printk("default-brightness-level = %d\r\n",def_value);
}
/* 4、获取数组元素 */
elemsize = of_property_count_elems_of_size(bl_nd, "brightness-levels", sizeof(u32));
if(elemsize < 0){
ret = -EINVAL;
goto fail_readele;
}else{
printk("brightness-levels elems size = %d\r\n",elemsize);
};
/* 内存申请 */
brival = kmalloc(elemsize * sizeof(u32), GFP_KERNEL);
if(!brival){
ret = -EINVAL;
goto fail_mem;
}
/* 获取数组里的数据 */
ret = of_property_read_u32_array(bl_nd, "brightness-levels", brival, elemsize);
if(ret < 0){
goto fail_read32array;
}else{
for(i=0;i<elemsize;i++){
printk("brightness-levels[%d] = %d\r\n",i,*(brival+i));
}
}
kfree(brival);
return 0;
fail_read32array:
kfree(brival);//释放内存
fail_mem:
fail_readele:
fail_read32:
fail_rs:
fail_finpro:
fail_finnd: