4. SPI 消息与传输:消息队列与同步/异步机制
SPI 从设备通信的核心是消息(Message)与传输(Transfer)对象。VxWorks 将一次 SPI 事务抽象为 struct spi_message,将每个片选连续的收发段抽象为 struct spi_ioc_transfer。理解这两个结构体的关系、消息队列的管理方式以及同步/异步传输模型,是编写高效 SPI 驱动的关键。
4.1 核心数据结构:spi_message 与 spi_ioc_transfer
| 结构体 | 作用 | 关键字段 |
|---|---|---|
struct spi_message | 一次完整的 SPI 事务,包含一个或多个 transfer 段 | transfers(链表头)、complete(回调)、context、status、actual_length |
struct spi_ioc_transfer | 单个片选连续的收发段,描述一次 tx/rx 操作 | tx_buf、rx_buf、len、speed_hz、delay_usecs、cs_change |
关键概念: 一个
spi_message 可以包含多个 spi_ioc_transfer,这些 transfer 在同一个片选有效期内连续执行,中间不会释放片选。每个 transfer 可以独立配置速率、延迟和片选行为。
4.1.1 spi_message 结构详解
typedef struct spi_message {
struct list_head transfers; /* transfer 链表头 */
struct spi_device *spi; /* 目标 SPI 设备 */
unsigned is_dma_mapped : 1; /* DMA 映射标志 */
/* 完成回调 */
void (*complete)(void *context);
void *context; /* 回调上下文 */
int status; /* 传输状态,0 成功 */
unsigned actual_length; /* 实际传输字节数 */
/* 内部链表节点 */
struct list_head queue; /* 消息队列节点 */
/* ... 其他内部字段 */
} SPI_MESSAGE;
- transfers:通过
list_add_tail()将多个spi_ioc_transfer挂入链表。 - complete:异步传输完成后的回调函数,在中断上下文或任务上下文中执行。
- status:驱动在传输完成后设置,0 表示成功,负值表示错误码。
- actual_length:所有 transfer 实际传输的总字节数。
4.1.2 spi_ioc_transfer 结构详解
typedef struct spi_ioc_transfer {
const void *tx_buf; /* 发送缓冲区,NULL 表示只接收 */
void *rx_buf; /* 接收缓冲区,NULL 表示只发送 */
unsigned len; /* 本次 transfer 的字节数 */
unsigned speed_hz; /* 本次传输速率,0 使用设备默认 */
uint16_t delay_usecs; /* 传输结束后延迟(微秒) */
uint8_t bits_per_word; /* 每字位数,0 使用设备默认 */
uint8_t cs_change; /* 传输结束后是否改变片选 */
/* ... 其他字段 */
} SPI_IOC_TRANSFER;
开发提示: 当
tx_buf 和 rx_buf 同时为 NULL 时,该 transfer 仅用于产生时钟(常用于 dummy 读)。cs_change 设置为 1 时,传输结束后会释放片选,下一个 transfer 重新拉低片选。
4.2 消息队列管理
VxWorks SPI 核心层维护一个全局消息队列,所有待处理的 spi_message 按优先级或 FIFO 顺序排队。驱动通过 spi_async() 将消息挂入队列,由 SPI 控制器中断或轮询机制逐个处理。
队列管理流程:
- 调用
spi_async()将消息加入队列尾部。 - 如果控制器空闲,立即启动第一个消息的传输。
- 每个 transfer 完成后,通过中断或 DMA 回调推进到下一个 transfer。
- 所有 transfer 完成后,调用消息的
complete回调,并从队列中移除该消息。 - 调度队列中的下一个消息开始传输。
4.3 同步传输 vs 异步传输
| 特性 | 同步传输(spi_sync) | 异步传输(spi_async) |
|---|---|---|
| 调用方式 | 阻塞等待传输完成 | 立即返回,通过回调通知完成 |
| 适用场景 | 简单读写、任务上下文 | 中断驱动、高并发、DMA 传输 |
| CPU 占用 | 等待期间占用 CPU | 等待期间可执行其他任务 |
| 实现复杂度 | 低,直接调用 | 高,需管理回调上下文 |
| 典型 API | spi_sync() | spi_async() |
4.3.1 同步传输实现
STATUS spi_sync(SPI_MESSAGE *pMsg)
{
/* 初始化完成标志 */
SEM_ID sem = semBCreate(SEM_Q_FIFO, SEM_EMPTY);
pMsg->complete = sync_complete;
pMsg->context = sem;
/* 将消息加入队列 */
spi_async(pMsg);
/* 阻塞等待完成信号量 */
semTake(sem, WAIT_FOREVER);
semDelete(sem);
return pMsg->status;
}
static void sync_complete(void *context)
{
SEM_ID sem = (SEM_ID)context;
semGive(sem); /* 释放信号量,唤醒等待任务 */
}
注意事项: 同步传输不能在中断上下文中调用,否则
semTake() 会导致死锁。在中断上下文中必须使用异步传输。
4.3.2 异步传输实现
/* 异步传输示例 */
void my_spi_async_transfer(SPI_DEVICE *pDev, uint8_t *tx, uint8_t *rx, int len)
{
SPI_MESSAGE msg;
SPI_IOC_TRANSFER transfer;
/* 初始化 transfer */
memset(&transfer, 0, sizeof(transfer));
transfer.tx_buf = tx;
transfer.rx_buf = rx;
transfer.len = len;
transfer.speed_hz = 1000000; /* 1MHz */
transfer.cs_change = 0;
/* 初始化 message */
memset(&msg, 0, sizeof(msg));
msg.spi = pDev;
msg.complete = my_complete_callback;
msg.context = my_context;
/* 将 transfer 加入 message */
spi_message_init(&msg);
spi_message_add_tail(&transfer, &msg);
/* 异步提交 */
spi_async(&msg);
}
static void my_complete_callback(void *context)
{
/* 在中断或任务上下文中执行 */
/* 处理传输完成后的数据 */
/* 唤醒等待的任务或发送信号 */
}
4.4 传输流程与状态机
SPI 控制器驱动内部维护一个传输状态机,管理消息队列的推进。典型状态包括:IDLE(空闲)、ACTIVE(传输中)、WAIT_CS(等待片选稳定)、COMPLETE(完成)。
4.5 消息队列深度与性能考量
- 队列深度:默认队列深度由
SPI_MSG_QUEUE_SIZE配置,通常为 16~64。超出深度时spi_async()返回 ERROR。 - 优先级反转:高优先级任务提交的消息可能被低优先级消息阻塞,建议使用优先级队列或单独的消息通道。
- DMA 对齐:使用 DMA 传输时,
tx_buf和rx_buf必须满足 DMA 对齐要求(通常 4 字节或缓存行对齐)。 - 超时处理:同步传输建议增加超时机制,防止硬件故障导致任务永久阻塞。
最佳实践: 对于频繁的小数据量传输(如传感器寄存器读写),使用同步传输简化代码;对于大数据块传输(如 Flash 页编程),使用异步传输配合 DMA 提高吞吐量。
4.6 本章小结
| 知识点 | 核心要点 |
|---|---|
| spi_message | 一次 SPI 事务的抽象,包含多个 transfer 段和完成回调 |
| spi_ioc_transfer | 单个片选连续的收发段,可独立配置速率和片选行为 |
| 消息队列 | FIFO 管理待处理消息,控制器空闲时自动调度 |
| 同步传输 | 阻塞等待,适合任务上下文,简单可靠 |
| 异步传输 | 非阻塞,回调通知,适合中断/DMA 场景 |
| 状态机 | IDLE → ACTIVE → WAIT_CS → COMPLETE 循环 |