第 1 章 引言
想象一下,你是一个刚刚加电的 CPU。寄存器空空如也,内存里只有静电,除了时钟在滴答作响,你什么都做不了。
你需要一个「唤醒者」。
这个唤醒者不能是普通的软件——因为此时此刻,你的磁盘驱动还没加载,文件系统不存在,甚至连 C 语言运行时需要的堆栈都没准备好。你需要一段极度底层的代码,它能直接指挥硬件电路,把内存控制器唤醒,把时钟树配好,然后小心翼翼地把一个庞大的操作系统内核搬运到内存里。
在这个过程中,有一个棘手的问题始终困扰着系统设计者:唤醒者(固件)如何告诉被唤醒者(操作系统)这台机器到底长什么样?
如果让操作系统自己去「猜」硬件,那它必须把每一种可能的板子配置都硬编码在源码里。这在 PC 世界里曾经行得通,因为硬件高度标准化;但在嵌入式世界里,每一块板子都可能不一样——内存型号不同、中断号不同、外设接法不同。说实话,硬编码只会让内核变成一个永远编译不完的巨型怪物,维护起来简直是噩梦。
我们需要一种通用的语言,一种在系统启动的极早期由固件生成并递交给操作系统的「硬件说明书」。这就是本章要讲的核心——Devicetree Specification(DTSpec)。它不仅仅是一份配置文件规范,它是连接底层的硬件世界和上层的软件世界的契约。有了它,操作系统才得以从一个通用的内核变成一个真正能驱动硬件的实体。
本章我们先来理清这个启动链条上的角色关系,看看 DTSpec 是如何从 IEEE 1275 的废墟中诞生的,并确立我们在后续章节中要反复使用的术语。
1.1 目的与范围
要初始化并启动一个计算机系统,得有一堆软件组件像接力赛一样传递指挥棒。首先是 Firmware(固件),它通常是上电后第一个运行的代码。它的任务极其繁重且枯燥:初始化内存控制器、设置时钟、配置 CPU 核心的基本状态。等它把硬件收拾得像模像样了,它会把控制权交给下一个组件。
接下来可能是 Bootloader(引导加载程序),或者是 Hypervisor(虚拟机监视器)。它们可能会加载操作系统,然后再把控制权传下去。但这里有一个微妙的点:这些角色之间的界限并不是绝对的。比如 Hypervisor,它既能从固件手里接过控制权(此时它是客户端),又能负责启动和管理虚拟机(此时它是引导者)。为了避免混淆,我们引入两个更抽象的术语来统摄这个链条。
Boot program(引导程序) 指的是任何负责初始化系统状态并执行另一个软件组件的软件,它可以是固件、Bootloader 或 Hypervisor。Client program(客户端程序) 则是被引导程序初始化并执行的程序,它可以是 Bootloader、操作系统,或者特殊用途的程序。
在这个接力过程中,最大的痛点是接口的不统一。每个固件开发者都可能发明一种私有的方式来描述硬件,这导致操作系统开发者不得不为每个板子写适配代码。而 Devicetree Specification (DTSpec) 存在的意义,就是定义一套完整的、标准的「引导程序 → 客户端程序」接口,以及为了让这套接口跑起来所需的最低系统要求。
为什么专门针对嵌入式系统?
DTSpec 是为 Embedded system(嵌入式系统) 量身定做的。你可能觉得现在都 2024 年了,嵌入式和通用计算机的界限已经很模糊了——树莓派不就是个小电脑吗?但从系统设计的角度看,它们有本质区别。
通用计算机(比如你的笔记本)是设计给用户定制和扩展的——你可以插任意品牌的显卡,换任意厂家的内存。操作系统必须能在启动时动态发现这些设备(比如通过 PCI 总线枚举)。而嵌入式系统通常是定制好的,用来干固定的几件事。它的特点决定了我们需要一种静态的、高效的描述方式。
首先是固定的 I/O 设备,板子做出来时就有哪些设备是确定的,不会变。然后是极度受限的资源,内存可能只有几十 MB,Flash 也只有几十 MB,这意味着描述硬件的数据结构必须极其紧凑,不能像 PC 那样动辄几百 KB 的 ACPI 表。最后还有多样的 OS 选择,跑 Linux 是常态,但也可能跑实时操作系统(RTOS),甚至私有系统,大家需要一种通用的「普通话」。
所以 DTSpec 的目标很明确:在资源受限的环境下,用一种标准化的方式,把硬件配置从「硬编码」中解放出来。
本书的地图
在开始深入细节之前,先给你一张地图,免得在后面的术语风暴里迷路。第 1 章(也就是本章)介绍 DTSpec 架构本身及其历史背景。第 2 章正式引入 Devicetree 的概念,解释它的逻辑结构和标准属性。第 3 章定义一个符合规范的设备树必须包含的基础节点集合——没这些,内核起不来。第 4 章讲 Device Bindings(设备绑定),这是重头戏,详细规定了特定类别的设备(如中断控制器、串口)该如何在树里表示。第 5 章描述 DTB (Devicetree Blob),也就是设备树的紧凑二进制编码格式——这才是机器读的东西。第 6 章则讲 DTS (Devicetree Syntax),描述我们人类写的源代码格式,以及如何用 DTC (Devicetree Compiler) 把它编译成 DTB。
1.2 与 IEEE 1275 和 ePAPR 的关系
Devicetree 的血统其实很老,它的灵魂深处站着 IEEE 1275。IEEE 1275,全称 Open Firmware,是 IEEE 90 年代制定的标准。它试图解决通用计算机的一个核心问题:如何让一个操作系统镜像在不同的机器上启动? 它不仅定义了硬件描述方式,还定义了一套基于 Forth 语言的编程接口,甚至包括一个可交互的命令行界面。
但在嵌入式世界,IEEE 1275 里的这些功能全是累赘。IEEE 1275 面向的是开放、可扩展的通用计算机,它假设你会插各种奇怪的插件卡,所以它必须支持即插即用驱动、基于 Forth 字节码的 FCode 固件语言,以及可编程的启动调试界面。说实话,谁会去插拔一颗焊死的 SoC?谁会在启动时用 Forth 脚本改寄存器?
所以 DTSpec 做了一次极其激进的减法:它删掉了所有与执行代码相关的部分,只保留了数据描述的核心——即 Devicetree。这正是 DTSpec 的精髓所在:它不是一个可编程的固件接口,它只是一个纯粹的数据结构。它保留了 IEEE 1275 中最核心的思想:让引导程序把硬件信息描述给客户端程序,从而消除客户端程序里硬编码的硬件描述。 只要做到了这一点,我们就赢了。
除了 IEEE 1275,DTSpec 还有一个重要的前身:ePAPR (Embedded Power Architecture Platform Requirements)。ePAPR 是 Power ISA 架构的一份规范,里面详细描述了 PowerPC 处理器怎么用 Devicetree。可以说,Linux 内核里 PowerPC 架构对设备树的早期支持,就是完全基于 ePAPR 的。DTSpec 的文本很大程度上直接来源于 ePAPR,但是 ePAPR 里包含了很多针对 PowerPC 架构的特定绑定——这对 ARM、RISC-V 这些架构来说是噪音。
DTSpec 做的事情是:提取通用部分,剔除架构特定部分。它把那些 PowerPC 专有的绑定要么删了,要么移到了附录里,只留下了一份对任何架构都适用的「纯」规范。
1.3 32 位与 64 位支持
DTSpec 虽然诞生于 32 位 ARM 盛行的年代,但它从设计之初就考虑到了 64-bit 寻址的未来。无论是在地址单元的定义,还是属性值的解析上,规范都同时兼容了 32 位和 64 位的 CPU。在后续的章节里,如果某个特性在 32 位和 64 位系统上的行为有显著差异,我会专门提醒你。
1.4 术语定义
这一节看起来像是一堆枯燥的名词解释,但我建议你不要跳过。在技术文档里,对同一个词理解不一致,是导致系统崩溃的元凶之一。这里定义的每一个词,在后面的代码里都会频繁出现。
AMP (Asymmetric Multiprocessing, 非对称多处理) 是一种多核架构,但核心们并不平等。CPU 被分成了几组,每组跑一个独立的操作系统镜像,比如 Core 0 跑 Linux,Core 1 跑一个实时 OS,它们之间通常通过共享内存或中断通信。Boot CPU (引导 CPU) 是第一个「醒」的 CPU,引导程序会跳转到这个 CPU 上执行客户端程序的入口点,通常是 Core 0。
Boot program (引导程序) 是一个泛指概念,涵盖了 Firmware、Bootloader、Hypervisor 等负责启动别人的软件。Book III-E 是 Power ISA 架构规范里的一个章节,专门定义了嵌入式 Power 处理器中用于 supervisor 模式的指令——如果你不玩 PowerPC,这个词大概率只是个背景板。Cell 是设备树里的基本计量单位,1 Cell = 32 bits,你会在 DTS 文件里看到大量的 <...> 这种尖括号包起来的数字,它们通常都是以 Cell 为单位的。
Client program (客户端程序) 就是被引导程序启动的那个倒霉孩子(或者说是幸运儿),通常是操作系统。DMA (Direct Memory Access, 直接内存访问) 指的是硬件外设绕过 CPU 直接读写内存的能力,在设备树里描述 DMA 控制器和其可访问的内存区域是一项精细活。DTB (Devicetree Blob) 是设备树的二进制大块头,这是给机器看的,紧凑、难读、解析快。DTC (Devicetree Compiler) 是那个把人类可读的 .dts 编译成机器可读的 .dtb 的工具链。DTS (Devicetree Syntax) 是设备树源码,你以后要写的就是这个。
Effective address (有效地址) 是 CPU 指令看到的地址,而 Physical address (物理地址) 是 CPU 外部总线(如内存控制器)看到的地址——在设置了 MMU 的情况下,这两个地址是不一样的。Interrupt specifier (中断描述符) 是一个关键概念,它是一个属性值用来描述一个中断,通常包含中断号、触发方式(高电平/低电平)、极性等信息,具体格式由绑定的中断控制器决定。
Power ISA 是 Power 指令集架构。Quiescent CPU (静默 CPU) 是一种特殊状态下的 CPU,它既不能干扰别的 CPU 运行,别的 CPU 也不能干扰它(除非特意去唤醒它)——在 SMP 启动过程中,非 Boot CPU 通常需要先进入这种状态等待指令。Secondary CPU (辅助 CPU) 是除了 Boot CPU 之外的其他 CPU。SMP (Symmetric Multiprocessing, 对称多处理) 是我们最常见的多核模式,所有 CPU 平等,共享内存和 I/O,跑同一个操作系统。
SoC (System on a Chip, 片上系统) 把 CPU 核心、内存控制器、各种外设(UART、SPI、I2C...)全都塞进这一块硅片里。Unit address (单元地址) 是节点名称的一部分,比如 serial@12340000 里的 @12340000,它规定了该节点在父总线地址空间里的位置。
有了这些词汇作为基础,我们就可以开始真正地拆解 Devicetree 了。下一章,我们将直面这个数据结构本身。
练习题
练习 1:understanding
题目:在系统启动流程中,以下关于软件组件角色的描述,哪一项是错误的?
A. Firmware(固件)通常负责硬件底层的初始化,随后将控制权传递给操作系统。 B. Hypervisor(虚拟机监视器)只能作为 Boot program(引导程序),不能作为 Client program(客户端程序)。 C. Bootloader(引导加载程序)可以加载操作系统,因此它既可以被视为 Boot program,也可以被视为 Client program。 D. DTSpec 定义了一个标准接口,允许 Boot program 将硬件信息传递给 Client program。
答案与解析
答案:B
解析:根据 1.1 节定义,Boot program 是泛指初始化系统状态并执行另一个软件组件的程序(如 Firmware、Bootloader),而 Client program 是被 Boot program 初始化并执行的程序(如 Bootloader、Hypervisor、OS)。文中明确指出:“A piece of software may be both a client program and a boot program (e.g. a hypervisor)”。因此,Hypervisor 既可以作为引导程序(引导 OS),也可以作为客户端程序(被 Firmware 引导),选项 B 说它“只能”作为 Boot program 是错误的。
练习 2:understanding
题目:根据 DTSpec 与 IEEE 1275 (Open Firmware) 的关系,以下哪项技术特性是 IEEE 1275 包含但 DTSpec 明确省略的?
A. 使用设备树数据结构描述硬件 B. 使用 FCode(基于 Forth 的字节码)进行设备驱动 C. 定义 Boot program 到 Client program 的接口 D. 支持内存和 I/O 的描述
答案与解析
答案:B
解析:根据 1.2 节内容,DTSpec 保留了 IEEE 1275 中关于设备树架构的概念,但针对嵌入式系统的特性,省略了通用计算机的一些功能。文中列出的省略特性包括:Plug-in device drivers、FCode、可编程的 Open Firmware 用户界面、FCode 调试等。因此,FCode 是 IEEE 1275 包含但 DTSpec 已省略的特性。
练习 3:application
题目:在嵌入式系统开发中,你需要将人类可读的硬件描述文件传递给只接受二进制格式的 Client program(如 Linux Kernel)。请按正确的顺序排列以下步骤:
- Boot program(如 U-Boot)将二进制数据加载到内存
- 工具将文本源码编译成二进制 Blob
- 硬件工程师编写硬件配置的文本源码
- Client program 读取二进制数据并初始化驱动
答案与解析
答案:3 -> 2 -> 1 -> 4
解析:这是实际开发中的典型流程:
- 首先由工程师编写 DTS (Devicetree Syntax) 文本文件。
- 使用 DTC (Devicetree Compiler) 工具将 DTS 编译成 DTB (Devicetree Blob) 二进制文件。
- Boot program 读取并将 DTB 传递给 Client program(通常通过指针或寄存器传递)。
- Client program 解析 DTB 中的硬件信息来加载相应的驱动程序。
练习 4:thinking
题目:假设你正在设计一个包含 4 个 CPU 核心的嵌入式 SoC 系统,该系统运行 Linux。在系统启动的早期阶段,Boot CPU 开始执行操作系统代码,而其他 3 个 CPU 尚未被激活。根据 DTSpec 中的术语定义,这 3 个未激活的 CPU 处于什么状态?
答案与解析
答案:Quiescent (静默状态)
解析:根据 1.4 节对术语的定义:
- Boot CPU: 第一个执行客户端程序入口点的 CPU。
- Secondary CPU: 属于客户端程序但非 Boot CPU 的 CPU。
- Quiescent CPU: 处于无法干扰其他 CPU 正常操作,且其状态也不会受其他运行 CPU 正常操作影响的 CPU。
题目中描述的场景是典型的 SMP 启动过程。在 Boot CPU 唤醒 Secondary CPU 之前,为了防止这些处于未知状态的 CPU 干扰系统初始化(例如执行乱码指令或抢占总线),它们必须被硬件或 Bootloader 置于“静默”状态,直到 Boot CPU 显式地使能它们。
本章回响
本章真正在做的事情,是建立「硬件即契约」这个认知的底层形式。表面上我们在配置一份文件,实际上我们在理解为什么两个完全陌生的系统——固件和内核——能够在一瞬间建立起信任。
还记得开头那个问题吗——唤醒者如何告诉被唤醒者机器长什么样?答案不再是以前那种「硬编码」的私有握手,而是一份标准化的「出生证明」。Devicetree 不仅仅是一份数据结构,它是连接底层的硬件世界和上层的软件世界的契约。有了这份契约,固件终于可以把「这台机器长什么样」这个包袱甩给内核,而内核也终于可以停止猜测,开始真正的工作。
下一章我们会把这个机制拉到一个新的场景里——届时你会发现,今天建立的直觉会以一种意想不到的方式派上用场,我们将直面这个数据结构本身。