Linux SPI 驱动框架 transfer 机制深度剖析:从 spi_message 到 DMA 传输的性能链路
一、从用户空间 write() 到硬件 MOSI/MISO:一次 SPI 传输经历了什么
嵌入式开发者常通过 /dev/spidevX.Y 操作 SPI 设备,ioctl(SPI_IOC_MESSAGE) 或 write() 调用看似简单。但当数据量超过 PIO FIFO 深度(典型的 64 字节),或在高速模式下(50MHz+)出现数据错位时,问题的根因往往隐藏在 SPI 子系统的层层抽象之下。
以一个典型的 TFT LCD 驱动为例:每次刷新一帧需要传输 320×240×2=153.6KB 的数据。使用默认的 PIO 模式时,CPU 占用率高达 60%,且帧率被限制在 15fps。切换到 DMA 模式后,CPU 占用率降至 5%,帧率提升至 30fps。这两者之间的差距,就隐藏在 SPI 子系统的 transfer 机制实现中。
本文从 spi_message 数据结构出发,逐层深入到 spi_transfer 的 PIO/DMA 双路径实现,并通过实际的 DMA 传输代码展示性能优化的关键点。
二、spi_message 队列模型与 transfer 执行路径
Linux SPI 子系统的核心模型是消息队列。每次 SPI 通信被抽象为一个 spi_message 对象,其中包含一个或多个 spi_transfer 链表节点。每个 spi_transfer 描述一次连续的数据传输,同一 message 内的多个 transfer 共享片选信号(CS),实现"保持片选有效期间的多段传输"。
flowchart TD
subgraph Userspace [用户空间]
U1["write(fd, buf, len)"] –> U2["ioctl(SPI_IOC_MESSAGE)"]
end
subgraph SPIDEV [spidev 驱动层]
U2 –> S1["spidev_message()"]
S1 –> S2["构建 spi_message + spi_transfer 链表"]
S2 –> S3["spi_async() / spi_sync()"]
end
subgraph Core [SPI 核心层]
S3 –> C1["__spi_queued_transfer()"]
C1 –> C2["插入控制器的传输队列 (kthread_worker)"]
C2 –> C3["spi_pump_messages() 从队列取出"]
C3 –> C4["__spi_pump_messages() 请求总线"]
C4 –> C5["master->transfer_one_message()"]
end
subgraph Transfer [传输执行路径]
C5 –> T1{"can_dma() ?"}
T1 –>|是| T2["DMA 路径\\nspi_transfer_one_message_dma()"]
T1 –>|否| T3["PIO 路径\\nspi_transfer_one_message_pio()"]
T2 –> T4["等待 DMA 完成中断"]
T3 –> T5["CPU 轮询/中断逐字节传输"]
T4 –> T6["spi_finalize_current_message()"]
T5 –> T6
end
T6 –> T7["向用户空间返回结果"]
style C3 fill:#0f3460,stroke:#16213e,color:#e0e0e0
style C5 fill:#533483,stroke:#3a2a6e,color:#e0e0e0
关键数据结构:spi_message 和 spi_transfer 定义在 <linux/spi/spi.h> 中。
// spi_message: 一次 SPI 通信事务的完整描述
struct spi_message {
struct list_head transfers; // spi_transfer 链表头
struct spi_device *spi;
unsigned is_dma_mapped:1; // 是否由调用者完成 DMA 映射
/* 完成回调 */
void (*complete)(void *context);
void *context;
unsigned frame_length;
unsigned actual_length; // 实际传输字节数
int status; // 传输结果: 0=成功, 负值=错误码
struct list_head queue; // 控制器内部队列节点
void *state; // 控制器私有状态
};
// spi_transfer: 一段连续数据传输的描述
struct spi_transfer {
const void *tx_buf; // 发送缓冲区
void *rx_buf; // 接收缓冲区
unsigned len; // 传输长度
dma_addr_t tx_dma; // DMA 发送地址 (is_dma_mapped=1 时有效)
dma_addr_t rx_dma; // DMA 接收地址
struct sg_table tx_sg; // 离散聚合发送表
struct sg_table rx_sg; // 离散聚合接收表
unsigned cs_change:1; // 传输后是否释放片选
unsigned cs_off:1; // 传输间是否关闭片选
u8 bits_per_word; // 字长
u16 delay_usecs; // 传输间延迟
u32 speed_hz; // 本次传输的时钟频率
struct list_head transfer_list; // 链表节点
};
在标准传输路径中,ISR 在 transfer_one 完成后调用 spi_finalize_current_message(),该函数唤醒等待中的用户空间调用者。
三、DMA 模式 SPI 驱动实现与性能优化
3.1 SPI 控制器驱动的 DMA 传输实现
/**
* @file spi-dma-engine.c
* @brief 基于 DMA 的 SPI 控制器驱动核心传输逻辑。
*
* 设计背景:
* 基于 STM32MP1 的 SPI6 控制器, 支持 DMA 发送和接收。
* FIFO 深度为 16×32bit, 超过 64 字节的传输自动切换到 DMA 模式。
*
* DMA 模式带来的核心优化:
* 1. CPU 从逐字节搬运中解放, 可并行处理其他任务。
* 2. 双缓冲 (Ping-Pong) DMA 进一步消除传输间的配置延迟。
* 3. 使用 scatter-gather 支持非连续内存区域的一次性传输。
*/
#include <linux/spi/spi.h>
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
#include <linux/completion.h>
#include <linux/interrupt.h>
/* 控制器私有数据结构 */
struct my_spi_master {
struct spi_controller *ctlr;
struct dma_chan *tx_chan; /* DMA 发送通道 */
struct dma_chan *rx_chan; /* DMA 接收通道 */
dma_addr_t tx_dma_addr; /* 发送缓冲区物理地址 */
dma_addr_t rx_dma_addr; /* 接收缓冲区物理地址 */
void __iomem *regs; /* SPI 寄存器基地址 */
struct completion dma_complete; /* DMA 完成信号 */
struct spi_transfer *cur_transfer; /* 当前正在处理的 transfer */
};
/* SPI 寄存器偏移 (示例, 实际由芯片手册定义) */
#define SPI_CR1 0x00
#define SPI_SR 0x08
#define SPI_DR 0x0C
#define SPI_CR2 0x04
#define SPI_SR_RXNE BIT(0) /* 接收非空 */
#define SPI_SR_TXE BIT(1) /* 发送空 */
#define SPI_CR2_TXDMAEN BIT(1) /* 发送 DMA 使能 */
#define SPI_CR2_RXDMAEN BIT(0) /* 接收 DMA 使能 */
/**
* @brief DMA 传输完成回调: 通知 SPI 核心层传输结束。
*
* 说明: 此回调在 DMA 中断上下文执行。
* 如果同时使用了 TX 和 RX DMA, 需要等待两者都完成后才触发完成信号。
* 此处通过 atomic 计数器跟踪完成状态。
*/
static void my_spi_dma_callback(void *arg) {
struct my_spi_master *spi_master = (struct my_spi_master *)arg;
/* 等待 RX DMA 也完成 (通过检查 SPI_SR 中的 BSY 位) */
unsigned long timeout = jiffies + msecs_to_jiffies(100);
while (readl_relaxed(spi_master->regs + SPI_SR) & BIT(7)) {
if (time_after(jiffies, timeout)) {
dev_err(&spi_master->ctlr->dev,
"DMA 传输超时: SPI 仍处于忙碌状态\\n");
break;
}
cpu_relax();
}
/* 禁用 DMA 请求 */
u32 cr2 = readl_relaxed(spi_master->regs + SPI_CR2);
cr2 &= ~(SPI_CR2_TXDMAEN | SPI_CR2_RXDMAEN);
writel_relaxed(cr2, spi_master->regs + SPI_CR2);
complete(&spi_master->dma_complete);
}
/**
* @brief DMA 模式下的 SPI 传输实现。
*
* 工作流:
* 1. 检查是否需要 DMA (len > FIFO_DEPTH * 2)。
* 2. 为 TX 和 RX 缓冲区建立 DMA 映射。
* 3. 配置并启动 DMA 传输。
* 4. 使能 SPI 控制器的 DMA 请求。
* 5. 等待 DMA 完成中断。
*
* @return 0 = 成功, 负值 = 错误码
*/
static int my_spi_transfer_one_dma(
struct spi_controller *ctlr,
struct spi_device *spi,
struct spi_transfer *transfer) {
struct my_spi_master *spi_master = spi_controller_get_devdata(ctlr);
struct dma_async_tx_descriptor *tx_desc = NULL, *rx_desc = NULL;
enum dma_ctrl_flags dma_flags = DMA_CTRL_ACK | DMA_PREP_INTERRUPT;
int ret;
if (transfer->len == 0) {
return 0; /* 空传输, 直接返回 */
}
/* Step 1: TX DMA 映射与配置 */
if (transfer->tx_buf) {
dma_addr_t tx_phys;
if (transfer->tx_dma) {
tx_phys = transfer->tx_dma; /* 使用调用者提供的物理地址 */
} else {
tx_phys = dma_map_single(
ctlr->dma_tx->device->dev,
(void *)transfer->tx_buf, /* 虚拟地址 */
transfer->len,
DMA_TO_DEVICE
);
if (dma_mapping_error(ctlr->dma_tx->device->dev, tx_phys)) {
dev_err(&ctlr->dev, "TX DMA 映射失败\\n");
return -ENOMEM;
}
}
tx_desc = dmaengine_prep_slave_single(
spi_master->tx_chan,
tx_phys,
transfer->len,
DMA_MEM_TO_DEV,
dma_flags
);
if (!tx_desc) {
dev_err(&ctlr->dev, "TX DMA 描述符准备失败\\n");
return -EIO;
}
/* 设置 DMA 完成回调 */
tx_desc->callback = my_spi_dma_callback;
tx_desc->callback_param = spi_master;
}
/* Step 2: RX DMA 映射与配置 */
if (transfer->rx_buf) {
dma_addr_t rx_phys;
if (transfer->rx_dma) {
rx_phys = transfer->rx_dma;
} else {
rx_phys = dma_map_single(
ctlr->dma_rx->device->dev,
transfer->rx_buf,
transfer->len,
DMA_FROM_DEVICE
);
if (dma_mapping_error(ctlr->dma_rx->device->dev, rx_phys)) {
dev_err(&ctlr->dev, "RX DMA 映射失败\\n");
return -ENOMEM;
}
}
rx_desc = dmaengine_prep_slave_single(
spi_master->rx_chan,
rx_phys,
transfer->len,
DMA_DEV_TO_MEM,
dma_flags
);
if (!rx_desc) {
dev_err(&ctlr->dev, "RX DMA 描述符准备失败\\n");
return -EIO;
}
}
/* Step 3: 提交 DMA 传输 */
reinit_completion(&spi_master->dma_complete);
if (tx_desc) {
dmaengine_submit(tx_desc);
dma_async_issue_pending(spi_master->tx_chan);
}
if (rx_desc) {
dmaengine_submit(rx_desc);
dma_async_issue_pending(spi_master->rx_chan);
}
/* Step 4: 使能 SPI 控制器的 DMA 请求 */
u32 cr2 = readl_relaxed(spi_master->regs + SPI_CR2);
if (tx_desc) cr2 |= SPI_CR2_TXDMAEN;
if (rx_desc) cr2 |= SPI_CR2_RXDMAEN;
writel_relaxed(cr2, spi_master->regs + SPI_CR2);
/* Step 5: 等待 DMA 完成 (带超时保护) */
if (!wait_for_completion_timeout(
&spi_master->dma_complete, msecs_to_jiffies(1000))) {
dev_err(&ctlr->dev, "DMA 传输超时!\\n");
ret = -ETIMEDOUT;
/* 强制终止 DMA */
if (tx_desc) dmaengine_terminate_all(spi_master->tx_chan);
if (rx_desc) dmaengine_terminate_all(spi_master->rx_chan);
goto err_cleanup;
}
ret = 0;
err_cleanup:
/* 取消 DMA 映射 (仅对自行映射的缓冲区) */
if (transfer->tx_buf && !transfer->tx_dma) {
dma_unmap_single(ctlr->dma_tx->device->dev,
spi_master->tx_dma_addr,
transfer->len, DMA_TO_DEVICE);
}
if (transfer->rx_buf && !transfer->rx_dma) {
dma_unmap_single(ctlr->dma_rx->device->dev,
spi_master->rx_dma_addr,
transfer->len, DMA_FROM_DEVICE);
}
return ret;
}
3.2 can_dma 决策函数的实现
/**
* @brief 判断本次传输是否应使用 DMA。
*
* 规则:
* 1. 传输长度 > FIFO 深度的 2 倍 (即 128 字节) → 使用 DMA
* 2. 传输长度 ≤ FIFO 深度 → 使用 PIO (DMA 建立开销 > PIO 开销)
* 3. 边界对齐要求: DMA 缓冲区必须与 Cache Line (64 字节) 对齐,
* 否则可能因 Cache 一致性回写导致数据损坏。
*
* 测量依据: 在 400MHz STM32MP1 上, DMA 建立开销约 5μs,
* PIO 传输一个字节约 0.12μs。平衡点约为 42 字节。
* 取 FIFO 深度 ×2 = 128 字节提供安全裕量。
*/
static bool my_spi_can_dma(
struct spi_controller *ctlr,
struct spi_device *spi,
struct spi_transfer *transfer) {
/* 规则1: 长度阈值 */
if (transfer->len <= 128) {
return false;
}
/* 规则2: 地址对齐检查 (dma_map_single 对非对齐地址行为与硬件相关) */
if (transfer->tx_buf &&
((unsigned long)transfer->tx_buf & (ARCH_DMA_MINALIGN – 1))) {
dev_dbg(&ctlr->dev,
"TX 缓冲区未对齐, 回退到 PIO 模式 (addr=%p)\\n",
transfer->tx_buf);
return false;
}
if (transfer->rx_buf &&
((unsigned long)transfer->rx_buf & (ARCH_DMA_MINALIGN – 1))) {
return false;
}
/* 规则3: DMA 通道可用性 */
struct my_spi_master *spi_master = spi_controller_get_devdata(ctlr);
if (!spi_master->tx_chan || !spi_master->rx_chan) {
return false;
}
return true;
}
四、PIO vs DMA 的边界效应与实时性权衡
SPI DMA 传输并非在所有场景下都是最优选择。其适用边界和代价需要精确评估:
小数据量传输:DMA 的配置开销(DMA 描述符准备、Memory Barrier、Cache 刷新)约 5~10μs。当传输长度小于约 40 字节时,PIO 速度更快。这是因为 dmaengine_prep_slave_single() 至少需要完成 3 次 MMIO 写入来配置 DMA 引擎。
实时性折衷:DMA 完成中断的延迟(Interrupt Latency)约 1~3μs,而 PIO 模式下数据立即可用。对于需要在 SPI 传输后立即读取计算结果的场景(如读取 ADC 采样值后快速处理),PIO 模式避免了中断延迟的不可预测性。
Cache 一致性问题:在带 Data Cache 的 CPU(如 Cortex-A7)上,DMA 传输前后必须进行 Cache 刷新操作。dma_map_single() 在 DMA_TO_DEVICE 方向会执行 Cache Clean(将 Cache 内容写回),DMA_FROM_DEVICE 方向会执行 Cache Invalidate。这两步操作对 L1 Cache 的扫描时间与传输数据量成正比——对于 64KB 的数据块,Cache 操作本身可能耗时 200~300μs。
离散内存的 Scatter-Gather 开销:当传输缓冲区在物理上不连续时,需要建立 SG List,每个 SG 条目增加约 16 字节的描述符开销和 1~2μs 的处理时间。对于高度碎片化的用户空间内存,建议先通过 kzalloc() 分配 DMA 一致性缓冲区,将用户数据拷贝到该缓冲区后再启动 DMA。
适用于:批量数据传输(>128 字节)、需并行处理其他任务的场景、对数据吞吐量(而非单次传输延迟)有要求的应用。不适用于:极短数据交互(如寄存器配置)、对实时性要求苛刻的闭环控制系统。
五、总结
Linux SPI 子系统的 transfer 机制通过 spi_message/spi_transfer 的数据结构和控制器队列模型,提供了一条从用户空间调用到硬件 DMA 传输的完整路径。理解这条路径的关键在于三点:
在实际开发中,建议通过 perf 或 ftrace 工具分析 SPI 传输路径的热点函数(spi_pump_messages、spi_transfer_one_message),基于数据而非直觉优化 can_dma 阈值。一个高效的 SPI 子系统实现是在 PIO 的低延迟与 DMA 的高吞吐之间的精准平衡。

