1. 设计背景与问题
在嵌入式系统中,SPI 通常是同步外设,但业务层(APPLICATION)不希望被慢速传输阻塞。直接逐字节收发会带来两个问题:
- 阻塞等待:APPLICATION 发起读/写后只能死等传输完成,浪费 CPU 时间。
- 请求堆积:当 APPLICATION 的请求频率高于 SPI 实际传输速度时,后续请求会丢失或被覆盖。
环形队列方案的核心思路是:把「请求产生」和「请求消费」解耦,APPLICATION 只负责把请求压入队列,之后立即返回;SPI 传输由底层驱动通过 DMA/中断逐步消费队列,完成后通过回调通知上层。这样既能异步化,又能用环形缓冲区安全地缓存排队中的请求。
2. 整体架构与流程图
整体数据流可以概括为「生产者—环形队列—消费者」模型:
图说明:整个链路从 APPLICATION 发起请求开始,经环形队列缓存后由驱动消费;传输完成回调后,驱动再从队列取出下一个请求,形成闭环。G 回到 B 表示「环形队列持续被消费」,直到队列为空才停止。
3. 环形队列示意图
环形队列的关键是「头指针(Head)指向下一个要消费的位置,尾指针(Tail)指向下一个可写入的位置」。写入推进 Tail,消费推进 Head:
说明:Head 到 Tail 之间的区域是已压入但尚未消费的数据。为了更直观地管理队列状态,可以在 Head/Tail 之外再增加一个计数变量Count:每次入队时Count加 1,每次出队时Count减 1。这样队空即可表示为Count == 0,队满即可表示为Count == N(N 为队列容量),避免 Head 与 Tail 相等时无法区分「队空」和「队满」。同时,对Count、Head、Tail的修改必须放在临界区中完成,防止中断和多个任务同时访问导致计数与指针状态错乱。
4. 流程分步讲解
下面逐条说明这 7 步在工程中分别做了什么:
① APPLICATION 请求 SPI 读/写
业务代码调用统一的 API(例如Spi_AsyncRead(buffer, len)/Spi_AsyncWrite(buffer, len)),只描述「要做什么」,不关心底层传输细节。
② 请求压入队列
API 内部把本次请求封装为一个描述符(目标地址、长度、读写标志、回调函数等),尝试压入环形队列:
- 若队列未满:写入 Tail 位置,Tail 前移,函数立即返回。
- 若队列已满:根据策略返回错误(如
QUEUE_FULL)或进入等待。
③ 请求数据进入 FIFO
这里的 FIFO 既指环形队列本身的先进先出语义,也指请求描述符按顺序进入缓冲区。因为队列遵循先进先出,APPLICATION 的请求顺序会被严格保留,保证传输顺序与业务提交顺序一致。
④ StartTransfer 触发传输
当队列从「空」变为「非空」时,驱动调用StartTransfer()启动一次 SPI 传输。这是异步链路的关键起点:只有检测到有货且硬件空闲时才真正开启传输。
⑤ 进入 SPI DMA / IRQ 开始传输
底层通过DMA或SPI 中断完成实际数据搬运。DMA 方式适合大数据块,搬运期间 CPU 可以继续处理其他事务;中断方式适合小数据或需要逐字节处理的场景。传输期间不占用 APPLICATION 的阻塞调用。
⑥ 传输完成,触发 callback
传输完成(DMA 完成中断 / SPI 收发完成中断)后,驱动调用该请求描述符里的回调函数,把传输结果(成功/失败、实际长度)通知给 APPLICATION。回调应在中断上下文或低优先级任务中谨慎处理,避免在中断里做过重操作。
⑦ StartNext 传输
回调处理完成后,驱动检查队列中是否还有未处理的请求:
- 若有下一个请求:取出并再次调用
StartTransfer(),回到第 ④ 步,形成连续流水。 - 若队列为空:硬件进入空闲状态,等待下一次 APPLICATION 写入时再次触发。
5. 关键实现要点
- 原子性保护:
Head、Tail指针以及计数变量Count的读写都必须在临界区内完成。APPLICATION(生产者)入队时会执行Count++,中断或驱动(消费者)出队时会执行Count--,如果两类操作同时发生且不加以保护,就会造成计数错乱、队列状态被破坏。典型做法是进入临界区前关闭中断或使用互斥锁,操作完成后立即释放。 - 队满/队空判定:引入
Count后可以更直观地判断状态:Count == 0表示队空,Count == N表示队满。相比「保留一个空位」的方式,这种方案不浪费队列空间,但必须保证Count的增减与Head/Tail的推进在同一个临界区里原子完成。 - 入队判定:APPLICATION 写入前先判断队列是否已满,若
Count >= N则本次请求不能入队,根据策略返回错误(如QUEUE_FULL)或等待,避免覆盖尚未消费的数据。 - 回调上下文:回调应尽量短小,如需复杂处理可投递到工作队列或任务中,避免拖慢中断响应。
- 传输状态机:空闲(IDLE)→ 传输中(BUSY)→ 完成回调(DONE)→ 取下一条,状态清晰便于调试。## 6. 小结
环形队列 SPI 异步通信的本质是:用环形缓冲区充当生产者和消费者之间的缓冲,让 APPLICATION 以非阻塞方式提交请求,让底层驱动以「完成即取下一个」的方式持续消费。其中 ① 到 ③ 是「入队」,④ 到 ⑦ 是「出队并传输」,两者通过环形队列衔接,实现高效、顺序、非阻塞的 SPI 通信。
环形异步通信伪代码
Std_ReturnTypeAsync_AeSpi_WriteReg(uint32 RegAddr,uint8 DataWidth,uint32 Data,AsyncSpi_ReqIdType SpiReqId){/* Step1: Build SPI Frame */SuspendOSInterrupts();/* Step2: Push Into Spi Queue *//* Step3: Actively request SPI Transmit, if an ongoing transfer is detected, push the request into the request queue */ResumeOSInterrupts();/* Step4: Reset the request buffer s_AsyncSpiRequestData */AsyncResetAsyncSpiRequestData();}Std_ReturnTypeAsync_AeSpi_ReadReg(uint32 RegAddr,uint8 DataWidth,uint32*RegValPtr,AsyncSpi_ReqIdType SpiReqId){/* Step1: Build SPI Frame */SuspendOSInterrupts();/* Step2: Push Into Spi Queue *//* Step3: Actively request SPI Transmit, if an ongoing transfer is detected, push the request into the request queue */ResumeOSInterrupts();/* Step4: Reset the request buffer s_AsyncSpiRequestData */AsyncResetAsyncSpiRequestData();}仅展示思路