想象你刚买下一套二手房。交房那天,物业递给你一本厚厚的《房屋使用手册》:里面有户型图、电路走向图、水管分布图,还有每间房的设施清单、门牌号、配电箱编号。有了这本手册,你不需要敲墙勘测、不需要打电话回访开发商,翻开手册,一目了然。早期 ARM Linux 世界里,恰恰没有这本手册——每一块开发板的硬件信息,都被维护者“背”在脑里,写进内核代码的板级文件中。新开发板一登场,就得修改内核源码、提交补丁、等待合并,周期长且极易冲突。后来,工程师们从 PowerPC 借鉴了设备树(Device Tree):把硬件描述从内核代码中剥离出来,变成一份独立的数据文件。系统启动时,固件把这份“硬件户型图”交到内核手里,内核照着地图接管整个板卡。本篇文章将带你从零认识设备树:它为什么诞生、长什么样、如何编译、如何参与启动、驱动如何匹配,以及遇到问题怎么排查。

岗位关联

掌握设备树是嵌入式与驱动开发的必备基本功。它能帮你读懂启动日志、快速定位外设无法工作的根因,是每位内核与裸机开发工程师的“入场券”。

目录
  1. 可视化演示
  2. 起源与动机
  3. 基本结构
  4. 编译与工具
  5. 启动流程
  6. 关键属性
  7. 驱动匹配
  8. 设备树覆盖
  9. 调试与排错
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

起源与动机

《房屋使用手册》不是一开始就有的。早年的物业公司里,每位老管家都把整栋楼的电路图、水管图记在自己脑子里。一旦老管家离职,新物业就抓瞎了。ARM Linux 早期的“老管家”,就是内核源码树里成堆的板级文件(board file):每个开发板都要有专属的 arch/arm/mach-xxx 目录,用 C 代码硬编码描述硬件。于是每推出一块新开发板,就要往内核里塞进一大坨代码。

这种用“代码写硬件”的方式带来了连锁难题:板子多了以后,内核补丁横飞,同一个驱动要为不同板子写不同的初始化分支。社区维护者疲于奔命,上游合并缓慢。设备树(Device Tree)正是从 PowerPC 的 Open Firmware 实践中借鉴来的“标准手册”:它规定,硬件信息不再用代码表达,而是写成一份纯粹的数据文件,跟着板卡走,内核只负责“读手册”。

boardi→旧方案kerneliboardi→新方案DTSi\text{board}_i \xrightarrow{\text{旧方案}} \text{kernel}_i \qquad \text{board}_i \xrightarrow{\text{新方案}} \text{DTS}_i

1. 开发板标识:$\text{board}_i$ 表示系统中的第 $i$ 块开发板,每块板的硬件配置都不同。

boardi\text{board}_i

2. 旧方案箭头:旧方式下,这块板子的硬件信息靠修改内核源码来表达。

→旧方案\xrightarrow{\text{旧方案}}

3. 专属内核:$\text{kernel}_i$ 表示因板子而产生的内核分支,互相难以复用。

kerneli\text{kernel}_i

4. 新方案箭头:新方式下,硬件信息被抽离为独立数据,内核不再感知具体板子。

→新方案\xrightarrow{\text{新方案}}

5. 设备树文件:$\text{DTS}_i$ 是描述该板子的设备树源文件,轻量且独立。

DTSi\text{DTS}_i

设备树的价值,正如物业管理行业的规范:手册一旦成为标准,任何一家物业公司(内核)都能在接钥匙后快速接管任何一栋楼(板卡)。这套“先定契约、再谈接管”的思路,奠定了后续所有章节的基础。

基本结构

你翻开《房屋使用手册》,会发现它不是一本流水账,而是一棵倒挂的“知识树”:第一页是整栋楼的总纲(根节点),下面分出“电气系统”“给排水系统”“网络布线”等大章节,每章再细分到楼层、房间、具体插座。设备树的数据结构正是这样一棵树——每个硬件都是一个“节点”,节点里挂着一串“属性”。

