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 链表,并触发队列处理。典型流程如下:

  1. 初始化消息:上层驱动填充 spi_message 和 spi_transfer,设置回调函数。
  2. 调用 spi_async():将消息提交给 SPI 核心层。
  3. 核心层处理:spi_async() 调用控制器驱动的 transfer_one_message 回调。
  4. 入队操作:驱动将消息加入 master->queue 链表,并检查队列是否为空。
  5. 调度执行:如果队列之前为空,立即启动消息处理;否则等待当前消息完成。
  6. 完成回调:消息处理完毕后,驱动调用 spi_finalize_current_message(),触发回调并调度下一条消息。
注意: 消息队列操作必须在自旋锁保护下进行,以防止 SMP 并发提交导致链表损坏。大多数控制器驱动使用 master->queue_lock 或驱动内部的自旋锁。

4.2.2 队列状态机

控制器驱动的消息队列通常包含三种状态:

状态 描述 触发条件
空闲(IDLE) 队列为空,控制器处于待机状态 上电初始化或最后一条消息完成
运行(RUNNING) 正在处理当前消息,队列中可能还有待处理消息 从空闲状态提交第一条消息
暂停(PAUSED) 当前消息处理暂停,等待 DMA 完成或中断 启动 DMA 传输后等待完成中断
优化要点: 在 FIFO 深度较大或支持 DMA 的控制器中,驱动可以在当前消息的传输段之间进行流水线操作——即提前准备下一个传输段的 DMA 描述符,减少 CPU 等待时间。

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 之间的层次关系,以及它们如何通过链表组织在一起:

spi_message 与 spi_transfer 层次结构 spi_message transfers → 链表头 spi_transfer #1 tx_buf / rx_buf / len speed_hz / cs_change spi_transfer #2 tx_buf / rx_buf / len speed_hz / cs_change spi_transfer #3 tx_buf / rx_buf / len speed_hz / cs_change 传输原子性说明 • 一个 spi_message 包含多个 spi_transfer,通过 transfer_list 链表串联 • 所有传输段在总线上连续执行,中间不插入其他设备操作 • cs_change 控制片选是否在传输段之间切换,默认保持有效

4.5 消息队列处理流程图

下图展示了控制器驱动处理消息队列的完整流程,从消息提交到完成回调:

SPI 消息队列处理流程 1. spi_async() 提交消息 2. 加锁 & 消息入队 队列空闲? 是 启动消息处理 否 等待当前消息完成 4. 遍历传输段链表 5. 执行硬件传输 6. 回调 & 调度下一条

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 框架,避免在回调中直接操作队列。