【Linux驱动开发】Linux SPI 通信详解:从硬件到驱动再到应用
Linux SPI 通信详解:从硬件到驱动再到应用
SPI(Serial Peripheral Interface)是 Motorola 于 1980 年代提出的同步全双工串行总线,采用主从式架构,信号线简单、吞吐高、协议灵活,被广泛用于 Flash、LCD 控制器、ADC/DAC、射频芯片、SD 卡等场景。Linux 将其抽象为 SPI Core 子系统,提供统一的总线/设备/驱动模型,并支持用户态字符设备、QSPI/OSPI、SPI-NOR/NAND Flash 子系统。本文从硬件原理、协议时序、底层驱动框架、内核与用户态接口、实际应用到调试方法,系统梳理 Linux SPI 通信全景。
1. 硬件原理
1.1 信号线
| 信号 | 方向(主视角) | 描述 |
|---|---|---|
| SCLK | 输出 | 时钟,空闲电平可配置(CPOL) |
| MOSI | 输出 | Master Out Slave In,主→从数据 |
| MISO | 输入 | Master In Slave Out,从→主数据 |
| SS/CS# | 输出(可选) | 片选,低有效,可多脚或解码器扩展 |
- 全双工:收发共用同一时钟,数据移位寄存器双向独立;
- 单主多从:主机提供时钟,从机无时钟输出能力(除非配置为 slave mode)。
1.2 电平与速度
- TTL 同主控 IO 电压(1.8 V/3.3 V/5 V);高速板需做 50 Ω 阻抗匹配;
- 时钟频率:几 kHz – 100 MHz+,受 PCB 线长、驱动能力、从芯片限制;
- 四线制为标准,另有:
- 三线制(MOSI/MISO 合并为 SIO),半双工;
- Dual/Quad/Octal SPI:并行数据线 2/4/8 根,提升带宽,常用于 Flash。
1.3 SPI 控制器框图

- 时钟生成:分频 PLL/APB 时钟,可配置极性/相位;
- 移位寄存器:Tx/Rx 双缓冲,位宽 8/16/32 可选;
- 片选解码:硬件 CS 线 1–8 根,或外接 74HC138;支持 CSAAT(连续传输不抬片选);
- DMA 接口:Tx/Rx FIFO 与内存散聚 DMA,减少 CPU 介入;
- 中断事件:Tx FIFO 空、Rx FIFO 满、传输完成、溢出;
- 可编程延时:CS→CLK 建立、CLK→CS 保持,满足从芯片时序要求。
2. 协议时序与模式
SPI 无起始/停止位,靠片选帧定界,同步时钟移位。

2.1 时钟极性/相位 (CPOL/CPHA)
| 模式 | CPOL | CPHA | 采样沿 | 描述 |
|---|---|---|---|---|
| 0 | 0 | 0 | 上升沿 | 时钟低空闲,第一沿采样 |
| 1 | 0 | 1 | 下降沿 | 时钟低空闲,第二沿采样 |
| 2 | 1 | 0 | 下降沿 | 时钟高空闲,第一沿采样 |
| 3 | 1 | 1 | 上升沿 | 时钟高空闲,第二沿采样 |
- 主从必须配置相同模式;
- 采样沿由 CPHA 决定:CPHA=0 → 奇数沿,CPHA=1 → 偶数沿。
2.2 传输单位
- 可 8/16/32 位字长;高位先出 (MSB) 为常态,部分控制器支持 LSB;
- 片选拉低→时钟启动→连续字→片选拉高,构成一次传输 (transfer);
- 支持“连续传输”:CS 保持低,Tx FIFO 不断填充,实现大长度 DMA。
2.3 与 I2C/UART 差异
- 全双工、无速度上限、无寻址、无 ACK;
- 线多(至少 4 线),无硬件仲裁,不适合多主;
- 协议简单,但不同芯片时序差异大(CS 建立/保持、空闲电平、字长)。
3. Linux SPI 驱动框架
Linux 采用“控制器驱动 + 协议驱动 + 片选 GPIO”分层,核心文件:drivers/spi/spi.c

