第一章:设备树基础概念
各位同学好,我是老张。做嵌入式Linux开发十几年了,今天咱们来聊聊设备树。
说实话,我刚入行那会儿,还没有设备树这东西。那时候改个硬件配置,得改一堆C语言代码,改完还得重新编译内核。有一次我改了个GPIO编号,编译内核花了快一个小时,结果发现改错了...那滋味,真不好受。
什么是设备树
设备树,英文叫Device Tree。说白了,它就是一个描述硬件信息的文本文件。
你想想看,一个嵌入式系统里有多少硬件?CPU、内存、I2C控制器、SPI控制器、GPIO、中断控制器...每个硬件都有它的地址、中断号、时钟频率等等。设备树就是把这些信息用树形结构组织起来。
我习惯把设备树理解成硬件的「身份证」。它告诉内核:
- 我是什么设备(比如:我是I2C控制器)
- 我在哪里(比如:我的寄存器基地址是0x10000000)
- 我怎么工作(比如:我支持100kHz和400kHz两种速率)
- 我和谁连接(比如:我的中断线连到了中断控制器的第5号输入)
核心要点:设备树是硬件描述和内核驱动之间的桥梁。它让驱动代码和硬件配置解耦。
为什么需要设备树
在设备树出现之前,Linux内核是怎么知道硬件信息的?
答案是:硬编码。每个板子都在arch/arm/mach-xxx目录下有个board-xxx.c文件,里面用C语言结构体描述硬件。比如:
static struct platform_device my_device = {
.name = "my-i2c",
.id = 0,
.num_resources = 2,
.resource = (struct resource[]) {
{
.start = 0x10000000,
.end = 0x10000FFF,
.flags = IORESOURCE_MEM,
},
{
.start = IRQ_I2C,
.end = IRQ_I2C,
.flags = IORESOURCE_IRQ,
},
},
};
这样做有什么问题?问题大了去了。
- 代码冗余:每个板子都要写类似的代码,改一个引脚就得改整个文件
- 维护困难:内核里充斥着各种板级文件,Linux 3.x时代光ARM平台就有上千个
- 升级麻烦:换一颗Flash芯片,得重新编译整个内核
我记得有一次,客户临时换了一颗不同型号的Flash。按老办法,我得改board文件、重新配置内核、编译、烧录...折腾了大半天。后来用设备树,改一行dts文件,重新编译设备树,5分钟搞定。
我的经验:设备树最大的价值在于「配置与代码分离」。硬件变了,改dts文件就行,驱动代码不用动。这在产品快速迭代时特别有用。
设备树在CPE系统中的角色
CPE设备,就是咱们常见的家庭网关、企业路由器这类产品。这类设备有个特点:硬件配置五花八门。
同一个方案,可能衍生出十几个型号。有的带WiFi 6,有的带5G模块,有的带VoIP口...如果用老办法,每个型号都得维护一套内核代码,那工程师得疯掉。
设备树在CPE系统中的角色,我总结为三点:
| 角色 | 说明 | 实际例子 |
|---|---|---|
| 硬件配置表 | 描述CPE板上所有外设的地址、中断、时钟等 | 定义GMAC的MDIO引脚、PHY地址 |
| 驱动匹配器 | 告诉内核该用哪个驱动来驱动哪个设备 | compatible = "realtek,rtl8367s" 匹配对应的switch驱动 |
| 运行时配置 | uboot可以修改设备树,实现不同型号的差异化 | 通过修改dts使能或禁用某个USB端口 |
嗯,这里要注意一点。CPE设备通常有多个以太网口、多个WiFi频段、多个USB口。设备树里要清晰地描述每个接口的对应关系。我曾经见过一个项目,因为设备树里把LAN口和WAN口的GPIO搞反了,导致整批设备出厂后才发现问题...那叫一个惨。
设备树的基本结构
设备树的源文件后缀是.dts,编译后的二进制文件是.dtb。它的结构就像一棵倒挂的树:
/dts-v1/;
/ {
model = "My CPE Router";
compatible = "vendor,cpe-router-v1";
chosen {
bootargs = "console=ttyS0,115200";
};
memory@0 {
device_type = "memory";
reg = <0x0 0x10000000>; // 256MB内存
};
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges;
uart0: serial@10000000 {
compatible = "ns16550a";
reg = <0x10000000 0x1000>;
interrupts = <0 5 4>;
clock-frequency = <50000000>;
};
i2c0: i2c@10001000 {
compatible = "vendor,i2c-controller";
reg = <0x10001000 0x1000>;
interrupts = <0 6 4>;
#address-cells = <1>;
#size-cells = <0>;
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
pagesize = <8>;
};
};
};
};
看到这个结构了吗?每个设备都是一个节点,节点里用属性来描述设备信息。compatible属性特别重要,它是驱动和设备匹配的关键。
避坑指南:我曾经在调试一个CPE项目时,发现I2C EEPROM死活读不到数据。查了两天才发现,是设备树里reg属性写错了地址。EEPROM的地址是0x50,我写成了0x05。这种低级错误,编译时不会报错,运行时就抓瞎了。
设备树在启动流程中的位置
CPE设备的启动流程大概是这样的:
- BootROM加载uboot
- uboot初始化基本硬件(DDR、串口等)
- uboot从Flash加载设备树(dtb)到内存
- uboot修改设备树(比如根据硬件版本调整配置)
- uboot加载内核,并把设备树地址传给内核
- 内核解析设备树,枚举设备,加载驱动
你看,设备树在uboot和内核之间扮演了信息传递的角色。uboot把硬件探测到的信息(比如内存大小、MAC地址)写进设备树,内核直接读取使用。
我个人习惯在uboot阶段加一些调试打印,确认设备树是否正确加载。比如:
# 在uboot命令行查看设备树
printenv fdtcontroladdr
fdt list /
这样能提前发现问题,不用等到内核启动失败再去排查。
小结
设备树不是什么高深的技术,它就是一套硬件描述规范。但它的出现,让嵌入式Linux开发效率提升了一大截。
说白了,设备树就是让搞硬件的人能专心改硬件,搞驱动的人能专心写驱动。各司其职,互不干扰。
下一章,咱们会深入设备树的语法细节,包括节点、属性、标签、引用这些概念。到时候我会拿一个真实的CPE设备树文件来拆解,保证让你看得明明白白。