第一章:设备树概述——什么是设备树、为什么需要设备树、设备树在数据采集系统中的角色
各位同学,咱们今天正式开讲。做嵌入式开发这么多年,我见过太多工程师被设备树搞得焦头烂额。说实话,我刚接触那会儿也踩过不少坑。但等你真正理解了它,你会发现——设备树其实是个很优雅的设计。
1.1 什么是设备树
设备树(Device Tree),说白了就是一份描述硬件信息的“说明书”。它用树形结构告诉操作系统:你的板子上有哪些外设、它们挂在哪条总线上、地址是多少、中断号是什么、需要哪些配置参数。
举个例子。你写一个Linux驱动,总得知道UART3的寄存器基地址是0x1000A000还是0x1000B000吧?传统做法是把这些信息硬编码在C代码里。但问题来了——换一块板子,硬件布局变了,你得改代码重新编译。设备树就是来解决这个问题的。
核心概念:设备树将硬件描述与内核代码分离。同一份内核镜像,搭配不同的设备树文件,就能适配不同的硬件平台。
设备树的源文件是.dts(Device Tree Source),编译后生成.dtb(Device Tree Blob)二进制文件。内核启动时,Bootloader把.dtb加载到内存,内核解析它,就知道怎么初始化硬件了。
一个典型的设备树节点长这样:
/ {
soc {
uart3: serial@1000A000 {
compatible = "ns16550";
reg = <0x1000A000 0x1000>;
interrupts = <0 33 4>;
clock-frequency = <1843200>;
status = "okay";
};
};
};
你看,compatible告诉内核用哪个驱动,reg指定地址和范围,interrupts描述中断信息。嗯,这里要注意——status = "okay"表示启用这个设备,如果改成"disabled",内核就不会去碰它。
1.2 为什么需要设备树
我刚开始做嵌入式Linux时,用的是ARM平台。那时候还没有设备树,所有硬件信息都写在arch/arm/mach-xxx目录下的板级文件里。每个板子一个.c文件,里面全是platform_device结构体、resource数组、i2c_board_info……
你想想看,一个SoC可能有几十种开发板,每种板子都得写一份板级文件。而且这些文件里90%的内容都是重复的——只是改了几个地址和中断号。维护起来简直是噩梦。
设备树解决了三个核心痛点:
| 痛点 | 传统方式 | 设备树方式 |
|---|---|---|
| 硬件变更 | 修改C代码,重新编译内核 | 修改.dts,重新编译.dtb |
| 多板支持 | 每个板子一份板级文件 | 一份内核镜像 + 多个.dtb |
| 代码耦合 | 硬件信息与驱动代码紧耦合 | 硬件描述与驱动代码完全分离 |
我记得有一次,客户临时换了一颗传感器,I2C地址从0x48改成了0x49。传统做法得改驱动代码、重新编译、烧录。用设备树的话,我只需要在.dts里改一行reg = <0x49>;,然后重新编译.dtb,重启就完事了。前后不到两分钟。
个人经验:我建议你在项目初期就把设备树文件纳入版本管理。别等到硬件改版了才想起来——那时候你可能已经忘了当初为什么把某个GPIO配成上拉了。
1.3 设备树在数据采集系统中的角色
数据采集系统,说白了就是“从传感器拿数据,处理后存起来或传出去”。这类系统对硬件配置的灵活性要求很高——今天用这个温度传感器,明天可能换另一个;今天用SPI接口的ADC,明天可能换成I2C的。
设备树在数据采集系统里扮演三个关键角色:
- 传感器配置中心——所有传感器的地址、速率、触发方式都在设备树里定义
- 总线拓扑描述——SPI、I2C、UART等总线上的设备挂载关系一目了然
- 资源分配清单——GPIO、DMA、中断等系统资源的分配情况
举个例子,一个典型的数据采集节点,设备树可能是这样的:
/ {
model = "DAQ-Node-3000";
compatible = "acme,daq-node";
i2c@40012000 {
#address-cells = <1>;
#size-cells = <0>;
reg = <0x40012000 0x1000>;
temperature-sensor@48 {
compatible = "ti,tmp117";
reg = <0x48>;
#io-channel-cells = <1>;
};
humidity-sensor@40 {
compatible = "ti,hdc2080";
reg = <0x40>;
};
};
spi@40013000 {
#address-cells = <1>;
#size-cells = <0>;
reg = <0x40013000 0x1000>;
adc@0 {
compatible = "ti,ads131a04";
reg = <0>;
spi-max-frequency = <20000000>;
#io-channel-cells = <1>;
};
};
};
你看,温度传感器挂在I2C总线上,地址0x48;ADC挂在SPI总线上,片选0,速率20MHz。这些信息一目了然。驱动代码只需要读取设备树节点,就能拿到所有配置参数。
我曾经踩过的坑:有一次我把SPI的spi-max-frequency写成了20000000(20MHz),但实际ADC芯片最高只支持10MHz。结果采集的数据全是乱码。查了两天才发现是速率配高了。所以——设备树里的参数一定要跟芯片手册对一遍,别想当然。
下面这张图展示了设备树在数据采集系统中的整体架构:
从这张图你能看到,设备树是整个数据采集系统的“配置中枢”。它把传感器参数、总线拓扑、系统资源这三块信息统一管理,驱动代码只需要按图索骥就行。
我个人觉得,设备树最大的价值在于——它让硬件配置变得可维护、可复用、可追溯。你想想看,一个数据采集项目可能要迭代好几版硬件,如果没有设备树,每次改版都得动驱动代码,测试工作量翻倍都不止。