今天跟大家分享的是设备树,设备树是Linux3.x以后的版本才引入的,设备树用于描述一个硬件平台的板级细节。
之前分享过字符设备原始的注册方法,后面又引进了总线的概念,总线式的驱动让驱动和硬件相分离,但是还不够,比如之前的platform总线,我们写一个驱动就要写设备文件和驱动文件,设备文件里保存了硬件信息,也就是“资源”,然后通过总线传给驱动文件,驱动文件完成具体的驱动逻辑。
如果硬件资源发生了改变,我们只需要修改设备文件就行了,但是这样还不够好,如果有非常多的设备,就要写非常多的设备文件,这些文件非常庞大,导致Linux内核非常臃肿。
于是,为了解决这个问题,引入了设备树。设备树到底是什么呢?其实说白了就是硬件资源的集合,就是把所有的硬件设备挂在一棵“树”上面,每个硬件设备就是一个节点,这个节点里保存了硬件的相关信息。如果我们要添加设备,就往设备树里添加节点,如果要移除设备,就去掉那个节点就行了,这样变得非常方便。而驱动文件可以去设备树上获取资源,所以驱动文件和之前是差不多的。
也就是说之前的资源是用一个设备文件来保存,现在是全部放在设备树文件上,而驱动基本不变。那么接下来就具体讲一下设备树的相关内容。
了解几个概念:
设备树文件的格式是怎么样的?
在Linux内核里有设备树文件,路径是
源码目录/arch/arm/boot/dts/imx6ull-seeed-npi.dts
来看一下设备树文件的格式:
Devicetree node格式:
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
};
看一个实例:
这是一个普通的节点,soc是节点名字,下面就是属性和值。比如compatible是一个属性,它的值是"simple-bus",具体的关于属性和值的内容后面会讲。ocrams就是一个标签,sram@90000是一个子节点,子节点里面有它自己的属性和值。
说明:
1、设备树可以包含.h或者.dtsi的头文件,和C语言非常类似。.dtsi就是一些公共的东西,可以被.dts文件包含。.dts文件包含了.dtsi文件之后就可以使用.dtsi文件里的内容,也可以改写里面的内容。
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"
2、设备树有且之后一个根节点。实际上.dts和.dtsi里都可以有根节点,但是它们最终会合并为一个。根节点没有名字,用“/”表示。
3、可以往已经存在的节点里追加内容。这时要加上一个符号“&”。比如要往soc节点追加内容,可以这样:
&soc{
/*具体追加内容*/
};
当然,也可以直接在原来的地方加上去就行了,只不过我们有时候不想去修改原文件。
接下来讲一下节点的属性和值。
每个节点里最重要的当然是节点的属性和值了,因为这里包含了要传递到内核的“板级硬件描述信息”,驱动中会通过一些API函数获取这些信息。
属性和值的写法有一定的规范。属性包括一些特殊的属性,还有一些我们自定义的属性。
属性的值通常有三种:
arrays of cells(1个或多个32位数据, 64位数据使用2个32位数据表示,u32)。
string(字符串)。
bytestring(1个或多个字节,u8)。
具体是怎么回事呢?来看几个例子:
第一种(u32):
interrupts = <17 0xc>;
interrupt是属性,后面用<>表示是arrays of cells,也就是说<>里面的每一个数都是32位的。如果要表示64位的数,那就要用两个32位的数来表示。其实里面随意有几个数,总之每个数是32位的,如何理解这些数取决于你自己在驱动文件中如何使用。
第二种(string):
compatible = "simple-bus";
这种就是字符串类型的,后面使用的是""。而且这种字符串是带结束符的,比如上面这个字符串就是占11个字节。
第三种(u8):
local-mac-address = [00 00 12 34 56 78];
local-mac-address = [000012345678];
这种后面跟着的是[]。它表示字节序列。每个byte使用2个16进制数来表示,比如00是一个字节,12也是一个字节。不管两个字节之间有没有空格,表示的含义是一样的,上面两种是等效的。
此外,也可以是各种值的组合, 用逗号隔开:
compatible = "ns16550", "ns8250";
example = <0xf00f0000 19>, "a strange property format";
这种写法估计比较少见。
以上讲的是属性的值的表示方法,其实无非就是“数值”和“字符串”,字符串就加"",数值使用第一种(使用<>)或第三种(使用[]),根据情况灵活使用即可。
接下来讲一下属性。属性我们需要了解一些特殊的属性,对于自定义的属性就随意了,所以讲一下几个特殊的属性。
1、compatible属性 属性值类型:字符串
设备树中的每一个代表了一个设备的节点都要有一个compatible属性。compatible是系统用来决定绑定到设备的设备驱动的关键。compatible属性是用来查找节点的方法之一,另外还可以通过节点名或节点路径查找指定节点。
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
2、model属性 属性值类型:字符串
model属性用于指定设备的制造商和型号,推荐使用“制造商, 型号”的格式,当然也可以自定义。
model = "Embedfire i.MX6ULL Board";
3、status属性 属性值类型:字符串
状态属性用于指示设备的“操作状态”,通过status可以去禁止设备或者启用设备,可用的操作状态如下表。默认情况下不设置status属性设备是使能的。
status = "disabled";
4、#address-cells 和 #size-cells 属性值类型:u32
#address-cells和 #size-cells属性同时存在,在设备树ocrams结构中, 它们用在有子节点的设备节点(节点),用于设置子节点的“reg”属性的“书写格式”。
#address-cells,用于指定子节点reg属性“地址字段”所占的长度(单元格cells的个数)。#size-cells,用于指定子节点reg属性“大小字段”所占的长度(单元格cells的个数)。
什么意思呢?比如上面的#address-cells和 #size-cells都指定为1,而reg=<0x900000 0x4000>.这就表示reg是从地址0x900000开始,长度为0x4000的一段内存。而有些地址可能是64位宽的,它就需要两个u32来表示,这时,就要指定#address-cells为2。
换句话说,reg是地址和长度交替的数据,#address-cells指定了几个u32是地址,#size-cells指定了从那个地址开始的内存的大小。就是里面哪些是地址,哪些是长度。
5、reg属性 属性值类型:地址、长度数据对
reg属性描述设备资源在其父总线定义的地址空间内的地址。通常情况下用于表示一块寄存器的起始地址(偏移地址)和长度, 在特定情况下也有不同的含义。例如上例中#address-cells = <1>,#address-cells = <1>,reg = <0x9000000 x4000>, 其中0x9000000表示的是地址,0x4000表示的是地址长度,这里的reg属性指定了起始地址为0x9000000,长度为0x4000的一块地址空间。
6、ranges 属性值类型:任意数量的 <子地址、父地址、地址长度>编码
该属性提供了子节点地址空间和父地址空间的映射(转换)方法,常见格式是ranges = <子地址, 父地址, 转换长度>。如果父地址空间和子地址空间相同则无需转换,如示例中所示,只写了renges,内容为空,我们也可以直接省略renges属性。比如对于#address-cells和#size-cells都为1的话,以ranges=<0x0 0x10 0x20>为例,表示将子地址的从0x0~(0x0 + 0x20)的地址空间映射到父地址的0x10~(0x10 + 0x20)。
7、name和device_type 属性值类型:字符串
这两个属性很少用(已经被废弃),不推荐使用。name用于指定节点名,在旧的设备树中它用于确定节点名, 现在我们使用的设备树已经弃用。device_type属性也是一个很少用的属性,只用在CPU和内存的节点上。
以上讲的是几个特殊的属性,这些属性有自己的含义,我们在写自定义的属性的时候就不要再用了。接下来讲几个特殊的节点:
1、aliases子节点
aliases子节点的作用就是为其他节点起一个别名,如下所示。
以“can0 = &flexcan1;”为例。“flexcan1”是一个节点的名字, 设置别名后我们可以使用“can0”来指代flexcan1节点,与节点标签类似。在设备树中更多的是为节点添加标签,没有使用节点别名,别名的作用是“快速找到设备树节点”。在驱动中如果要查找一个节点,通常情况下我们可以使用“节点路径”一步步找到节点。也可以使用别名“一步到位”找到节点。
2、chosen子节点
chosen子节点不代表实际硬件,它主要用于给内核传递参数。这里只设置了“stdout-path =&uart1;”一条属性,表示系统标准输出stdout使用串口uart1。此外这个节点还用作uboot向linux内核传递配置参数的“通道”, 我们在Uboot中设置的参数就是通过这个节点传递到内核的, 这部分内容是uboot和内核自动完成的,作为初学者我们不必深究。
如何获取设备树节点信息?
前面我们已经说了,设备树节点里包含了硬件的信息,所以我们写驱动的时候就需要从这些设备树节点里去获取这些信息,内核提供了一组函数用于从设备节点获取资源(设备节点中定义的属性)的函数,这些函数以of_开头,称为OF操作函数。常用的OF函数介绍如下:
这些函数大致可以分为三类,第一种是查找节点,第二种是提取属性值,第三种是内存映射相关。接下来逐一介绍:
1、查找节点函数(内核源码/include/linux/of.h)
a、根据节点路径查找节点:
struct device_node *of_find_node_by_path(const char *path)
device_node结构体如下所示:
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
};
b、根据节点名字查找节点函数
struct device_node *of_find_node_by_name(struct device_node *from,const char *name);
c、根据节点类型查找节点
struct device_node *of_find_node_by_type(struct device_node *from,const char *type)
d、根据节点类型和compatible属性寻找节点
struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible)
e、根据匹配表查找节点
static inline 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)
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};
f、寻找父节点
struct device_node *of_get_parent(const struct device_node *node)
g、寻找子节点
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)
2、获取属性值函数(内核源码/include/linux/of.h)
a、查找节点属性
struct property *of_find_property(const struct device_node *np,const char *name,int *lenp)
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
};
b、读取整型属性
//8位整数读取函数
int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz)
//16位整数读取函数
int of_property_read_u16_array(const struct device_node *np, const char *propname, u16 *out_values, size_t sz)
//32位整数读取函数
int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz)
//64位整数读取函数
int of_property_read_u64_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz)
如果读取的长度是1,可以使用下面简化后的函数:
//8位整数读取函数
int of_property_read_u8 (const struct device_node *np, const char *propname,u8 *out_values)
//16位整数读取函数
int of_property_read_u16 (const struct device_node *np, const char *propname,u16 *out_values)
//32位整数读取函数
int of_property_read_u32 (const struct device_node *np, const char *propname,u32 *out_values)
//64位整数读取函数
int of_property_read_u64 (const struct device_node *np, const char *propname,u64 *out_values)
c、读取字符串属性
在设备节点中存在很多字符串属性,例如compatible、status、type等等,这些属性可以使用查找节点属性函数of_find_property来获取,但是这样比较繁琐。内核提供了一组用于读取字符串属性的函数,介绍如下:
int of_property_read_string(const struct device_node *np,const char *propname,const char **out_string)
这个函数使用相对繁琐,推荐使用下面这个函数:
int of_property_read_string_index(const struct device_node *np,const char *propname, int index,const char **out_string)
相比前面的函数增加了参数index,它用于指定读取属性值中第几个字符串,index从零开始计数。第一个函数只能得到属性值所在地址,也就是第一个字符串的地址,其他字符串需要我们手动修改移动地址,非常麻烦,推荐使用第二个函数。
3、内存映射相关的函数(内核源码/drivers/of/address.c)
在设备树的设备节点中大多会包含一些内存相关的属性,比如常用的reg属性。通常情况下,得到寄存器地址之后我们还要通过ioremap函数将物理地址转化为虚拟地址。现在内核提供了of函数,自动完成物理地址到虚拟地址的转换。介绍如下:
void __iomem *of_iomap(struct device_node *np, int index)
内核也提供了常规获取地址的of函数,这些函数得到的值就是我们在设备树中设置的地址值。介绍如下:
int of_address_to_resource(struct device_node *dev, int index, struct resource *r)
resource结构体如下所示:
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
unsigned long desc;
struct resource *parent, *sibling, *child;
};
以上就是关于设备树的相关知识点,那么我们应该如何向设备树里添加节点呢?
第一步:打开内核源码里的设备树文件,这个文件在(以imx6ull为例)
源码目录/arch/arm/boot/dts/imx6ull-seeed-npi.dts
然后根据上面讲过的规则往里面添加节点即可
第二步,编译设备树
编译内核时会自动编译设备树,但是编译内核很耗时,所以我们推荐使用如下命令只编译设备树。
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- npi_v7_defconfig
make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs
编译成功后生成的设备树文件(.dtb)位于源码目录下的/arch/arm/boot/dts/,文件名为“imx6ull-seeed-npi.dtb”
第三步,将刚刚编译好的dtb文件拷贝到开发板,替换原来的/boot/dtbs/4.19.71-imx-r1/imx6ull-seeed-npi.dtb。
第四步,重启开发板
这时,我们就可以在/proc/device-tree/目录下看到我们添加的节点。
以上就是我们今天设备树的所有内容。总结一下,主要是讲了为什么要有设备树,设备树文件的结构是怎样的,如何从设备树文件中获取节点信息,包括查找节点,获取节点属性,以及内存映射等,最后讲了编译设备树。
今天的内容主要是知识性的,没有太多要思考和理解的地方,后面将会写一个驱动程序,用实例来解释如何使用设备树。
参考资料:
http://doc.embedfire.com/linux/imx6/base/zh/latest/linux_driver/driver_tree.html
https://www.100ask.net/detail/p_5e61a9f374112_5P2wQoy0/8