第一百四十一章:设备树
合道期涉及内核源码:
一
林小源在设备山脉的最高处,看到了一棵巨大的树。
树干从地面直插云霄,枝干向四面八方伸展,每根枝条上都挂着发光的果实。果实里嵌着各种设备的缩影——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 字符串。"
/*
* 设备树的结构:
*
* / {
* 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");#include <stdio.h>
/*
* 设备树的结构:
*
* / {
* 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
*/
int main() {
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");
return 0;
}二
"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 即可,不需要重新编译内核
设备树是"硬件的地图"——让内核了解硬件结构。
设备树之试
本章讲硬件信息不写死在驱动里,而是由哪种树状描述传给内核?