☰
从DTB到device_node:内核设备树解析完整链路详解
2026/10/28 1:01:48 网站建设 项目流程

很多朋友调设备树都是这么个状态:改 dts、编 dtb、烧进板子,看到内核打印Unflattening device tree... Done就松了口气,然后直接去点灯了。说句实话,如果你不清楚这一行打印的背后,内核到底把你的设备树变成了什么,那后面遇到"节点明明在,驱动却不 probe""属性值读出来全是错的"这类问题,你真的连排查的第一步都不知道往哪迈。

这篇内容我就专门盯着一个环节讲:内核把 DTB 解析成device_node的完整链路。从 DTB 怎么进内核、内核怎么校验它的头、早期扫描阶段干了什么,到__unflatten_device_tree怎么把一棵扁平二进制"种"成内存里的节点树,再到这些节点怎么被后续驱动模型消费。整个过程会牵扯drivers/of/fdt.c、drivers/of/base.c里的一堆函数,我会把它们串成一条能走通的线,包括我自己在真机上调试时踩过的坑。

适合谁看?正在学设备树的嵌入式工程师、想搞懂内核驱动模型后半程的人,以及啃源码啃到unflatten就看不懂了的新手。看完你至少能做到:拿到一块板子,能自己确认 DTB 有没有被内核收下、展开后的树长什么样、出问题时该去查哪一层。

1. 从 dts 到 device_node 的最后一公里:为什么值得翻这段内核代码

设备树整套流程你可以理解成一条加工链,每一环都有明确的输入输出。搞清楚每一环的输入输出,遇到问题时你才能判断故障出在哪个环节。

1.1 先把整条链路摊开看

从一份可读的 dts 文件,到驱动能用的平台设备,中间大概是这样一条流水线:

环节输入工具/过程输出说明
1dts/dtsi 源文件dtc 编译器dtb 二进制编译、包含、宏展开都在这一步
2dtb 二进制bootloader 加载到内存指定物理地址处的 DTBbootloader 负责放入约定地址
3dtb 二进制内核setup_machine_fdt/unflatten_device_treedevice_node树本篇主角
4device_node树of_platform_populate等platform_device之后驱动才能 bind

很多人只熟悉第 1 环和第 4 环,第 2、3 环基本是黑盒。但恰恰是第 3 环决定了第 4 环能不能干成。device_node可以粗略理解为设备树在内存里的"实物",它长成一棵有父子兄弟关系的链表树,驱动匹配、寄存器解析、中断映射全部围绕这棵树展开。

1.2 为什么"会改 dts"不等于"懂设备树"

我见过太多同行是这样排查问题的:板子起来后某个外设没工作,第一反应是"我改一下 dts 里的 compatible 试试",改完还是不行,再改,最后把status从okay改成disabled又改回来,全靠瞎试。

真正高效的定位方式应该是逐步排除:先确认内核拿到的 ioremap 资源和 dts 里写的一致不一致,再确认节点到底有没有被展开成device_node,最后才怀疑驱动匹配逻辑。这三步里至少有一步落在内核解析 DTB 上。不懂第 3 环,你连"节点是否存在"这个问题都答不上来,就只能瞎猜。

另外,很多"诡异"问题其实都是解析期的细节造成的。比如同一个节点里有多个reg项,地址位宽是 64 位还是 32 位,读取出来的值完全不一样;又比如某些属性是__be32大端,在小端机器上直接用会有问题。这些都属于第 3 环的知识范畴。

1.3 这篇文章涉及的内核文件

给你一张"代码地图",边读边对照源码会轻松很多:

  • drivers/of/fdt.c:early_init_dt_scan、__unflatten_device_tree、unflatten_dt_nodes、内存保留映射处理都在这里。这是本篇主战场。
  • drivers/of/base.c:of_scan_flat_dt、of_platform_populate相关的底层设施,以及各种属性读取 API。
  • include/linux/of.h:struct device_node、struct property的定义。
  • scripts/dtc/:dtc 工具源码,虽然不在内核运行时里,但理解 DTB 布局时很有参考价值。

2. DTB 进内核的第一站:引导程序传参和 header 体检

DTB 是一块二进制数据,bootloader 把它加载到内存后,必须想办法把它的物理地址告诉内核。这个"告诉"的过程,不同架构做法不同,而内核拿到地址后做的第一件事是检查这个 blob 是不是一个合法的 DTB。

