Lazy loaded image
Linux驱动基础06-设备树引入和使用
Words 6427Read Time 17 min
2024-9-11
2025-4-9
type
date
slug
category
icon
password
 
我们可以通过开发板测试,快速体验设备树。
/sys/firmware/devicetree 目录下是以目录结构程现的 dtb 文件
  • 根节点对应base 目录
  • 每一个节点对应一个目录
  • 每一个属性对应一个文件
属性的值如果是字符串,可以使用 cat 命令把它打印出来;对于数值,可以用 hexdump 把它打印出来
一个单板启动时,u-boot 先运行,它的作用是启动内核。U-boot 会把内核和设备树文件都读入内存,然后启动内核。在启动内核时会把设备树在内存中的地址告诉内核。

一、设备树语法

设备树dts文件在如下路径
从100ask_imx6ull-14x14.dts截取代码片段解释设备树语法。

格式-node 的格式

  • label 是标号,可以省略。作用是方便引用node
    • 可以通过2种方法修改 uart@fe001000这个node:

格式-properties 的格式

  1. 格式1 [label:] property-name = value;
    1. arrays of cells(1 个或多个 32 位数据, 64 位数据使用 2 个 32 位数据表示);<0x00000001 0x00000000>
    2. string(字符串); "simple-bus"
    3. bytestring(1 个或多个字节)。 [00 00 12 34 56 78]; [000012345678];
    4. 上面三种组合
      1. compatible = "ns16550", "ns8250";
        example = <0xf00f0000 19>, "a strange property format";
  1. 格式2 [label:] property-name;

格式-dts 文件包含 dtsi 文件

内核的 arch/arm/boot/dts/xxxx.dtsi 设备树模板
  1. dtsi 的 ”i“ 表示“include”,被dts文件引用;
  1. 自己单板,少部分不同需要修改 xxxx.dtsi 对应资源;
  1. dtsi 文件跟 dts 文件的语法是完全一样的;
  1. dts 中可以包含.h 头文件,也可以包含 dtsi 文件。

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

  • address-cells:address 要用多少个 32 位数来表示(cells 指一个32 位的数值);
  • size-cells:size 要用多少个 32 位数来表示。
如上一段内存,一个数表示地址,一个数表示大小。即<addr size>

属性-兼容 compatible

对于某个 LED,内核中可能有 A、B、C 三个驱动都支持它,那可以这样写:
  • 按优先顺序为它找到驱动程序
  • 根节点下也有 compatible 属性,用来选择machine desc,形式:"manufacturer,model",即“厂家名,模块名”。

属性-model

model 用来准确地定义这个硬件是什么。
比如根节点中可以这样写:
它表示这个单板,可以兼容内核中的“smdk2440”,也兼容“mini2440”。
从 compatible 属性中可以知道它兼容哪些板,但是它到底是什么板?用model 属性来明确。

属性-status

dtsi 文件中定义了很多设备,但是在你的板子上某些设备是没有的。
notion image

属性-寄存器地址 reg

reg 属性的值,是一系列的“address size”,用多少个 32 位的数来表示address 和 size,由其父节点的#address-cells、#size-cells 决定。

常用节点(node) - 根节点

常用节点(node) - CPU 节点

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

常用节点(node) - memory 节点

芯片厂家不可能事先确定你的板子使用多大的内存,所以 memory 节点需要板厂设置,比如:

常用节点(node) - chosen 节点

我们可以通过设备树文件给内核传入一些参数,这要在 chosen 节点中设置bootargs 属性:

二、编译、更换设备树

2.1 内核直接make

2.2 手动编译

2.3 给开发板更换设备树文件

编译的dtb文件在内核源码路径
通过nfs 方式拷贝到如下路径替换
重启设备

2.4 启动后查看设备树

