前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux笔记(22)| 设备树初探

Linux笔记(22)| 设备树初探

作者头像
飞哥
发布2021-03-03 11:14:27
1.9K0
发布2021-03-03 11:14:27
举报

今天跟大家分享的是设备树,设备树是Linux3.x以后的版本才引入的,设备树用于描述一个硬件平台的板级细节。

之前分享过字符设备原始的注册方法,后面又引进了总线的概念,总线式的驱动让驱动和硬件相分离,但是还不够,比如之前的platform总线,我们写一个驱动就要写设备文件和驱动文件,设备文件里保存了硬件信息,也就是“资源”,然后通过总线传给驱动文件,驱动文件完成具体的驱动逻辑。

如果硬件资源发生了改变,我们只需要修改设备文件就行了,但是这样还不够好,如果有非常多的设备,就要写非常多的设备文件,这些文件非常庞大,导致Linux内核非常臃肿。

于是,为了解决这个问题,引入了设备树。设备树到底是什么呢?其实说白了就是硬件资源的集合,就是把所有的硬件设备挂在一棵“树”上面,每个硬件设备就是一个节点,这个节点里保存了硬件的相关信息。如果我们要添加设备,就往设备树里添加节点,如果要移除设备,就去掉那个节点就行了,这样变得非常方便。而驱动文件可以去设备树上获取资源,所以驱动文件和之前是差不多的。

也就是说之前的资源是用一个设备文件来保存,现在是全部放在设备树文件上,而驱动基本不变。那么接下来就具体讲一下设备树的相关内容。

了解几个概念:

设备树文件的格式是怎么样的?

在Linux内核里有设备树文件,路径是

代码语言:javascript
复制
源码目录/arch/arm/boot/dts/imx6ull-seeed-npi.dts

来看一下设备树文件的格式:

代码语言:javascript
复制
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文件里的内容,也可以改写里面的内容。

代码语言:javascript
复制
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"

2、设备树有且之后一个根节点。实际上.dts和.dtsi里都可以有根节点,但是它们最终会合并为一个。根节点没有名字,用“/”表示。

3、可以往已经存在的节点里追加内容。这时要加上一个符号“&”。比如要往soc节点追加内容,可以这样:

代码语言:javascript
复制
&soc{
/*具体追加内容*/
};

当然,也可以直接在原来的地方加上去就行了,只不过我们有时候不想去修改原文件。

接下来讲一下节点的属性和值。

每个节点里最重要的当然是节点的属性和值了,因为这里包含了要传递到内核的“板级硬件描述信息”,驱动中会通过一些API函数获取这些信息。

属性和值的写法有一定的规范。属性包括一些特殊的属性,还有一些我们自定义的属性。

属性的值通常有三种:

arrays of cells(1个或多个32位数据, 64位数据使用2个32位数据表示,u32)。

string(字符串)。

bytestring(1个或多个字节,u8)。

具体是怎么回事呢?来看几个例子:

第一种(u32):

代码语言:javascript
复制
interrupts = <17 0xc>;

interrupt是属性,后面用<>表示是arrays of cells,也就是说<>里面的每一个数都是32位的。如果要表示64位的数,那就要用两个32位的数来表示。其实里面随意有几个数,总之每个数是32位的,如何理解这些数取决于你自己在驱动文件中如何使用。

第二种(string):

代码语言:javascript
复制
compatible = "simple-bus";

这种就是字符串类型的,后面使用的是""。而且这种字符串是带结束符的,比如上面这个字符串就是占11个字节。

第三种(u8):

代码语言:javascript
复制
local-mac-address = [00 00 12 34 56 78];
local-mac-address = [000012345678];

这种后面跟着的是[]。它表示字节序列。每个byte使用2个16进制数来表示,比如00是一个字节,12也是一个字节。不管两个字节之间有没有空格,表示的含义是一样的,上面两种是等效的。

此外,也可以是各种值的组合, 用逗号隔开:

代码语言:javascript
复制
compatible = "ns16550", "ns8250";
example = <0xf00f0000 19>, "a strange property format";

这种写法估计比较少见。

