Linux内核SPI控制器驱动:FIFO与DMA传输优化 - 第1节

1.1 SPI 总线物理层与信号定义

SPI(Serial Peripheral Interface,串行外设接口)是一种全双工、同步、主从式的串行通信总线,由 Motorola 公司于 20 世纪 80 年代提出。在物理层上,标准 SPI 总线采用 四线制 结构,分别承担时钟、数据输出、数据输入和片选功能。

信号线 全称 方向(主设备视角) 功能描述
SCK Serial Clock 输出 由主设备产生,同步数据传输的时钟信号
MOSI Master Out Slave In 输出 主设备发送数据至从设备的数据线
MISO Master In Slave Out 输入 从设备发送数据至主设备的数据线
CS Chip Select 输出 片选信号,低电平有效,用于选中目标从设备

关键特性: SPI 是全双工通信,在每个 SCK 时钟周期内,主设备通过 MOSI 发送一位数据,同时通过 MISO 接收一位数据。这种对称结构使得 SPI 在寄存器读写、传感器数据采集等场景中具有天然优势。

嵌入式工程师提示: 在硬件设计时,MOSI 与 MISO 不可接反。若主从设备均为 3.3V 逻辑电平,建议在靠近主设备端串联 22Ω~33Ω 电阻以抑制信号过冲。

1.2 四线制时序与数据采样

SPI 的时序核心在于 SCK 时钟边沿 与 数据变化/采样 的配合。下图展示了标准 SPI 单次传输的时序关系:

SPI 四线制基本时序图 SPI 四线制基本时序(CPOL=0, CPHA=0) CS CS 低电平有效,选中从设备 SCK MOSI D7 D6 D5 D4 D3 D2 D1 D0 MISO D7 D6 D5 D4 D3 D2 D1 D0 ▲ 主设备在 SCK 上升沿采样 MISO 数据 数据在 SCK 下降沿变化(CPHA=0)

上图展示了 CPOL=0, CPHA=0 模式下的时序:

  • CS 拉低后,SCK 开始产生连续时钟脉冲。
  • 主设备在 SCK 的 上升沿 采样 MISO 上的数据(从设备输出)。
  • 主设备在 SCK 的 下降沿 更新 MOSI 上的数据(从设备输入)。
  • 一次传输通常为 8 位或 16 位,传输完成后 CS 拉高。

时序避坑: 在高速 SPI(≥20MHz)下,信号线之间的走线长度差会导致 时钟偏移。建议在 PCB 布局时使 SCK、MOSI、MISO 三线等长,并在从设备端靠近引脚处放置 10pF~22pF 对地电容以抑制振铃。

1.3 SPI 四种工作模式(CPOL / CPHA)

SPI 协议通过 时钟极性(CPOL) 和 时钟相位(CPHA) 两个参数组合出四种工作模式。CPOL 决定 SCK 空闲时的电平,CPHA 决定数据采样发生在 SCK 的哪个边沿。

模式 CPOL CPHA 空闲 SCK 电平 数据采样边沿 数据变化边沿 典型从设备
模式 0 0 0 低电平 上升沿 下降沿 W25Qxx Flash, ADXL345
模式 1 0 1 低电平 下降沿 上升沿 MAX31855, MCP3008
模式 2 1 0 高电平 下降沿 上升沿 LTC6804, 部分 DAC
模式 3 1 1 高电平 上升沿 下降沿 SD 卡(SPI 模式), CC1101

记忆口诀: “CPOL 看空闲,低 0 高 1;CPHA 看采样,第一个边沿为 0,第二个边沿为 1。” 模式 0 和模式 3 最为常用,分别对应空闲低电平/上升沿采样和空闲高电平/上升沿采样。

下面通过一张 SVG 结构图对比四种模式的 SCK 波形与采样点:

SPI 四种工作模式对比 SPI 四种工作模式(CPOL / CPHA)对比 模式 0 (CPOL=0, CPHA=0) ▲ 上升沿采样 模式 1 (CPOL=0, CPHA=1) ▲ 下降沿采样 模式 2 (CPOL=1, CPHA=0) ▲ 下降沿采样 模式 3 (CPOL=1, CPHA=1) ▲ 上升沿采样 模式选择要点 • 模式 0 和模式 3 的采样边沿均为上升沿,区别仅在于空闲电平。 • 多数 SPI Flash 和 SD 卡默认使用模式 0 或模式 3,驱动中需根据从设备数据手册配置。 • 在 Linux 内核中,通过 struct spi_device 的 mode 成员设置 SPI_MODE_0 ~ SPI_MODE_3。 • 错误配置模式会导致数据移位或 CRC 错误,调试时可使用逻辑分析仪对比 SCK 与 MOSI 边沿关系。

1.4 多从设备拓扑与片选管理

SPI 总线支持 一主多从 拓扑结构。主设备通过独立的 CS 线分别控制每个从设备,而 SCK、MOSI、MISO 则为所有从设备共享。

SPI 一主多从拓扑结构 SPI 一主多从拓扑结构 主设备 (SoC / MCU) 从设备 1 Flash 从设备 2 ADC 从设备 3 传感器 SCK MOSI MISO CS1 CS2 CS3 每个从设备独占一条 CS 线,同一时刻只能有一个 CS 为低电平,避免总线冲突 从设备的 MISO 引脚在 CS 为高时需处于高阻态(三态),否则会干扰其他从设备通信

驱动开发提示: 在 Linux 内核中,SPI 从设备通过设备树描述,每个从设备节点包含 reg 属性(对应 CS 的 GPIO 编号或硬件片选索引)和 spi-max-frequency 属性。主控制器驱动在发起传输时,会通过 .set_cs() 回调函数控制 CS 电平。

1.5 SPI 传输的基本流程

一次完整的 SPI 传输在软件层面通常包含以下步骤:

  1. 配置模式与速率: 根据从设备数据手册设置 CPOL、CPHA 以及 SCK 频率。
  2. 构建消息: 将待发送数据填充到发送缓冲区,准备接收缓冲区。
  3. 发起传输: 主控制器驱动将消息提交到 SPI 控制器硬件。
  4. 硬件移位: 每个 SCK 周期,MOSI 移出一位,MISO 移入一位,8/16 位后完成一个字节/半字传输。
  5. 传输完成中断或 DMA 完成回调: 通知上层数据已就绪。
  6. 处理接收数据: 从接收缓冲区读取从设备返回的数据。

性能要点: 在 Linux 内核中,SPI 传输通常通过 spi_message 和 spi_transfer 结构体组织。一次 spi_message 可以包含多个 spi_transfer 段,每个段可以有不同的长度、速率和延迟,这为实现复杂协议(如先发送命令再接收数据)提供了灵活性。

下面通过一个简单的代码片段展示 SPI 传输在用户空间的典型调用方式(通过 spidev 驱动):

#include <linux/spi/spidev.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int spi_transfer(int fd, uint8_t *tx, uint8_t *rx, int len) {
    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx,
        .rx_buf = (unsigned long)rx,
        .len = len,
        .speed_hz = 1000000,   // 1 MHz
        .delay_usecs = 0,
        .bits_per_word = 8,
        .cs_change = 0,
    };
    return ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
}

int main() {
    int fd = open("/dev/spidev0.0", O_RDWR);
    if (fd < 0) return -1;

    uint8_t mode = SPI_MODE_0;
    ioctl(fd, SPI_IOC_WR_MODE, &mode);

    uint8_t tx_buf[4] = {0x90, 0x00, 0x00, 0x00}; // 读 ID 命令
    uint8_t rx_buf[4] = {0};
    spi_transfer(fd, tx_buf, rx_buf, 4);

    // rx_buf[1..3] 包含设备 ID
    close(fd);
    return 0;
}

常见错误: 在 SPI 传输中,如果接收缓冲区未清零,返回数据中可能混入旧数据。建议每次传输前使用 memset 将 rx_buf 清零。另外,spidev 设备节点需要确保用户空间有访问权限,通常通过 udev 规则设置 0666 权限。

1.6 本章小结

本章从物理层信号定义出发,详细讲解了 SPI 四线制的时序关系、