DTS(Device Tree Source)是人类可读的源文件格式,长得像 C 语言,却没有任何执行逻辑。根节点用 / {} 表示,下面可以挂 cpus、memory、soc 等子节点;子节点下还能继续挂孙节点。属性就是键值对,比如给某个串口节点写上 compatible = "acme,uart",就相当于在房间门口贴了一张铭牌。此外,DTS 还允许给节点加“标签”(label),方便其他节点引用——如同手册里给房间编号,水电工一看编号就知道该去哪间房。

node::=label?  name  {  (property  ∣  node)∗  }\text{node} ::= \text{label}? \; \text{name} \; \{ \; (\text{property} \;|\; \text{node})^* \; \}

1. 可选标签:$\text{label}?$ 表示标签可有可无,它像房间的别名,让其他节点能方便地引用本节点。

label?\text{label}?

2. 节点名字:$\text{name}$ 是节点的正式名称,如 uart@1000,@ 后常跟地址以区分同类节点。

name\text{name}

3. 节点体:$\{ \}$ 这对大括号圈定了节点的领地,所有内容都写在括号内。

⋯{⋯ }\cdots \{ \cdots \}

4. 属性或子节点:$\text{property} \;|\; \text{node}$ 表示括号内装的要么是属性(键值对),要么是下级节点。

property  ∣  node\text{property} \;|\; \text{node}

5. 任意多次重复:$^*$ 意为前面的内容可以出现零次或多次,所以一个节点的内容可多可少。

(⋯ )∗(\cdots)^*

理解了节点与属性,设备树就不再神秘:整棵树就是对一台设备“自顶向下”的目录式描述。就像看手册找房间一样,内核按照树形路径一路下钻,就能精确定位任何一个硬件的全部信息。

编译与工具

《房屋使用手册》有两种形态:一种是给业主看的印刷排版稿(DTS),另一种是物业扫描枪一秒钟就能识别的二维码(DTB)。印刷稿人可以读,机器读起来费劲;二维码机器扫得快,人眼却看不懂。设备树同样有两种形态,由 DTC(Device Tree Compiler)这台“排版印刷机”完成转换。

DTS 经过 dtc 编译后,产出 DTB(Device Tree Blob)二进制文件。DTB 体积小、解析快,正是启动时内核真正消费的格式。反过来,当我们手里只有 DTB、想看内容时,可以用 fdtdump 或 dtc -I dtb -O dts 反编译,把二维码“翻译”回人眼的排版稿。在内核构建系统中,.dts 文件会被自动编译成 .dtb,烧写到启动分区的 dtb 区域——手册与二维码随房子一起交付。

DTS→  dtc  DTB→  fdtdump  DTS′≈DTS\text{DTS} \xrightarrow{\;dtc\;} \text{DTB} \xrightarrow{\;fdtdump\;} \text{DTS}' \approx \text{DTS}

1. 源文件:$\text{DTS}$ 是设备树的源文本,人可读、可注释、可维护。

DTS\text{DTS}

2. 编译动作:$\xrightarrow{\;dtc\;}$ 表示 DTC 编译器执行格式转换,把文本变二进制。

→  dtc  \xrightarrow{\;dtc\;}

3. 二进制产物:$\text{DTB}$ 是树形结构打包后的二进制块,启动时由引导程序加载。

DTB\text{DTB}

4. 反编译:$\xrightarrow{\;fdtdump\;}$ 表示用反编译工具把二进制重新展开成文本。

→  fdtdump  \xrightarrow{\;fdtdump\;}

5. 近似还原:$\text{DTS}' \approx \text{DTS}$ 说明反编译结果与原稿高度一致,但注释等信息可能丢失。

DTS′≈DTS\text{DTS}' \approx \text{DTS}

工欲善其事,必先利其器。掌握了 DTC 与反编译工具,你就同时拥有了“读手册”和“验真伪”的能力:拿到一块陌生板子,先反编译它的 DTB,看看这本手册到底写了什么,一切硬件布局尽收眼底。

