引言
在上一篇文章《深入探究 Linux 总线-设备-驱动模型》中,我分别介绍了总线、设备和驱动这三个部分,并对相应的内核源码进行了分析,还描述了他们之间的协同工作关系,说白了,其实这三个部分是互成掎角之势的。
如下图,左边是驱动,就是我们自己写的逻辑代码,告诉内核操作这个设备的方法。
右边是设备,它是用来描述硬件资源的一种数据结构,负责告诉内核这是哪种类型的设备,地址在哪,中断号是多少,但有一点要注意,这里的设备指的并不是我们日常生活中的使用的硬件设备,而是内核中的 struct device 结构体。
中间是总线,用来匹配驱动和设备的,这里的匹配过程是有优先级的(上一篇文章的1.2节讲过),但这并不是现在的重点,只需要知道总线通过比较二者一个特殊的字符串是否相同来决定他们能否匹配成功,这个特殊的字符串到底是什么取决于优先级。

驱动代码是咱们自己写的,咱们当然熟了,但是那个跟驱动配对的 struct device,如果只是单纯的去写一个驱动,而不深入底层去探究原理,其实你会发现对这个结构体并没有什么印象,那么问题就来了,它到底是怎么进入内核并与驱动进行配对的呢?
如果在用单片机进行裸机编程,这压根就不能称得上是个问题,因为代码和硬件是焊死的,想用哪个外设直接在代码里面写死就行,也就是说代码中既包含了驱动逻辑,也包含了硬件地址。
但在 Linux 中,为了保证驱动的通用性,对于在代码中出现具体硬件地址的行为是极度反感的。
既然不能写在驱动里面,那这些硬件信息到底应该写在哪,他又是怎么变成内核里面的 struct device 的?
这就是我们这篇文章要讲的主题------设备树。
1. 为什么要引入设备树
在谈及为什么要引入设备树这个问题时,往往是与当时面临的窘迫环境脱不开关系的。
1.1 当时的环境
从 2000 年代末期,ARM 架构就迎来了井喷式的发展速度,但是相比 X86 架构的标准化 ,ARM却呈现出了截然不同的状态:
那时候,各家 SOC 厂商百家争鸣,像高通,三星,TI,NVIDIA,Rockchip等等,每家都推出自己的 ARM 芯片,而每款 SOC 又会衍生出无数的开发板和终端设备,并且就算是同一款 SOC 也可能有着不同的外设配置(GPIO,UART等引脚复用不同)。
但是呢,Linux 又想统一支持这些所有的设备,从而成为嵌入式系统的首选。但是实际上,那时的 ARM 设备基本都是靠硬编码来告诉内核硬件长啥样的,而不像 x86 那样有着标准引导和硬件发现机制。
这就导致了 Linux 内核的 arch/arm/ 目录下,代码量急速膨胀。为了支持一块新的板子,开发者就必须要往内核源码中添加这个板子的专属文件。
1.2 board file 的缺陷
早期 ARM Linux 采用的是board file ,也就是板级支持文件模式 。具体来说,就是每个设备板子都需要一个独立的board-xxx.c文件放在arch/arm/mach-xxx/目录下。
这些文件中硬编码了硬件的所有信息,比如内存映射,时钟配置,GPIO 引脚复用等等。
举个例子,要想支持一个新的开发板,就要写一个board-xxxxx.c,里面用 C 代码手动struct platform_device一个一个注册 UART、I2C 等。
这种模式在早期还能凑合,但在 2010 年左右,问题就彻底暴露出来了:
内核里有数千个 board file,arch/arm/目录下文件数量激增,整个 ARM 子架构代码占了内核的很大比例,却大多是重复的没有营养的代码。
并且不同厂商的代码互相独立,修改一个通用驱动时,可能要同步改几十上百个 board file ,合并 pull request 时,冲突也十分频繁。
除此之外,一个编译好的内核只能支持一个或少数几个特定的板子,如果想支持多个板子,要么就维护多个内核镜像,要么只能在配置里搞上一大堆的#ifdef。
更要紧的是,每个 SOC 厂商都自己维护分支,上游合并慢,导致 Linux 主线内核支持的新硬件严重滞后。理想中一个 ARM 内核镜像应该能跑在多种 SOC 上,像 x86 就能一个内核跑所有 PC ,但 ARM 的 board file 模式根本无法做到。这已经严重违背了创始人 Linus Torvalds 的理念。
Linus 看到内核正被这些重复的 board file 拖累,维护成本越来越高,如果不改,ARM 支持会越来越乱,主线内核可能被厂商分支彻底抛弃。
在 2011 年,他在回复一个 OMAP 平台的 pull request 时,说到:"Gaah. Guys, this whole ARM thing is a fucking pain in the ass."。他批评了 ARM 社区的碎片化,即不同厂商的代码互相冲突,合并时总是出问题,要求大家推动根本性变革。这封邮件成为了转折点,直接点燃了 ARM Linux 社区的改革热情。
1.3 设备树的引入
从那之后,社区开始认真寻求出路,最终灵感来自于 PowerPC 架构的 Open Firmware (Device Tree) 机制。
核心想法是把硬件描述从 C 源码中剥离出来,用一种独立的数据文件来描述硬件,并且由 bootloader 传入内核,让内核在运行时动态解析这些数据,而不是在编译时硬编码。
用 .dts (Device Tree Source,人类可以阅读的文本文件)文件描述硬件树状结构,编译成 .dtb (Device Tree Blob,二进制文件)文件传给内核。
使用设备树带来的好处是显而易见的,首先是可以删除掉成千上万行的重复代码,其次是只需要更换 dtb 文件就可以使用一个内核镜像就能支持多种硬件,除此之外,硬件描述的易读性和可维护性也得到了提升,而厂商也只需要提供 dts 和 dtb 文件,这对于上游合并来讲是更加容易的。
2. 初识设备树
设备树是用来描述硬件的,它也有一套自己的语法规则,但其实并不是很难,本质上可以理解为就是一棵由节点 和属性 构成的树,这也是它的名字设备树的来源。
2.1 三个 D
我们首先要搞清楚经常出现的三个缩写。
第一个是 DTS :这就是我们要写的源码文件 .dts,我们修改硬件配置主要就是修改它。
第二个是 DTC :这是编译器 ,它负责把我们写的 .dts 源码编译成二进制文件,可以类比 gcc 把 .c 文件编译成 .o 文件。
第三个是 DTB :这是编译出来的二进制文件 .dtb,这个文件会被烧录到 flash 中或者放在文件系统里面。Bootloader 启动时,会把这个文件加载到内存中,然后把内存首地址传给 Linux 内核,Linux 内核再去解析它。
2.2 类似于头文件包含的关系
设备树采用了类似 C 语言的分层 和包含机制,现在的 SOC 极其复杂,如果每块板子都把 CPU 内部的寄存器定义重写一遍,那代码维护将会变得十分困难。
Linux 采用了一种面向对象 式的继承与重写策略,我们以经典的 NXP i.MX6ULL 芯片为例,直接看源码(位于内核源码 arch/arm/boot/dts/ 目录下)。
2.2.1 SOC级文件 (.dtsi)
这个文件是由芯片原厂提供的,比如 imx6ull.dtsi,这个文件就在目录arch/arm/boot/dts/下面,它描述的也是这颗芯片内部固有的硬件资源 ,不管把这颗芯片焊在什么板子上,这些控制器的物理地址(reg) 和中断号(interrupts) 是永远不会变的。
我们打开 **imx6ull.dtsi**这个文件,下面这两张截图是这个文件的全部内容(前几行是注释):


