Linux中设备树的分析与实现

简介: Linux中设备树的分析与实现

第一:设备树简介

   设备树可以被bootloader(uboot)传递到内核,内核从中获取设备树中的硬件信息。

  1、设备树的两个特点:

  (1):以树状结构描述硬件资源。

  (2):设备树可以像头文件使用,一个设备树文件引用另外一个设备树文件。

   2、Linux中常用的几个缩写

 (1):DTS:是指.dts格式的文件,是一种ASCII文本格式的设备树描述,也是我们要编写的设备树资源,一般一个.dts文件对应一个硬件平台,位于Linux源码的/arch/arm64/boot/dts目录下。


      (2):DTC:是指编译设备树源码的工具,一般情况下,需要手动安装这个编译工具。


      (3):DTB:  是设备源码编译生成的文件。


    (4).dts :设备树源文件。


    (5).dtsi :设备树头文件。


       (6)  .dtb:设备树可执行文件

第二:设备树框架

      设备树是由一个根节点和多个子节点组成。

     1、设备树格式

(1)、DTS文件布局如下,第一行表示版本,第二行表示保留的内存区域,比如板子有64M内存,里面想留下4M给自己使用,不想让内核使用,就可以定义这个选项,如果想让内核使用全部内存就可以省略这个选项。

/dts-v1/;
[memory reservations]        /* 格式为:/memreserve/ <address> <length>; */
/{                           /* /表示根,设备树的起点 */
    [property definitions]   /* 首先有属性来描述硬件 */
    [child nodes]            /* 子节点,在子节点中还可以有子节点 */
};

2、node格式

    (1)、node为设备树中的基本单元。格式为:

[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
};

 label:是节点标签。可以省略,方便引用node。节点标签通常为基点名称的缩写,一般用于追加内容时使用。

   node-name:节点名称。长度为1-31个字符。可由0-9  a-z  A-Z , .  + -组成,且开头只能是大小写字母。

注意:根节点没有节点名,使用/来表示。

@unit-address:是单元地址。@ 为分隔符。

注意:节点中 reg 属性的第一个地址要和这个 单元地址 一致。

注意:如果节点中没有 reg 属性值,则可以省略该 单元地址,但此时必须保证同级别的子节点节点名唯一。反之,若同级别的子节点节点名相同,则单元地址要求不一样。就是说 node-name@unit-address 整体同级唯一。

3、properties格式

  就是name = value

   格式1:

[label:] property-name;
  • 格式2:(支持三种取值
  • arrays of cell:一个或多个 32 位数据,64 位数据使用 2 个 32 位数据表示。
  • string:字符串。
  • bytestring:一个或多个字符串。
[label:] property-name = value;
  • 例子1:64bit 用两个 cell 表示,使用 尖括号
clock-frequency = <0x00000001 0x00000000>;

  • 例子2:字符串,用 双引号
compatible = "lzm-bus";

  • 例子3:字节序列,用 中括号
local-mac-address = [00 00 12 34 56 78]; // 每个 byte 使用 2 个 16 进制数来表示
local-mac-address = [000012345678]; // 每个 byte 使用 2 个 16 进制数来表示
  • 例子4:各种组合,用 逗号 隔开。
example = <0x84218421 23>, "hello world";

例子1:64bit 用两个 cell 表示,使用 尖括号

clock-frequency = <0x00000001 0x00000000>;

  • 例子2:字符串,用 双引号
compatible = "lzm-bus";

  • 例子3:字节序列,用 中括号
local-mac-address = [00 00 12 34 56 78]; // 每个 byte 使用 2 个 16 进制数来表示
local-mac-address = [000012345678]; // 每个 byte 使用 2 个 16 进制数来表示
  • 例子4:各种组合,用 逗号 隔开。
example = <0x84218421 23>, "hello world";

  4、包含dtsi

   一般设备树都不要从零开始写,只需要包含芯片厂商提供的设备树模板,然后添加,修改即可。dts可以包含dtsi文件,也可以包含.h文件。.h文件可以定义一些宏。

/dts-v1/;
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"
/ {
……
};

第三:修改、追加设备树节点

   修改、追加设备树节点都可在文件末尾或新文件修改或追加。而修改节点,可参考以下两种方法:标签法或全路径法:

// 方法一:在根节点之外使用标签引用节点
&red_led
{
    status = "okay";
}
// 方法二:使用全路径引用节点
&{/led@0x020C406C}
{
    status = "okay";
}

全路径法:

追加节点,类似新建一个简易的设备树一样。包含根节点到需要新建节点的全路径。

第四:常用属性

   在节点{}中包含的内容时节点属性。这些属性信息就是板级硬件描述的信息,驱动会通过内核提供的API去获取这些资源信息。

注意:节点属性可分为标准属性和自定义属性,即是可以自行添加属性。

1、常用标准属性


compatible属性:


(1)、属性值类型:字符串。双引号。


(2)、compatible表示兼容。


(3)、每一个代表设备的节点都必须有一个compatible属性值。


(4)、由一个或多个字符串组成,使用“,”分隔即可。


(5)、如:compatible= “A”,“B”,“C”;内核启动时,会按顺序A,B,C找到对应的驱动程序,与驱动中of_match_table中的值进行匹配,然后加载对应的驱动。


(6)、compatible是查找节点的方法之一,还可以通过节点名或节点路径找到指定节点。                           compatible建议格式:“manufacture,model”,即是“厂家名,模块名”


  2、model属性:


  (1)、属性值类型:字符串。双引号。


  (2)、model定义硬件是什么。


  (3)、推荐指定设备的制造商和型号,推荐格式“制造商,型号”。


  (4)、如:model = “lzm,IMX”;


     3、status 属性


      (1)、属性值类型:字符串。双引号。


      (2)、status 表示当前设备节点状态,用于禁止和启动设备。


      (3)、有如下值可选:


     #address-cells、#size-cells属性:


       (1)、属性值类型:u32。尖括号


       (2)、#address-cells、#size-cells是同时出现的。


       (3)、#address-cells:表示address要用多少个32位数来表示。


       (4)、#size-cells:表示size要用多少个32位数来表示。


       (5)、用于设置子节点reg、ranges等地址相关属性的书写格式。


      4、reg属性


           (1)、属性值类型:地址、长度数据对。尖括号。


           (2)、reg就是register,用于表示寄存器地址。


           (3)、用于描述一段内存空间。

/dts-v1/;
/ {
    #address-cells = <1>;
    #size-cells = <1>;
    memory {
        reg = <0x80000000 0x20000000>;
    };
};

5、ranges属性

(1)、属性值类型:任意数量的<子地址、父地址、地址长度>编码。尖括号。

(2)、该属性提供了子节点地址空间和父地址空间的映射(转换)方法。

(3)、如:ranges= <0x05 0x10 0x20>

第五:常用节点

(1)、根节点

   dts文件中必须有一个根节点。

  • 根节点 必须有以下属性:
  • #address-cells
  • #size-cells
  • compatible:定义一些列的字符串,用于指定内核中哪个 machine_desc 可以支持本设备。即是兼容性。
  • model:表示本硬件型号。

(2)、CPU

     一般都在 dtsi 文件中定义好了,不需要我们设置。

(3)、memory

     这个是表示板子内存大小,一般由开发板开发者定义的。

(4)、chosen

     该节点主要作用于向内核传递参数。如:

chosen
{
    bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};

(5)、aliases

     aliases 是为了给其它节点起个别名。如:

aliases {
    can0 = &flexcan1;
    gpio0 = &gpio1;
}
  • can0 就是 flexcan1 的别名。

第六:编译、更换设备树

一般的程序猿会修改设备树即可,不必从零开始。


(1)、在内核中编译设备树(推荐)


编译时需要设置一下三个环境变量 ARCH、CROSS_COMPILE、PATH。


在开发环境中进入板子对应的内核源码目录,使用内核中的 makefile 即可,执行如下命令来编译 dtb 文件:

make dtbs V=1

上述命令是单独编译设备树。

会编译以下设备树:

在arch/arm/Makefile 或 arch/arm/boot/Makefile 或 arch/arm/boot/dts/Makefile 等相关 Makefile 中找到 dtb-$(xxx) ,该值包含的就是要编译的 dtb 。

如该文件中宏 dtb-$(CONFIG_SOC_XXX) 包含的 .dtb 就会被编译出来。

如果想编译自己的设备树,添加该值内容,并把自己的设备树放在 arch/arm/boot/dts 下即可。

(具体查看该 arch/arm/boot/Makefile 内容)


(2)、人工编译(不推荐)


意思是手工使用 dtc 工具直接编译。


dtc 工具存放于内核目录scripts/dtc下。

若直接使用 dtc 工具手工编译的话,包含其它文件时不能使用 #include,而必须使用 /include。


因为内核中 make dtb 时能使用 #include 是因为使用了 交叉编译链。


编译、反编译的示例命令如下,-I 指定输入格式,-O 指定输出格式,-o 指定输出文件:

./scripts/dtc/dtc -I dts -O dtb -o tmp.dtb arch/arm/boot/dts/xxx.dts  // 编译 dts 为 dtb
./scripts/dtc/dtc -I dtb -O dts -o tmp.dts arch/arm/boot/dts/xxx.dtb  // 反编译 dtb 为 dts

(3)、更换设备树

一般步骤:

  • 确保好三个环境变量。
  • 在内核源码目录中执行 make dtbs
  • 生成的设备树文件一般保存在内核目录 arch/arm/boot/dts/ 下。
  • 把生成的设备树文件替换到板子上。开发板使用的设备树一般放在 /boot/ 目录下。
  • 若需要自定义新的设备树文件名称,则修改 /boot/ 目录下的 uEnv.txt 文件内容。


(4)、查看设备树

目录 /sys/firmware/devicetree 下以目录结构呈现的 dtb 文件。

  • 根节点对应 base 目录。
  • 每一个节点对应一个目录。
  • 每一个属性对应一个文件。
  • 若属性值为字符串,则可以使用 cat 命令打印出来。
  • 若属性值为数值,则可以使用 hexdump 命令打印出来。
  • 目录 /sys/firmware/fdt 文件,就是 dtb 格式的设备树文件。
  • 可以将其赋值出来,反编译。

第七 内核处理设备树

(1)、设备树过程

设备树生命过程

DTS --(PC)--> DTB --(内核)--> device_node -·(内核)·-> platform_device

流程

  1. dts 在 PC 机上被编译为 dtb 文件。
  2. u-bootdtb 文件传给内核。
  3. 内核解析 dtb 文件,把每一个节点都转换为 device_node 结构体。
  4. 对于某些 device_node 结构体,会被转换为 platform_device 结构体。

对于 device_node 和 platform_device,建议去内核源码看看它们的成员。


(2)、转换为 platform_device 的条件


      根节点下有 compatile 属性的子节点。


      含有特定 compatile 属性的节点的子节点。

如果一个节点的 compatile 属性是以下 4 个值之一,那么该节点含有 compatile 属性的 子节点也可以转换为 platform_device

  • arm,amba-bus
  • isa;
  • simple-mfd;
  • simple-bus;
  • 总线 I2C、SPI 节点下的子节点 不转换platform_device
  • 某个总线下的子节点,不应该被转换为 platform_device。而应该交给对应的总线驱动来处理。

第八:获取节点函数

      在驱动程序中,内核加载设备树后。可以通过以下函数获取到设备树节点中的资源信息。获取节点函数及获取节点内容函数称为 of 函数。

(1)、重要结构体内容

device_node

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
};
  • name:节点中的 name 属性值。
  • type:节点中的 device_type 属性值。
  • full_name:节点的名字。
  • properties:链表,连接该节点的所有属性。
  • parent:指向父节点。
  • child:指向子节点。
  • sibling:指向兄弟节点。