启动流程

交房那天有一场“仪式”:开发商把钥匙和《房屋使用手册》一起放在茶几上,并提前告诉物业“手册放在进门左手边第二个抽屉”。ARM 启动流程也有类似约定:固件先把 DTB 加载进内存,然后把 DTB 的物理地址放进内核约定的寄存器——ARM 平台上这个寄存器是 r2,内核一启动就直奔 r2 取手册。

完整链路是这样的:芯片上电后由固化 ROM 引导,加载 U-Boot 等引导程序;U-Boot 初始化内存,把 DTB 从存储介质读到内存某地址,再用 bootz/bootm 启动内核,同时保证 r2 指向 DTB。内核入口处拿到地址后,先做“展平树”解析(unflatten),把紧凑的二进制结构还原成一棵内存中的节点树,随后挂载到内核的设备模型上。至此,硬件清单才真正变成内核看得懂的“活目录”。

固件→U-Boot→r2=addr(DTB)Kernel→unflatten(DTB)\text{固件} \to \text{U-Boot} \xrightarrow{r_2 = \text{addr}(\text{DTB})} \text{Kernel} \to \text{unflatten}(\text{DTB})

1. 片上固件:$\text{固件}$ 是芯片出厂自带的 ROM 程序,负责第一段引导。

固件\text{固件}

2. 引导程序:$\text{U-Boot}$ 是嵌入式最常用的引导程序,负责加载内核与 DTB。

U-Boot\text{U-Boot}

3. 地址约定:$r_2 = \text{addr}(\text{DTB})$ 表示把 DTB 在内存中的物理地址写入 r2 寄存器,这是双方的握手约定。

r2=addr(DTB)r_2 = \text{addr}(\text{DTB})

4. 内核接棒:$\text{Kernel}$ 启动后第一时间从 r2 读取地址并校验。

Kernel\text{Kernel}

5. 展平解析:$\text{unflatten}(\cdot)$ 把扁平二进制还原成树状结构,供驱动模型使用。

unflatten(DTB)\text{unflatten}(\text{DTB})

这套“固定位置交钥匙”的仪式保证了任何引导程序与任何内核之间都能默契配合。正如物业不必打电话问开发商,内核也不必猜硬件长什么样——寄存器里那只“手”,已经稳稳地把手册递了过来。

关键属性

手册里最实用的信息往往是几类固定的“标签”:门口铭牌说明房间用途,门牌号说明房间位置,应急指示说明报警器连到哪个总控台。设备树也有几个高频属性,理解了它们就理解了绝大多数 DTS 文件。

  • compatible:格式为 "厂商,设备型号",如 "arm,cortex-a9"。它是驱动匹配的“指纹”,内核靠它认出“这是谁家的什么设备”;还可以列出多个值,从最具体到最通用,像简历上的多个头衔。
  • reg:描述设备的寄存器地址与长度,如 <0x1000 0x100>。它相当于门牌号加房间面积:告诉内核“从地址 0x1000 开始,占用 0x100 个字节”。地址和长度各占几个 32 位单元,由父节点的 #address-cells 与 #size-cells 决定。
  • interrupts:描述中断号与触发方式,配合 interrupt-parent 指定中断控制器。好比“烟雾报警器编号 29,接到总控台 1 号端口”。
  • status:"okay" 表示设备启用,"disabled" 表示保留但不启用。这如同手册里标注“此房间暂未交付”,内核便不会为它创建设备。

compatible="vendor,device"reg=⟨addr0,len0,addr1,len1⟩\text{compatible} = \text{"vendor,device"} \qquad \text{reg} = \langle \text{addr}_0, \text{len}_0, \text{addr}_1, \text{len}_1 \rangle

1. 兼容串:$\text{compatible}$ 是设备节点上的关键属性,驱动依据它来认领设备。

compatible\text{compatible}

2. 厂商字段:$\text{vendor}$ 是厂商名,如 arm、acme,用于区分来源。