最底层的文件 imx6ul.dtsi 描绘了这系列芯片最完整的蓝图,在截图中的头文件引用部分可以看到这个文件。请注意区分**imx6ull.dtsi和 imx6ul.dtsi**是两个不同的文件。如果去阅读源码,就会发现imx6ul.dtsi这个文件有上千行,而imx6ull.dtsi这个文件却只有几十行,这恰恰体现出分层包含机制的强大之处,对于新的硬件,只需要在新的文件里面包含老硬件对应的文件,在进行简单的修改就可以极大程度的减少代码量。
大家注意看第 14 行的 &cpu0。这里的 & 符号非常关键,它表示引用 ,这就像是面向对象编程里的重写 ,我们并不需要重新定义 CPU0 的所有属性,只需要引用父类中定义好的标签,这是它从头文件imx6ul.dtsi中继承过来的,然后只修改我们需要修改的部分就行了。
图中 47 行定义的 uart8 节点,reg定义了它的物理基地址是0x02288000,interrupts定义了它的中断号,status默认为"disabled",原厂之所以默认把它关掉,是因为他不知道你这块板子会不会用到串口 8,但是如果要打开它,也是很简单的。
更有趣的是图中的第 10 行:
c
/delete-node/ &uart8;
这行代码的大致意思是这样的,父类里虽然定义了 uart8,但我这块板子上压根没引出这个口,或者跟别的设备冲突了,所以我直接把它删掉。
这里体现了设备树极其灵活的一面,子类不仅可以修改 父类的属性,比如图中的 &cpu0 修改了电压频率表,还可以直接删除父类的节点。
但这里可能会有人发问,为什么同一个文件中的第10 行刚删除这个节点,而在后面的47行又对它进行配置呢?
其实在老款 i.MX6UL 芯片里,UART8 这个外设是挂载在 AIPS-1 总线下的,截图中第九行的注释就能说明这个问题。这里新款芯片对应的文件在头文件中包含了老款芯片的文件,但在新款 i.MX6ULL 芯片里,NXP 的硬件工程师修改了芯片内部设计,把 UART8 挪到了 AIPS-3 总线下,所以在新款芯片的文件中需要将这个节点删除,并配置新的节点,这是硬件层面的问题,我们不需要知道为什么,只需要知道他确实这么做了就行,既然硬件上它搬家了,那我们在设备树里也得把它搬过去,否则驱动去旧地址找设备肯定会扑空。
请仔细观察一下截图中大括号的布局,你就会发现 uart8 这个节点是在 aips3 内部的。
2.2.2 板级文件 (.dts)
开发板厂商编写了自己的描述文件 imx6ull-14x14-evk.dts,依然在arch/arm/boot/dts/这个目录下面。我们打开看看。

