第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文件通常分三层:
我实际项目中,一个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.dtsisoc-name-board-common.dtsi— 板级公共,如imx6ull-evk-common.dtsisoc-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是血肉。骨架搭好了,血肉怎么长都行。