4. 设备树基础与SPI节点

设备树(Device Tree)这东西,刚接触时觉得挺玄乎。说白了,它就是一套描述硬件信息的“配置文件”。内核通过它才知道你板子上接了哪些外设、地址是多少、中断挂在哪条线上。

我刚开始做移植那会儿,还遇到过直接改板级文件(board-*.c)的老代码。后来设备树统一了江湖,所有平台都得按这套规矩来。全志T113也不例外,它的SPI控制器信息全写在dts里。

4.1 设备树语法速览

设备树的语法其实不复杂。一个节点就是一个设备,节点里放属性和值。看个最简单的例子:

/dts-v1/;

/ {
    model = "Allwinner T113 S3 Board";
    compatible = "allwinner,sun8i-t113";

    cpu@0 {
        compatible = "arm,cortex-a7";
        reg = <0x0>;
    };
};

这里有几个关键点:

  • /dts-v1/ — 版本声明,必须写在最前面
  • / — 根节点,整棵树从这里开始
  • model — 板子名称,字符串
  • compatible — 兼容性标识,内核靠它匹配驱动
  • reg — 地址信息,通常是基地址+长度

节点可以嵌套,可以引用。你想想看,一个SoC里有几十个外设,每个外设一个节点,树状结构管理起来确实清晰。

个人习惯:我写dts时喜欢把节点按功能分组——时钟放一起、中断放一起、外设放一起。这样后期查问题不用满文件翻。

4.2 T113设备树文件结构

全志T113的设备树文件分三层:

层级 文件名 作用
SoC级 sun8i-t113.dtsi 定义所有内置控制器,SPI、I2C、UART都在这里
板级 sun8i-t113-mangopi.dts 继承SoC级,使能具体外设、配置引脚
头文件 sun8i-t113.h / sun8i-t113.dtsi 宏定义、时钟索引、中断号

实际编译时,dtc会把.dts和.dtsi合并成一个完整的设备树。我在项目中遇到过有人直接在板级文件里重写SoC级节点,结果覆盖了关键配置,SPI死活不工作。嗯,这里要注意:板级文件只做“使能”和“引脚配置”,不要动SoC级的基础地址和中断。

4.3 SPI控制器节点详解

全志T113有4个SPI控制器。在sun8i-t113.dtsi里,SPI节点的定义长这样:

spi0: spi@04025000 {
    compatible = "allwinner,sun8i-t113-spi";
    reg = <0x04025000 0x1000>;
    interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <>;
    clock-names = "ahb", "mod";
    resets = <>;
    status = "disabled";
    #address-cells = <1>;
    #size-cells = <0>;
};

我来拆开讲几个关键属性:

  • compatible — 驱动匹配的关键字。内核里sun8i-t113-spi这个驱动就是靠它绑定的
  • reg — 基地址0x04025000,长度0x1000(4KB)。这就是SPI控制器的寄存器空间
  • interrupts — 中断号50,触发类型高电平。GIC_SPI表示这是共享外设中断
  • clocks — 两个时钟:AHB总线时钟和模块时钟。缺一个SPI都转不起来
  • status — 默认disabled,板级文件里改成okay才能用
我曾经踩过的坑:有一次移植SPI驱动,reg地址写错了最后一位,0x04025000写成了0x04025001。内核没报错,但读写寄存器全偏移了一个字节,数据全乱套。查了两天才发现是地址没对齐。地址必须按4字节对齐,这是硬件决定的。

4.4 reg属性与interrupts属性

这两个属性是设备树里最常用的,我单独拿出来说。

reg属性描述设备的地址空间。格式是:

reg = <address1 length1 address2 length2 ...>;

对于SPI控制器,通常只有一段地址空间。但有些设备有多个地址段,比如DMA控制器有描述符地址和配置地址。每个地址段由#address-cells和#size-cells决定宽度。T113里这两个值分别是1和1,所以每个地址段占两个32位数。

interrupts属性描述中断信息。T113使用GIC(通用中断控制器),格式是:

interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>;

三个字段的含义:

字段 值 说明
中断类型 GIC_SPI (0) 共享外设中断,多个CPU核都能响应
中断号 50 SPI0的中断号,硬件手册里查到的
触发方式 IRQ_TYPE_LEVEL_HIGH 高电平触发。SPI通常用电平触发,边沿触发容易丢中断

你想想看,如果中断号写错了,驱动注册中断时就会失败。我见过有人把SPI0的中断号写成SPI1的,结果两个设备抢同一个中断线,系统跑起来就死机。

4.5 在设备树中使能SPI节点

SoC级dtsi里SPI节点默认是disabled的。要在板级dts里使能它,需要做两件事:

  1. 引用节点 — 用&spi0引用SoC级定义的节点
  2. 覆盖属性 — 把status改成okay,配置引脚和片选

实际代码长这样:

&spi0 {
    pinctrl-names = "default";
    pinctrl-0 = <&spi0_pins>;
    status = "okay";

    spidev@0 {
        compatible = "rohm,dh2228fv";
        reg = <0>;
        spi-max-frequency = <50000000>;
    };
};

这里有几个细节:

  • pinctrl — 引脚复用配置,告诉SoC这些引脚要当SPI用,不是GPIO
  • spidev@0 — 子节点,代表挂在SPI总线上的设备。@0表示片选CS0
  • spi-max-frequency — 最大时钟频率,50MHz是T113 SPI控制器能跑的上限
核心原则:SoC级dtsi只描述“硬件有什么”,板级dts描述“硬件怎么用”。SPI控制器在SoC级定义好地址和中断,板级只管使能和配引脚。这个分层设计让代码复用性大大提高。

我习惯在使能SPI节点后,先编译dts生成dtb,再用dtc -I dtb -O dts反编译看看最终结果。这样能确认所有&spi0引用都正确展开了,避免编译时没报错但实际节点没生效的情况。

T113 SPI设备树节点结构 / (根节点) soc@01c00000 spi0: spi@04025000 compatible = "allwinner,sun8i-t113-spi" 关键属性 reg = <0x04025000 0x1000> interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH> clocks = <> status = "disabled" (SoC级) &spi0 { status = "okay"; } 板级dts覆盖使能 + 引脚配置 SoC级dtsi定义硬件资源(地址/中断/时钟),板级dts只做使能和引脚配置

这张图把T113 SPI设备树的层级关系画清楚了。从上到下依次是根节点、soc节点、spi0节点,再到属性和板级覆盖。你写dts时脑子里要有这个结构,知道每个属性该放在哪一层。