Linux 启动流程实战:Device Tree 全解析与驱动绑定机制
> 适用平台:以 **ARM/ARM64/RISC-V** 等 **Device Tree(DT)** 主导的 SoC 为主;x86/服务器多为 ACPI,流程不同。
> 目标读者:做板级移植、驱动开发、内核定制的嵌入式工程师。
> 产出导向:给你一条能“从位到物”的**可操作路径**——DTB 如何被传入、如何被解析成内核对象、如何与驱动绑定、最终如何在 **/sys** 里“长出”可读写的属性文件。
---
## 摘要
本文系统梳理 **从 Bootloader 到 sysfs** 的完整链路:Bootloader 如何准备并传递 **DTB**;内核如何将 **扁平化的 FDT** 解析为内存中的 **`device_node` 树**;如何据此在 **Linux 设备模型** 中实例化真实的 **`struct device`**;驱动如何基于 **`compatible`** 完成匹配与 `probe()`;以及 **sysfs(/sys)** 中的目录与属性文件如何生成、由谁维护、如何被用户空间使用。文末给出端到端范例、关键函数路线图、常见属性与 API 的对应表、调试清单、以及可复用的驱动骨架。
---
## 1. 引言:设备树的角色与演进
### 1.1 为什么需要设备树(DT)
早年的嵌入式 Linux 通过 **board-file**(如 `arch/arm/mach-xxx/board-yyy.c`)把板级硬件**硬编码**进内核。任何硬件微调(I²C 外设地址变更、GPIO 复用变化等)都要**改内核、重编译**,难以维护与复用。
**设备树**把这类“非自发现(non-discoverable)”硬件的**结构化描述**(地址、中断、时钟、引脚、电源、拓扑)从内核代码里**抽离**出去,放到可独立分发的 **DTB** 中,实现 **一个内核 + 多 DTB** 适配多板卡的范式。
### 1.2 DT 的基本构成(概念速览)
* **节点命名**:`node-name@unit-address`。`unit-address` 是该设备在父总线地址空间的基址(与 `reg` 对齐)。
* **核心属性**:
* `compatible`:驱动匹配桥梁(按“最具体 → 最通用”排序)。
* `#address-cells` / `#size-cells`:子节点 `reg` 中地址/长度各占多少个 32 位 cell。
* `phandle` / `&label` 与 `/aliases`:节点跨引用与稳定别名。
* `/chosen`:`bootargs`、`stdout-path`、`initrd` 等启动必需信息。
* `/memory` 与 `reserved-memory`:物理内存与保留区 carved-out。
---
## 2. Bootloader 阶段:DTB 的生成、修补与传递
### 2.1 DTB 的生产与存储
* 以 **.dts** 为板级入口,复用 **.dtsi**(SoC/外设公共片段)。
* 用 **dtc** 编译为 **DTB**,与 `Image/zImage/uImage`、可选 `initramfs` 一起放入存储介质。
### 2.2 U-Boot 的职责(“上游修补工”)
* **加载**:把 **内核镜像 + DTB (+ initramfs)** 搬到 RAM。
* **修补**:更新 `/chosen/bootargs`、`/chosen/stdout-path`、内存布局、MAC、SN、随机种子等;必要时叠加 **overlay**。
* **传参**:将 **DTB 物理地址** 交给内核入口。
### 2.3 传递寄存器(多架构对照)
* **ARM32**:`r0=0`、`r1=mach id`(旧)、`r2=FDT 物理地址`。
* **ARM64**:`x0=FDT 物理地址`。
* **RISC-V**:`a1=FDT 物理地址`。
#### 表 1:ATAGS vs Device Tree(概览)
| 特性 | ATAGS(旧) | Device Tree(现代) |
| ---- | ------------- | --------------- |
| 数据结构 | 标签链表 | 扁平化树(DTB) |
| 传递方式 | `r2` 指向 ATAGS | 寄存器指向 DTB |
| 描述能力 | 少量基础信息 | 完整硬件拓扑与资源 |
| 维护模式 | 硬编码进内核 | **数据驱动,解耦** |
| 现状 | 已弃用 | 嵌入式主流 |
---
## 3. 内核早期:从 FDT 到 `device_node`
### 3.1 进入 C 世界前的“轻解析”
* 入口汇编完成**最小** CPU/缓存/MMU 准备,跳入 `start_kernel()` 之前,必须先读出 DTB 的关键内容。
* **libfdt** + `of_scan_flat_dt()` 在 **未启用 MMU** 的条件下**直接遍历二进制 DTB**,调用回调提取:
* `early_init_dt_scan_chosen()`:拿 `bootargs`、`stdout-path`、initrd 范围等。
* `early_init_dt_scan_memory()`:登记物理内存范围到 **memblock**。
* 解析 `/reserved-memory`,避免被伙伴系统分配。
### 3.2 “展开”成内核常驻对象
* MMU/内存管理就绪后,调用 **`unflatten_device_tree()`**(亦称 `of_fdt_unflatten_tree()`)把 **FDT** 转化为内核的 **`struct device_node` 树**,节点与属性持久化到内核堆。
* 扫描 `/aliases` 形成 alias 表(如 `serial0`、`ethernet1`)供命名/实例化排序使用。
* 这一步完成后,**DT 已成为“活”的内核对象**(根保存在 `of_root`)。
---
## 4. 从 DT 到“设备”:设备模型实例化
### 4.1 of\_platform 与各总线桥接
* **simple-bus** / SoC bus:`of_platform_populate()` 遍历子节点,把带 `compatible` 的节点**实例化为 `platform_device`**,计算 `reg/ranges` 映射;在 **/sys/devices/platform/** 下出现设备目录。
* **中断域**:`interrupt-controller` 节点注册 **irqdomain**,子设备的 `interrupts` 通过 `of_irq` 系列映射为 Linux **irq** 号。
* **I²C**:控制器驱动注册 **`i2c_adapter`** 后,`of_i2c_register_devices()` 读取子节点生成 **`i2c_client`**。
* **SPI**:控制器注册后,`spi_of_register_spi_devices()` 生成 **`spi_device`**。
* **MDIO/PHY、MMC、PCIe、USB、MFD** 等各有专用 “OF → 设备” 桥接逻辑。
### 4.2 常见 DT 属性如何被“消费”(property → 资源句柄)
| 类别 | 常见属性 | 驱动侧 API/用法 |
| --- | --------------------------------- | ------------------------------------------------------- |
| 匹配 | `compatible` | `of_match_device()`;驱动里的 `of_device_id` 表 |
| 可用性 | `status` | `of_device_is_available()`(`okay` 才会枚举) |
| 寄存器 | `reg`/`reg-names` | `platform_get_resource()` → `devm_ioremap_resource()` |
| 中断 | `interrupts`/`interrupt-parent` | `platform_get_irq()` / `of_irq_get()` |
| 时钟 | `clocks`/`clock-names` | `devm_clk_get()` → `clk_prepare_enable()` |
| 复位 | `resets` | `devm_reset_control_get()` → `reset_control_deassert()` |
| 电源 | `*-supply` | `devm_regulator_get()` → `regulator_enable()` |
| 引脚 | `pinctrl-0`/`pinctrl-names` | `devm_pinctrl_get()` → `pinctrl_select_state()` |
| DMA | `dmas`/`dma-names`/`dma-coherent` | `dma_request_chan()`/一致性标志 |
| 物理层 | `phys`/`phy-names` | `devm_phy_get()` |
| 频率 | `assigned-clocks/parents/rates` | 时钟框架在上电/late init 应用 |
| 性能 | `operating-points-v2` | OPP/`cpufreq`/`devfreq` 框架 |
| 映射 | `ranges`/`dma-ranges` | 计算子总线与系统地址空间映射 |
> 这些属性**不是直接生成 /sys 文件**;而是被驱动/子系统读入,转化为**资源句柄与对象**(时钟、复位、irq、regulator、pinctrl、DMA 等),随后由设备模型与 sysfs 暴露“状态/控制”接口。
---
## 5. 驱动绑定:从 `compatible` 到 `probe()`
### 5.1 匹配过程(bus match)
* 设备(`platform_device`/`i2c_client`/`spi_device` 等)注册到相应 **bus** 后,bus 的 `match()` 调用 `of_match_device()`,把设备节点的 **`compatible` 列表**与驱动的 **`of_device_id` 表**比对。
* 匹配成功:调用驱动的 **`probe()`**;未匹配:设备**保留待命**(可能稍后其他驱动加载)。
### 5.2 模块自动加载与延迟探测
* 新设备注册会触发 **uevent**,包含 `MODALIAS=of:N*T*Cvendor,device...`。
* **udev/systemd-udevd** 根据 `MODALIAS` 调用 `modprobe` 自动装载模块。
* 依赖(如某 regulator/clk/phy)暂未就绪时,驱动 `probe()` 返回 **`-EPROBE_DEFER`**,内核稍后**重试**,保证供应者-消费者顺序。
### 5.3 `probe()` 中都干了啥(典型清单)
1. 获取 **`dev->of_node`**,读取属性。
2. 解析 **MMIO**、**IRQ**、**clk**、**reset**、**regulator**、**GPIO**、**PHY**、**DMA** 等资源。
3. 初始化硬件、申请中断、设置初始速率/电源/引脚态。
4. 向上层子系统注册(如 **netdev**、**input**、**hwmon**、**drm**、**sound/soc**…)。
5. (可选)创建 **sysfs 属性**、**debugfs** 节点、**chrdev**/**blkdev** 等。
---
## 6. /sys 是怎么“长出来的”:kobject → sysfs
### 6.1 设备模型与 sysfs 的关系
* **sysfs** 是 **kobject 树**的只读/可写**投影**。
* 每个 `struct device/driver/class/bus` 都带一个 **kobject** → 在 **/sys** 下对应目录:
* **/sys/devices/**:真实物理/逻辑拓扑(`platform/`、`soc/`、`pci/` …)。
* **/sys/bus/**:按总线维度呈现 `devices/`、`drivers/`。
* **/sys/class/**:按“功能类”(`net/`、`tty/`、`input/`、`leds/`、`hwmon/` 等)聚合。
* **/sys/firmware/devicetree/base/**:**只读**地暴露**当前 DT 树**(验证 Bootloader 修补、生效属性的最佳观察窗)。
### 6.2 属性文件(attributes)是谁创建的
* 驱动或子系统通过 `DEVICE_ATTR()` / `sysfs_create_group()` 暴露 `show()`/`store()` 回调:
* 读:`cat /sys/.../foo` → 调 `show()` 返回当前状态。
* 写:`echo X > /sys/.../foo` → 调 `store()` 配置硬件或软件状态。
* 许多上层子系统(如 **netdev、hwmon、power、thermal、leds**)**自动**导出大量标准化属性;你也可在驱动里自定义。
### 6.3 /dev 与 /sys 的分工
* **/sys**:结构与属性的**控制/观测**界面。
* **/dev**:进行 **I/O** 的**字符/块设备节点**(由内核 `uevent` + **udev 规则**创建与命名)。
---
## 7. 端到端范例:I²C 温度传感器如何“走完流程”
**DTS 片段:**
```dts
i2c0: i2c@40003000 {
compatible = "vendor,i2c-master";
reg = <0x40003000 0x1000>;
interrupts = <12>;
status = "okay";
tmp: tmp102@48 {
compatible = "ti,tmp102";
reg = <0x48>;
vdd-supply = <&vdd_3v3>;
};
};
```
**发生了什么:**
1. Bootloader 传入 DTB;内核早期解析 `/chosen`、内存与保留区;随后 **unflatten** 成 `device_node` 树。
2. `of_platform_populate()` 把 `i2c@...` 实例化为 **`platform_device`**;匹配 I²C 控制器驱动 → `probe()` → 注册 **`i2c_adapter`**。
3. I²C 栈读子节点 `tmp102@48` → 创建 **`i2c_client`**,发 `uevent` → 自动加载 `ti_tmp102` 驱动。
4. 传感器驱动 `probe()`:读 `reg`=0x48、拿 `vdd-supply` 上电、与 **hwmon** 框架对接。
5. 你会看到:
* `/sys/bus/i2c/devices/0-0048/`(总线视角)
* `/sys/devices/platform/soc/.../i2c@40003000/0-0048/`(拓扑视角)
* `/sys/class/hwmon/hwmonX/temp1_input`(功能视角,可 `cat` 得温度)
---
## 8. 关键函数路线图(把“黑盒”拆开看)
```text
start_kernel()
├─ setup_arch()
│ ├─ early_init_dt_verify()
│ ├─ of_scan_flat_dt(early_init_dt_scan_chosen / _memory / _reserved_mem ...)
│ └─ unflatten_device_tree() ← FDT → device_node 树
├─ of_alias_scan()
├─ irq_init(); time_init(); console_init(); ...
├─ subsys_initcall()
│ ├─ of_platform_default_populate_init()
│ │ └─ of_platform_populate() ← simple-bus → platform_device
│ ├─ 各总线控制器注册(i2c/spi/mmc/...)
│ │ └─ of_*_register_*_devices() ← DT 子节点 → 设备
│ └─ driver_register() / module_init() ← 驱动进入可匹配状态
├─ late_initcall()
│ └─ 供应者框架就绪(clk/regulator/phy/...)
└─ 用户空间(initramfs → switch_root → systemd/udevd)
```
---
## 9. 设备树 Overlay 与运行时改变
* **Overlay**:把 `*.dtbo` 叠加到主 DT(Bootloader 或内核态)。
* **内核态**:启用 `CONFIG_OF_OVERLAY` 后,可通过 **configfs** 在
`/sys/kernel/config/device-tree/overlays/<name>/` 写入 `dtbo` 实现**热插拔式**增删设备(随之触发 uevent、驱动 probe/remove)。
---
## 10. 调试与排障清单(强烈建议收藏)
1. **验证 DT 生效**:`ls -R /sys/firmware/devicetree/base`、`fdtdump/fdtget`。
2. **看设备是否被枚举**:`ls /sys/bus/*/devices`、`ls /sys/devices/platform/`。
3. **看匹配**:`zcat /proc/config.gz | grep CONFIG_OF`;`dmesg | grep -i of:`;为驱动加 `pr_info` 或 `dyndbg="file drivers/foo.c +p"`。
4. **看自动加载**:`udevadm monitor` 查看 uevent / `MODALIAS`;`modinfo <module>` 看别名。
5. **看依赖是否就绪**:`-EPROBE_DEFER` 出现很正常;确认 clk/regulator/phy/pinctrl 驱动加载顺序。
6. **早期日志**:启动参数加 `earlycon loglevel=8 initcall_debug`,观察各 initcall 的时序与耗时。
7. **子系统自检**:`i2cdetect`、`ethtool`、`devlink`、`pinctrl` debugfs、`clk_summary`、`regulator` debugfs 等。
---
## 11. 常见误解澄清
* **/sys 不是把 DT“渲染”成文件**:/sys 映射的是 **kobject/设备模型**;DT →(枚举/匹配/`probe`)→ 设备对象 → /sys。
* **DT 不是“配置开关”**:DT 记录**硬件事实**(拓扑与连线);启停与策略由驱动/子系统决定。
* **看不到设备目录 ≠ DT 无效**:多数时候是**未匹配**或**依赖未就绪**(defer),或 `status="disabled"`。
* **/dev 与 /sys**:/dev 是 I/O 节点(字符/块设备),/sys 是结构与属性(控制/观测)。
---
## 12. 可复用的最小驱动骨架(platform 版)
```c
static const struct of_device_id demo_of_match[] = {
{ .compatible = "acme,demo" }, {}
};
MODULE_DEVICE_TABLE(of, demo_of_match);
static ssize_t mode_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
/* 读设备状态 */
return sysfs_emit(buf, "normal\n");
}
static ssize_t mode_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
/* 写设备配置 */
return count;
}
static DEVICE_ATTR_RW(mode);
static int demo_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
int irq;
if (!of_device_is_available(pdev->dev.of_node))
return -ENODEV;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base)) return PTR_ERR(base);
irq = platform_get_irq(pdev, 0);
if (irq < 0) return irq;
/* clk/regulator/gpio/phy/reset 等资源同理获取并使能 */
/* 注册到上层子系统或自建 class 视情况 */
sysfs_create_file(&pdev->dev.kobj, &dev_attr_mode.attr); /* /sys/.../mode */
dev_info(&pdev->dev, "demo probed\n");
return 0;
}
static int demo_remove(struct platform_device *pdev)
{
sysfs_remove_file(&pdev->dev.kobj, &dev_attr_mode.attr);
return 0;
}
static struct platform_driver demo_drv = {
.probe = demo_probe,
.remove = demo_remove,
.driver = {
.name = "acme-demo",
.of_match_table = demo_of_match,
},
};
module_platform_driver(demo_drv);
```
---
## 13. 一图看全程(时间线)
```
BootROM → (SPL/TF-A) → U-Boot
↓ 加载 Image/DTB/initramfs,修补 /chosen、/aliases、MAC、内存等
→ 跳转:ARM32 r2 / ARM64 x0 / RISC-V a1 = FDT 物理地址
Linux 入口(汇编)
↓ libfdt + of_scan_flat_dt() 取 bootargs/memory/reserved
↓ MMU/内存子系统就绪
↓ unflatten_device_tree() → device_node 树
↓ of_alias_scan()
↓ of_platform_populate() & 各总线注册:把 DT 节点变成 device
↓ 驱动注册(module_init/driver_register)
↓ 匹配(compatible)→ probe()
↓ 驱动消费属性(reg/irq/clk/...),注册到子系统,导出 sysfs 属性
↓ 触发 uevent → udev 创建设备节点(/dev)与规则
↓ initramfs/init → switch_root → systemd/udevd
↓ 用户空间通过 /sys 观测/控制,通过 /dev 进行 I/O
```
---
## 14. 实操速查:把 DT 节点“落地”到 /sys
1. **写 DTS**:补齐 `compatible/reg/interrupts/clocks/resets/...`,`status="okay"`;确认 `#address-cells/#size-cells`。
2. **编译 & 传递**:`dtc` 生成 DTB;U-Boot `fdt addr/fdt apply` 或固化进引导;必要时 overlay。
3. **确认生效**:`/sys/firmware/devicetree/base` 对照属性。
4. **看设备出现**:`/sys/devices/platform/...`、`/sys/bus/<bus>/devices/`。
5. **看驱动匹配**:`dmesg`、`udevadm monitor` 观察 `MODALIAS` 与 `probe()` 日志。
6. **读写属性**:`cat/echo` 到 `/sys/...`;或通过子系统工具(如 `ethtool`、`hwmon`)。
---
## 15. 结论与展望
* **结论**:/sys 里的设备目录与属性并非 DT 的“直接投影”,而是 **“DT → 设备对象 → 驱动 `probe()` → 设备模型/kobject → sysfs”** 的链式产物。DT 提供**硬件事实**,驱动把事实转成**可操作对象**,设备模型把对象**可视化**给用户空间。
* **展望**:Overlay 提供运行时的硬件描述演进能力;标准化子系统使属性接口更一致;数据驱动的范式让“主线内核 + 板级 DTB”成为主流工程实践。
---
## 附录 A:常见问题(FAQ)
* **Q**:看到了 `/sys/firmware/devicetree/base/...`,但 `/sys/devices/...` 没有对应设备?
**A**:多半是 `status="disabled"`、`compatible` 无匹配、或依赖(clk/regulator/phy)未就绪导致 `-EPROBE_DEFER`。
* **Q**:为何属性名/目录名与 DTS 不完全一致?
**A**:/sys 命名来自设备模型与驱动实现(class/bus/device 命名规则),不是 DTS 的逐字镜像。
* **Q**:如何确认模块自动加载?
**A**:`udevadm monitor` 看 `MODALIAS`,`modinfo` 看别名是否覆盖 `compatible`。
---
如果你愿意,把你的 **DTS 片段 + dmesg 启动日志 + /sys 树截图**发我,我可以给你做一版**板级 bring-up 对照表**(哪一条属性被谁消费,在哪个时刻生成哪个 /sys 节点),并附上**定制的驱动骨架**与**调试脚本**,直接可用于发表与项目交付。