vendor\text{vendor}

3. 设备型号:$\text{device}$ 是具体型号,如 cortex-a9,用于精确匹配。

device\text{device}

4. 地址长度组:$\langle \text{addr}_0, \text{len}_0, \ldots \rangle$ 表示 reg 是一串“地址+长度”交替的元组,可描述多个寄存器段。

⟨addr0,len0,addr1,len1⟩\langle \text{addr}_0, \text{len}_0, \text{addr}_1, \text{len}_1 \rangle

这些属性就像手册里约定俗成的“表格栏目”,人人遵守,内核才能高效解析。写设备树时,最忌讳的就是自创栏目名——内核看不懂、驱动配不上,最后只能对着启动日志抓耳挠腮。

驱动匹配

有了手册,物业公司还得有人能干对应工种:看到“施耐德配电箱”,得找到负责施耐德的电工师傅。Linux 驱动与设备节点的配对过程,就是一次“按铭牌找人”的匹配。内核维护一张大大的“员工花名册”,每个驱动都登记自己擅长处理的 compatible 串,这张表在驱动代码里就叫 of_match_table。

当内核遍历设备树时,每个节点会被转换成平台设备(platform device),然后总线把这些设备与驱动一一配对。配对的首要依据是 compatible 属性:逐一比对设备节点的兼容串与驱动表中登记的串,命中即建立绑定关系,并调用驱动的 probe 函数,让驱动正式“上岗”。若 compatible 都没匹配上,内核还会退而求其次,用节点名称等老规则兜底,就像保安实在找不到电工师傅,先找个“万能工”顶着。

M(node)={d∈D:compat(d)∈compat(node)}\mathcal{M}(\text{node}) = \{ d \in \mathcal{D} : \text{compat}(d) \in \text{compat}(\text{node}) \}

1. 匹配函数:$\mathcal{M}(\text{node})$ 表示对某个设备节点执行的匹配操作,返回命中的驱动集合。

M(node)\mathcal{M}(\text{node})

2. 驱动全集:$d \in \mathcal{D}$ 表明候选范围是系统中所有驱动 $d$ 组成的集合 $\mathcal{D}$。

d∈Dd \in \mathcal{D}

3. 驱动声明:$\text{compat}(d)$ 是驱动通过 of_match_table 声明的兼容串集合。

compat(d)\text{compat}(d)

4. 节点声明:$\text{compat}(\text{node})$ 是设备节点上 compatible 属性声明的兼容串集合。

compat(node)\text{compat}(\text{node})

5. 命中条件:$\in$ 表示驱动声明与节点声明存在交集,即“对上暗号”。

compat(d)∈compat(node)\text{compat}(d) \in \text{compat}(\text{node})

匹配机制保证了“设备与驱动各说各话也能对上”,这正是设备树解耦的威力。驱动作者不需要知道板子长什么样,只需登记自己擅长的 compatible;板子作者也不需要迁就某个驱动,只需把兼容串写标准。一次握手,两边安心。

设备树覆盖

房子住久了,你打算加装智能门锁。难道要把整本《房屋使用手册》重写一遍?当然不——物业允许你在手册上贴一张“补充说明”:写出新增设备的节点、改名或修正确认过的属性即可。设备树覆盖(Device Tree Overlay)干的就是这件事。

覆盖机制把“基础树”和“补丁片段”分开管理:基础 DTB 描述板卡出厂时的硬件,overlay 片段(编译产物为 .dtbo)描述运行时追加的硬件。启动后,用户在系统里通过 configfs 或 fdtoverlay 工具把 .dtbo 加载进去,内核在内存中把片段合并进现有设备树,新节点立即被扫描、匹配驱动、创建设备。整个过程不需要重新编译内核,也不需要重新烧写 DTB 分区。

T′=T⊕OT' = T \oplus O

1. 基础树:$T$ 是板卡出厂自带的设备树,描述基本硬件。

TT