以上讲的是属性的值的表示方法,其实无非就是“数值”和“字符串”,字符串就加"",数值使用第一种(使用<>)或第三种(使用[]),根据情况灵活使用即可。

接下来讲一下属性。属性我们需要了解一些特殊的属性,对于自定义的属性就随意了,所以讲一下几个特殊的属性。

1、compatible属性 属性值类型:字符串

设备树中的每一个代表了一个设备的节点都要有一个compatible属性。compatible是系统用来决定绑定到设备的设备驱动的关键。compatible属性是用来查找节点的方法之一,另外还可以通过节点名或节点路径查找指定节点。

代码语言:javascript
复制
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

2、model属性 属性值类型:字符串

model属性用于指定设备的制造商和型号,推荐使用“制造商, 型号”的格式,当然也可以自定义。

代码语言:javascript
复制
model = "Embedfire i.MX6ULL Board";

3、status属性 属性值类型:字符串

状态属性用于指示设备的“操作状态”,通过status可以去禁止设备或者启用设备,可用的操作状态如下表。默认情况下不设置status属性设备是使能的。

代码语言:javascript
复制
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、根据节点路径查找节点:

代码语言:javascript
复制
struct device_node *of_find_node_by_path(const char *path)

device_node结构体如下所示:

代码语言:javascript
复制
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、根据节点名字查找节点函数

代码语言:javascript
复制
struct device_node *of_find_node_by_name(struct device_node *from,const char *name);

c、根据节点类型查找节点

代码语言:javascript
复制
struct device_node *of_find_node_by_type(struct device_node *from,const char *type)

d、根据节点类型和compatible属性寻找节点

代码语言:javascript
复制
struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible)

e、根据匹配表查找节点

代码语言:javascript
复制
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)
代码语言:javascript
复制
struct of_device_id {
    char    name[32];
    char    type[32];
    char    compatible[128];
    const void *data;
};

f、寻找父节点

代码语言:javascript
复制
struct device_node *of_get_parent(const struct device_node *node)

g、寻找子节点

代码语言:javascript
复制
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)

2、获取属性值函数(内核源码/include/linux/of.h

a、查找节点属性

代码语言:javascript
复制
struct property *of_find_property(const struct device_node *np,const char *name,int *lenp)
代码语言:javascript
复制
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
};
  • name: 属性名
  • length: 属性长度
  • value: 属性值
  • next: 下一个属性

b、读取整型属性

代码语言:javascript
复制
//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,可以使用下面简化后的函数:

代码语言:javascript
复制
//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来获取,但是这样比较繁琐。内核提供了一组用于读取字符串属性的函数,介绍如下:

代码语言:javascript
复制
int of_property_read_string(const struct device_node *np,const char *propname,const char **out_string)

这个函数使用相对繁琐,推荐使用下面这个函数:

代码语言:javascript
复制
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函数,自动完成物理地址到虚拟地址的转换。介绍如下:

代码语言:javascript
复制
void __iomem *of_iomap(struct device_node *np, int index)

内核也提供了常规获取地址的of函数,这些函数得到的值就是我们在设备树中设置的地址值。介绍如下:

代码语言:javascript
复制
int of_address_to_resource(struct device_node *dev, int index, struct resource *r)

resource结构体如下所示:

代码语言:javascript
复制
struct resource {
    resource_size_t start;
    resource_size_t end;
    const char *name;
    unsigned long flags;
    unsigned long desc;
    struct resource *parent, *sibling, *child;
};
  • start: 起始地址
  • end: 结束地址
  • name: 属性名字

以上就是关于设备树的相关知识点,那么我们应该如何向设备树里添加节点呢?

第一步:打开内核源码里的设备树文件,这个文件在(以imx6ull为例)

代码语言:javascript
复制
源码目录/arch/arm/boot/dts/imx6ull-seeed-npi.dts

然后根据上面讲过的规则往里面添加节点即可

第二步,编译设备树

编译内核时会自动编译设备树,但是编译内核很耗时,所以我们推荐使用如下命令只编译设备树。

代码语言:javascript
复制
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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-02-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 电子技术研习社 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档