第一章:设备树概述——什么是设备树、为什么需要设备树、设备树在数据采集系统中的角色

各位同学,咱们今天正式开讲。做嵌入式开发这么多年,我见过太多工程师被设备树搞得焦头烂额。说实话,我刚接触那会儿也踩过不少坑。但等你真正理解了它,你会发现——设备树其实是个很优雅的设计。

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。结果采集的数据全是乱码。查了两天才发现是速率配高了。所以——设备树里的参数一定要跟芯片手册对一遍,别想当然。

下面这张图展示了设备树在数据采集系统中的整体架构:

设备树在数据采集系统中的角色 设备树 (.dts/.dtb) 内核设备树解析器 (of_* API) 传感器配置 总线拓扑 资源分配 传感器配置 • 地址/速率/分辨率 • 触发方式/滤波参数 • 校准系数/量程 总线拓扑 • SPI/I2C/UART挂载关系 • 片选/地址分配 • 时钟/速率配置 资源分配 • GPIO/中断号 • DMA通道/优先级 • 电源域/时钟域 数据采集驱动初始化 稳定可靠的数据采集

从这张图你能看到,设备树是整个数据采集系统的“配置中枢”。它把传感器参数、总线拓扑、系统资源这三块信息统一管理,驱动代码只需要按图索骥就行。

我个人觉得,设备树最大的价值在于——它让硬件配置变得可维护、可复用、可追溯。你想想看,一个数据采集项目可能要迭代好几版硬件,如果没有设备树,每次改版都得动驱动代码,测试工作量翻倍都不止。