Skip to content

第一百四十一章:设备树

合道期

涉及内核源码:

林小源在设备山脉的最高处,看到了一棵巨大的树。

树干从地面直插云霄,枝干向四面八方伸展,每根枝条上都挂着发光的果实。果实里嵌着各种设备的缩影——UART 控制器、I2C 控制器、GPIO 引脚、时钟源、中断控制器。

"这是什么树?"林小源仰头望着,被它的宏伟震撼到了。

"设备树。"守山老者说,"Device Tree——描述整个硬件系统的数据结构。"

林小源走近树干,发现树皮上刻着一种他从未见过的语法:

/ {
  cpus {
    cpu@0 { ... };
  };
  memory { reg = <0x40000000 0x20000000>; };
  soc {
    uart@10000000 {
      compatible = "ns16550";
      reg = <0x10000000 0x100>;
      interrupts = <10>;
      clock-frequency = <24000000>;
    };
  };
};

"这就是设备树的源文件——DTS。"老者说,"用一种类 C 的语法描述硬件结构。每个节点代表一个设备或总线,节点的属性描述设备的特征——寄存器地址、中断号、时钟频率、compatible 字符串。"

c
/*
 * 设备树的结构:
 *
 * / {
 *   cpus {
 *     cpu@0 { ... };
 *   };
 *   memory { reg = <0x40000000 0x20000000>; };
 *   soc {
 *     uart@10000000 {
 *       compatible = "ns16550";
 *       reg = <0x10000000 0x100>;
 *       interrupts = <10>;
 *       clock-frequency = <24000000>;
 *     };
 *   };
 * };
 *
 * 设备树的关键属性:
 *   compatible — 匹配驱动
 *   reg — 寄存器地址
 *   interrupts — 中断号
 *   clocks — 时钟
 *
 * 设备树的使用:
 *   DTS (源文件) → DTC (编译器) → DTB (二进制)
 *   内核在启动时解析 DTB
 */

printf("=== 设备树 — 硬件的描述语言 ===\n\n");

printf("设备树的结构:\n\n");
printf("/ {\n");
printf("  cpus {\n");
printf("    cpu@0 { ... };\n");
printf("  };\n");
printf("  memory {\n");
printf("    reg = <0x40000000 0x20000000>;\n");
printf("  };\n");
printf("  soc {\n");
printf("    uart@10000000 {\n");
printf("      compatible = \"ns16550\";\n");
printf("      reg = <0x10000000 0x100>;\n");
printf("      interrupts = <10>;\n");
printf("    };\n");
printf("  };\n");
printf("};\n\n");

printf("--- 关键属性 ---\n");
printf("compatible:\n");
printf("  匹配驱动\n");
printf("  \"vendor,device\"\n\n");
printf("reg:\n");
printf("  寄存器地址和大小\n\n");
printf("interrupts:\n");
printf("  中断号\n\n");
printf("clocks:\n");
printf("  时钟源\n\n");

printf("--- 编译流程 ---\n");
printf("DTS (源文件)\n");
printf("  ↓ dtc (设备树编译器)\n");
printf("DTB (二进制)\n");
printf("  ↓ 内核启动时解析\n");
printf("内核设备树\n\n");

printf("--- 设备树 vs 硬编码 ---\n");
printf("硬编码:\n");
printf("  设备信息在代码中\n");
printf("  修改需要重新编译\n\n");
printf("设备树:\n");
printf("  设备信息在 DTB 中\n");
printf("  修改 DTB 即可\n");
printf("  同一内核支持多种硬件\n\n");

printf("--- OF API ---\n");
printf("of_find_node_by_name(): 查找节点\n");
printf("of_property_read_u32(): 读取属性\n");
printf("of_irq_get(): 获取中断号\n");
printf("of_clk_get(): 获取时钟\n");

"compatible 属性……"林小源盯着树干上那个 UART 节点,"它看起来就是一串字符串——"ns16550"。这怎么就能匹配驱动了?"

"compatible 是设备树中最关键的属性。"老者走到树干旁,指着那串字符串,"它告诉内核:'我是谁,我兼容哪个驱动。'驱动在注册时会声明一个 ,表里列着它支持的 compatible 字符串。内核遍历设备树节点,把 compatible 字符串跟驱动的 of_match_table 一一比较。匹配上了,就调用 probe。"

"一个设备可以有多个 compatible?"

"对。"老者说,"compatible 是一个字符串列表,按优先级排列。比如 compatible = "vendor,my-uart", "ns16550"——意思是'我是 vendor 公司的 my-uart,但我兼容 ns16550 驱动'。如果有专门的 my-uart 驱动,优先用它;如果没有,退而求其次用 ns16550 驱动。"

林小源明白了:compatible 是设备和驱动之间的"通用语言"。设备用它说"我能被谁驱动",驱动用它说"我能驱动谁"。总线在中间做匹配。

"为什么要把硬件信息放在设备树里,而不是写在代码里?"林小源问了一个他一直在想的问题。

老者叹了口气:"以前的 Linux 内核,确实是把硬件信息硬编码在代码里的。每换一块开发板,就要改一堆代码。同一个 UART 控制器,在这块板子上地址是 0x10000000,在那块板子上是 0x20000000——每块板子都要单独的代码。"

"那得维护多少代码啊。"

"所以设备树出现了。"老者说,"设备树把硬件信息从代码中分离出来。驱动代码只关心'我怎么操控 UART 控制器'——怎么读写寄存器、怎么处理中断。至于这个 UART 控制器的地址是多少、中断号是多少、时钟频率是多少——全由设备树描述。"

他拍了拍那棵大树的树干:"同一份驱动代码,配不同的 DTB,就能跑在不同的硬件上。你换了一块开发板,只需要修改设备树,重新编译 DTB,不用动驱动代码。这就是分离的威力。"

林小源仰望着那棵巨大的设备树,心中浮现出一个画面:DTS 源文件经过 dtc 编译器变成 DTB 二进制,DTB 被打包进 bootloader,内核启动时解析 DTB,在内存中构建设备树结构,然后遍历每个节点,创建对应的 platform_device,匹配驱动,调用 probe——整个硬件初始化流程,从一棵树开始。


道藏笔记

内核启示

设备树是描述硬件结构的数据结构。

设备树的关键属性:

  • — 匹配驱动
  • — 寄存器地址
  • — 中断号
  • — 时钟

编译流程:

  • DTS → DTB → 内核解析

设备树的优势:

  • 同一内核支持多种硬件
  • 修改 DTB 即可,不需要重新编译内核

设备树是"硬件的地图"——让内核了解硬件结构。


破关试炼

设备树之试

本章讲硬件信息不写死在驱动里,而是由哪种树状描述传给内核?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道