环形队列 SPI 异步通信思路总结
2026/9/24 17:14:53 网站建设 项目流程

1. 设计背景与问题

在嵌入式系统中,SPI 通常是同步外设,但业务层(APPLICATION)不希望被慢速传输阻塞。直接逐字节收发会带来两个问题:

  • 阻塞等待:APPLICATION 发起读/写后只能死等传输完成,浪费 CPU 时间。
  • 请求堆积:当 APPLICATION 的请求频率高于 SPI 实际传输速度时,后续请求会丢失或被覆盖。

环形队列方案的核心思路是:把「请求产生」和「请求消费」解耦,APPLICATION 只负责把请求压入队列,之后立即返回;SPI 传输由底层驱动通过 DMA/中断逐步消费队列,完成后通过回调通知上层。这样既能异步化,又能用环形缓冲区安全地缓存排队中的请求。

2. 整体架构与流程图

整体数据流可以概括为「生产者—环形队列—消费者」模型:

消费者(底层驱动 + SPI)

环形队列(生产者/消费者共享)

生产者(APPLICATION)

① APPLICATION
发起 SPI 读/写请求

② 请求压入环形队列
(临界区保护,Count+1)

③ 请求数据进入 FIFO
(Head/Tail 推进)

④ StartTransfer 触发传输

⑤ 进入 SPI DMA / IRQ 传输

⑥ 传输完成,触发 callback

⑦ StartNext 取出下一个请求
(临界区保护,Count-1)

图说明:整个链路从 APPLICATION 发起请求开始,经环形队列缓存后由驱动消费;传输完成回调后,驱动再从队列取出下一个请求,形成闭环。G 回到 B 表示「环形队列持续被消费」,直到队列为空才停止。

3. 环形队列示意图

环形队列的关键是「头指针(Head)指向下一个要消费的位置,尾指针(Tail)指向下一个可写入的位置」。写入推进 Tail,消费推进 Head:

环形缓冲区(容量 N)

slot 0

slot 1

slot 2

slot 3

slot 4

Head(消费指针)

Tail(写入指针)

说明:Head 到 Tail 之间的区域是已压入但尚未消费的数据。为了更直观地管理队列状态,可以在 Head/Tail 之外再增加一个计数变量Count每次入队时Count加 1,每次出队时Count减 1。这样队空即可表示为Count == 0,队满即可表示为Count == N(N 为队列容量),避免 Head 与 Tail 相等时无法区分「队空」和「队满」。同时,对CountHeadTail的修改必须放在临界区中完成,防止中断和多个任务同时访问导致计数与指针状态错乱。

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 开始传输

底层通过DMASPI 中断完成实际数据搬运。DMA 方式适合大数据块,搬运期间 CPU 可以继续处理其他事务;中断方式适合小数据或需要逐字节处理的场景。传输期间不占用 APPLICATION 的阻塞调用。

⑥ 传输完成,触发 callback

传输完成(DMA 完成中断 / SPI 收发完成中断)后,驱动调用该请求描述符里的回调函数,把传输结果(成功/失败、实际长度)通知给 APPLICATION。回调应在中断上下文或低优先级任务中谨慎处理,避免在中断里做过重操作。

⑦ StartNext 传输

回调处理完成后,驱动检查队列中是否还有未处理的请求:

  • 若有下一个请求:取出并再次调用StartTransfer(),回到第 ④ 步,形成连续流水。
  • 若队列为空:硬件进入空闲状态,等待下一次 APPLICATION 写入时再次触发。

5. 关键实现要点

  • 原子性保护HeadTail指针以及计数变量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();}

仅展示思路

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询