2.1 不同架构下,DTB 地址是怎么交到内核手里的

以最常见的几个平台为例:

  • ARM32:按照启动约定,r2 寄存器存放 DTB 物理地址。内核入口汇编代码会把这个地址保存到__atags_pointer之类的变量里,后续setup_arch再用。老一点的 bootloader 只传 ATAG,内核可以通过CONFIG_ARM_ATAG_DTB_COMPAT把 ATAG 模拟成 DTB 的一部分。
  • ARM64:x0 寄存器存放 DTB 物理地址,内核在汇编入口把它保存到__fdt_pointer。setup_arch()里调用setup_machine_fdt(__fdt_pointer)来初始化。
  • RISC-V:a1 寄存器传 DTB 地址,同样在很早期就被记录下来。

这里有一个容易被忽略的约束:bootloader 放的 DTB 必须位于内核早期就能访问到的内存区域。ARM64 在 MMU 尚未完全初始化时,会用 fixmap 临时映射的方式去访问 DTB;ARM32 则在早期直接按物理地址访问。如果你的 bootloader 把 DTB 放得很"偏",内核可能根本读不到,表现就是启动卡死或者直接报找不到合法设备树。

提示:真机上如果 bootloader 没传好 DTB 地址,内核日志里经常出现Error: invalid device tree blob或者干脆静默死机。排查时第一件事就是确认启动参数里有没有console能看到早期打印,以及 bootloader 到底把 dtb 放到了哪个地址。

2.2 fdt_header:一见面要先做身份检查

DTB 文件开头是一个固定结构的头,定义在include/linux/fdt.h,关键字段如下:

struct fdt_header { uint32_t magic; // 固定是 0xd00dfeed uint32_t totalsize; // 整个 DTB 的总字节数 uint32_t off_dt_struct; // structure block 的偏移 uint32_t off_dt_strings; // strings block 的偏移 uint32_t off_mem_rsvmap; // memory reserve map 的偏移 uint32_t version; // 版本 uint32_t last_comp_version; // 最小的兼容版本 uint32_t boot_cpuid_phys; // 启动 CPU 的物理 ID uint32_t size_dt_strings; // strings block 大小 uint32_t size_dt_struct; // structure block 大小 };

magic固定是0xd00dfeed,这个小端看是ed fe 0d d0的字节串,很多人第一次用十六进制编辑器打开 DTB 时都会看到这个"奇怪"的开头。内核通过fdt_check_header()检查 magic、totalsize、off_dt_struct等字段是否合理,如果头不对,直接拒绝继续。

totalsize这个字段很有用。调试时我经常拿它和实际文件大小对比,如果 bootloader 把 DTB 做了裁剪或传输出了问题,totalsize 和实际大小不一致,内核会在校验时报错。这比你自己一行行对数可靠得多。

2.3 早期映射那点事

这里稍微展开讲一个很多人没注意的细节。在 ARM64 上,setup_machine_fdt被调用时,MMU 可能已经开了但页表还比较简陋。内核使用early_fixmap_init()建立临时映射来访问 DTB,并把initial_boot_params指向这块映射。所有后续的扫描、展开操作,读的都是initial_boot_params。

所以你会看到内核里很多函数都带一个参数叫blob,其实就是initial_boot_params。它一旦被设置好,整个设备树解析期间都只读这块内存,直到unflatten完成,才会被链接进全局的设备树链表。理解这一点,你就明白为什么"展开前扫描"和"展开"读的是同一份原始数据——因为内核始终在拿原始 DTB 做参考。

3. 在展开前先"扫描":early_init_dt_scan 里干了哪些事

你可能以为内核拿到 DTB 后直接就展开成树了,其实不是。在正式展开之前,内核会在扁平二进制上做几轮"扫描",把一些直接影响启动流程的关键信息先提取出来。这个阶段的工作全部围绕early_init_dt_scan()展开。

3.1 of_scan_flat_dt:一个在二进制上"走路"的迭代器

drivers/of/base.c里有一个非常核心的函数of_scan_flat_dt()。它把 DTB 当成一段连续内存,用一个偏移量从头走到尾,遇到FDT_BEGIN_NODE就认为是一个节点开始,调用你传入的回调函数;遇到FDT_END_NODE就认为节点结束,返回上一层。

int __init of_scan_flat_dt(int (*it)(unsigned long node, const char *uname, int depth, void *data), void *data) { const void *blob = initial_boot_params; int depth = -1; unsigned long offset = 0; ... while (offset < fdt_totalsize(blob)) { tag = fdt_next_tag(blob, offset, &offset); switch (tag) { case FDT_BEGIN_NODE: depth++; ... if (it(node, uname, depth, data)) return offset; break; case FDT_END_NODE: depth--; ... } } }

早期扫描就是靠这个函数,配合各种回调,把启动需要的零散信息逐个抽出来。比如启动参数、initrd 地址、内存大小等,都是在树还没正式"立"起来之前就被拿走的。

3.2 chosen 节点:bootargs 和 initrd 的"中转站"

early_init_dt_scan_nodes()会依次调用几个回调,其中early_init_dt_scan_chosen()专门干一件事:找到路径为/chosen的节点,读取其中的bootargs属性,填充到boot_command_line;读取linux,initrd-start和linux,initrd-end,记录 initrd 的内存区间。

这个设计很有意思,它保证了"内核还没完整解析设备树,就已经能拿到命令行参数"。因为内核启动早期需要知道console=xxx、root=xxx这些参数来决定后续打印和挂载根文件系统。所以 chosen 节点在设备树里被称为"给内核留的小纸条",位置和属性名都是约定好的,不能随便改。

3.3 memreserve:DTB 自带的内存圈地

DTB 里还有一个不太显眼但很重要的区块:memory reserve map。它位于文件头之后,structure block 之前,是一串(address, size)对,每项占 8 个字节,以size = 0结束。

static __initdata struct { phys_addr_t start; phys_addr_t size; }

内核在early_init_fdt_scan_reserved_mem()里遍历这张表,把需要保留的内存区间标记为 reserved,之后通用内存管理就不会把这部分内存分配出去。这个机制常用于给某些硬件保留 DMA 内存段,或者在 DTB 里直接声明"这块内存谁都不能动"。

注意它和 DTS 里reserved-memory节点的区别:memreserve 表是 DTB 头级别的保留,早于任何节点解析,优先级最高,也最"硬";而reserved-memory节点展开后还要靠early_init_fdt_scan_reserved_mem之外的机制去处理。有一次我遇到 DMA 内存不连续的问题,查到最后就是 memreserve 表里保留了错误区域,后来用reserved-memory节点重新声明才解决。

4. __unflatten_device_tree:一颗大树怎么从扁平二进制里长出来

如果前文是"热身",这一步才是正戏。unflatten_device_tree()不是一个复杂算法,但细节很多。我把它拆成几个层次来讲。

4.1 两遍扫描:先算大小,再正式建树

__unflatten_device_tree()的核心思想是"两遍扫描"。第一遍只把整个 DTB 里的节点和属性遍历一遍,计算出需要分配的总内存大小;第二遍再重新遍历一遍,真正填充device_node和property结构,并把它们串成链表。

为什么要这么做?直接每遇到一个节点就 kmalloc 不行吗?技术上可以,但早期阶段内核内存管理还没完全就绪,频繁分配小对象既慢又容易碎片化。一次分配一大块连续内存,然后在这块内存里"切豆腐"一样切出一个个结构体,是早期内核代码里常见的做法。

static void *unflatten_dt_nodes(const void *blob, void *mem, struct device_node *dad) { ... // 如果 mem 为 NULL,说明是第一遍,只算 size // 如果 mem 非 NULL,说明是第二遍,正式填充 }

第一次遍历结束后,unflatten_dt_nodes会得到一个总大小,这个大小包含所有device_node、property、字符串、路径名所需的空间。第二遍时在已分配的mem里逐个放置对象,指针按 4 字节对齐滑动。

4.2 递归遍历 structure block:放进树里

展开过程是一个递归下降的过程。内核用一个状态机沿着 structure block 走,遇到FDT_BEGIN_NODE就创建一个子节点,遇到FDT_PROP就创建一个 property,遇到FDT_END_NODE就回到父节点。核心逻辑在populate_node()和populate_properties()两个函数里。

populate_node()的工作:

  • 从 DTB 中读取节点名字(比如uart@0x10000000),分配struct device_node空间,设置name和full_name。
  • full_name是从根节点一路拼出来的完整路径,比如/soc/uart@0x10000000,它由当前节点的名字和父节点的full_name拼接而成。
  • 把当前节点挂到父节点的child链表上,同时更新父节点的sibling关系。

populate_properties()的工作:

  • 读取每个属性的名字、长度、值。
  • 分配struct property,把属性的 name 指针指向 strings block 中对应的字符串(或拷贝到新内存),把 value 指针指向 unpacked 后的数据。
  • 把property串进节点的properties单向链表。

4.3 展开后的模样:链表与引用计数

展开完成后,内存里就有了一个和 DTS 层级完全对应的树。struct device_node大概长这样:

struct device_node { const char *name; // 节点名,比如 "uart" const char *type; // 兼容性 type phandle phandle; // phandle 值 const char *full_name; // 完整路径 struct property *properties; // 属性链表 struct device_node *parent; struct device_node *child; struct device_node *sibling; ... };

这棵树的遍历从全局变量of_root开始。内核里大量代码用of_find_node_by_path("/soc/uart@10000000")这类 API 在树上查找节点,其实就是在遍历这棵链表的递归结构。

这里有个常见的引用计数误区。device_node是带引用计数的对象,of_node_get()和of_node_put()成对出现。早期展开时引用计数初始化为 1,但如果某个驱动拿了节点却不释放,会导致节点永远无法释放,出现内存泄漏和后续 probe 问题。我自己排查过一个问题:同一个节点被两个驱动各自of_find_node_by_path找到却没有of_node_put,导致 module 卸载后再加载,驱动匹配直接失败。

4.4 一处值得留心的内存设计

展开所用的内存不是普通 kmalloc,而是早期内存分配器切出来的一块。这意味着这棵树一旦建立,基本就是"只读"的,驱动只能查询,不能修改(除了少数故意用of_update_property的特例)。如果你在内核模块里想改某个属性值,大多数情况下是改不动的,也不是那么设计的。

还有一点,unflatten_device_tree()的执行时机是在setup_arch()内部,此时 slab 分配器可能还没完全就绪,所以它用的是早期分配函数early_alloc_aligned()之类的路径,本质就是 memblock。这也解释了为什么有时候你会看到内核日志里Unflattening device tree打印的时机比较早。

5. DTB 的数据布局和大小端:看懂解析代码前的必修课

我看过很多人在没弄懂 DTB 内部布局的情况下去读fdt.c,结果一脸懵。其实 DTB 的数据布局非常规整,花半小时搞清楚,后面读代码效率高十倍。

5.1 structure block 里的"标签流"

整个 DTB 的正文部分是 structure block,它由一系列带 tag 的片段组成。每个 tag 是一个 4 字节的大端整数,内核通过fdt_next_tag()逐个读取并分发。

tag 值宏名含义
0x1FDT_BEGIN_NODE节点开始,后面跟着节点名
0x2FDT_END_NODE节点结束
0x3FDT_PROP属性定义,后面跟着属性名偏移和值
0x4FDT_NOP空操作,可忽略
0x9FDT_ENDDTB 结束

你可以用fdtdump命令直接查看一段 DTB 的结构,也可以自己写个十六进制解析脚本。我第一次看到这个"流"结构时,觉得它很像读一个压缩包里的文件目录:先标记"开始",然后逐项列出文件名和内容,再标记"结束"。这个类比能帮你快速建立直觉。

5.2 strings block:为什么属性名要单独囤一仓库

DTB 里属性名并不是和节点待在一起,而是统一放到文件末尾的 strings block 里。FDT_PROP片段里的"名字"字段存放的是一个偏移量,指向 strings block 中的某个字符串首地址。

这么设计的好处是压缩冗余。想象一棵树里五十个节点都有compatible属性,如果每个节点都把 "compatible" 这 9 个字节重复存一遍,浪费很大。统一放一个字符串池,每个属性只存 4 字节的偏移,整体体积能小不少。内核在populate_properties()里处理属性时,会把property->name设置成指向这个池中对应字符串的指针(或者重新拷贝一份保存在新内存里)。

这个设计看起来简单,但影响到了一个问题:如果你直接改 DTB 里的字符串池,所有引用它的属性名都会跟着变。所以内核在解析时通常是拷贝字符串到新内存,而不是直接引用原始 DTB 中的位置,防止后续内存被覆盖后指针失效。

5.3 __be32 双刃剑

DTB 里所有数字类型都是大端字节序(Big-Endian)存储的,即最高有效字节在前。内核里大量使用__be32类型来表示。在小端机器(x86、arm64 常见配置)上解析时,必须用be32_to_cpu()这类宏做转换。

这个坑我踩过不止一次。你写一个驱动,直接访问property->value想读一个整数,没做be32_to_cpu(),读出来的数字就像是"反过来"的,比如写到 dts 里的0x10000000读出来变成0x00000010。更隐蔽的是,字符串属性不需要转换(因为是字节序列),数值属性必须转换,两者混在一起时特别容易搞错。

内核提供了一整套 API 帮你处理这些事:of_property_read_u32()、of_property_read_u64()、of_property_count_elems_of_size()等,它们内部都做了字节序转换。所以驱动代码里尽量不要自己裸读property->value,而是用这些封装好的接口。

5.4 节点名的两个概念:浅析 name 和 full_name

展开后的节点有两个名字概念,常被搞混:

  • node->name:短名,比如uart或uart@10000000,取的是路径最后一段(不含父路径)。
  • node->full_name:完整路径,比如/soc/serial@10000000。

内核里有的 API 按 short name 匹配,有的按 full name 匹配,用错就找不到节点。驱动里常见的of_find_node_by_name()匹配的是 short name,of_find_node_by_path()匹配的是 full name。如果你在调试时用of_find_node_by_path("/uart@10000000")去查一个挂在/soc下的节点,那永远查不到,因为路径压根不完整。

6. device_node 建立之后,设备模型如何循着它找到驱动

树展开完,工作还没结束。device_node只是一棵数据树,它得和 Linux 的设备驱动模型挂钩,才能创建出驱动的platform_device,触发 probe。

6.1 从 device_node 到 platform_device

展开后的device_node会由of_platform_populate()之类的函数"消费",生成对应的struct platform_device。这个函数会遍历树的可用节点,对满足条件的节点调用of_platform_device_create_pdata()创建平台设备。

需要注意:并不是所有device_node都会变成platform_device。如果一个节点设置了status = "disabled",of_device_is_available()会返回 false,默认就不创建平台设备。另外,根节点下的某些兼容字符串(比如simple-bus)会触发递归展开,让总线下的子节点也能成为平台设备。

这里又是一个经典排查点:compatible拼写错误、status忘了改回okay、或者父节点没声明simple-bus导致子节点不展开,三种症状都表现为"设备没工作",但原因完全不同。你得靠展开后的device_node树和platform_device的生成情况区分。

6.2 驱动匹配的真相

驱动和设备的匹配是通过platform_match()完成的。流程大致如下:

  1. 优先检查of_driver_match_device(),即驱动of_match_table里的每个compatible字符串是否和设备节点的compatible属性匹配。
  2. 若没匹配上,再检查平台设备 ID 表里是否有匹配名字。
  3. 再退一步,检查设备节点的name和type。

所以你在驱动里写of_match_table = { .compatible = "vendor,device" },内核匹配时本质上就是拿这个字符串去和device_node->properties里的compatible属性做比对。数据结构就是一个字符串列表的包含关系判断,非常朴素。

有一点值得注意:compatible属性是一个字符串数组,可以有多个值,匹配时按顺序逐一比对。所以你会经常看到 dts 里写compatible = "vendor,device", "something-generic";。内核匹配时优先用第一个,失败后依次尝试后面的。如果第一个拼错,内核会忽略它去试第二个,很容易让你误以为"匹配成功了",结果驱动拿到的资源是通用处理逻辑的资源,行为变得很古怪。

6.3 从 sysfs 看树的"实物"

调试时最直观的手段是看 sysfs。设备树展开后,内核会把整棵树导出到/sys/firmware/devicetree/base,其中每个目录对应一个节点,每个文件对应一个属性。

ls -R /sys/firmware/devicetree/base/soc/ cat /sys/firmware/devicetree/base/soc/serial@10000000/compatible

打开compatible文件看到的是一串可读字符串,reg属性在 shell 里看起来是二进制,你需要用hexdump解码。这个视图强烈建议每个做设备树的工程师多用用——你不需要任何打印语句就能确认内核到底"看到"了什么。

另外,/sys/firmware/fdt是原始 DTB 的导出文件,大小和 bootloader 加载的 DTB 一致。调试时我经常把两份对比:一份是编译出来的原始 dts,一份是内核导出的 fdt,用脚本解析后对照属性值,能快速定位是否在传递过程中被改坏。

7. 我从调试现场总结的三条定位路径

理论讲完,说点实际的。设备树相关的问题是嵌入式开发里最容易让人绕进去的坑,我归纳了三条最常见的排查路径。

7.1 现象一:整个树都是空的——先确认 DTB 有没有被内核"签收"

如果你ls /sys/firmware/devicetree/base发现目录为空,或者启动日志里根本没有Unflattening device tree,那大概率 DTB 根本没被正确传递。这时不要急着怀疑 dts 内容,先确认 bootloader 的加载命令、地址配置,以及内核启动参数。

我的检查顺序是:

  1. 看引导日志,确认 bootloader 是否打印了load dtb to 0x...。
  2. 比较 bootloader 设置的加载地址和内核CONFIG_ARM64下__fdt_pointer期望的地址是否一致。
  3. 打开/sys/firmware/fdt,对比文件大小和原始 dtb 是否一致,不一致说明引导过程有裁剪或校验失败。

去年帮朋友看一块板子,症状是系统起来后 i2c 节点全没加载,查了一圈才发现 bootloader 里 dtb 加载命令拼错了文件名,加载的是一份旧的 512K 文件。这种问题不看引导日志,靠猜是猜不出来的。

7.2 现象二:节点在但数值不对——大小端、属性名拼写、reg 位宽

如果/sys/firmware/devicetree/base里能看到节点,但驱动读到的寄存器地址、中断号不对,那就是解析期的数值转换出了问题。常见诱因有这些:

症状常见原因验证方法
读出的数值像翻转了没做be32_to_cpuhexdump原始属性,对比驱动值
寄存器地址不对父节点的#address-cells或#size-cells写错检查父节点reg的 cell 数量
属性读不到属性名大小写/下划线拼错catsysfs 下对应属性
中断号错乱interrupt-parent的 phandle 解析失败确认父中断控制器节点有phandle且正确引用

其中最隐蔽的是#address-cells和#size-cells,它们决定了reg属性怎么被解释。比如reg = <0x0 0x10000000 0x0 0x1000>在#address-cells = <2>、#size-cells = <2>下是"地址 0x10000000、长度 0x1000",但在 32 位 cell 下就变成"地址 0x0、长度 0x10000000、接着是另一个 reg 项的字段"。同一个 DTB,解析方式完全由祖先节点的 cell 属性决定。遇到寄存器地址异常时,我第一反应永远先检查 cell 属性配置。

7.3 现象三:驱动始终不 probe——状态和 compatible 的表象

节点存在、属性也对,但驱动就是不起。这时把注意力转移到匹配逻辑上:

  1. 先看节点status,是不是okay。status = "disabled"会直接导致of_device_is_available()返回 false,平台设备压根不创建。
  2. 再看驱动of_match_table里的 compatible 和节点compatible是否完全一致(包括厂商前缀和逗号)。
  3. 最后看父节点是否需要simple-bus之类的兼容字符串来触发子设备创建。

很多"我明明把 compatible 改对了"实际上没改对,因为内核匹配的是节点里的完整字符串列表,而不是你印象里的部分匹配。可以用这句话自查:内核不是在做模糊搜索,是在做字符串精确匹配,一个字节都不能差。

7.4 调试利器清单

最后给你一个我平时调试设备树相关问题的工具箱:

工具用途说明
fdtdump离线查看 DTB 结构检查编译产物是否符合预期
dtc(dtc -I dtb -O dts)DTB 反编译回 dts确认最终烧进去的 DTB 内容
ls /sys/firmware/devicetree/base查看展开后的节点内核视角的树
cat /proc/device-tree旧路径兼容部分系统指向 sysfs
ftracefunction_graph追踪of_platform_populate看平台设备创建到哪一步停了
gdb + vmlinux单步调试解析代码适合深入排查内核崩溃点

我的习惯是:拿到设备树问题,先不看驱动代码,先用fdtdump看 DTB 里到底有什么,再用 sysfs 看内核展开后到底看到什么。这两步对不上,问题就在中间;对得上,问题才在驱动。

写这篇文章时我正好在帮人查一个瑞芯微平台下 GPIO 节点失效的问题,查到最后就是 compatible 拼错了一个字母,从 DTB 反编译出来一眼就看到了。设备树解析这段代码并不难,难的是你遇到问题时有没有意识到"应该先验证解析结果,而不是去改驱动"。如果你下次再碰到莫名其妙的设备树问题,别急着"试一试",先把这棵device_node树翻出来看看,答案往往一直都在那里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询