4. SPI 消息与传输:核心结构与传输机制

SPI 子系统将一次完整的设备交互抽象为 消息 (Message),而消息内部则由一个或多个 传输段 (Transfer) 组成。理解 spi_message 与 spi_transfer 的结构及其队列机制,是掌握 SPI 驱动开发的基础。

4.1 spi_message 结构体:事务的容器

spi_message 代表一次完整的 SPI 事务,它包含多个传输段,并携带完成回调与状态信息。驱动开发者通过填充该结构体来定义一次通信的语义。

成员 类型 描述
transfers struct list_head 挂载该消息下所有 spi_transfer 的链表头
spi struct spi_device * 指向目标 SPI 从设备
is_dma_mapped unsigned 标记缓冲区是否已由调用者完成 DMA 映射
complete void (*)(void *) 消息完成时的回调函数
context void * 传递给回调函数的上下文指针
status int 消息执行后的状态码(0 成功,负值错误)
queue struct list_head 用于将消息挂入控制器消息队列的链表节点
state void * 控制器驱动私有状态指针
关键设计: spi_message 通过 transfers 链表串联多个传输段,使得一次片选有效期间可以执行多个不同方向、不同长度的数据交换,无需重新发起片选信号。

4.2 spi_transfer 结构体:数据传输的原子单元

spi_transfer 描述一段连续的数据传输,是 SPI 通信的最小执行单元。每个传输段可以独立指定发送/接收缓冲区、传输长度、时钟频率、字长和延迟。

struct spi_transfer {
    /* 数据缓冲区 */
    const void  *tx_buf;        // 发送缓冲区(可为 NULL)
    void        *rx_buf;        // 接收缓冲区(可为 NULL)
    unsigned    len;            // 传输字节数

    /* 时序与配置覆盖 */
    unsigned    speed_hz;       // 本次传输的时钟频率(0 表示使用设备默认值)
    u16         bits_per_word;  // 每字位数(0 表示使用设备默认值)
    u8          bits_per_word;  // 字长(旧版本兼容字段)
    u16         delay_usecs;    // 传输完成后的延迟(微秒)
    u32         speed_hz;       // 速度(旧版本兼容字段)

    /* 控制标志 */
    u8          cs_change;      // 传输完成后是否切换片选
    u8          tx_nbits;       // 发送线宽(1/2/4 线)
    u8          rx_nbits;       // 接收线宽(1/2/4 线)
    u8          word_delay_usecs; // 字间延迟(微秒)

    /* 内部使用 */
    struct list_head transfer_list; // 挂入 spi_message 的链表节点
};
缓冲区规则: 若 tx_buf 为 NULL,则发送全 0;若 rx_buf 为 NULL,则丢弃接收数据。至少一个缓冲区必须非 NULL,且 len 必须大于 0。

4.3 消息队列机制:异步流水线

SPI 核心层维护一个 消息队列,驱动通过 spi_async() 将消息提交到队列尾部,控制器按 FIFO 顺序处理。队列机制实现了发送者与硬件操作的解耦,支持高吞吐场景。

SPI 消息队列处理流程 spi_message A spi_message B spi_message C 正在处理 spi_async() 入队 控制器出队 complete() 回调 队列特性: • 消息按提交顺序串行执行 • 支持高优先级消息插入(紧急场景)

4.4 同步传输与异步传输

SPI 子系统提供两种传输模式,满足不同场景需求:

特性 同步传输 (spi_sync) 异步传输 (spi_async)
核心函数 spi_sync() / spi_write_then_read() spi_async()
阻塞行为 调用进程休眠,等待消息完成 立即返回,不阻塞调用进程
完成通知 函数返回即表示完成 通过 complete 回调通知
上下文要求 可在进程上下文调用 可在中断/进程上下文调用
适用场景 简单设备读写、配置寄存器 高吞吐、DMA、实时性要求高
内部实现 封装 spi_async + 等待队列 直接提交到控制器消息队列
注意事项: 在中断上下文或持有自旋锁时,禁止调用 spi_sync(),否则会导致内核死锁。此时必须使用 spi_async() 并确保回调函数在安全上下文中执行。

4.5 消息构建与提交示例

以下代码展示如何构建一个包含两个传输段的消息,并通过同步方式提交:

#include <linux/spi/spi.h>

static int spi_example_transfer(struct spi_device *spi)
{
    struct spi_message msg;
    struct spi_transfer xfer[2];
    u8 tx_buf[] = { 0xAA, 0xBB, 0xCC };
    u8 rx_buf[3] = { 0 };

    /* 初始化消息 */
    spi_message_init(&msg);

    /* 配置传输段 1:发送命令 */
    memset(&xfer[0], 0, sizeof(xfer[0]));
    xfer[0].tx_buf = tx_buf;
    xfer[0].len = 3;
    xfer[0].speed_hz = 1000000;  // 1 MHz
    spi_message_add_tail(&xfer[0], &msg);

    /* 配置传输段 2:接收响应 */
    memset(&xfer[1], 0, sizeof(xfer[1]));
    xfer[1].rx_buf = rx_buf;
    xfer[1].len = 3;
    xfer[1].delay_usecs = 10;    // 段间延迟 10us
    spi_message_add_tail(&xfer[1], &msg);

    /* 同步提交 */
    return spi_sync(spi, &msg);
}
最佳实践: 使用 spi_message_init_with_transfers() 辅助函数可以一步完成消息和传输段的初始化,减少样板代码。对于只发送或只接收的简单操作,优先使用 spi_write()、spi_read() 封装函数。

4.6 传输段间的片选控制

通过 cs_change 标志可以精细控制片选信号的行为:

  • cs_change = 0:当前传输完成后保持片选有效,后续传输段继续使用同一片选。
  • cs_change = 1:当前传输完成后释放片选,下一个传输段重新拉低片选。
  • 最后一个传输段:无论 cs_change 为何值,消息完成后都会释放片选。
片选信号与传输段关系 CS Transfer 1 (cs=0) Transfer 2 (cs=0) Transfer 3 (cs=1) Transfer 1→2 片选保持低电平;Transfer 3 完成后释放片选

4.7 传输完成后的清理与状态检查

消息执行完毕后,必须检查 msg.status 以确认传输是否成功:

/* 异步传输回调示例 */
static void spi_complete_callback(void *context)
{
    struct spi_message *msg = (struct spi_message *)context;

    if (msg->status < 0) {
        pr_err("SPI transfer failed: %d\n", msg->status);
        /* 执行错误恢复或重试逻辑 */
    } else {
        pr_info("SPI transfer succeeded, %d bytes transferred\n",
                msg->actual_length);
    }

    /* 注意:不要在回调中长时间阻塞 */
    complete(&msg->context);  // 若需要同步等待
}
性能提示: 对于批量小数据块传输,尽量合并到同一个 spi_message 的多个 spi_transfer 中,避免多次提交消息带来的队列调度开销。每次消息提交都会触发一次完整的队列入队和调度流程。