of_device_id

of_device_id 结构体如下:

/* Struct used for matching a device  */
struct of_device_id
{
    char    name[32];
    char    type[32];
    char    compatible[128];
    const void *data;
};

  • name:节点中属性为 name 的值。
  • type:节点中属性为 device_type 的值。
  • compatible:节点的名字,在 device_node 结构体后面放一个字符串,full_name 指向它。
  • data:链表,连接该节点的所有属性。

(2)、据节点路径寻找节点

of_find_node_by_path()

  • 函数原型:struct device_node *of_find_node_by_path(const char *path)
  • 源码路径:内核源码/include/linux/of.h
  • path:节点在设备树中的路径。
  • 返回值:
  • 成功:返回 device_node 结构体指针。
  • 失败:NULL。

(3)、据节点类型寻找节点

of_find_node_by_type()

  • 函数原型:struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
  • 源码路径:内核源码/include/linux/of.h。
  • from:指定从哪里开始找(不包含本身),若要从根节点开始找,且包含根节点,则该值未 NULL。type: 要查找节点的类型,这个类型是 device_node->type。
  • 返回值:
  • 成功:返回 device_node 结构体指针。
  • 失败:NULL。


(5)、据匹配表寻找节点


of_find_matching_node_and_match()

函数原型:struct inline device_node *of_find_matching_node_and_match(struct device_node *from, const struct of_device_id *matches, const struct of_device_id **match)。源码路径:内核源码/include/linux/of.h。

  • from:指定从哪里开始找(不包含本身),若要从根节点开始找,且包含根节点,则该值未 NULL
  • matchesof_device_id 匹配表,也就是在此匹配表里面查找节点。
  • match:找到的匹配的 of_device_id
  • 返回值:
  • 成功:返回 device_node 结构体指针。
  • 失败:NULL。


