当前位置: 首页 > news >正文

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 节点),并附上**定制的驱动骨架**与**调试脚本**,直接可用于发表与项目交付。

http://www.dtcms.com/a/321471.html

相关文章:

  • 【LLM实战】RAG高级
  • 从0到1开发剧本杀小程序:全流程指南与避坑指南
  • 使用 C# 通过 .NET 框架开发应用程序的安装与环境配置
  • 网吧在线选座系统|基于java和小程序的网吧在线选座小程序系统设计与实现(源码+数据库+文档)
  • [202403-E]春日
  • 小程序难调的组件
  • 悬赏任务系统网站兼职赚钱小程序搭建地推抖音视频任务拉新源码功能详解二开
  • LangChain学习笔记05——多模态开发与工具使用
  • react+echarts实现变化趋势缩略图
  • LabVIEW数字抽取滤波
  • 点播服务器
  • RabbitMQ 中无法路由的消息会去到哪里?
  • Spring AMQP 入门与实践:整合 RabbitMQ 构建可靠消息系统
  • Android12 Framework Sim卡pin与puk码解锁
  • 用LaTeX优化FPGA开发:结合符号计算与Vivado工具链(二)
  • Nature论文-预测和捕捉人类认知的基础模型-用大模型模拟人类认知
  • 麦芽:寻常食材的中医智慧 多炮制方式各显养生价值
  • 动态规划进阶:转移方程优化技巧全解
  • 安卓应用内WebView页面调试技巧
  • WPF 双击行为实现详解:DoubleClickBehavior 源码分析与实战指南
  • 政治社会时间线
  • Java 之 多态
  • UE5太空射击游戏入门(一):项目创建与飞船控制
  • HEVC视频扩展免费下载
  • ISL9V3040D3ST-F085C一款安森美 ON生产的汽车点火IGBT模块,绝缘栅双极型晶体管ISL9V3040D3ST汽车点火电路中的线圈驱动器
  • Redis对象编码
  • 分布式系统性能优化实战:从瓶颈定位到架构升级
  • J2000与WGS84坐标系
  • Docker--docker的学习
  • Visual Studio 2019 + Qt + MySQL 开发调试全过程问题详解