【Linux驱动开发】第22天:SPI 设备树 + spi_driver

一、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 外设总线时钟与功能时钟,名称为pclkspi
#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. 代码核心说明

  1. 驱动匹配 :通过of_match_tablecompatible与设备树节点匹配,匹配成功后执行probe
  2. struct spi_device :对应一个SPI从设备,包含片选号、模式、时钟、所属控制器等全部信息,对应I2C的i2c_client
  3. spi_setup:配置SPI控制器的工作参数(模式、位宽、时钟),参数变更后必须调用。
  4. spi_write_then_read :最常用的封装API,自动构造spi_transferspi_message,完成一次先写后读事务,对应I2C的组合读写。
  5. 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. 验证步骤

  1. 编译设备树与驱动模块,烧录到开发板
  2. 加载驱动:insmod spi_demo.ko
  3. 查看内核日志:dmesg | grep spi,正常应打印probe信息与JEDEC ID
  4. 查看SPI总线设备:ls /sys/bus/spi/devices/,应出现对应从设备节点
相关推荐
WI8LbH7881 小时前
Ubuntu 部署Harbor
linux·运维·ubuntu
huainingning2 小时前
华三ACL单向TCP互通组网-通过Established状态回包实现
运维·网络·tcp/ip
researcher-Jiang2 小时前
高性能计算之MPI:第一次MPI并行程序设计练习
linux·运维·服务器
Wireless_wifi62 小时前
Why Choose IPQ9574 for Your WiFi 7 Solution
linux·人工智能·5g
Promise微笑2 小时前
工业微量水分监测:精密露点仪选型逻辑与行业应用实证深度报告
大数据·运维
MYMOTOE63 小时前
国内对标腾讯 WorkBuddy 的桌面 AI 智能体软件大全
linux
小c君tt3 小时前
linux学习笔记1
linux·笔记·学习
RisunJan3 小时前
Linux命令-read(Bash 内建读取输入)
linux
联盟分享专家4 小时前
垂直工具型 SaaS 的增长实战:如何把用户变成推广者?
运维
江畔柳前堤4 小时前
第16章:docker企业级实战综合项目
运维·git·安全·docker·容器·eureka