(6)、寻找父节点


of_get_parent()

  • 函数原型:struct device_node *of_get_parent(const struct device_node *node)
  • 源码路径:内核源码/include/linux/of.h
  • node:需要查找要查找父节点的节点。
  • 返回值:
  • 成功:返回 device_node 结构体指针。
  • 失败:NULL。

(7)、寻找子节点

of_get_child()

  • 函数原型:struct device_node *of_get_child(const struct device_node *node, struct device_node *prev)。
  • 源码路径:内核源码/include/linux/of.h。
  • node:需要查找要查找子节点的节点。prev:需要寻找的节点的前一个节点,即是本函数需要寻找 prev 节点的后一个节点。
  • 返回值:
  • 成功:返回 device_node 结构体指针。
  • 失败:NULL。

第九:提取节点中的属性值

(1)、重要结构体内容

property 结构体

property

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:属性名。
  • lenght:属性长度。
  • value:属性值。
  • next:下一个属性。

(2)、resource 结构体

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;
};

(3)、查找节点属性值

of_find_property()

  • 函数原型:struct property *of_find_property(const struct device_node *np,const char *name,int *lenp)
  • 源码路径:内核源码/include/linux/of.h
  • np:设备节点。
  • name:属性名称。
  • lenp:实际获得属性值的长度(函数输出参数)。
  • 返回值:
  • 成功:返回 property 结构体,获取得到的属性。
  • 失败:返回 NULL。
  • 可以了解下 获取属性值函数 of_get_property() ,与 of_find_property() 的区别是一个返回属性值,一个返回属性结构体。

(4)、获取整型属性

of_property_read_u8_array

  • 以下函数分别读取 8、16、32、64 位数据:
//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)
  • 源码路径:内核源码/include/linux/of.h
  • np:指定设备节点。
  • propname:哪个属性。
  • out_values:保存读取到的数据(函数输出参数)。
  • sz:设置读取的长度。

返回值:

  • 成功:0.
  • 失败:非零值
  • -EINVAL:属性不存在。
  • -ENODATA:没有要读取的数据。
  • -EOVERFLOW:属性值列表太小。


(5)、简化后的读取整型属性函数

of_property_read_u8

  • 其读取长度为 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)

(6)、读取字符串属性

