1 为什么需要同步和异步接口
先说下什么是同步和异步。就是字面意思啦。
- 同步:调用者发起一个操作,原地等待,直到操作完成,期间不能做别的事情。
- 异步:调用着发起一个操作,立即返回,不等待结果,可以处理其他事情,任务完成后通知调用者处理
我们看一个场景

假设上面一次SPI通信流程为:发送寄存器地址;等待SPI clock;读取数据。
一个线程A调用同步接口spi_sync(),此时线程A会被阻塞,一直等待整个过程完成再返回。

一个线程B调用异步接口spi_async(),此时线程B不用等待,可以去忙其他的事,等SPI传输完成再处理

2 spi_sync源码分析
我们先来看同步的接口函数spi_sync,它的作用就是同步提交一个message并等待完成
源码位置:drivers/spi/spi.c
1 2 3 4 5 6 7 8 9 10 11
| int spi_sync(struct spi_device *spi, struct spi_message *message) { int ret;
mutex_lock(&spi->controller->bus_lock_mutex); ret = __spi_sync(spi, message); mutex_unlock(&spi->controller->bus_lock_mutex);
return ret; } EXPORT_SYMBOL_GPL(spi_sync);
|
其中重要的就是上锁,为什么上锁?一个SPI Controller可能挂多个设备,统一时间只允许一个设备占用资源进行传输,保证传输不冲突。
整个调用过程如下:

其中在__spi_sync里面会调用一个重要函数wait_for_completion(),
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| static int __spi_sync(struct spi_device *spi, struct spi_message *message) { DECLARE_COMPLETION_ONSTACK(done); int status; struct spi_controller *ctlr = spi->controller; unsigned long flags;
message->complete = spi_complete; message->context = &done;
spin_lock_irqsave(&ctlr->bus_lock_spinlock, flags); status = __spi_async(spi, message); spin_unlock_irqrestore(&ctlr->bus_lock_spinlock, flags);
if (status == 0) { wait_for_completion(&done); status = message->status; }
message->complete = NULL; message->context = NULL;
return status; }
|
spi_sync() 并没有重新实现一套“同步传输”,而是先使用异步方式提交 spi_message,然后调用 wait_for_completion() 等待消息完成,从而表现为同步接口。
当消息传输完成时,会产生一个中断,从而唤醒线程。这个中断函数的实现由具体的SPI 控制器驱动。以RK系列为例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| static irqreturn_t rockchip_spi_isr(int irq, void *dev_id) { struct spi_controller *ctlr = dev_id; struct rockchip_spi *rs = spi_controller_get_devdata(ctlr);
if (!rs->rx_left) { spi_enable_chip(rs, false);
writel_relaxed(0, rs->regs + ROCKCHIP_SPI_IMR);
writel_relaxed(0xffffffff, rs->regs + ROCKCHIP_SPI_ICR);
spi_finalize_current_transfer(ctlr); }
return IRQ_HANDLED; }
|
中断处理函数通过 spi_finalize_current_transfer() 通知 SPI Core。最终 SPI Core 完成整个消息并唤醒 spi_sync() 线程。
3 spi_async源码分析
前面我们说过spi_async提交SPI传输,但是不等待。
调用关系如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| int spi_async(struct spi_device *spi, struct spi_message *message) { struct spi_controller *ctlr = spi->controller; int ret; unsigned long flags;
ret = spi_maybe_optimize_message(spi, message); if (ret) return ret;
spin_lock_irqsave(&ctlr->bus_lock_spinlock, flags);
if (ctlr->bus_lock_flag) ret = -EBUSY; else ret = __spi_async(spi, message);
spin_unlock_irqrestore(&ctlr->bus_lock_spinlock, flags);
return ret; } EXPORT_SYMBOL_GPL(spi_async);
|
上面首先会获取SPI控制器,接着关闭当前 CPU 中断,获取 bus_lock_spinlock 自旋锁,把message加入消息队列。
怎么告诉控制器传输完成呢?通过回调机制
1 2 3 4 5 6 7
| struct spi_message {
void (*complete)(void *arg);
void *context;
};
|
当硬件传输完成时,会调用message->complete()。
4 实际选择
| 对比维度 |
spi_sync |
spi_async |
| 是否等待 |
是,调用者原地阻塞 |
否,立即返回 |
| 线程状态 |
阻塞(睡眠) |
继续运行 |
| 完成通知 |
函数返回值 |
回调函数 (callback) |
| 并发能力 |
低,锁住整个 bus |
高,仅队列操作加锁 |
| 适用场景 |
简单、低频、低速设备 |
复杂、高频、高速设备 |
| 在实际操作时,如果数据量小,频率比较低,那么可以选择同步;如果数据量较大,高频率,用到DMA,那么可以选择异步。 |
|
|
总结
Linux SPI Framework提供同步和异步两种传输接口。同步接口spi_sync会阻塞当前线程直到SPI传输完成,实现简单,适用于低速、小数据量设备。异步接口spi_async通过消息队列和completion回调机制,在提交传输后立即返回,适用于高速、大数据量以及DMA传输场景。
互动
SPI设备驱动发起一次数据传输时,当前线程是否需要等待硬件完成?