第4章 设备树包含机制:#include与/dts-v1/,公共dtsi文件的组织与复用技巧

设备树文件写多了你就会发现,很多内容其实是重复的。同一个SoC要支持好几款开发板,每款板子都得写一份dts,里面全是相同的外设节点、时钟配置、中断信息……复制粘贴?那简直是给自己挖坑。

设备树的包含机制就是来解决这个问题的。说白了,它跟C语言的#include差不多——把公共部分抽出来,放到.dtsi文件里,然后各板子按需引用。我最早接触设备树时,看到.dtsi还以为是写错了后缀,后来才明白这是设备树包含文件的约定。

核心原则:.dts是板级描述文件,.dtsi是SoC级或模块级公共描述文件。一个SoC对应一个.dtsi,多个板子共享它。

4.1 /dts-v1/ 版本声明

每个设备树源文件的第一行,必须是这个:

/dts-v1/;

这行声明告诉编译器:“我用的设备树语法是v1版本”。没有它,dtc会按旧版语法解析,很多新特性就用不了。我见过有人把.dtsi文件也加了这个声明,其实没必要——被包含的文件不需要版本声明,只有最顶层的.dts文件需要。

我的习惯:所有.dts文件第一行都写/dts-v1/;,.dtsi文件一律不写。这样一眼就能看出哪个是入口文件。

4.2 #include 的两种用法

设备树的#include有两种场景,别搞混了:

用法 包含对象 示例 说明
包含.dtsi 设备树片段 #include "imx6ull.dtsi" 复用SoC级描述
包含.h头文件 C语言宏定义 #include "imx6ull-pinfunc.h" 使用宏定义常量

注意:包含头文件时,dtc会先让C预处理器展开宏,再编译设备树。所以你可以这样写:

#include "imx6ull-pinfunc.h"

pinctrl_uart1: uart1grp {
    fsl,pins = <
        MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX  0x1b0b1
        MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX  0x1b0b1
    >;
};

这里的MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX就是一个宏,展开后是一串数字。没有头文件的话,你得手动查手册填数字,又容易写错又难看。

我曾经踩过的坑:头文件路径问题。dtc的-I参数指定搜索路径,如果头文件放得深,编译时找不到就会报错。我建议把所有.h和.dtsi统一放在一个include目录下,Makefile里加一行DTC_FLAGS += -I arch/arm/boot/dts/include,省心。

4.3 公共dtsi文件的组织策略

一个成熟的设备树项目,dtsi文件通常分三层:

dtsi 文件三层组织架构 SoC级 .dtsi imx6ull.dtsi / stm32mp157.dtsi 板级公共 .dtsi imx6ull-evk-common.dtsi / 内存配置.dtsi 板级 .dts(最终编译入口) imx6ull-myboard.dts / stm32mp157a-dk1.dts 说明: SoC级dtsi定义CPU、内存控制器、时钟、中断控制器等基础外设。 板级公共dtsi定义该系列板子共用的外设(如以太网、USB、SDIO)。 板级dts只做差异化配置(如GPIO复用、设备使能/禁用)。

我实际项目中,一个SoC的dtsi文件大概长这样:

// imx6ull.dtsi(简化版)
#include "dt-bindings/clock/imx6ul-clock.h"
#include "dt-bindings/interrupt-controller/irq.h"
#include "dt-bindings/gpio/gpio.h"

/ {
    #address-cells = <1>;
    #size-cells = <1>;

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

        cpu0: cpu@0 {
            compatible = "arm,cortex-a7";
            device_type = "cpu";
            reg = <0>;
            clocks = <&clks IMX6UL_CLK_ARM>;
        };
    };

    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";
        ranges;

        aips1: bus@2000000 {
            compatible = "fsl,aips-bus", "simple-bus";
            reg = <0x02000000 0x100000>;
            #address-cells = <1>;
            #size-cells = <1>;
            ranges;

            uart1: serial@2020000 {
                compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart";
                reg = <0x02020000 0x4000>;
                interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
                clocks = <&clks IMX6UL_CLK_UART1_IPG>,
                         <&clks IMX6UL_CLK_UART1_SERIAL>;
                clock-names = "ipg", "per";
                status = "disabled";
            };
        };
    };
};