3.1 关键数据结构
| 结构体 | 描述 |
|---|---|
struct spi_controller | 代表 SPI 主/从控制器(原 spi_master),含总线号、片选数量、队列/线程化传输模型; |
struct spi_device | 描述一个从设备:总线号、片选、模式 (CPOL/CPHA)、字长、最大速度; |
struct spi_driver | 协议驱动,匹配 spi_device,提供 probe/remove/suspend/resume; |
struct spi_transfer | 一次传输片段:Tx/Rx 缓冲区、长度、速度、字长、延时等; |
struct spi_message | 由多个 spi_transfer 组成,片选连续保持,可一次性 DMA; |
3.2 传输模型
- 早期 PIO:
spi_sync()在中断上下文忙等待,适合短数据; - 线程化 + 队列:
spi_queued_transfer(默认),内核线程kworker/spi处理消息,降低延迟; - DMA 散聚:驱动实现
can_dma()+dma_tx/rx映射,实现零拷贝; - 同步/异步:
spi_sync()阻塞;spi_async()注册回调,用于 SPI LCD 刷新等实时场景。
3.3 控制器驱动示例(简化)
#include <linux/spi/spi.h>
static int spi_xxx_setup(struct spi_device *spi)
{/* 根据 spi->mode 配置寄存器 CPOL/CPHA *//* 根据 spi->max_speed_hz 配置分频 */return 0;
}
static int spi_xxx_transfer_one(struct spi_controller *ctlr,struct spi_device *spi,struct spi_transfer *t)
{/* 1. 配置字长 t->bits_per_word *//* 2. 写 Tx FIFO (t->tx_buf) *//* 3. 等待中断/Rx DMA 完成 *//* 4. 读 Rx FIFO 到 t->rx_buf */return 0;
}
static struct spi_controller *ctlr;
static int spi_xxx_probe(struct platform_device *pdev)
{ctlr = spi_alloc_controller(&pdev->dev, sizeof(*drv_data));ctlr->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;ctlr->bits_per_word_mask = SPI_BPW_MASK(8) | SPI_BPW_MASK(16);ctlr->min_speed_hz = 100000;ctlr->max_speed_hz = 50000000;ctlr->setup = spi_xxx_setup;ctlr->transfer_one = spi_xxx_transfer_one;ctlr->num_chipselect = 4;return devm_spi_register_controller(&pdev->dev, ctlr);
}
3.4 协议驱动示例(RF 芯片)
static int rf_probe(struct spi_device *spi)
{struct spi_transfer t[2] = {{ .tx_buf = &cmd, .len = 1, .speed_hz = 10000000 },{ .rx_buf = buf, .len = 4, .speed_hz = 10000000 },};struct spi_message m;spi_message_init(&m);spi_message_add_tail(&t[0], &m);spi_message_add_tail(&t[1], &m);spi_sync(spi, &m); /* 读 4 字节寄存器 */
}
static struct spi_driver rf_driver = {.driver = { .name = "rf_xxx", },.probe = rf_probe,
};
module_spi_driver(rf_driver);
4. 用户态接口
4.1 spidev 字符设备 /dev/spidevB.C
- 内核配置
CONFIG_SPI_SPIDEV; - B = 总线号,C = 片选号;
ioctl列表:
SPI_IOC_WR_MODE // 设置 SPI_MODE_0/1/2/3
SPI_IOC_RD_MODE
SPI_IOC_WR_MAX_SPEED_HZ
SPI_IOC_WR_BITS_PER_WORD
SPI_IOC_MESSAGE(N) // 一次传输多段,支持全双工
- 用户态示例(Python
spidev模块):
import spidev
spi = spidev.SpiDev()
spi.open(0, 0) # /dev/spidev0.0
spi.max_speed_hz = 10_000_000
spi.mode = 0b00 # CPOL=0 CPHA=0
resp = spi.xfer2([0x9F, 0x00, 0x00, 0x00]) # 读 Flash JEDEC ID
4.2 sysfs 节点(已逐步淡化)
/sys/bus/spi/devices/spi0.0/提供modalias/uevent;调试时可用cat driver/name。
4.3 调试与跟踪
dynamic_debug:
echo 'file spi.c +p' > /sys/kernel/debug/dynamic_debug/control
- 逻辑分析仪:抓 SCLK/MOSI/MISO/CS,看模式、字长、空闲电平;
spidev_test官方工具:
./spidev_test -D /dev/spidev0.0 -s 10000000 -p "\x9f\x00\x00\x00" -v
5. 实际应用与场景
| 外设 | 模式/速度 | 注意要点 |
|---|---|---|
| W25Q64 Flash | SPI_MODE_0, 80 MHz | 支持 Quad SPI,先 0xEB 读,DMA 大页 |
| TFT LCD (ST7789) | SPI_MODE_3, 40 MHz | 9-bit 命令/数据(DCX)用 GPIO 或硬件 D/C 线 |
| NRF24L01 射频 | SPI_MODE_0, 8 MHz | 连续 CS,内部 FIFO 状态寄存器轮询 |
| ADC (MCP3208) | SPI_MODE_0, 1 MHz | 16 位采样,先 0x06 + 通道号,再读 12 位结果 |
| 以太网 ENC28J60 | SPI_MODE_0, 20 MHz | 7 位地址 + 5 位数据,硬件 DMA 支持 |
| 音频 codec | SPI_MODE_1, 24 MHz | 24 位字长,MSB 先出,需连续时钟 |
5.1 多从片选方式
- 独立 CS 线:控制器硬件 CS,切换简单;
- GPIO 片选:
spi_device.cs_gpio = gpio#,驱动自动拉低/拉高; - 解码器:3-8 译码器 74HC138,节省引脚,需
cs_setup_hz延时。
5.2 大吞吐量优化
- 双缓冲 DMA:Tx/Rx 各用环形 buffer,LCD 刷新不丢帧;
- 中断线程化:PREEMPT_RT 下,
spi->irq线程化,抖动 < 100 µs; - 连续传输:一次
spi_message包含 240×320×2 字节,CS 保持低,DMA 链完成; - CPU 亲和:SPI DMA 中断绑到独立核,避免任务迁移。
6. 故障排查与性能优化
| 现象 | 可能原因 | 排查/解决 |
|---|---|---|
| 模式不匹配 | CPOL/CPHA 错 | 逻辑分析仪看采样沿,主从统一模式 |
| 数据偏移 | 字长/位序错 | 示波器测 8/16/32 位,MSB/LSB 先出 |
| 片选抖动 | CS 建立不足 | 加 cs_setup/delay 或降速,用 GPIO CS |
| 丢字节 | DMA 未完成 | 查看 spi->dma_tx/rx 返回,开 dynamic_debug |
| 高误码 | 线长/阻抗失配 | 降速 < 10 MHz,加 22 Ω 串阻,用双绞屏蔽线 |
| 实时抖动 | 中断抢占 | 绑核 + irqthread + spi_async 回调 |
7. 小结
SPI 以“4 线 + 全双工 + 无寻址”提供极简高速串行链路,协议灵活但时序多样。Linux 通过“spi_controller/spi_device/spi_driver”模型把控制器与协议分离,统一使用 spi_message/spi_transfer 进行 DMA 友好传输;用户态则通过 spidev 字符设备快速验证外设。掌握 CPOL/CPHA、字长、CS 建立/保持、DMA 散聚与实时绑核技巧,可在嵌入式显示、高速数据采集、射频通信等场景中发挥 SPI 的最大带宽与确定性。
附录:参考文档
- Linux Kernel:
Documentation/spi/,drivers/spi/spi.c,spidev.c - Motorola SPI Block Guide
- 芯片手册:W25Q64JV、ST7789、NRF24L01、MCP3208、ENC28J60
- 书籍:《Linux 设备驱动开发详解》《嵌入式 Linux 开发教程》
本文档示例基于 Linux 4.4.94,不同版本内核接口基本一致,高速/DMA/实时细节请以实际 SoC 手册与驱动为准。
