4. SPI 消息与传输队列:spi_message 结构体、spi_transfer 结构体、消息队列管理与传输原子性
在 Linux SPI 子系统中,一次完整的 SPI 通信被抽象为 消息(Message) 与 传输段(Transfer) 两个层次。控制器驱动通过管理消息队列,确保多个 SPI 设备请求能够原子化、有序地执行,避免总线冲突与数据错乱。本章深入剖析核心数据结构、队列操作机制以及传输原子性的实现原理。
4.1 核心数据结构:spi_message 与 spi_transfer
SPI 子系统使用两个关键结构体来描述一次通信任务:spi_message 代表一次完整的“会话”,而 spi_transfer 代表会话中的一个“数据段”。一个消息可以包含多个传输段,这些传输段在总线上连续执行,中间不会插入其他设备的操作。
| 结构体 | 作用 | 关键字段 | 生命周期 |
|---|---|---|---|
spi_message |
封装一次完整的 SPI 事务,包含一个或多个传输段 | transfers、complete、context、status、queue |
由上层驱动(如 MTD、IIO)分配,异步回调后释放 |
spi_transfer |
描述一段连续的 SPI 数据收发,包含缓冲区与传输参数 | tx_buf、rx_buf、len、speed_hz、bits_per_word、cs_change |
作为消息的链表节点存在,由消息管理其生命周期 |
4.1.1 spi_message 结构体详解
spi_message 是 SPI 通信的顶层容器,定义在 include/linux/spi/spi.h 中。其核心字段包括:
- transfers:指向传输段链表的头节点,所有
spi_transfer通过transfer_list链接。 - complete:异步完成回调函数指针,当消息处理完毕(成功或失败)时被调用。
- context:回调函数的私有数据指针,通常指向上层请求的上下文。
- status:消息执行结果状态码,0 表示成功,负值表示错误。
- queue:用于将消息挂入控制器驱动的待处理队列。
- spi:指向目标 SPI 设备结构体,标识该消息属于哪个从设备。
- is_dma_mapped:标志位,指示缓冲区是否已经由 DMA 映射,避免重复映射。
spi_message 一旦提交给控制器驱动,驱动必须保证其所有传输段在总线上连续执行,中间不允许插入其他设备的操作。这是 SPI 总线事务原子性的基本保证。
4.1.2 spi_transfer 结构体详解
spi_transfer 描述一段具体的 SPI 数据交换。其关键字段如下:
struct spi_transfer {
const void *tx_buf; // 发送缓冲区,NULL 表示只接收
void *rx_buf; // 接收缓冲区,NULL 表示只发送
unsigned len; // 传输字节数
u32 speed_hz; // 该段传输的时钟频率(覆盖设备默认值)
u16 bits_per_word; // 每字位数(覆盖设备默认值)
u8 bits_per_word; // 每字位数(覆盖设备默认值)
u16 delay_usecs; // 传输完成后的延迟微秒数
u32 cs_change; // 传输完成后是否切换片选状态
u32 tx_nbits; // 发送线宽(1/2/4 线模式)
u32 rx_nbits; // 接收线宽
struct list_head transfer_list; // 链表节点,用于挂入 spi_message
};
每个传输段可以独立设置 speed_hz 和 bits_per_word,这为多协议设备(如同时支持 SPI 和 Dual SPI)提供了灵活性。cs_change 字段控制片选信号的行为:
- 0:传输完成后保持片选有效,下一个传输段继续使用同一片选。
- 1:传输完成后释放片选,下一个传输段重新拉低片选。
spi_message 中,并将 cs_change 设置为 0(最后一个传输段除外)。这样可以避免片选信号抖动,提高通信效率。
4.2 消息队列管理机制
SPI 控制器驱动需要管理多个并发提交的消息。Linux 内核使用 消息队列 来串行化这些请求,确保同一时刻只有一条消息在总线上传输。队列管理涉及消息的提交、排队、调度和完成处理。
4.2.1 消息提交与排队流程
上层驱动通过 spi_async() 函数提交消息。该函数将消息挂入控制器驱动的 queue 链表,并触发队列处理。典型流程如下:
- 初始化消息:上层驱动填充
spi_message和spi_transfer,设置回调函数。 - 调用 spi_async():将消息提交给 SPI 核心层。
- 核心层处理:
spi_async()调用控制器驱动的transfer_one_message回调。 - 入队操作:驱动将消息加入
master->queue链表,并检查队列是否为空。 - 调度执行:如果队列之前为空,立即启动消息处理;否则等待当前消息完成。
- 完成回调:消息处理完毕后,驱动调用
spi_finalize_current_message(),触发回调并调度下一条消息。
master->queue_lock 或驱动内部的自旋锁。
4.2.2 队列状态机
控制器驱动的消息队列通常包含三种状态:
| 状态 | 描述 | 触发条件 |
|---|---|---|
| 空闲(IDLE) | 队列为空,控制器处于待机状态 | 上电初始化或最后一条消息完成 |
| 运行(RUNNING) | 正在处理当前消息,队列中可能还有待处理消息 | 从空闲状态提交第一条消息 |
| 暂停(PAUSED) | 当前消息处理暂停,等待 DMA 完成或中断 | 启动 DMA 传输后等待完成中断 |
4.3 传输原子性保证
SPI 总线的传输原子性是指:一个 spi_message 中的所有 spi_transfer 必须在总线上连续执行,中间不允许插入其他设备的操作。这是 SPI 协议的基本要求,也是驱动设计的关键约束。
4.3.1 原子性实现策略
控制器驱动通过以下机制保证传输原子性:
- 队列串行化:所有消息通过单一队列串行执行,同一时刻只有一条消息被处理。
- 片选控制:在消息执行期间,片选信号保持有效(除非
cs_change明确要求切换)。 - 中断/轮询同步:每个传输段完成后,驱动等待硬件状态就绪再启动下一个传输段。
- DMA 链式传输:对于支持 DMA 的控制器,将多个传输段构建为 DMA 描述符链,硬件自动连续执行。
4.3.2 原子性破坏场景与防范
| 风险场景 | 后果 | 防范措施 |
|---|---|---|
| 消息提交过程中被高优先级中断抢占 | 队列链表损坏或状态不一致 | 使用自旋锁保护队列操作,中断中仅标记状态 |
| DMA 完成中断与 CPU 轮询冲突 | 传输段顺序错乱或数据丢失 | 使用完成量(completion)同步,避免竞态 |
| 多个设备共享同一总线时片选竞争 | 总线冲突导致数据错误 | 严格保证消息级原子性,片选只在消息边界切换 |
| 传输段参数(速度、字长)动态切换 | 硬件配置不一致导致通信失败 | 在传输段切换前等待硬件空闲,再重新配置寄存器 |
CONFIG_SPI_DEBUG 和 CONFIG_DYNAMIC_DEBUG 可以打开 SPI 子系统的调试日志,观察消息提交、排队和完成的时间序列,帮助定位原子性破坏问题。
4.4 消息与传输段的关系图
下图展示了 spi_message 与 spi_transfer 之间的层次关系,以及它们如何通过链表组织在一起:
4.5 消息队列处理流程图
下图展示了控制器驱动处理消息队列的完整流程,从消息提交到完成回调:
4.6 传输段处理的核心代码模式
控制器驱动的 transfer_one_message 回调是消息处理的核心。以下是一个典型的处理框架,展示了如何遍历传输段并保证原子性:
static int my_spi_transfer_one_message(struct spi_master *master,
struct spi_message *msg)
{
struct spi_transfer *xfer;
int ret = 0;
unsigned long flags;
// 遍历消息中的所有传输段
list_for_each_entry(xfer, &msg->transfers, transfer_list) {
// 配置硬件寄存器:速度、字长、片选
my_spi_setup_transfer(master, msg->spi, xfer);
// 执行传输(轮询或 DMA 方式)
ret = my_spi_do_transfer(master, msg->spi, xfer);
if (ret < 0) {
dev_err(&master->dev, "transfer failed: %d\n", ret);
goto out;
}
// 处理 cs_change:如果要求切换片选,则释放并重新拉低
if (xfer->cs_change) {
my_spi_chipselect(master, msg->spi, 0); // 释放片选
if (!list_is_last(&xfer->transfer_list, &msg->transfers))
my_spi_chipselect(master, msg->spi, 1); // 重新拉低
}
}
out:
msg->status = ret;
spi_finalize_current_message(master); // 完成消息,触发回调
return ret;
}
spi_finalize_current_message() 必须在所有传输段完成后调用。该函数会释放队列锁、调用消息的 complete 回调,并自动调度队列中的下一条消息。驱动开发者不应在回调中直接操作队列,以免破坏原子性。
4.7 本章小结
- spi_message 是 SPI 通信的顶层抽象,包含一个或多个 spi_transfer 传输段。
- 传输段通过
transfer_list链表串联,在总线上连续执行,保证事务原子性。 - 消息队列由控制器驱动管理,使用自旋锁保护,确保并发安全。
- 传输原子性通过队列串行化、片选控制和同步机制共同保证。
- 驱动开发者应遵循
transfer_one_message框架,避免在回调中直接操作队列。