可以看到一共就这几行内容。文件开头首先引用了上面的头文件,这就相当于把头文件中的所有代码都复制过来了。
c
#include "imx6ull.dtsi"
就相当于它已经拥有了正确的 CPU 频率配置,并且自动剔除了不存在的 UART8,开发者根本不需要关心这些底层细节,只需要专注于板子上的外设配置即可。
虽然这张截图里的内容很少,但在实际开发中,这个 .dts 文件是我们打交道最多的地方。
我们一般会在这个文件的最外层 使用 &标签名 的方式引用节点,来开启 .dtsi 里默认关闭的外设。或者在 / 根节点内部添加板子上外接的新设备(比如 LED、按键等)。
2.3 如何与驱动匹配
在上一篇文章中,我们反复提到一个概念:总线会拿着设备和驱动进行匹配。
在没有设备树的时候,platform_device 的名字必须 和 platform_driver 的名字一模一样才能匹配。
但在设备树的世界里,匹配规则发生了变化,主角变成了 compatible 属性,也就是说内核会优先使用这个属性进行匹配了,只需要保证设备树节点和驱动程序的 compatible 属性相同,那么他们的名字不一样也无所谓。
我们在回过头去看**imx6ull.dtsi**文件中的 uart8 节点,截图中的 48 和 49 行,这里的 compatible 属性就是一个字符串列表,通俗一点来讲,这相当于设备在向内核喊话,我是 fsl,imx6ul-uart 类型的设备,帮我找找有没有适配的驱动,如果没有这个驱动,用 fsl,imx6q-uart 的驱动来凑合一下也行。
这个字符串的格式通常是 " 厂商,芯片型号-模块名 " ,一来方便我们识别,二来使用标准的规则进行命名能预防名称冲突。
现在我们已经清楚了设备树是怎么做的,接下来我们需要看看驱动那边是怎么做的。我们去查看一下上面串口对应的驱动源码,路径是:
bash
/kernel/drivers/tty/serial/imx.c

