第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属性是设备树里最重要的属性之一。内核驱动通过它来识别硬件。匹配规则是这样的:
- 驱动注册时,会声明一个
of_device_id表 - 内核遍历设备树节点,拿节点的compatible值去匹配驱动表
- 匹配顺序:先精确匹配,再尝试通配
- 匹配成功后,调用驱动的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子节点,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走线长度、信号完整性、以及从设备的能力。建议先用手册里的典型值,稳定后再慢慢往上调。