2. 覆盖片:$O$ 是记录增量修改的覆盖片段,例如新增一个 GPIO 扩展器节点。

OO

3. 叠加运算:$\oplus$ 表示把覆盖片中的节点插入或替换到基础树的对应位置,不是简单拼接。

⊕\oplus

4. 合并结果:$T'$ 是合并后的新树,后续驱动匹配都基于这棵新树进行。

T′T'

覆盖机制让“房型升级”变得像贴便签一样轻巧。在 FPGA 动态配置、树莓派扩展板(HAT)等场景中,硬件能力可以在运行期被动态“点亮”。手册与贴纸并存,既保住了出厂信息,又拥抱了无限扩展。

调试与排错

装修完入住,你发现客厅插座没电。正常业主的做法是:翻手册找到该插座对应的回路编号,去电闸箱确认空气开关有没有合上,再用万用表量一量线路。设备树调试也是同一套逻辑:先看“手册”(DTS/DTB 预期内容),再看“实际接线”(内核运行时视图),最后用“仪表”(日志与属性文件)逐段定位。

内核启动后,会在 /proc/device-tree/ 下按节点路径导出设备树视图,每个节点是一个目录,每个属性是一个文件,用 ls 和 cat 就能直接读取。而“手册原件”则通过 fdtdump 查看 DTB 的原始内容。两者对照,再加上启动日志中驱动报错信息,就能找到问题归属:是 compatible 拼写不一致?reg 地址写错?status 忘了改成 "okay"?还是中断号对不上?排查思路永远是一层层比对:理想状态是什么,实际状态是什么,中间哪一步出了偏差。

DTS意图→dtcDTB实际→unflattenproc视图\text{DTS}_{\text{意图}} \xrightarrow{dtc} \text{DTB}_{\text{实际}} \xrightarrow{unflatten} \text{proc}_{\text{视图}}

1. 设计意图:$\text{DTS}_{\text{意图}}$ 是开发者在源文件里写的理想描述。

DTS意图\text{DTS}_{\text{意图}}

2. 编译成型:$\xrightarrow{dtc}$ 表示源码经编译器变成 DTB,语法错误在此暴露。

→dtc\xrightarrow{dtc}

3. 实际二进制:$\text{DTB}_{\text{实际}}$ 是烧写入系统、被引导程序加载的真实数据。

DTB实际\text{DTB}_{\text{实际}}

4. 内核落地:$\xrightarrow{unflatten}$ 表示内核启动时把二进制还原成内存树。

→unflatten\xrightarrow{unflatten}

5. 运行时视图:$\text{proc}_{\text{视图}}$ 是 /proc/device-tree 下内核实际暴露的节点与属性。

proc视图\text{proc}_{\text{视图}}

调试设备树没有玄学,只有“三份对照”:意图、实际、视图。哪一份对不上,就往哪一份的方向深挖。正如老电工常说的:“手册不会骗人,骗人的是你没看懂手册。”

AI 应用场景

SoC 板卡 Bring-up 阶段

阶段:硬件抽象与设备接入阶段

解决的问题:AI 推理板卡(如 NPU 加速卡)接入新主控平台时,传统方式需要反复修改内核源码适配不同芯片,周期长、风险高。设备树用数据描述硬件,使得“换平台”变成“换手册”——只需在新系统上写好 DTS 并编译成 DTB,内核无需改动即可识别板卡上的 NPU、内存、串口。

具体案例:某团队将自研 NPU 加速卡从 i.MX8 平台移植到 RK3588 平台。工程师根据新平台手册编写 DTS,描述 PCIe 枚举出的地址窗口与中断映射,编译 DTB 后与 U-Boot 一起烧写,内核启动即识别设备并加载驱动,整体适配时间从两周缩短到两天。

边缘设备外设驱动适配

阶段:数据采集与传感器接入阶段