在引言部分,我们可以查看节点属性
可以到将/sys/firmware/fdt 文件(就是 dtb 格式的设备树文件),复制出来放到 ubuntu 上,执行下面的命令反编译出来(-I dtb:输入格式是 dtb,-O dts:输出格式是 dts):

三、内核对设备树处理

从源代码文件 dts 文件开始,设备树的处理过程为:
notion image
  1. dts 在 PC 机上被编译为 dtb 文件;
  1. u-boot 把 dtb 文件传给内核;
  1. 内核解析 dtb 文件,把每一个节点都转换为 device_node 结构体;
  1. 对于某些 device_node 结构体,会被转换为 platform_device 结构体。

节点转换 device_node

 

节点转换 platform_device

  1. 哪些节点
  1. 怎么转换

四、内核里操作设备树的常用函数

 
从设备树定义的设备节点获取我们想要的数据。内核提供了一组函数用于从设备节点获取资源(设备节点中定义的属性)函数,这些函数以of_开头,称为OF操作函数。常用的OF函数介绍如下:

4.1 查找节点函数

从设备树中的设备节点“获取”到驱动。

4.1.1 根据节点路径寻找节点

参数:
  • path: 指定节点在设备树中的路径。
返回值:
  • device_node: 结构体指针,如果查找失败则返回NULL,否则返回device_node类型的结构体指针,它保存着设备节点的信息。
device_node结构体如下所示。
  • name: 节点中属性为name的值
  • type: 节点中属性为device_type的值
  • full_name: 节点的名字,在device_node结构体后面放一个字符串,full_name指向它
  • properties: 链表,连接该节点的所有属性
  • parent: 指向父节点
  • child: 指向子节点
  • sibling: 指向兄弟节点
  • 得到device_node结构体之后我们就可以使用其他of 函数获取节点的详细信息。

4.1.2 根据节点名字寻找节点

参数:
  • from: 指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找。
  • name: 要寻找的节点名。
返回值:
  • device_node: 结构体指针,如果查找失败则返回NULL,否则返回device_node类型的结构体指针,它保存着设备节点的信息。

4.1.3 根据节点类型寻找节点

参数:
  • from: 指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找。
  • type: 要查找节点的类型,这个类型就是device_node-> type。
返回值:
  • device_node: device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。

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

参数:
  • from: 指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找。
  • type: 要查找节点的类型,这个类型就是device_node-> type。
  • compatible: 要查找节点的compatible属性。
返回值:
  • device_node: device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。
 

4.1.5 根据匹配表寻找节点

可以看到,该结构体包含了更多的匹配参数,也就是说相比前三个寻找节点函数,这个函数匹配的参数更多,对节点的筛选更细。参数match,查找得到的结果。
参数:
  • from: 指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找。
  • matches: 源匹配表,查找与该匹配表想匹配的设备节点。
  • of_device_id: 结构体如下。
返回值:
  • device_node: device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。
 
  • name: 节点中属性为name的值
  • type: 节点中属性为device_type的值
  • compatible: 节点的名字,在device_node结构体后面放一个字符串,full_name指向它
  • data: 链表,连接该节点的所有属性

4.1.6 寻找父/子节点

  • node: 指定谁(节点)要查找它的父/子节点。
  • prev: 前一个子节点,寻找的是prev节点之后的节点。这是一个迭代寻找过程,例如寻找第二个子节点,这里就要填第一个子节点。参数为NULL 表示寻找第一个子节点。
这里介绍了7个寻找节点函数,这7个函数有一个共同特点——返回值类型相同。只要找到了节点就会返回节点对应的device_node结构体,在驱动程序中我们就是通过这个device_node获取设备节点的属性信息、顺藤摸瓜查找它的父、子节点等等。第一函数of_find_node_by_path与后面六个不 同,它是通过节点路径寻找节点的,“节点路径”是从设备树源文件(.dts)中的到的。而中间四个函数是根据节点属性在某一个节点之后查找符合要求的设备节点,这个“某一个节点”是设备节点结构体(device_node),也就是说这个节点是已经找到的。最后两个函数与中间四个类似,只不过最后两个没有使用节点属性 而是根据父、子关系查找。

