2. SPI协议详解与驱动框架

SPI,全称Serial Peripheral Interface,摩托罗拉在80年代搞出来的东西。说白了就是四根线搞定板级通信,简单、快速、可靠。我这些年做过的项目里,SPI出现的频率仅次于I2C,但论速度,SPI甩I2C好几条街。

不过,简单归简单,坑也不少。时序搞反了、模式配错了、设备树写漏了,板子就是死活不通。咱们这一章就把SPI从物理层到驱动层彻底捋一遍。

2.1 SPI物理层与时序基础

先看最基本的四根线:

信号 全称 方向 作用
SCLK Serial Clock Master → Slave 时钟,由主机产生
MOSI Master Out Slave In Master → Slave 主机输出,从机输入
MISO Master In Slave Out Slave → Master 从机输出,主机输入
CS Chip Select Master → Slave 片选,低电平有效

SPI是全双工的。什么意思?主机发数据的同时,从机也在往回发数据。每次传输,主从双方各移出一个bit,各移入一个bit。所以一次SPI传输,本质上是一个环形移位寄存器的交换过程。

核心要点:SPI没有应答机制,主机不知道从机是否真的收到了数据。这是SPI和I2C最大的区别之一。所以SPI从机的时序必须严格对齐,差一个时钟周期都不行。

2.2 四种模式——CPOL与CPHA

这是SPI最容易出问题的地方。我见过太多人板子调不通,最后发现是模式配错了。

两个参数决定模式:

  • CPOL(Clock Polarity):时钟空闲时的电平。0=低电平空闲,1=高电平空闲。
  • CPHA(Clock Phase):数据采样沿。0=第一个跳变沿采样,1=第二个跳变沿采样。

组合起来就是四种模式:

模式 CPOL CPHA 空闲时钟电平 数据采样沿
Mode 0 0 0 低电平 上升沿
Mode 1 0 1 低电平 下降沿
Mode 2 1 0 高电平 下降沿
Mode 3 1 1 高电平 上升沿

实际项目中,Mode 0和Mode 3最常见。为什么?因为很多SPI Flash、ADC、DAC芯片默认就支持这两种模式。Mode 0更是事实上的"通用模式"。

我的经验:拿到一个新芯片,先看数据手册里的时序图。找到SCLK的波形,看空闲时是高还是低(确定CPOL),再看数据是在哪个沿被采样的(确定CPHA)。别靠猜,一定要看手册。

我曾经在一个项目里,把一颗SPI NOR Flash配成了Mode 2,结果读ID一直返回0xFF。折腾了两天,最后发现是CPOL搞反了。从那以后,我每次写设备树都会再三确认从机的模式。

2.3 SPI时序分析——以Mode 0为例

咱们拿Mode 0画个时序图看看:

SPI Mode 0 时序图(CPOL=0, CPHA=0) CS 片选拉低,传输开始 SCLK ▲ 采样 ▲ 采样 ▲ 采样 MOSI bit7 bit6 bit5 bit4 主机在上升沿前准备好数据 MISO bit7 bit6 bit5 从机在下降沿更新数据 Mode 0:空闲低电平,上升沿采样,下降沿更新。这是最常用的模式。

看到没?在Mode 0下:

  • SCLK空闲时是低电平
  • 主机在上升沿采样MISO上的数据
  • 从机在下降沿更新MOSI上的数据

换句话说,主机在时钟的"前半段"读数据,从机在"后半段"写数据。这样错开半个周期,保证数据稳定。

注意:SPI的时序参数不只是模式。还有SCLK频率、建立时间(tSU)、保持时间(tH)、CS到SCLK的延迟等。高速SPI(比如50MHz以上)对PCB走线长度、信号完整性都有要求。我遇到过SPI跑20MHz没问题,一提到40MHz就丢数据,最后发现是走线太长导致信号反射。

2.4 Linux SPI子系统驱动框架

Linux内核的SPI子系统分三层,我习惯这么理解:

Linux SPI子系统三层架构 SPI设备驱动层 spi_driver / spi_device SPI核心层 spi.c / spi.h / 总线注册与匹配 SPI控制器驱动层 spi_master / 硬件寄存器操作 数据流向:设备驱动 → 核心层 → 控制器驱动 → SPI硬件 配置流向:设备树 → 核心层 → 匹配设备驱动与控制器

这三层各司其职:

  • 控制器驱动层:直接操作SPI控制器的寄存器。比如i.MX6的ecspi、STM32的spi、Zynq的spi。这一层跟硬件绑定,每个SoC都不一样。
  • SPI核心层:提供通用的框架机制。包括spi_bus_type、spi_alloc_master()、spi_register_master()、spi_sync()等。这一层你不用改,内核已经写好了。
  • 设备驱动层:你写的就是这一层。比如SPI Flash驱动、SPI ADC驱动、SPI LCD驱动。你只需要实现probe、remove和具体的读写函数。

