【i.MX6ULL】驱动开发5——设备树原理与点亮LED
1 什么是设备树
1.1 背景介绍
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文件。
1.2 设备树介绍
设备树的作用就是描述硬件平台的硬件资源。它可以被bootloader传递到内核,内核可以从设备树中获取硬件信息。
设备树描述硬件资源时有两个特点:
-
以树状结构描述硬件资源。以系统总线为树的主干,挂载到系统地总线的IIC控制器、SPI控制器等为树的枝干,IIC控制器下的IIC设备资源,又可以再分IIC1和IIC2,而IIC1上又可以连接MPU6050这类的IIC器件...
-
可以像头文件那样,一个设备树文件引用另外一个设备树文件,实现代码重用。例如多个硬件平台都使用i.MX6ULL作为主控芯片,可以将 i.MX6ULL 芯片的硬件资源写到一个单独的设备树文件中(.dtsi文件)。
1.3 DTS、DTSI、DTB、DTC
-
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这个主机文件
2 设备树框架与DTS语法
2.1 设备树代码分析
在学习设备树时,可以先看一下NXP关于i.MX6ULL已有的设备树文件,来大致了解一下设备树文件是什么样子的。
2.1.1 imx6ull-14x14-evk-emmc.dts
下面是/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)。
2.1.2 imx6ull-14x14-evk.dts
下面是/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 {...}等
2.1.3 imx6ull.dtsi
#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”就是我们所说的节点标签。通常节点标签是节点名的简写,它的作用是当其它位置需要引用时可以使用节点标签来向该节点中追加内容。
2.2 设备节点基本格式
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键-值对。
node-name@unit-address{
属性1 = ...
属性2 = ...
子节点...
}
2.2.1 节点名称
node-name用于指定节点名称,其长度为1~31个字符:
-
数字:
0~9
-
字母:
a~z
A~Z
-
英文符号:
,
.
_
+
-
节点名应使用字母开头,并能描述设备类别(根节点用斜杠表示,不需要节点名)
2.2.2 单元地址
@unit-address用于指定单元地址,其中@符号表示一个分隔符,unit-address是实际的单元地址,它的值要和节点reg属性的第一个地址一致,如果没有reg属性值,则可以省略单元地址。
2.2.3 节点属性
在节点的大括号“{}”中包含的内容是节点属性, 一个节点可以包含多个属性信息,例如根节点的属性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 确定此地址长度所占用的字长
-
2.2.4 特殊节点
-
aliases子节点:其作用是为其他节点起一个别名,例如:
aliases { i2c3 = &i2c4; };
-
chosen子节点:该节点位于根节点下,它不代表实际硬件, 它主要用于给内核传递参数,例如:
chosen { stdout-path = &uart1; };
表示系统标准输出 stdout 使用串口 uart1。
3 设备树编程之OF函数
内核提供了一系列函数用于从设备节点获取设备节点中定义的属性,这些函数以 of_ 开头,称为OF函数。在编写设备树版的LED驱动时,在进行硬件配置方面,就是要用这些OF函数,将寄存器地址等信息从设备树文件中获取出来,然后进行GPIO配置。
先来列举一下这些函数:
3.1 查找节点的OF函数
-
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)
3.2 查找父/子节点的OF函数
-
of_get_parent
用于查找父节点
/**
* node: 要查找的父节点的节点
* return: 找到的父节点
*/
struct device_node *of_get_parent(const struct device_node *node)
-
of_get_next_child
用迭代的方式查找子节点
/**
* node: 父节点
* prev: 前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点,为NULL表示从第一个子节点开始
* return: 找到的下一个子节点
*/
struct device_node *of_get_next_child(const struct device_node *node,
struct device_node *prev)
3.3 提取属性值的OF函数
-
of_find_property
查找指定的属性
/**
* np: 设备节点
* name: 属性名字
* lenp: 属性值的字节数
* return: 找到的属性
*/
property *of_find_property(const struct device_node *np,
const char *name,
int *lenp)
-
of_property_count_elems_of_size
用于获取属性中元素的数量
/**
* np: 设备节点
* propname: 属性名字
* elem_size: 元素长度
* return: 属性元素数量
*/
int of_property_count_elems_of_size(const struct device_node *np,
const char *propname,
int elem_size)
-
of_property_read_u32_index
用于从属性中获取指定标号的u32类型数据值
/**
* np: 设备节点
* propname: 属性名字
* index: 要读取的值标号
* out_value: 读取到的值
* return: 0读取成功,负值读取失败
*/
nt of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index,
u32 *out_value)
-
of_property_read_u8_array
用于读取属性中 u8类型的数组数据(类似的函数还有u16、u32 和 u64)
/**
* np: 设备节点
* propname: 属性名字
* out_values: 读取到的数组值
* return: 0读取成功,负值读取失败
*/
int of_property_read_u8_array(const struct device_node *np,
const char *propname,
u8 *out_values,
size_t sz)
-
of_property_read_u8
用于读取只有一个整形值的属性(类似的函数还有u16、u32 和 u64)
/**
* np: 设备节点
* propname: 属性名字
* out_values: 读取到的数组值
* return: 0读取成功,负值读取失败
*/
int of_property_read_u8(const struct device_node *np,
const char *propname,
u8 *out_value)
-
of_property_read_string
用于读取属性中字符串值
/**
* np: 设备节点
* propname: 属性名字
* out_values: 读取到的字符串值
* return: 0读取成功,负值读取失败
*/
int of_property_read_string(struct device_node *np,
const char *propname,
const char **out_string)
-
of_n_addr_cells
用于获取#address-cells 属性值
/**
* np: 设备节点
* return: 获取到的#address-cells属性值
*/
int of_n_addr_cells(struct device_node *np)
-
of_n_size_cells
用于获取#size-cells 属性值
/**
* np: 设备节点
* return: 获取到的#size-cells属性值
*/
int of_n_size_cells(struct device_node *np)
3.4 其他常用的OF函数
-
of_device_is_compatible
用于查看节点的compatible属性是否有包含compat指定的字符串,也就是检查设备节点的兼容性
/**
* device: 设备节点
* compat: 要查看的字符串
* return: 0不包含,正数包含
*/
int of_device_is_compatible(const struct device_node *device,
const char *compat)
-
of_get_address
用于获取地址相关属性
/**
* dev: 设备节点
* index: 要读取的地址标号
* size: 要读取的地址标号
* flags: 参数
* return: 读取到的地址数据首地址,NULL表示失败
*/
const __be32 *of_get_address(struct device_node *dev,
int index,
u64 *size,
unsigned int *flags)
-
of_translate_address
用于将设备树读取到的地址转换为物理地址
/**
* dev 设备节点
* in_addr: 要转换的地址
* return: 得到的物理地址
*/
u64 of_translate_address(struct device_node *dev,
const __be32 *in_addr)
-
of_address_to_resource
用于将reg属性值,转换为resource结构体类型
/**
* dev: 设备节点
* index: 地址资源标号
* r: 得到的 resource 类型的资源值
* return: 0成功,负值失败
*/
int of_address_to_resource(struct device_node *dev,
int index,
struct resource *r)
-
of_iomap
用于直接内存映射
/**
* np: 设备节点
* index: reg属性中要完成内存映射的段
* return: 经过内存映射后的虚拟内存首地址,为NULL表示失败
*/
void __iomem *of_iomap(struct device_node *np,
int index)
4 设备树LED驱动程序与实验
回忆之前的LED字符设备驱动的编写方法:直接在驱动文件regled.c中定义有关寄存器物理地址,然后使用io_remap函数进行内存映射得到对应的虚拟地址,最后操作寄存器对应的虚拟地址完成对GPIO的初始化。
使用设备树编写字符设备驱动,主要的一点区别是:使用设备树向Linux内核传递相关的寄存器物理地址,Linux驱动文件使用OF函数从设备树中获取所需的属性值,然后使用获取到的属性值来初始化相关的IO,所以,其本质还是配置寄存器。
所以,使用设备树进行LED驱动,需要的修改主要为:
-
修改imx6ull-myboard.dts设备树文件,在其中添加RGB-LED的设备节点
-
编写RGB-LED驱动程序,获取设备树中的相关属性值,并使用相关的属性值进行GPIO的初始化
-
编写RGB-LED应用程序,控制RGB-LED的亮灭
4.1 修改设备树文件
/ {
model = "Freescale i.MX6 ULL 14x14 EVK Board";
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
//省略...
/*myboard led*/
myboardled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "myboard-led";
status = "okay";
reg = < 0X020C406C 0x04 /*CCM_CCGR1_BASE*/
0X02290014 0x04 /*SW_MUX_SNVS_TAMPER3_BASE*/
0X02290058 0x04 /*SW_PAD_SNVS_TAMPER3_BASE*/
0X020AC000 0x04 /*GPIO5_DR_BASE*/
0X020AC004 0x04 >; /*GPIO5_GDIR_BASE*/
};
};
编译设备树,在内核源码的根目录下(我的是~/myTest/imx6ull/kernel/nxp_kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga),执行如下make命令即可单独编译自己修改的设备树:
make imx6ull-myboard.dtb
4.2 测试设备树
4.2.1 测试环境切换
由于这次是修改了设备树文件,而我的板子已经烧录了固件到emmc,因此,这次实验,重新将板子设为从SD卡启动uboot并从网络启动NFS文件系统的方式,方便修改测试设备树。(板子从网络启动的方式,可参考之前的文章),若之前SD的uboot配置还在,将板子切换到SD卡启动,并确保网络畅通,即可从网络启动。
若nfs服务器(ubuntu虚拟器)的IP发生变化,需要和之前一样进行类似如下的bootargs和bootcmd配置:
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.5.104:/home/xxpcb/myTest/nfs/rootfs,proto=tcp,nfsvers=4 rw ip=192.168.5.102:192.168.5.104:192.168.5.1:255.255.255.0::eth1:off'
setenv bootcmd 'tftp 80800000 nxp/zImage; tftp 83000000 nxp/imx6ull-myboard.dtb; bootz 80800000 - 83000000'
saveenv
boot
注意这里的192.168.5.104是我的ubuntu的IP,192.168.5.102是板子的IP。
4.2.2 设备树修改后的效果
在测试设备树之前,可以先看一下目前板子的设备树中都有什么:
将编译后的dtb文件放到网络启动位置,比如我的是复制到这里:
xxpcb@ubuntuTest:~/myTest/imx6ull/kernel/nxp_kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/boot/dts$ cp imx6ull-myboard.dtb ~/myTest/tftpboot/nxp/
然后重启板子,再次查看/proc/device-tree/目录:
可以看到,出现了新加的myboardled节点,进入myboardled目录下,可以看到其属性信息。
4.3 修改LED驱动程序
驱动程序整体框架和上一篇的寄存器版配置程序基本相同,主要的不同是修改硬件配置的方式,
/*
* @description : LED硬件初始化(IO映射、时钟、GPIO配置)
* @param : 无
* @return : 0 成功;其他 失败
*/
static int dtsled_hardware_init(void)
{
u32 val = 0;
int ret;
u32 regdata[14];
const char *str;
struct property *proper;
/* 获取设备树中的属性数据 */
/* 1、获取设备节点:myboardled */
dtsled.nd = of_find_node_by_path("/myboardled");
if(dtsled.nd == NULL)
{
printk("myboardled node nost find!\r\n");
return -EINVAL;
}
else
{
printk("myboardled node find!\r\n");
}
/* 2、获取compatible属性内容 */
proper = of_find_property(dtsled.nd, "compatible", NULL);
if(proper == NULL)
{
printk("compatible property find failed\r\n");
}
else
{
printk("compatible = %s\r\n", (char*)proper->value);
}
/* 3、获取status属性内容 */
ret = of_property_read_string(dtsled.nd, "status", &str);
if(ret < 0)
{
printk("status read failed!\r\n");
}
else
{
printk("status = %s\r\n",str);
}
/* 4、获取reg属性内容 */
ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10);
if(ret < 0)
{
printk("reg property read failed!\r\n");
}
else
{
u8 i = 0;
printk("reg data:\r\n");
for(i = 0; i < 10; i++)
{
printk("%#X ", regdata[i]);
}
printk("\r\n");
}
/* 初始化LED */
#if 0
/* 1、寄存器地址映射(使用ioremap) */
IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);
SW_MUX_SNVS_TAMPER3 = ioremap(regdata[2], regdata[3]);
SW_PAD_SNVS_TAMPER3 = ioremap(regdata[4], regdata[5]);
GPIO5_DR = ioremap(regdata[6], regdata[7]);
GPIO5_GDIR = ioremap(regdata[8], regdata[9]);
#else
/* 1、寄存器地址映射(直接使用of_iomap) */
IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0);
SW_MUX_SNVS_TAMPER3 = of_iomap(dtsled.nd, 1);
SW_PAD_SNVS_TAMPER3 = of_iomap(dtsled.nd, 2);
GPIO5_DR = of_iomap(dtsled.nd, 3);
GPIO5_GDIR = of_iomap(dtsled.nd, 4);
#endif
/* 2、使能GPIO1时钟 */
//省略... 后面的配置与上一篇的相同
}
上面的程序修改部分,从整个LED驱动的框架来看,修改的只是如下图中的黄色框部分:
4.4 实验测试
编译设备树版的LED驱动程序,并将编译好的ko文件发送到nfs文件系统对应的文件夹下。
LED是应用程序不需要修改,仍使用上一篇文章中的程序即可。
测试方法与之前基本相同:
使用设备树的方式,再次点亮LED:
5 总结
本篇介绍了设备树的基本原理以及设备树的使用方法,在上一篇点亮LED的代码基础上,通过设备树的方式,实现了LED点灯,总结一下主要的修改就是先在设备树中添加LED节点,然后在驱动文件中通过OF函数来读取设备树中的寄存器信息,再进行GPIO的初始化,其它部分的程序与上一篇的基本一样。
- 点赞
- 收藏
- 关注作者
评论(0)