4.2 提取属性值的of函数

“获取”成功后我们再通过一组of函数从设备节点结构体(device_node)中获取我们想要的设备节点属性信息。

4.2.1 查找节点属性函数

参数:
  • np: 指定要获取那个设备节点的属性信息。
  • name: 属性名。
  • lenp: 获取得到的属性值的大小,这个指针作为输出参数,这个参数“带回”的值是实际获取得到的属性大小。
返回值:
  • property: 获取得到的属性。property结构体,我们把它称为节点属性结构体,如下所示。失败返回NULL。从这个结构体中我们就可以得到想要的属性值了。
 
  • name: 属性名
  • length: 属性长度
  • value: 属性值
  • next: 下一个属性

4.2.2 读取整形属性函数

读取属性函数是一组函数,分别为读取8、16、32、64位数据。
参数:
  • np: 指定要读取那个设备节点结构体,也就是说读取那个设备节点的数据。
  • propname: 指定要获取设备节点的哪个属性。
  • out_values: 这是一个输出参数,是函数的“返回值”,保存读取得到的数据。
  • sz: 这是一个输入参数,它用于设置读取的长度。
返回值:
  • 返回值,成功返回0,错误返回错误状态码(非零值),-EINVAL(属性不存在),-ENODATA(没有要读取的数据),-EOVERFLOW(属性值列表太小)。

4.2.3  简化后的读取整型属性函数

这里的函数是对读取整型属性函数的简单封装,将读取长度设置为1。用法与读取属性函数完全一致,这里不再赘述。

4.2.4 读取字符串属性函数

在设备节点中存在很多字符串属性,例如compatible、status、type等等,这些属性可以使用查找节点属性函数of_find_property来获取,但是这样比较繁琐。内核提供了一组用于读取字符串属性的函数,介绍如下:
参数:
  • np: 指定要获取那个设备节点的属性信息。
  • propname: 属性名。
  • out_string: 获取得到字符串指针,这是一个“输出”参数,带回一个字符串指针。也就是字符串属性值的首地址。这个地址是“属性值”在内存中的真实位置,也就是说我们可以通过对地址操作获取整个字符串属性(一个字符串属性可能包含多个字符串,这些字符串在内存中连续存储,使用’0’分隔)。
返回值:
  • 返回值:成功返回0,失败返回错误状态码。
这个函数使用相对繁琐,推荐使用下面这个函数。
int of_property_read_string_index(const struct device_node \*np,const char \*propname, int index,const char \**out_string)
相比前面的函数增加了参数index,它用于指定读取属性值中第几个字符串,index从零开始计数。 第一个函数只能得到属性值所在地址,也就是第一个字符串的地址,其他字符串需要我们手动修改移动地址,非常麻烦,推荐使用第二个函数。

4.2.5 读取布尔型属性函数

在设备节点中一些属性是BOOL型,当然内核会提供读取BOOL型属性的函数,介绍如下:
of_property_read_string_index函数 (内核源码/include/linux/of.h)
static inline bool of_property_read_bool(const struct device_node \*np, const char \*propname):
参数:
  • np: 指定要获取那个设备节点的属性信息。
  • propname: 属性名。
返回值:
这个函数不按套路出牌,它不是读取某个布尔型属性的值,仅仅是读取这个属性存在或者不存在。如果想要或取值,可以使用之前讲解的“全能”函数查找节点属性函数of_find_property。

4.3 内存映射相关的of函数

在设备树的设备节点中大多会包含一些内存相关的属性,比如常用的reg属性。通常情况下,得到寄存器地址之后我们还要通过ioremap函数将物理地址转化为虚拟地址。现在内核提供了of函数,自动完成物理地址到虚拟地址的转换。介绍如下:
参数:
  • np: 指定要获取那个设备节点的属性信息。
  • index: 通常情况下reg属性包含多段,index 用于指定映射那一段,标号从0开始。
