4、SPI协议栈实现:SPI总线协议与四种模式、Linux SPI子系统架构、SPI设备驱动与DMA传输

SPI,全称是Serial Peripheral Interface。说白了,就是一根时钟线,两根数据线,再加一根片选线。简单,但坑不少。我最早接触SPI是在一个传感器项目上,当时觉得这协议太简单了,结果调试了整整两天——嗯,就是模式没配对。

4.1 SPI总线协议与四种模式

SPI是主从结构。主设备负责产生时钟,从设备被动响应。数据线有两根:MOSI(主出从入)和MISO(主入从出)。片选线CS,低电平有效,谁拉低谁说话。

SPI的四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。我见过不少工程师在这上面翻车,包括我自己。

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

模式0和模式3最常用。为什么?因为很多Flash和传感器默认就支持这两种。我个人习惯先用模式0,如果通信不正常,再切模式3试试。

我曾经踩过的坑: 主设备和从设备的模式必须一致。不一致时,数据全错,但波形看起来好像又有点对。别问我怎么知道的。

SPI的传输速率,理论上可以很高。但实际受限于PCB走线、从设备响应时间、以及驱动能力。我一般先看从设备数据手册里的最大时钟频率,然后留20%余量。

4.2 Linux SPI子系统架构

Linux的SPI子系统,分三层:

  • SPI核心层:提供总线注册、设备枚举、数据传输的通用框架。
  • SPI控制器驱动:操作具体的硬件寄存器,比如配置时钟、模式、片选。
  • SPI设备驱动:面向具体外设,比如Flash、ADC、传感器。

你想想看,这三层分工很明确。核心层帮你把脏活累活都干了,你只需要写设备驱动,或者控制器驱动。

下面这张图,是我画的一个SPI子系统架构图,方便你理解层级关系。

Linux SPI 子系统架构 用户空间 (User Space) 内核空间 (Kernel Space) SPI 核心层 (SPI Core) SPI 控制器驱动 SPI 设备驱动 硬件层 (SPI 控制器 + 外设) 说明:设备驱动通过核心层提供的接口,间接操作控制器驱动,完成数据传输

核心层提供的关键数据结构是 spi_device 和 spi_message。你写驱动时,主要就是填充这两个结构体。

关键点: spi_message 里可以挂多个 spi_transfer,实现连续传输。这在操作Flash时特别有用——先发命令,再发地址,最后读数据,一次搞定。

4.3 SPI设备驱动与DMA传输

写一个SPI设备驱动,其实不复杂。核心步骤就这几步:

  1. 在设备树里描述SPI从设备:片选引脚、模式、最大频率。
  2. 注册一个spi_driver,实现probe和remove回调。
  3. 在probe里,用spi_setup配置模式、速率。
  4. 用spi_write_then_read或spi_sync_transfer收发数据。

我贴一段伪代码,你看一眼就明白了:

static int my_spi_probe(struct spi_device *spi)
{
    spi->mode = SPI_MODE_0;
    spi->max_speed_hz = 10000000;
    spi_setup(spi);

    // 注册字符设备或输入设备
    return 0;
}

static int my_spi_transfer(struct spi_device *spi, u8 *tx, u8 *rx, int len)
{
    struct spi_transfer t = {
        .tx_buf = tx,
        .rx_buf = rx,
        .len = len,
    };
    struct spi_message m;

    spi_message_init(&m);
    spi_message_add_tail(&t, &m);
    return spi_sync(spi, &m);
}

这段代码,说白了就是最基础的PIO模式。CPU一个一个字节地搬数据。如果数据量大,比如操作一个几兆字节的Flash,CPU就被占死了。

这时候,DMA就该上场了。

4.3.1 DMA传输的优势

DMA,直接存储器访问。数据从外设到内存,或者从内存到外设,不经过CPU。CPU只需要告诉DMA控制器:从哪搬,搬到哪,搬多少。然后就可以去干别的事了。

我做过一个项目,用SPI读取摄像头数据,每帧图像几百KB。用PIO模式,CPU占用率飙到80%。换成DMA后,直接降到5%。效果立竿见影。

传输方式 CPU占用率 吞吐量 适用场景
PIO模式 高(60-90%) 受限于CPU频率 小数据量、低频通信
DMA模式 低(5-20%) 接近硬件极限 大数据量、高速通信

4.3.2 Linux SPI DMA的实现

在Linux里,启用SPI DMA通常不需要你改驱动代码。控制器驱动如果支持DMA,核心层会自动帮你做。你只需要在设备树里配好DMA通道。

设备树里大概是这样:

&spi1 {
    status = "okay";
    dmas = <&dma 0 1>, <&dma 1 1>;
    dma-names = "tx", "rx";

    my_device@0 {
        compatible = "my,spi-device";
        reg = <0>;
        spi-max-frequency = <20000000>;
    };
};

这里 dmas 指定了发送和接收的DMA通道。内核在传输时,如果检测到有DMA通道,会自动使用DMA,否则回退到PIO。

我的建议: 调试DMA时,先确认DMA控制器驱动是否正常。可以用 dmatest 模块测试。我曾经因为DMA中断没配好,数据一直传不出去,查了两天才发现是中断号写错了。

4.3.3 DMA传输的注意事项

DMA虽好,但有几个坑要注意:

  • Cache一致性:DMA直接操作内存,如果CPU有Cache,数据可能不一致。需要调用 dma_map_single 或 dma_sync_single_for_device。
  • 缓冲区对齐:DMA通常要求缓冲区地址对齐到Cache Line大小(一般是32或64字节)。不对齐的话,性能会下降,甚至出错。
  • 中断处理:DMA传输完成后,会触发中断。中断处理函数要尽量短,别在里面做复杂操作。
我曾经踩过的坑: 有一次用DMA传输,数据总是错几个字节。查了半天,发现是发送缓冲区和接收缓冲区在内存中重叠了。DMA传输时,两个缓冲区必须完全不重叠。

4.4 小结

SPI协议,模式选对,速率别超,基本就稳了。Linux SPI子系统,三层架构清晰,设备驱动写起来不复杂。DMA传输,能大幅降低CPU占用,但要注意Cache一致性和缓冲区对齐。

嗯,这一章就到这里。记住,调试SPI时,示波器是你最好的朋友。先看时钟,再看数据,最后看片选。波形对了,代码基本没问题。