设备驱动层的核心数据结构是spi_driver:

static struct spi_driver my_spi_driver = {
    .driver = {
        .name   = "my_spi_device",
        .of_match_table = my_spi_of_match,
    },
    .probe  = my_spi_probe,
    .remove = my_spi_remove,
};

module_spi_driver(my_spi_driver);

probe函数里你会拿到一个struct spi_device *spi指针,通过它设置模式、频率、字长:

static int my_spi_probe(struct spi_device *spi)
{
    spi->mode = SPI_MODE_0;
    spi->max_speed_hz = 10 * 1000 * 1000;  // 10MHz
    spi->bits_per_word = 8;
    spi_setup(spi);

    // 然后就可以用 spi_write() / spi_read() / spi_sync() 通信了
    ...
}

我个人的习惯:probe函数里一定要先读一次从机的ID寄存器,确认通信正常再往下走。如果ID读不对,直接返回-ENODEV。这样能尽早暴露问题,而不是等到后面莫名其妙地挂掉。

2.5 设备树配置——让内核认识你的SPI设备

设备树是Linux下描述硬件的主要方式。SPI设备的设备树节点长这样:

&ecspi1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_ecspi1>;
    cs-gpios = <&gpio4 9 GPIO_ACTIVE_LOW>;
    status = "okay";

    spiflash: flash@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;              // 片选索引,对应第0个CS
        spi-max-frequency = <40000000>;
        spi-cpol;               // CPOL=1,如果配Mode 3
        spi-cpha;               // CPHA=1,如果配Mode 3
        #address-cells = <1>;
        #size-cells = <1>;
    };

    adc@1 {
        compatible = "ti,ads1256";
        reg = <1>;              // 片选索引,对应第1个CS
        spi-max-frequency = <10000000>;
        // 不写spi-cpol/spi-cpha,默认Mode 0
    };
};

几个关键点:

  • reg属性决定挂在哪个片选上。0对应CS0,1对应CS1,以此类推。
  • spi-max-frequency是从机支持的最大频率,实际频率由控制器和从机协商决定。
  • spi-cpol和spi-cpha是可选属性。不写的话默认Mode 0。
  • cs-gpios可以用GPIO模拟片选,灵活性更高。

我曾经踩过的坑:设备树里配了spi-cpol和spi-cpha,但驱动里又用spi->mode重新设了一遍。结果设备树和驱动各管各的,最后模式冲突,通信失败。记住:设备树和驱动二选一,别两边都设。我个人建议在设备树里配,驱动里只做校验。

2.6 驱动开发流程——从设备树到读写

一个完整的SPI设备驱动开发流程,我总结为五步:

  1. 确认硬件连接:用示波器或逻辑分析仪看SCLK、MOSI、MISO、CS的波形。确保电平匹配(3.3V还是1.8V?)。
  2. 编写设备树节点:配好compatible、reg、spi-max-frequency、模式属性。
  3. 编写驱动probe函数:设置spi_device参数,注册字符设备或输入设备等。
  4. 实现读写函数:使用spi_write()、spi_read()或spi_sync()进行数据传输。
  5. 验证通信:读从机ID寄存器,确认数据正确。

一个简单的读写示例:

/* 发送命令并读取响应 */
static int spi_cmd_read(struct spi_device *spi, u8 cmd, u8 *buf, int len)
{
    int ret;
    struct spi_transfer t = {
        .tx_buf = &cmd,
        .rx_buf = buf,
        .len    = len + 1,   // 第一个字节发命令,后面收数据
        .cs_change = 0,
    };
    struct spi_message m;

    spi_message_init(&m);
    spi_message_add_tail(&t, &m);

    ret = spi_sync(spi, &m);
    if (ret < 0)
        dev_err(&spi->dev, "spi_sync failed: %d\n", ret);

    return ret;
}

这里要注意,spi_sync是同步接口,会阻塞直到传输完成。如果在中断上下文或需要高性能的场景,用spi_async。

性能提示:SPI传输的瓶颈往往不在SPI总线速度,而在CPU处理中断和DMA的开销。如果数据量大,一定要用DMA模式。在设备树里加dmas属性,驱动里用spi_async配合DMA,吞吐量能提升一个数量级。

2.7 本章小结

SPI协议本身不复杂,但细节决定成败。模式配错、时序不对、设备树写漏,任何一个环节出问题,板子就是黑的。我建议你每次调SPI,都按这个顺序排查:

  • 先看波形——逻辑分析仪是最好的老师
  • 再对模式——CPOL/CPHA必须和从机手册一致
  • 再看频率——别超过从机规格
  • 最后看驱动——spi_setup()有没有调用,返回值有没有检查

把这套流程走熟了,SPI对你来说就是小菜一碟。