返回值:
  • 成功,得到转换得到的地址。失败返回NULL。
 
内核也提供了常规获取地址的of函数,这些函数得到的值就是我们在设备树中设置的地址值。介绍如下:
参数:
  • np: 指定要获取那个设备节点的属性信息。
  • index: 通常情况下reg属性包含多段,index 用于指定映射那一段,标号从0开始。
  • r: 这是一个resource结构体,是“输出参数”用于返回得到的地址信息。
返回值:
  • 成功返回0,失败返回错误状态码。
 
resource结构体如下所示:
  • start: 起始地址
  • end: 结束地址
  • name: 属性名字

五、实验

5.1 修改设备树添加led设备节点

  1. 驱动要求设备树提供什么,设备树就得添加什么
    1. 比如驱动程序中根据 pin 属性来确定引脚,那么我们就在设备树节点中添加pin 属性。
      设备树节点中:
  1. 驱动要和设备树匹配过程
    1. 设备树要有compatible属性值,它的值是一个字符串;
    2. platform_driver 中要有 of_match_table,其中一项的.compatible 成员设置为一个字符串
    3. 以上两个字符要一致
    4. notion image
  1. 平台驱动从设备树节点获取属性
    1. 设备树 reg 属性,IORESOURCE_MEM 类型
    2. 设备树 interrupts 属性,IORESOURCE_IRQ 类型
    3. 驱动程序中,可以从 platform_device 中得到 device_node,再用of_property_read_u32 得到属性的值:
 
综上,对百问网 imx6ull Pro 板,设备树文件是:内核源码目录中 arch/arm/boot/dts/100ask_imx6ull-14x14.dts在dts文件根节点添加如下内容:

5.2 修改 platform_driver 源码

  1. 更改probe获取资源方式,总线平台设备是通过提供接口提供,对于支持设备树的内核。通过提供的OF函数,从设备节点获取属性。
  1. 指定 of_match_table 属性,用来跟设备树节点匹配的,如果设备树节点中有 compatile 属性,并且值匹配,则调用probe函数。

5.3 上机实验

  1. 编译后得到 arch/arm/boot/dts/100ask_imx6ull-14x14.dtb 文件。参照二、编译、更换设备树 编译更换。
  1. 使用新的设备树 dtb 文件启动单板,查看/sys/firmware/devicetree/base 下有无节点
    1. 查看/sys/devices/platform 目录下有无对应的 platform_device。测试没有设备,设备在 /sys/devices/soc0 路径下
      4. 加载驱动
      1. 测试驱动

        六、调试方法

        6.1 设备树信息

        以下目录对应设备树的根节点,可以从此进去找到自己定义的节点。
        节点是目录,属性是文件
        属性值是字符串时,用 cat 命令可以打印出来;
        属性值是数值时,用 hexdump命令可以打印出来。

        6.2 platform_device 信息

        以下目录含有注册进内核的所有 platform_device:
        一个设备对应一个目录,进入某个目录后,如果它有“driver”子目录,就表示这个 platform_device 跟某个 platform_driver 配对了。
        如下,检查felix_led@0 设备,发现下面有driver子目录,指向bus/platform/drivers/felix_led驱动

        6.3 platform_driver 信息

        以下目录含有注册进内核的所有 platform_driver:
        一个 driver 对应一个目录,进入某个目录后,如果它有配对的设备,可以直接看到。
        如下felix_led 驱动匹配了两个平台设备, felix_led@0和 felix_led@1。
        注意:一个平台设备只能配对一个平台驱动,一个平台驱动可以配对多个平台设备。
        上一篇
        Linux驱动基础05-平台设备驱动
        下一篇
        Linux驱动基础07-GPIO和Pinctrl

        Comments
        Loading...