注意看,这里所有外设的status都是"disabled"。为什么?因为SoC级dtsi是“最大公约数”,它不知道具体板子上哪些外设接了、哪些没接。板级dts里再逐个&uart1 { status = "okay"; };打开。

我建议的命名规范:

  • soc-name.dtsi — SoC级,如imx6ull.dtsi
  • soc-name-board-common.dtsi — 板级公共,如imx6ull-evk-common.dtsi
  • soc-name-board.dts — 最终板级,如imx6ull-myboard.dts

这样看文件名就知道文件的作用和层级,不用打开文件才知道。

4.4 复用技巧:增量覆盖与引用

设备树的复用,核心就两个操作:包含和覆盖。

包含我们已经说了。覆盖呢?就是在一个dts里,用&节点名语法修改另一个文件里定义的节点:

// imx6ull-myboard.dts
#include "imx6ull.dtsi"
#include "imx6ull-evk-common.dtsi"

/ {
    model = "My Custom Board";
    compatible = "mycompany,imx6ull-myboard", "fsl,imx6ull";
};

&uart1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart1>;
    status = "okay";
};

&usdhc1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_usdhc1>;
    bus-width = <4>;
    non-removable;
    status = "okay";
};

这里&uart1引用了imx6ull.dtsi里定义的uart1节点,然后添加了pinctrl配置,并把status改成okay。dtc在编译时会合并这两个地方的属性——同一个属性以后者为准,不同属性则叠加。

我曾经遇到的一个坑:覆盖节点时,如果父节点不存在,dtc会报错。比如你在dts里写&some_device { ... };,但所有被包含的dtsi里都没有定义some_device这个节点,编译就过不了。所以覆盖前一定要确认节点存在。

4.5 实战:组织一个项目的dtsi文件

假设我们要做一个基于i.MX6ULL的项目,有三款板子:基础版、工业版、带LCD版。我的文件组织方式是这样的:

文件名 内容 被谁包含
imx6ull.dtsi SoC所有外设定义,status=disabled 所有板级dts
imx6ull-common.dtsi 所有板子共用的内存、时钟、调试串口 所有板级dts
imx6ull-lcd.dtsi LCD控制器、背光、触摸屏节点 带LCD的板子
imx6ull-base.dts 基础版:包含common,使能基本外设 —
imx6ull-industrial.dts 工业版:包含common,使能CAN、RS485 —
imx6ull-lcd.dts LCD版:包含common和lcd.dtsi —

这样,如果SoC升级了,只需要改imx6ull.dtsi一个文件。如果所有板子都要加一个新外设,改imx6ull-common.dtsi就行。如果只有LCD版需要改,那就只动imx6ull-lcd.dtsi。

复用原则总结:

  • 把不变的放dtsi(SoC定义、固定外设)
  • 把变化的放dts(板级差异化、引脚复用)
  • 把可选的放独立dtsi(LCD、摄像头、WiFi模块)

4.6 关于#include路径的补充

最后说一个细节。设备树的#include支持两种路径写法:

  • 双引号:#include "imx6ull.dtsi" — 优先搜索当前目录,再搜索-I指定目录
  • 尖括号:#include <dt-bindings/gpio/gpio.h> — 只搜索-I指定目录

我个人习惯:包含同目录下的dtsi用双引号,包含系统级的头文件用尖括号。这样别人看代码时,一眼就能区分“这是项目自己的文件”还是“这是内核提供的文件”。

嗯,关于设备树的包含机制,核心就是这些。记住一句话:dtsi是骨架,dts是血肉。骨架搭好了,血肉怎么长都行。