解决的问题:边缘 AI 设备常搭配摄像头、激光雷达、温湿度传感器等外设。传统方式下每换一个传感器就要改驱动代码;设备树方式下,驱动只需声明自己支持的 compatible 串,传感器的型号、地址、中断在 DTS 中配置即可,硬件更换不再触碰代码。

具体案例:一辆无人小车的视觉系统需要把摄像头从 OV5640 换成 IMX219。工程师在 DTS 中修改 I2C 总线下的摄像头节点 compatible 与复位 GPIO,重新编译 DTB 后重启,新摄像头被自动枚举并匹配到驱动,图像采集链路立即恢复。

设备树覆盖用于产品扩展

阶段:产品量产与运维扩展阶段

解决的问题:AI 盒子类产品往往提供扩展接口(USB、SPI、GPIO),用户插入不同扩展板就需要不同的硬件配置。传统方式需要厂家发布新固件;设备树覆盖机制允许把每款扩展板的描述做成一个 .dtbo,运行时按需加载,实现“插入即用”。

具体案例:某 AI 边缘网关支持用户选配 4G 模块或 CAN 总线板。厂家随产品提供两个 .dtbo 文件,用户在系统里通过 configfs 加载对应覆盖片,内核动态追加节点并创建设备,无需重启或刷机。

Python 演示

python86 行
#!/usr/bin/env python3"""极简设备树解析器:解析 DTS 缩进文本,构建节点树,支持按路径查找与 compatible 匹配。"""  class DeviceNode:    def __init__(self, name):        self.name = name        self.props = {}        self.children = []     def find(self, path):        node = self        for part in path:            target = None            for child in node.children:                if child.name == part:                    target = child                    break            if target is None:                return None            node = target        return node     def match(self, compatible):        if compatible in self.props.get('compatible', ''):            return True        return any(child.match(compatible) for child in self.children)     def dump(self, indent=0):        lines = ['  ' * indent + self.name]        for key, value in self.props.items():            lines.append('  ' * (indent + 1) + key + ' = ' + value)        for child in self.children:            lines.extend(child.dump(indent + 1))        return lines  def parse_dts(text):    root = None    stack = []    for raw in text.splitlines():        line = raw.strip()        if not line:            continue        if line == '};':            if stack:                stack.pop()        elif '=' in line and line.endswith(';'):            if stack:                key, value = line.rstrip(';').split('=', 1)                stack[-1].props[key.strip()] = value.strip().strip('"')        elif line.endswith('{'):            name = line.rstrip('{').strip()            if name == '/':                name = 'root'            node = DeviceNode(name)            if stack:                stack[-1].children.append(node)            else:                root = node            stack.append(node)    return root  if __name__ == '__main__':    sample = '''    / {        compatible = "acme,board-v1";        cpus {            cpu@0 {                compatible = "arm,cortex-a9";                reg = <0>;            };        };        serial@1000 {            compatible = "acme,uart";            reg = <0x1000 0x100>;        };    };    '''    tree = parse_dts(sample)    print('\n'.join(tree.dump()))    node = tree.find(['serial@1000'])    print('find serial@1000:', node.name if node else 'None')    print('match acme,uart:', tree.match('acme,uart'))    print('match acme,gpio:', tree.match('acme,gpio'))
目的:用约 80 行代码实现一个可运行的迷你设备树解析器,模拟内核“读节点、查属性、按 compatible 匹配设备”的核心逻辑。
  1. class DeviceNode:定义节点类,节点是设备树的骨架,相当于“房间”。
  2. self.children = []用列表保存子节点,构建出树形层级。
  3. def find(self, path):按路径逐级下钻查找节点,类似在 /proc/device-tree 里找设备目录。
  4. def match(self, compatible):深度优先搜索,检查自身或任意子树是否声明了该兼容串。
  5. self.props.get('compatible', '')从属性字典中取 compatible 值,没有则为空串,对应内核读取设备树属性。
  6. if compatible in ...判断驱动登记的兼容串是否命中节点声明,正是内核匹配驱动的基本操作。
  7. elif '=' in line and line.endswith(';'):识别属性行并解析键值对,对应 DTC 编译时的语法解析。
  8. elif line.endswith('{'):识别节点开始行并压栈,构造父子关系。
  9. tree.match('acme,uart')返回 True,模拟驱动与串口节点成功“握手”。
  10. tree.match('acme,gpio')返回 False,说明没有任何节点声明该兼容串,驱动不会上岗。