我们通过查找关键词可以找到这个结构体数组,可以看到第 275 行,确实存在与设备树那边匹配的compatible属性,这样一来,Platform 总线就会让他们成功匹配。
3. 内核如何处理设备树
到这里,又有新的问题浮现出来了,我们在 .dts 文件里写的那些节点,最终是变成了什么,是直接被驱动读取了吗?
答案是否定的,驱动程序是跑在内核里的,它只认 C 语言的结构体,压根不认识文本格式的设备树。
所以,内核在启动过程中,必须充当一个中间人的角色,把设备树从文本 变成结构体。
3.1 从 flash 到 RAM
我们在编译 .dts 文件得到 .dtb 文件之后,通常会把 .dtb 和内核镜像一起烧录到板子里,当开发板上电启动时,Bootloader (U-Boot)会先运行,他会把内核镜像加载到内存(RAM)的某个地址,然后把 .dtb 文件也加载到内存的另一个地址。
然后,U-Boot 启动内核时,会把存放 .dtb 文件的内存首地址 通过寄存器(ARM 架构通常是 r2 寄存器)传给内核。
这样,内核刚一启动,就会发现记录着硬件配置信息的.dtb文件就在那个内存地址里面放着呢。
3.2 展开
此时在内存里的 .dtb 文件,本质上还是一坨二进制数据。
虽然在内核启动的最早期(这个时期要获取内存容量等关键信息),内核确实会直接解析这个二进制文件,但这种方式效率极低,因为它是线性存储的,每次查找节点都得从头扫描,而且无法灵活地在这个树上反复遍历(比如无法高效的找到父节点)。
所以,为了后续驱动能高效地使用,内核在 setup_arch 阶段会执行一个叫 unflatten_device_tree 的函数,这个函数会解析二进制数据,把它们展开成内核里的结构体链表。
它会为设备树中的每一个节点 ,都创建一个 struct device_node 结构体。
此时这些 struct device_node 还仅仅是节点 ,它们还不是设备,驱动程序此时还无法使用它们。
3.3 生成设备
接下来是最关键的一步,也是连接设备树 和驱动模型的桥梁。
内核会在随后的初始化过程中,调用一个核心函数of_platform_populate(),这个函数会遍历刚才生成的 device_node 树,把那些看起来像是设备 的节点,转换成 Linux 驱动模型中的 struct platform_device。
但实际上并不是所有的节点都会被转换成设备,比如 /cpus(CPU信息)、/memory(内存信息)、/chosen(启动参数),这些节点只是提供信息的,不需要绑定驱动,所以它们不会被转换。
通常情况下,只有挂载在根节点 / 下,或者挂载在 simple-bus(简单总线)下的子节点,才会被转换。
当内核决定把一个节点转换成 platform_device 时,它会先malloc 一个 struct platform_device,然后把设备树里面的节点名塞进去,再解析资源,把 reg 属性转换成 IORESOURCE_MEM, interrupts 属性转换成 IORESOURCE_IRQ,最后调用 platform_device_register 把它注册到总线上。
当设备被注册到总线上后,该总线就会为他寻找相应的驱动进行匹配,匹配成功后,驱动程序的probe函数就会开始执行。
那么我们在 DTS 里面写了那么多属性(reg, interrupts等等),在 C 代码里怎么拿出来用呢?
4. 驱动怎么读取设备树
到现在,设备树节点已经转换成了 struct platform_device,并且顺利地和驱动程序匹配上了,内核调用了驱动的 probe 函数,在probe函数里,我们通常需要获取硬件资源 和设备树里面的一些自定义配置。
为此,Linux 内核提供了一套以 of_ 开头的函数。
4.1 获取标准资源
前面第三章讲过of_platform_populate,它在生成设备的时候已经自动把设备树里的 reg 属性转换成了 IO资源 (IORESOURCE_MEM),把 interrupts 属性转换成了 中断资源 (IORESOURCE_IRQ)。
所以,对于这两个标准属性,我们不需要去调用复杂的 OF 函数,直接用 Platform 子系统的标准接口就行。
具体用法如下:
c
//在驱动的 probe 函数中
static int my_driver_probe(struct platform_device *pdev)
{
struct resource *res;
int irq;
//获取寄存器地址,第三个参数0表示第0组reg
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENOMEM;
//拿到物理地址后,要把它映射成虚拟地址才能用
void __iomem *base_addr = devm_ioremap_resource(&pdev->dev, res);
//获取中断号,参数0表示第0个中断
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
//......
}
只要是 reg 和 interrupts,我们就可以直接用上面的 platform_get_...,这是最标准、最通用的做法。
获取寄存器地址的函数原型如下:
c
struct resource *platform_get_resource(struct platform_device *dev,
unsigned int type,
unsigned int num);
第一个参数 dev 指向当前的 platform_device 设备,第二个参数type是资源类型 ,获取寄存器这里填 IORESOURCE_MEM,第三个参数num是索引号,如果 reg 属性里有多段地址,0 表示第一段,1 表示第二段。
该函数成功时返回一个 struct resource 指针,里面包含了 start(起始物理地址)和 end(结束物理地址)。失败时返回NULL。
获取中断号的函数原型如下:
c
int platform_get_irq(struct platform_device *dev, unsigned int num);
第一个参数dev指向当前的 platform_device 设备。
第二个参数num中断索引号,通常填 0。
成功时返回内核映射后的虚拟中断号,失败时返回一个负的错误码。
4.2 获取自定义属性
除了标准资源,我们在设备树里还经常写一些自定义的属性,比如:
c
my_node {
compatible = "my-driver";
my-delay-ms = <100>; //数字
my-name = "demo-device"; //字符串
my-data = <0x11 0x22 0x33>; //数组
};
这是一个节点的示例,第一行的compatible就是用来匹配的属性。后面三行都是我们自定义的属性,对于这些属性,内核并没有帮我们自动转换,我们需要拿着节点指针,去手动查表,这些函数都在 <linux/of.h> 中。
4.2.1 读取数字
具体读取方法如下:
c
struct device_node *np = pdev->dev.of_node; //先拿到节点指针
u32 delay_time;
//寻找名为"my-delay-ms"的属性,并读出数值放入delay_time
if (of_property_read_u32(np, "my-delay-ms", &delay_time) == 0)
{
printk("get delay time: %d\n", delay_time);
}
else
{
printk("未找到该属性\n");
}
在操作设备树之前,我们需要先获取到当前设备的节点指针 device_node,它存储在 pdev 中。
读取整形数字的函数原型:
c
int of_property_read_u32(const struct device_node *np,
const char *propname,
u32 *out_value);
第一个节点np是指向设备节点的指针,第二个参数propname表示属性名字,第三个参数out_value用于保存读取到的数值。
读取成功时返回0,读取失败时返回负数。
4.2.2 读取字符串
方法如下:
c
const char *str;
//指针str会直接指向内核里存放那个字符串的地址
of_property_read_string(np, "my-name", &str);
函数原型如下:
c
int of_property_read_string(const struct device_node *np,
const char *propname,
const char **out_string);
前两个参数与前面读取整形数字的函数相同,第三个参数out_string是一个指针,它最终会指向 内核中存储字符串的内存地址。
返回 0 表示成功,返回负数表示失败。
4.2.3 读取数组
方法如下:
c
u32 data_array[3];
//读取数组,需要指定读取的元素个数
of_property_read_u32_array(np, "my-data", data_array, 3);
函数原型如下:
c
int of_property_read_u32_array(const struct device_node *np,
const char *propname,
u32 *out_values,
size_t sz);
前两个参数依旧与前面相同。第三个参数是out_values,这里需要预先定义一个用来接受数据的数组,把数组首地址传进去。
第四个参数sz表示需要读取的元素个数。
4.3 两个特殊的 API
在驱动开发中,还有两个使用非常高频的 API,我们把他们拿出来单独讲:
4.3.1 获取 GPIO
GPIO 是嵌入式里最常用的资源。在设备树里,我们通常这样来定义它:
c
reset-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
在驱动里,早期的内核用 of_get_named_gpio,但现代内核推荐使用更高级的 GPIO Descriptor (gpiod) 接口,它会自动处理高低电平的有效性:
c
struct gpio_desc *rst_gpio;
//只要写"reset",内核会自动去设备树找 "reset-gpios"
rst_gpio = devm_gpiod_get(&pdev->dev, "reset", GPIOD_OUT_LOW);
在使用 devm_gpiod_get 时,我们传入的参数是 "reset",内核会自动在设备树里寻找 reset-gpios 属性(注意看 DTS 文件中编写这个属性时用了 -gpios 后缀),这是现代 GPIO 子系统的命名规范。
4.3.2 查找节点
有时候,驱动需要访问的节点不是 通过 probe 传进来的(也就是说它并没有转换成 platform_device),而是设备树里某个角落的一个孤立的节点。
这时,我们可以主动去寻找它:
c
struct device_node *target_node;
//根据路径找
target_node = of_find_node_by_path("/backlight/lcd-backlight");
//根据compatible找
target_node = of_find_compatible_node(NULL, NULL, "my-custom-node");
if (target_node) {
//找到后,就可以用of_property_read_xxx去读它的属性了
}
这里要注意,使用 of_find_... 系列函数得到的节点,使用完后通常需要调用 of_node_put(target_node) 来释放引用计数,否则会导致内存泄漏。
5. 在系统运行时查看设备树
Linux 内核有着一切皆文件 的设计思想,它把解析好的设备树结构,通过 procfs 文件系统展现给了用户。
这意味着,我们不需要任何特殊的工具,只需要用 cd、ls、cat 这些最简单的命令,就能直接查看内核里的设备树。
这里要注意一下,大家在测试的时候不要在虚拟机上面测试,虚拟机里面运行的是 x86 架构,硬件描述的标准是 ACPI ,而不是设备树,所以在虚拟机下是找不到这个目录的。
而设备树主要用于嵌入式架构(如 ARM,RISC-V等),所以大家可以去自己的开发板上面测试。
5.1 设备树在哪
设备树在文件系统中的实体位于/proc/device-tree。
但是实际上,这是一个软链接,它指向的真正物理路径是/sys/firmware/devicetree/base。
我们可以在开发板的终端上面去看一下:

这里使用ls命令我们看到的是以列表排列的文件,看着并不是很清晰,这里可以使用另一条命令来看。

我们使用命令 tree -L 2来查看,这条命令只看设备树的前两层,往往看不到设备树的最末端。注意tree这个命令需要下载。
还可以使用 tree .这个命令来查看整个设备树:

这条命令就可以看到设备树的最末端了,从这张图中,我们可以看到具体设备里面的compatible和status等属性。
5.2 如何验证修改的设备树是否生效
假如我们在 DTS 里面添加了一个节点,这里我用之前写过的一个 LED 驱动来做个测试。

从设备树里面是可以找到这个节点的,可以看到它下面定义了四个属性,分别是compatible,led-gpios,name,status。
下面的截图是我在设备树 DTS 中添加的节点的部分:

可以看到我们在 DTS 文件中定义的属性都可以在设备树的拓扑结构中看到,唯独有一个 name 比较特殊,DTS文件中明明没有定义这个属性,为什么在设备树拓扑结构中却可以看到呢。
我们不妨先来看看它的值:

可以看到使用cat命令得到的结果是my_led_test,与我们定义的节点名是相同的。
这是因为在设备树规范中,每个节点都需要有一个 name 属性来标识自己。即使我们并没有定义这个属性,内核在解析节点时,也会自动把节点的名字 封装成 name 属性暴露在文件系统中。
这里我们再使用cat命令查看一下其他三个属性:

可以看到,除了led-gpios这个属性没有输出之外,其他都是正常的。那么为什么对 led-gpios 使用 cat 没有输出呢?
其实并不是因为文件是空的,而是因为它的内容是二进制数据,而不是文本数据。
注意在 DTS 文件中的 led-gpios 的定义,它与其他属性的诶定义是不同的,它使用的是 <> 这种尖括号,在设备树语法里,尖括号代表 32位整数数组 ,它在内存里存的是 3 个数字 ,即 Phandle 引用、引脚号、标志位。关键点就在这,比如 &gpio0 的引用 ID 可能是 0x00000015,因为设备树是大端序 存储,所以在内存里它的头两个字节是 00 00,而 00 在 ASCII 码里代表空字符(NULL) ,用 cat 命令去读它时,终端一看到开头的 00,以为字符串结束了,或者是个不可见字符,所以屏幕上什么都不显示,或者显示一堆乱码。
下面我们用正确方法验证一下,对于这种数值型的属性(reg, interrupts, gpios 等),我们要用 hexdump 命令把它按照十六进制打印出来:

图中第一列的00000000和0000000c是hexdump 命令显示的内存偏移量,他们告诉该行的内容是从文件的第几个字节开始读取的。我们在 DTS 文件中定义的数组中有三个元素,每个元素都是 32 位的,也就是 4 个字节,总共 12 字节。而从 00000000 开始往后推12个字节正是 0000000c 。
数组中的三个元素用十六进制表示也就是00000037,00000008,00000000。第一个 00000037是 &gpio0 的 Phandle ,即引用 ID 它是大端序存储,也就是最高位在前面,因为数字比较小,所以高位是 0,而正是由于它以 00 开头,才会被cat当成了空字符串处理,导致什么都打印不出来。第二和第三个元素分别对应宏 RK_PB0 的值(即 8 号引脚)和宏 GPIO_ACTIVE_HIGH 的值(即 0)。
到这里我们已经成功验证节点的存在性了,并且该节点的各个属性都睡符合预期的。
但是,节点存在不代表设备加载成功了。
5.3 验证设备是否生成
在 /proc/device-tree 下看到节点,只能说明内核解析 了设备树,但并不能证明内核已经把它转换 成了 platform_device。
如果节点的 status 是 disabled,或者父节点没开启,那么它只会存在于 /proc/device-tree 中,而不会出现在总线上。
要验证设备是否真正生成,我们需要去 Platform 总线 的目录下看看:

在/sys/bus/platform/devices/目录下执行ls命令,我们能看到截图中的结果。在第五列的最后一行,我们能看到熟悉的my_led_test。这就说明设备树不仅解析成功,而且已经成功注册为板载设备,正在那静静地等待驱动程序来认领它。
6. 总结
这篇文章到这里就要结束了,设备树的引入彻底结束了 ARM 社区代码脏乱差的时代,对于我们驱动开发者来说,它让代码变得更加纯粹------驱动只管逻辑,硬件交给设备树。