上篇文章(【i.MX6ULL】驱动开发4--点亮LED(寄存器版))介绍了在驱动程序中,直接操作寄存器了点亮LED。本篇,介绍另外一种点亮LED的方式——设备树,该方式的本质也是操作寄存器,只是寄存器的相关信息放在了设备树中,配置寄存器时需要使用OF函数从设备树中读取处寄存器数据后再进行配置。
Linux3.x之前是没有设备树的,设备树是用来描述一个硬件平台的板级细节。对应ARM-Linux开发,这些板级描述文件存放在linux内核的 /arch/arm/plat-xxx和/arch/arm/mach-xxx 中。随着ARM硬件设备的种类增多,与板子相关的设备文件也越来越多,这就导致Linux内核越来越大,而实际这些ARM硬件相关的板级信息与Linux内核并无相关关系。
2011年,Linux之父Linus Torvalds发现这个问题后,就通过邮件向ARM-Linux开发社区发了一封邮件,不禁的发出了一句“This whole ARM thing is a f*cking pain in the ass”。之后,ARM社区就引入了PowerPC等架构已经采用的设备树(Flattened Device Tree)机制,将板级信息内容都从Linux内核中分离开来,用一个专属的文件格式来描述,即现在的.dts文件。
设备树的作用就是描述硬件平台的硬件资源。它可以被bootloader传递到内核,内核可以从设备树中获取硬件信息。
设备树描述硬件资源时有两个特点:
以树状结构描述硬件资源。以系统总线为树的主干,挂载到系统地总线的IIC控制器、SPI控制器等为树的枝干,IIC控制器下的IIC设备资源,又可以再分IIC1和IIC2,而IIC1上又可以连接MPU6050这类的IIC器件...
可以像头文件那样,一个设备树文件引用另外一个设备树文件,实现代码重用。例如多个硬件平台都使用i.MX6ULL作为主控芯片,可以将 i.MX6ULL 芯片的硬件资源写到一个单独的设备树文件中(.dtsi文件)。
DTS ,Device Tree Source,是设备树源码文件
DTSI ,Device Tree Source Include,是设备树源码文件要用到的头文件
DTB ,Device Tree Binary,是将DTS 编译以后得到的二进制文件
DTC ,Device Tree Compiler,是将.dts 编译为.dtb需要用到的编译工具
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这个主机文件
在学习设备树时,可以先看一下NXP关于i.MX6ULL已有的设备树文件,来大致了解一下设备树文件是什么样子的。
下面是/arch/arm/boot/dts/imx6ull-14x14-evk-emmc.dts
#include "imx6ull-14x14-evk.dts"
?
&usdhc2 {
pinctrl-names = "default", "state_100mhz", "state_200mhz";
pinctrl-0 = <&pinctrl_usdhc2_8bit>;
pinctrl-1 = <&pinctrl_usdhc2_8bit_100mhz>;
pinctrl-2 = <&pinctrl_usdhc2_8bit_200mhz>;
bus-width = <8>;
non-removable;
status = "okay";
};
该文件就这几行,描述了emmc版本板子的usdhc信息。该文件的主要的功能是通过头文件的形式包含了另一个imx6ull-14x14-evk.dts设备树文件。
DTS语法:设备树是可以使用“#include”引用其它文件(.dts、.h、.dtsi)。
下面是/arch/arm/boot/dts/imx6ull-14x14-evk.dts
/dts-v1/;
?
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"
?
/ {
model = "Freescale i.MX6 ULL 14x14 EVK Board";
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
?
chosen {
stdout-path = &uart1;
};
?
memory {
reg = <0x80000000 0x20000000>;
};
?
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
?
linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0x14000000>;
linux,cma-default;
};
};
?
backlight {
compatible = "pwm-backlight";
pwms = <&pwm1 0 5000000>;
brightness-levels = <0 4 8 16 32 64 128 255>;
default-brightness-level = <6>;
status = "okay";
};
?
pxp_v4l2 {
compatible = "fsl,imx6ul-pxp-v4l2", "fsl,imx6sx-pxp-v4l2", "fsl,imx6sl-pxp-v4l2";
status = "okay";
};
?
regulators {
compatible = "simple-bus";
//省略...
};
//省略...
};
?
&cpu0 {
arm-supply = <®_arm>;
soc-supply = <®_soc>;
dc-supply = <®_gpio_dvfs>;
};
?
&clks {
assigned-clocks = <&clks IMX6UL_CLK_PLL4_AUDIO_DIV>;
assigned-clock-rates = <786432000>;
};
?
//省略...
?
&wdog1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_wdog>;
fsl,wdog_b;
};
该文件也是先包含一些头文件,然后是一个斜杠+一些大括号,后面还出现了&符号。
DTS语法:
/ {?} 斜杠+大括号,表示根节点,一个设备只有一个根节点
(注:一个dts包含另一个dts,两个文件里的根节点,其实也是同一个根节点)
xxx {?} 根节点内部单独的大括号,表示子节点,如reserved-memory {...}、pxp_v4l2 {...}等
&xxx {?} 根节点外部单独的&符号与大括号,表示节点的追加内容,如&cpu0 {...}等
#include <dt-bindings/interrupt-controller/irq.h>
#include "imx6dl-pinfunc.h"
#include "imx6qdl.dtsi"
?
/ {
aliases {
i2c3 = &i2c4;
};
?
cpus {
#address-cells = <1>;
#size-cells = <0>;
?
cpu0: cpu@0 {
compatible = "arm,cortex-a9";
device_type = "cpu";
//省略...
};
?
cpu@1 {
compatible = "arm,cortex-a9";
device_type = "cpu";
reg = <1>;
next-level-cache = <&L2>;
};
};
?
reserved-memory {
//省略...
};
?
soc {
//省略...
ocram: sram@00905000 {
compatible = "mmio-sram";
reg = <0x00905000 0x1B000>;
clocks = <&clks IMX6QDL_CLK_OCRAM>;
};
//省略...
};
};
?
//省略...
?
&vpu_fsl {
iramsize = <0>;
};
该文件是设备树的头文件,其格式与设备树基本相同。
DTS语法:节点标签
节点名“cpu”前面多了个“cpu0”, 这个“cpu0”就是我们所说的节点标签。通常节点标签是节点名的简写,它的作用是当其它位置需要引用时可以使用节点标签来向该节点中追加内容。
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键-值对。
node-name@unit-address{
属性1 = ...
属性2 = ...
子节点...
}
node-name用于指定节点名称,其长度为1~31个字符:
数字:0~9
字母:a~z A~Z
英文符号:, . _ + -
节点名应使用字母开头,并能描述设备类别(根节点用斜杠表示,不需要节点名)
@unit-address用于指定单元地址,其中@符号表示一个分隔符,unit-address是实际的单元地址,它的值要和节点reg属性的第一个地址一致,如果没有reg属性值,则可以省略单元地址。
在节点的大括号“{}”中包含的内容是节点属性, 一个节点可以包含多个属性信息,例如根节点的属性model = "Freescale i.MX6 ULL 14x14 EVK Board"
,编写设备树最主要的内容是编写节点的节点属性。属性包括自定义属性和标准属性,下面来看几个标准属性:
model属性:用于指定设备的制造商和型号,多个字符串使用“,”分隔开
compatible 属性:由一个或多个字符串组成,是用来查找节点的方法之一
status属性:用于指示设备的“操作状态” ,通过status可以禁用或启用设备
reg属性:描述设备资源在其父总线定义的地址空间内的地址,通常情况下用于表示一块寄存器的起始地址(偏移地址)和长度
#address-cells 和 #size-cells:这两个属性同时存在,在设备树ocrams结构中,用在有子节点的设备节点,用于设置子节的“reg”属性的“书写格式”
ranges属性:它是一个地址映射/转换表,由子地址、父地址和地址空间长度这三部分组成:
child-bus-address: 子总线地址空间的物理地址, 由父节点的#address-cells 确定此物理地址所占用的字长
parent-bus-address:父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长
length:子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长
aliases子节点:其作用是为其他节点起一个别名,例如:
aliases {
i2c3 = &i2c4;
};
chosen子节点:该节点位于根节点下,它不代表实际硬件, 它主要用于给内核传递参数,例如:
chosen {
stdout-path = &uart1;
};
表示系统标准输出 stdout 使用串口 uart1。
内核提供了一系列函数用于从设备节点获取设备节点中定义的属性,这些函数以 of_ 开头,称为OF函数。在编写设备树版的LED驱动时,在进行硬件配置方面,就是要用这些OF函数,将寄存器地址等信息从设备树文件中获取出来,然后进行GPIO配置。
先来列举一下这些函数:
of_find_node_by_name
通过节点名字查找指定的节点
/**
* from: 开始查找的节点,若为NULL表示从根节点开始查找整个设备树
* name: 要查找的节点名字
* return: 找到的节点,若为NULL表示查找失败
*/
struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
of_find_node_by_type
通过device_type属性查找指定的节点
/**
* from: 开始查找的节点,若为NULL表示从根节点开始查找整个设备树
* type: 要查找的节点对应的type字符串,也就是device_type属性值
* return: 找到的节点,若为NULL表示查找失败
*/
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
of_find_compatible_node
根据device_type和compatible这两个属性查找指定的节点
/**
* from: 开始查找的节点,若为NULL表示从根节点开始查找整个设备树
* type: 要查找的节点对应的type字符串,也就是device_type属性值,为NULL表示忽略掉device_type属性
* compatible: 要查找的节点所对应的compatible属性列表
* return: 找到的节点,若为NULL表示查找失败
*/
struct device_node *of_find_compatible_node(struct device_node *from, ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? const char *type, ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? const char *compatible) ?
of_find_matching_node_and_match
通过of_device_id匹配表来查找指定的节点
/**
* from: 开始查找的节点,若为NULL表示从根节点开始查找整个设备树
* matches: of_device_id匹配表,也就是在此匹配表里面查找节点
* match: 找到的匹配的of_device_id
* return: 找到的节点,若为NULL表示查找失败
*/
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)
of_find_node_by_path
通过路径来查找指定的节点
/**
* path: 带有全路径的节点名
* return: 找到的节点,若为NULL表示查找失败
*/
inline struct device_node *of_find_node_by_path(const char *path)
【i.MX6ULL】驱动开发系列文章汇总入口:
【i.MX6ULL】驱动开发——by DDZZ669 - ARM技术 - 电子工程世界-论坛 (eeworld.com.cn)
分目录:
【i.MX6ULL】驱动开发6——Pinctrl子系统与GPIO子系统点亮LED
引用: 火辣西米秀 发表于 2021-10-10 10:46 开始看设备树还挺麻烦 总结一下, 1.在设备树中添加LED节点, 2.在驱动文件中通过OF函数来读取设 ...
是的,设备树我也是研究了好长时间才看明白,实际使用起来就是这个步骤