第4章 设备树基础:语法入门与SPI节点配置

设备树(Device Tree)这东西,说白了就是一块“硬件描述板”。你告诉内核:我板子上挂了什么外设,地址在哪,中断用几号。内核拿到这个描述,就能自动把驱动绑上去。

我刚接触设备树时,总觉得它很玄乎。后来调一块SPI Flash,死活读不到ID,最后发现是reg属性少写了一个地址单元。嗯,从那以后,我对设备树的每个字段都格外小心。

4.1 设备树语法速览

设备树的语法其实不复杂。核心就几个概念:节点、属性、值。我们直接看一个最小结构:

/dts-v1/;

/ {
    compatible = "allwinner,sun8i-t113";
    
    soc {
        compatible = "simple-bus";
        
        spi0: spi@04006000 {
            compatible = "allwinner,sun8i-spi";
            reg = <0x04006000 0x1000>;
            interrupts = <0 11 4>;
            status = "disabled";
        };
    };
};

这里有几个关键点:

  • 节点(Node):每个硬件外设就是一个节点,用花括号包裹
  • 属性(Property):节点里的键值对,描述硬件特征
  • 标签(Label):spi0: 这种写法,方便其他地方引用
  • 地址单元:reg = <地址 长度>,地址和长度各占几个32位,由父节点决定
我的习惯:写设备树时,先把芯片手册的地址映射表放在手边。reg属性的地址值,必须和手册里的基地址完全一致,差一个字节都不行。

4.2 SPI节点结构拆解

全志T113的SPI控制器,在设备树里长这样。我拿一个实际项目中的节点做例子:

spi1: spi@04007000 {
    compatible = "allwinner,sun8i-t113-spi", "allwinner,sun8i-spi";
    reg = <0x04007000 0x1000>;
    interrupts = <GIC_SPI 12 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&ccu CLK_BUS_SPI1>, <&ccu CLK_SPI1>;
    clock-names = "ahb", "mod";
    resets = <&ccu RST_BUS_SPI1>;
    dmas = <&dma 22>, <&dma 23>;
    dma-names = "rx", "tx";
    status = "disabled";
    #address-cells = <1>;
    #size-cells = <0>;
    
    spiflash@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;
        spi-max-frequency = <50000000>;
    };
};

我们来逐行拆解:

属性 含义 我的经验
compatible 驱动匹配字符串,内核靠它找驱动 第一个是芯片专用,第二个是系列通用,驱动匹配时逐个尝试
reg 寄存器基地址和范围 0x04007000是SPI1基址,0x1000是4KB空间,别写小了
interrupts 中断号、触发类型 T113用GIC_SPI宏,12号中断,高电平触发
clocks 时钟源,通常两个:AHB总线和模块时钟 漏掉一个时钟,SPI直接不工作,我踩过这个坑
dmas DMA通道,rx和tx各一个 不用DMA可以删掉,但大块数据传输时CPU占用率会很高
#address-cells 子节点reg地址占几个32位 SPI子设备用片选号做地址,所以是1
#size-cells 子节点reg长度占几个32位 片选号没有长度概念,所以是0
注意:#address-cells和#size-cells这两个属性,直接影响子节点reg的解析方式。如果写错了,内核解析子设备地址时会完全错位。我曾经把#size-cells写成1,结果子节点的reg=<0>被解析成地址0、长度0,驱动直接报错。

4.3 compatible属性的匹配逻辑

compatible属性是设备树里最重要的属性之一。内核驱动通过它来识别硬件。匹配规则是这样的:

  1. 驱动注册时,会声明一个of_device_id表
  2. 内核遍历设备树节点,拿节点的compatible值去匹配驱动表
  3. 匹配顺序:先精确匹配,再尝试通配
  4. 匹配成功后,调用驱动的probe函数

看一个驱动里的匹配表例子:

static const struct of_device_id sun8i_spi_dt_ids[] = {
    { .compatible = "allwinner,sun8i-t113-spi", .data = &t113_spi_variant },
    { .compatible = "allwinner,sun8i-spi", .data = &sun8i_spi_variant },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, sun8i_spi_dt_ids);

这里有个细节:设备树节点里写了两个compatible值,驱动表里也有两个。内核匹配时,先拿节点第一个值"allwinner,sun8i-t113-spi"去匹配,如果驱动表里有,就用对应的data结构体。如果没有,再试第二个。

为什么写两个?这是为了兼容性。如果内核版本较老,不认识t113专用字符串,还能用sun8i通用字符串兜底。我在做BSP移植时,经常用这个技巧让同一份设备树兼容多个内核版本。

4.4 reg属性的地址编码规则

reg属性是设备树的“地址本”。它的编码规则由父节点的#address-cells和#size-cells决定。我们画一张图来理解:

reg属性编码规则 父节点:spi@04007000 #address-cells = <1> #size-cells = <0> 子节点:spiflash@0 reg = <0> → 片选号0 另一种常见配置 #address-cells = <1> #size-cells = <1> 子节点 reg = <0 0x1000000> 表示:片选0,地址空间16MB 适用于:SPI设备需要映射到 CPU地址空间的场景 (如SPI NAND、FPGA配置) 规则:子节点reg的地址单元数 = #address-cells,长度单元数 = #size-cells SPI子设备通常用片选号做地址,所以#size-cells = 0

这张图把reg的编码规则说清楚了。对于SPI子节点,reg的值就是片选号。比如reg = <0>表示片选0,reg = <1>表示片选1。因为#size-cells=0,所以不需要写长度。

避坑指南:如果你在SPI子节点里写了reg = <0 0x1000000>,但父节点的#size-cells=0,内核解析时会认为reg只有1个单元,也就是只读到0,后面的0x1000000会被忽略。这不是bug,是规则。我曾经在这个问题上浪费了半天,后来才发现是size-cells没对齐。

4.5 实战:配置一个完整的SPI设备节点

假设我们要在T113的SPI1上挂一个W25Q128 Flash。完整的设备树配置如下:

&spi1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&spi1_pins>;
    
    w25q128: flash@0 {
        compatible = "winbond,w25q128", "jedec,spi-nor";
        reg = <0>;
        spi-max-frequency = <80000000>;
        spi-cpol;
        spi-cpha;
        
        partitions {
            compatible = "fixed-partitions";
            #address-cells = <1>;
            #size-cells = <1>;
            
            boot@0 {
                label = "boot";
                reg = <0x000000 0x100000>;
            };
            kernel@100000 {
                label = "kernel";
                reg = <0x100000 0x500000>;
            };
            rootfs@600000 {
                label = "rootfs";
                reg = <0x600000 0xA00000>;
            };
        };
    };
};

这里有几个要点:

  • status = "okay":使能SPI控制器,默认是disabled
  • pinctrl:配置引脚复用,这个后面章节会详细讲
  • spi-max-frequency:最大时钟频率,W25Q128支持80MHz
  • spi-cpol/spi-cpha:时钟极性和相位,Flash通常用模式0或模式3
  • partitions:分区表,告诉内核Flash怎么划分
注意:spi-max-frequency这个值不是随便写的。我见过有人直接写200000000,结果SPI通信全是乱码。实际能跑多快,取决于PCB走线长度、信号完整性、以及从设备的能力。建议先用手册里的典型值,稳定后再慢慢往上调。