3、SPI子系统详解:从协议到实战

SPI总线,说白了就是嵌入式世界里最常用的「短距离高速通道」。我做了这么多年驱动,接触过的传感器里,十有七八都走SPI。瑞芯微RK平台的SPI控制器设计得挺规整,但坑也不少。这一章咱们就把SPI子系统从头捋一遍,从协议到代码,再到调试手段,一次说透。

3.1 SPI总线协议回顾

SPI全称Serial Peripheral Interface,摩托罗拉发明的。四根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。全双工,一主多从,靠片选区分设备。

为什么SPI在传感器领域这么流行?

  • 速度快:轻松跑到几十MHz,比I2C快一个数量级
  • 全双工:收发同时进行,适合数据流场景
  • 硬件简单:没有地址仲裁,没有应答机制
  • 灵活:4种模式(CPOL/CPHA组合),兼容各种奇葩芯片

这里有个关键点——SPI模式。我见过太多人栽在这个上面。模式由时钟极性(CPOL)和时钟相位(CPHA)决定:

模式 CPOL CPHA 采样边沿 典型场景
模式0 0 0 上升沿采样 最常用,大部分传感器默认
模式1 0 1 下降沿采样 少数老器件
模式2 1 0 下降沿采样 某些ADI芯片
模式3 1 1 上升沿采样 部分无线芯片
避坑指南:我曾经调试一个陀螺仪,死活读不出正确数据。折腾了两天,最后发现是SPI模式配错了——芯片手册写的是模式1,我习惯性配了模式0。记住:先看手册配模式,别想当然。

3.2 RK平台SPI控制器驱动

瑞芯微的SPI控制器,在RK3568/RK3588这些芯片上,设计思路基本一致。核心是DesignWare SPI IP,Synopsys家的。驱动代码在drivers/spi/spi-dw-*.c里。

控制器驱动的职责很清晰:

  • 初始化硬件寄存器(时钟、模式、位宽、片选)
  • 管理DMA或PIO传输
  • 提供spi_controller结构体给上层
  • 处理中断和错误

我个人习惯,拿到一块新板子,先看设备树里SPI节点的配置:

// RK3588设备树SPI节点示例
&spi2 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&spi2m2_cs0 &spi2m2_pins>;
    max-freq = <50000000>;  // 50MHz,别超
    num-cs = <1>;

    sensor@0 {
        compatible = "vendor,xxx-sensor";
        reg = <0>;           // 片选0
        spi-max-frequency = <10000000>;  // 10MHz
        spi-cpha;            // 如果模式1,加上这个
        // spi-cpol;         // 模式2或3需要
    };
};

这里有个细节:max-freq是控制器支持的最高频率,spi-max-frequency是具体设备的工作频率。我建议先降频调试,比如从1MHz开始,通了再往上提。为什么?高频下信号完整性容易出问题,尤其是飞线连接传感器的时候。

3.3 SPI设备驱动编写模板

写一个SPI传感器驱动,核心就是填充struct spi_driver。我直接给模板,你们照着改就行:

#include <linux/spi/spi.h>
#include <linux/module.h>

static int sensor_probe(struct spi_device *spi)
{
    int ret;
    u8 tx_buf[2], rx_buf[2];

    // 1. 设置SPI参数
    spi->mode = SPI_MODE_0;          // 根据芯片手册
    spi->bits_per_word = 8;          // 大部分传感器8位
    spi->max_speed_hz = 10000000;    // 10MHz
    ret = spi_setup(spi);
    if (ret) {
        dev_err(&spi->dev, "spi_setup failed\n");
        return ret;
    }

    // 2. 读取设备ID验证通信
    tx_buf[0] = 0x0F;  // 读ID寄存器命令
    tx_buf[1] = 0x00;  // dummy byte
    ret = spi_write_then_read(spi, tx_buf, 1, rx_buf, 1);
    if (ret || rx_buf[0] != 0x5A) {
        dev_err(&spi->dev, "ID mismatch: got 0x%02x\n", rx_buf[0]);
        return -ENODEV;
    }

    dev_info(&spi->dev, "sensor detected, ID=0x%02x\n", rx_buf[0]);
    return 0;
}

static void sensor_remove(struct spi_device *spi)
{
    // 清理资源
}

static const struct of_device_id sensor_of_match[] = {
    { .compatible = "vendor,xxx-sensor" },
    { }
};
MODULE_DEVICE_TABLE(of, sensor_of_match);

static struct spi_driver sensor_driver = {
    .driver = {
        .name = "xxx_sensor",
        .of_match_table = sensor_of_match,
    },
    .probe = sensor_probe,
    .remove = sensor_remove,
};
module_spi_driver(sensor_driver);
我的经验:probe函数里第一件事就是读设备ID。如果ID不对,后面所有操作都是白费。我习惯把ID检查做成独立函数,方便调试时单独调用。

SPI数据传输有几种方式,我列个对比:

API 特点 适用场景
spi_write_then_read() 先写后读,自动处理CS 寄存器读写,最常用
spi_write() 只写,不读 配置寄存器、写数据
spi_read() 只读,需要先发dummy 批量数据读取
spi_sync_transfer() 自定义传输序列 复杂时序、DMA传输

3.4 SPI通信调试技巧

这部分是我最想分享的。调试SPI,说白了就是三板斧:

  1. 看波形:逻辑分析仪或示波器,抓SCLK、MOSI、MISO、CS
  2. 看返回值:驱动里加printk,打印每次传输的收发数据
  3. 降速测试:把频率降到1MHz以下,排除信号完整性问题

我遇到过最典型的故障场景:

  • CS拉不下来:检查GPIO复用,确认pinctrl配置正确
  • MISO一直高电平:传感器没上电,或者片选没选中
  • 数据全是0xFF:MOSI没通,或者模式配反了
  • 数据偶尔错位:时钟太快,或者走线太长

调试利器:spidev

如果不想写完整驱动,可以先在设备树里使能spidev:

// 设备树中
&spi2 {
    status = "okay";
    spidev@0 {
        compatible = "spidev";
        reg = <0>;
        spi-max-frequency = <10000000>;
    };
};

然后用户态用spi-tools或自己写ioctl程序,直接操作SPI。我经常用这招先验证硬件通路,再写内核驱动。

还有一个技巧——环回测试。把MOSI和MISO短接,发数据看能不能收回来。如果能,说明控制器和驱动没问题;如果不能,查硬件连接。

曾经踩过的坑:RK3588的SPI控制器,DMA传输时有个bug——如果传输长度不是4字节对齐,DMA会卡死。后来我查了社区补丁,才知道要在spi_dw_prepare_transfer_hardware()里加对齐检查。所以,遇到DMA问题,先试试PIO模式,排除法定位。

最后,我画了一张SPI子系统整体架构图,方便大家理解各层关系:

SPI子系统架构图 用户空间 (spidev / 应用程序) SPI核心层 (spi.c / spi-dev.c) SPI控制器驱动 (spi-dw-*.c / spi-rockchip.c) SPI控制器硬件 (DesignWare IP) SPI外设 (传感器 / ADC / 显示屏) 调试时从下往上查:先确认硬件波形,再检查控制器寄存器,最后看驱动返回值