常见误区

误区“设备树里写了 hardware 的信息,那它是不是就是驱动?我把 DTS 改对,外设就能工作了吧?”

正解设备树只是数据,不是代码,更不是驱动。它好比房屋手册,手册画得再详细,也不会自己动手修电路——真正干活的是电工(驱动)。设备树负责告诉内核“这里有个什么设备、地址在哪”,驱动负责实际操控硬件。两者是“图纸”与“施工队”的关系,缺一不可。

为何常见:DTS 语法带大括号、分号,长得像 C 语言,初学者容易误以为它是可执行程序。

误区“设备树文件放在内核源码目录里,所以改了它要重新 make 内核,再刷整个内核镜像才生效。”

正解DTS 只是被内核构建系统顺手编译成 DTB,它本质上是独立数据文件。多数平台只需重新编译 DTB 并烧写 dtb 分区(或打包进 boot 镜像),内核本体无需重编。改设备树与改内核代码是两码事,正如重印手册不必重建房子。

为何常见:设备树源码确实位于内核源码树内(arch/arm/boot/dts/),路径上的“亲缘关系”误导了直觉。

误区“compatible 不就是个字符串吗?我随便拼一个,驱动那边也拼一样的,不就能匹配了?”

正解compatible 有严格的 "厂商,型号" 规范,并承载“从具体到通用”的回退语义。乱写会导致:内核日志里没有驱动认领设备,社区代码无法复用,上游维护者无法 review。正确的做法是沿用厂商前缀与标准命名,如 "arm,cortex-a9",必要时附加多个回退值。

为何常见:字符串看起来没有类型约束,容易让人忽视其背后承担的“命名空间”职责。

误区“设备树描述一切硬件,所以 USB 鼠标、PCIe 显卡、内存条型号都要在 DTS 里写清楚。”

正解设备树主要描述“不可自描述”的总线上挂载的设备,如内存映射外设、I2C/SPI 设备。USB、PCIe 这类总线支持枚举(即插即用),设备会被自动发现,不需要也不应该在设备树里逐个描述。内存的大小也由引导程序动态修改 DTB 或由内核探测,而不是写死。

为何常见:初学者看到 DTS 里写了一大堆节点,容易以偏概全,以为所有硬件都要“点名”。

面试题

Q1. 设备树 DTS 源文件经过 DTC 编译后的产物是什么?

单选题 · easy

  • A.可执行 ELF 映像文件
  • B.DTB 设备树二进制← 正确答案
  • C.设备树源文本文件
  • D.内核镜像压缩文件

答案:B。DTC 的作用正是把人类可读的 DTS 文本编译成机器高效的 DTB 二进制。DTB 会被引导程序加载并在启动时交给内核,而不是可执行程序。选项 A、D 是内核或程序产物,C 是输入而非输出。

Q2. 设备树中承载硬件信息的基本单位是什么?

单选题 · easy

  • A.属性的键值对
  • B.可执行指令
  • C.节点与子节点← 正确答案
  • D.内存地址段

答案:C。设备树是一棵层级树,节点是基本单位,每个节点代表一个设备或总线;属性则挂在节点下描述细节。指令是程序的单位,地址段只是 reg 属性的一部分内容,均非基本单位。

Q3. 关于设备树的 compatible 属性,下列说法正确的有哪些?

多选题 · mid

  • A.采用厂商与型号格式← 正确答案
  • B.驱动匹配的核心依据← 正确答案
  • C.用于描述内存大小
  • D.可提供多个备选值← 正确答案

