Linux 启动流程实战:Device Tree 全解析与驱动绑定机制


适用平台:以  ARM/ARM64/RISC-V  等  Device Tree(DT)  主导的 SoC 为主
目标读者:做板级移植、驱动开发、内核定制的嵌入式工程师。
产出导向:给你一条能“从位到物”的 可操作路径 ——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(现代)

传递方式 r2 指向 ATAGS寄存器指向 DTB
描述能力少量基础信息完整硬件拓扑与资源
维护模式硬编码进内核 数据驱动,解耦
数据结构标签链表扁平化树(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 温度传感器如何“走完流程”


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. 关键函数路线图(把“黑盒”拆开看)

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 版)


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`。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值