一、SPI设备树标准规范与片选机制
SPI设备树结构与I2C高度同源,采用控制器节点为父、从设备节点为子 的层级结构,核心差异在于reg属性的含义与片选配置逻辑。
1. 整体层级结构
SPI控制器节点(SoC级dtsi预定义,如SPI0)
├── 通用硬件属性(reg基地址、中断、时钟、pinctrl)
├── #address-cells = <1> // 子节点reg占1个cell
├── #size-cells = <0> // 子节点无地址空间
└── 从设备节点1(板级dts添加,如Flash@0)
├── compatible:与驱动匹配
├── reg:片选号(0/1/2...)
├── spi-max-frequency:最大时钟
└── 时序模式属性(spi-cpol/spi-cpha)
2. 控制器节点核心属性(SoC级dtsi定义)
RK3568的SPI控制器统一在rk3568.dtsi中预定义,板级DTS仅需启用并追加从设备。
| 属性 | 必选 | 说明 |
|---|---|---|
compatible |
是 | 固定为"rockchip,rk3568-spi",与SPI控制器驱动匹配 |
reg |
是 | 控制器寄存器基地址+地址空间大小 |
interrupts |
是 | SPI控制器中断号 |
clocks / clock-names |
是 | 外设总线时钟与功能时钟,名称为pclk、spi |
#address-cells |
是 | 固定为<1>,片选号用1个cell表示 |
#size-cells |
是 | 固定为<0>,SPI从设备无独立地址空间 |
cs-gpios |
否 | GPIO模拟片选时使用,数组形式,索引对应从设备reg值 |
3. 从设备节点核心属性(板级DTS添加)
| 属性 | 必选 | 说明 |
|---|---|---|
compatible |
是 | 与spi驱动的of_match_table匹配 |
reg |
是 | 片选通道号(非设备地址),对应控制器的第n个片选,从0开始。这是与I2C最核心的区别 |
spi-max-frequency |
是 | 设备支持的最大SPI时钟频率,单位Hz,实际运行不会超过该值 |
spi-cpol |
否 | 布尔属性,存在则CPOL=1(空闲时钟高电平),默认CPOL=0 |
spi-cpha |
否 | 布尔属性,存在则CPHA=1(第二个边沿采样),默认CPHA=0 |
spi-cs-high |
否 | 布尔属性,存在则片选高电平有效,默认低电平有效 |
spi-3wire |
否 | 布尔属性,使用3线模式(MOSI/MISO复用一根线) |
与I2C关键对比:I2C的
reg是7位从设备物理地址;SPI的reg是片选通道索引,仅用于选择哪个CS引脚。
4. 两种片选实现方式
方式1:硬件原生片选(推荐)
- 使用SPI控制器自带的CS引脚,由控制器硬件自动控制片选电平,时序精准、CPU开销小。
- RK3568每个SPI控制器支持2路硬件片选(CS0、CS1)。
- 配置方式:从设备
reg填0/1,pinctrl配置对应CS引脚,无需额外属性。
方式2:GPIO模拟片选
-
当硬件片选数量不足时,使用普通GPIO引脚模拟片选,由内核SPI核心层控制GPIO电平。
-
配置方式:控制器节点添加
cs-gpios属性,指定用哪些GPIO做片选,从设备reg对应数组索引。 -
示例:
dts&spi0 { cs-gpios = <&gpio0 RK_PA3 GPIO_ACTIVE_LOW>, // 第0路片选:GPIO0_A3 <&gpio0 RK_PB0 GPIO_ACTIVE_LOW>; // 第1路片选:GPIO0_B0 status = "okay"; flash@0 { reg = <0>; // 对应cs-gpios数组第0个GPIO // ...其他属性 }; };
二、RK3568 SPI设备树通用模板
前置说明
RK3568共4路SPI控制器(SPI0SPI3),每路原生支持2个硬件片选,默认时钟源为APB总线时钟。以下模板以SPI0为例,可直接复用至SPI1SPI3。
模板1:硬件原生片选(标准推荐)
包含pinctrl配置、控制器启用、W25Q系列Flash从设备,可直接复制到板级DTS使用。
dts
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/interrupt-controller/irq.h>
&pinctrl {
spi0 {
/omit-if-no-ref/
spi0_clk: spi0-clk {
rockchip,pins = <0 RK_PA0 RK_FUNC_1 &pcfg_pull_none>;
};
spi0_mosi: spi0-mosi {
rockchip,pins = <0 RK_PA1 RK_FUNC_1 &pcfg_pull_none>;
};
spi0_miso: spi0-miso {
rockchip,pins = <0 RK_PA2 RK_FUNC_1 &pcfg_pull_up>;
};
spi0_cs0: spi0-cs0 {
rockchip,pins = <0 RK_PA3 RK_FUNC_1 &pcfg_pull_none>;
};
};
};
&spi0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi0_clk &spi0_mosi &spi0_miso &spi0_cs0>;
/* W25Q128 SPI Flash 示例 */
flash@0 {
compatible = "winbond,w25q128", "jedec,spi-nor";
reg = <0>; // 硬件片选0
spi-max-frequency = <80000000>; // 最大时钟80MHz
spi-cpol; // CPOL=1
spi-cpha; // CPHA=1 → SPI模式3
status = "okay";
};
/* 可追加第二个从设备,使用CS1
sensor@1 {
compatible = "vendor,xxx-sensor";
reg = <1>;
spi-max-frequency = <10000000>;
status = "okay";
};
*/
};
模板2:GPIO模拟片选
当原生CS引脚被占用时,使用GPIO模拟片选的配置模板:
dts
&spi0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi0_clk &spi0_mosi &spi0_miso>; // 不配置硬件CS引脚
// GPIO模拟片选:2路片选分别用GPIO0_A3、GPIO0_B0
cs-gpios = <&gpio0 RK_PA3 GPIO_ACTIVE_LOW>,
<&gpio0 RK_PB0 GPIO_ACTIVE_LOW>;
oled@0 {
compatible = "solomon,ssd1306";
reg = <0>; // 对应第0路GPIO片选
spi-max-frequency = <10000000>; // 10MHz
// 模式0:无spi-cpol、spi-cpha
dc-gpios = <&gpio0 RK_PB1 GPIO_ACTIVE_HIGH>;
reset-gpios = <&gpio0 RK_PB2 GPIO_ACTIVE_LOW>;
status = "okay";
};
};
三、spi_driver最简可运行驱动代码
对应上述W25Q Flash设备,实现最简spi_driver框架:设备树匹配、probe初始化、SPI读写测试、remove清理。
1. 完整驱动代码(spi_demo.c)
c
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
/* 设备树匹配表 */
static const struct of_device_id spi_demo_of_match[] = {
{ .compatible = "winbond,w25q128" },
{ /* 哨兵 */ }
};
MODULE_DEVICE_TABLE(of, spi_demo_of_match);
/* 读取Flash JEDEC ID的测试函数 */
static int spi_demo_read_id(struct spi_device *spi)
{
int ret;
u8 tx_buf[4] = {0x9f, 0, 0, 0}; // 0x9f是JEDEC ID读取指令
u8 rx_buf[4] = {0};
/* 最简API:先写后读,内部自动封装spi_message */
ret = spi_write_then_read(spi, tx_buf, 1, rx_buf, 3);
if (ret < 0) {
dev_err(&spi->dev, "read JEDEC ID failed, ret=%d\n", ret);
return ret;
}
dev_info(&spi->dev, "JEDEC ID: 0x%02x 0x%02x 0x%02x\n",
rx_buf[0], rx_buf[1], rx_buf[2]);
return 0;
}
/* 设备匹配成功后执行 */
static int spi_demo_probe(struct spi_device *spi)
{
int ret;
dev_info(&spi->dev, "spi demo probe, max_freq=%dHz, mode=0x%x\n",
spi->max_speed_hz, spi->mode);
/* 可选:重新配置SPI参数(模式、位宽等),一般设备树已配置 */
spi->mode = SPI_MODE_3;
spi->bits_per_word = 8;
ret = spi_setup(spi);
if (ret < 0) {
dev_err(&spi->dev, "spi setup failed\n");
return ret;
}
/* 测试:读取Flash ID */
spi_demo_read_id(spi);
return 0;
}
/* 驱动卸载时执行 */
static void spi_demo_remove(struct spi_device *spi)
{
dev_info(&spi->dev, "spi demo remove\n");
}
/* SPI驱动结构体 */
static struct spi_driver spi_demo_driver = {
.driver = {
.name = "spi-demo",
.owner = THIS_MODULE,
.of_match_table = spi_demo_of_match,
},
.probe = spi_demo_probe,
.remove = spi_demo_remove,
};
/* 模块注册与注销 */
module_spi_driver(spi_demo_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Demo");
MODULE_DESCRIPTION("RK3568 SPI minimal driver demo");
2. 配套Makefile
makefile
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
obj-m += spi_demo.o
all:
make -C $(KERNELDIR) M=$(PWD) modules
clean:
make -C $(KERNELDIR) M=$(PWD) clean
3. 代码核心说明
- 驱动匹配 :通过
of_match_table的compatible与设备树节点匹配,匹配成功后执行probe。 struct spi_device:对应一个SPI从设备,包含片选号、模式、时钟、所属控制器等全部信息,对应I2C的i2c_client。spi_setup:配置SPI控制器的工作参数(模式、位宽、时钟),参数变更后必须调用。spi_write_then_read:最常用的封装API,自动构造spi_transfer和spi_message,完成一次先写后读事务,对应I2C的组合读写。module_spi_driver:一键注册/注销SPI驱动的宏,对应I2C的module_i2c_driver。
四、核心传输API与验证方法
1. 常用API分级
| API类型 | 函数 | 适用场景 |
|---|---|---|
| 便捷封装 | spi_write_then_read() |
先写指令/地址、再读数据,90%外设场景适用 |
| 便捷封装 | spi_write() / spi_read() |
纯写/纯读单段数据 |
| 标准接口 | spi_sync() |
自定义多段传输,需手动构造spi_message + spi_transfer |
| 异步接口 | spi_async() |
非阻塞传输,通过回调通知完成 |
2. 验证步骤
- 编译设备树与驱动模块,烧录到开发板
- 加载驱动:
insmod spi_demo.ko - 查看内核日志:
dmesg | grep spi,正常应打印probe信息与JEDEC ID - 查看SPI总线设备:
ls /sys/bus/spi/devices/,应出现对应从设备节点