of_property_read_string_index:(推荐

  • 函数原型:int of_property_read_string_index(const struct device_node *np,const char *propname, int index, const char **out_string)
    源码路径:内核源码/include/linux/of.h
  • np:指定设备节点。
  • propname:哪个属性。index:指定要读取该属性值得第几个字符串。index 从 0 开始。
  • out_string:获取到的字符串的指针(函数输出参数)。
  • 返回:
  • 成功:0;
  • 失败:失败错误码。
  • of_property_read_string:(不推荐
  • 函数原型:int of_property_read_string(const struct device_node *np,const char *propname,const char **out_string)
  • 源码路径:内核源码/include/linux/of.h。参数同上。
    (7)、读取 bool 型属性函数

of_property_read_bool()

  • 函数原型:static inline bool of_property_read_bool(const struct device_node *np, const char *propname)
    np:设备节点。
  • propname:属性名称。
  • 返回值:只返回该属性存不存在。
  • 若要读取该属性值,需要用到函数 of_find_property

(8)、内存映射相关 of 函数

设备树提供寄存器的地址段,但是一般情况下都会使用 ioremap 映射为虚拟地址使用。

of_address_to_resource 只是获取 reg 的值,也就是寄存器值。

of_iomap 函数就是获取 reg 属性值&指定哪一段内存&映射为虚拟地址。

of_address_to_resource

  • 函数原型:int of_address_to_resource(struct device_node *dev, int index, struct resource *r)
  • 源码路径:内核源码/drivers/of/address.c
  • np:设备节点。
  • index:指定映射那一段内存。通常情况下,reg 属性包含多段。标号从 0 开始。
  • rresource 结构体,得到的地址信息(函数输出参数)。
  • 返回:
  • 成功:0;
  • 失败:失败错误码。

of_iomap

  • 函数原型:void __iomem *of_iomap(struct device_node *np, int index)
  • 源码路径:内核源码/include/linux/of.h
  • np:设备节点。
  • index:指定映射那一段内存。通常情况下,reg 属性包含多段。标号从 0 开始。
  • 返回:
  • 成功:转换后的地址。
  • 失败:NULL。
目录
相关文章
|
16天前
|
Linux 编译器 开发者
Linux设备树解析:桥接硬件与操作系统的关键架构
在探索Linux的庞大和复杂世界时🌌,我们经常会遇到许多关键概念和工具🛠️,它们使得Linux成为了一个强大和灵活的操作系统💪。其中,"设备树"(Device Tree)是一个不可或缺的部分🌲,尤其是在嵌入式系统🖥️和多平台硬件支持方面🔌。让我们深入了解Linux设备树是什么,它的起源,以及为什么Linux需要它🌳。
Linux设备树解析:桥接硬件与操作系统的关键架构
|
1月前
|
Linux Android开发
嵌入式linux中Framebuffer 驱动程序框架分析
嵌入式linux中Framebuffer 驱动程序框架分析
27 0
|
1月前
|
Linux C语言 SoC
嵌入式linux总线设备驱动模型分析
嵌入式linux总线设备驱动模型分析
32 1
|
1月前
|
监控 Shell Linux
【Shell 命令集合 网络通讯 】Linux 分析串口的状态 statserial命令 使用指南
【Shell 命令集合 网络通讯 】Linux 分析串口的状态 statserial命令 使用指南
33 0
|
22天前
|
Prometheus 监控 数据可视化
linux分析方法与技巧
【4月更文挑战第3天】在Linux环境中,进行日志分析和系统性能分析的关键方法包括:使用`cat`, `less`, `tail`查看和过滤日志,`logrotate`管理日志文件,`rsyslog`或`syslog-ng`聚合日志,以及通过`top`, `mpstat`, `pidstat`, `free`, `iostat`, `netstat`, `strace`, `sar`, `dstat`等工具监控CPU、内存、磁盘I/O和网络。对于高级分析,可利用Brendan Gregg的性能工具,以及Grafana、Prometheus等可视化工具。
17 2
linux分析方法与技巧
|
29天前
|
监控 Linux Shell
Linux 进程问题调查探秘:分析和排查频繁创建进程问题
Linux 进程问题调查探秘:分析和排查频繁创建进程问题
39 0
|
29天前
|
消息中间件 存储 网络协议
Linux IPC 进程间通讯方式的深入对比与分析和权衡
Linux IPC 进程间通讯方式的深入对比与分析和权衡
69 0
|
29天前
|
存储 监控 Linux
Linux 使用getrusage系统调用获取cpu信息:一个C++实例分析
Linux 使用getrusage系统调用获取cpu信息:一个C++实例分析
49 0
|
29天前
|
存储 算法 Linux
【Linux 系统标准 进程资源】Linux 创建一个最基本的进程所需的资源分析,以及线程资源与之的差异
【Linux 系统标准 进程资源】Linux 创建一个最基本的进程所需的资源分析,以及线程资源与之的差异
25 0
|
1月前
|
Linux 编译器 测试技术
探索Linux设备树:硬件描述与驱动程序的桥梁
探索Linux设备树:硬件描述与驱动程序的桥梁
64 0