答案:ABD。compatible 的规范格式是 "厂商,型号",内核驱动通过它来认领设备,且可以列出多个值作为从具体到通用的回退。内存大小由 reg 或引导程序管理,与 compatible 无关,因此 C 错误。

Q4. ARM Linux 启动时,内核从哪个寄存器获取 DTB 的物理地址?

单选题 · easy

  • A.r0 寄存器
  • B.r1 寄存器
  • C.r2 寄存器← 正确答案
  • D.sp 寄存器

答案:C。ARM 启动约定规定 r0 为 0、r1 为机器号(部分平台已不用)、r2 存放 DTB 物理地址。这是引导程序与内核之间的“交钥匙”契约,sp 与栈相关,与传参无关。

Q5. 将 DTB 反编译为 DTS 以便阅读的命令是哪一个?

单选题 · mid

  • A.dtc -I dtb -O dts← 正确答案
  • B.dtc -I dts -O dtb
  • C.gcc -I dtb -O dts
  • D.objcopy -I dtb -O dts

答案:A。dtc 的 -I 指定输入格式、-O 指定输出格式,因此 -I dtb -O dts 实现二进制到文本的反编译。B 方向相反,是正向编译;C、D 使用了错误工具,gcc 和 objcopy 不处理设备树格式。

Q6. 关于内核中驱动与设备节点的匹配,下列说法正确的有哪些?

多选题 · hard

  • A.compatible 属性优先级最高← 正确答案
  • B.无匹配时可回退看节点名称← 正确答案
  • C.匹配成功后调用 probe 函数← 正确答案
  • D.匹配失败会导致内核崩溃

答案:ABC。内核优先用 compatible 匹配,兼容串全部失败后会按历史规则回退到设备类型和节点名称。匹配成功才创建绑定关系并调用 probe,驱动正式生效。匹配失败最多是设备“没人认领”、不创建设备,不会导致内核崩溃,D 错误。

Q7. 设备树覆盖(Device Tree Overlay)最典型的使用场景是什么?

单选题 · mid

  • A.加快内核编译速度
  • B.运行时扩展硬件描述← 正确答案
  • C.替代驱动代码编写
  • D.压缩内核日志输出

答案:B。覆盖机制把基础树与增量片段 .dtbo 在运行时合并,从而动态描述新接入的硬件,常见于扩展板、FPGA 动态配置。它不参与编译优化,也不替代驱动,更与日志无关。

Q8. 在运行中的 Linux 系统里,查看内核实际设备树视图的目录是哪个?

单选题 · mid

  • A./proc/device-tree← 正确答案
  • B./dev/device-tree
  • C./sys/device-tree
  • D./etc/device-tree

答案:A。内核展开设备树后,会在 /proc/device-tree/ 下以目录和文件形式导出节点与属性。/dev 是设备文件,/etc 是配置文件目录,/sys 下虽有固件相关信息,但设备树的标准视图在 /proc/device-tree。

Q9. 外设无法工作时,下列属于设备树排查手段的有哪些?

多选题 · hard

  • A.fdtdump 反编译核对内容← 正确答案
  • B.检查节点和 status 状态← 正确答案
  • C.核对 compatible 拼写← 正确答案
  • D.直接重装整个内核

答案:ABC。正确的排查逻辑是“手册、视图、日志”三层对照:用 fdtdump 看 DTB 原始内容,用 /proc/device-tree 看内核实际视图,再核对 compatible 与 status。直接重装内核属于无差别操作,既非排查手段也浪费时间。

Q10. 某设备节点 status 属性被设为 "disabled" 时,内核通常会怎样处理?

单选题 · extreme

  • A.仍创建对应设备
  • B.保留节点不建设备← 正确答案
  • C.忽略整个子树信息
  • D.启动报错直接退出

答案:B。status 为 “disabled“ 的节点会保留在设备树中,但内核在把设备树转换成平台设备时会跳过它,因此设备不会出现。子树信息并不被粗暴丢弃——之后通过覆盖机制把 status 改为 ”okay” 仍可启用,这正是覆盖机制的用武之地。