1. QuadSPI控制器:从标准SPI到高性能串行通信的演进
在嵌入式系统开发中,SPI(串行外设接口)几乎是工程师最常打交道的通信协议之一。它简单、高效,从传感器、存储器到无线模块,无处不在。然而,随着系统复杂度的提升,尤其是面对高速、大容量外部存储(如串行闪存)或需要极低延迟的实时数据流时,传统SPI控制器在吞吐量、时序控制和系统资源占用方面的局限性开始显现。这时,像PXD10微控制器中集成的QuadSPI这类增强型控制器,就从“够用”的工具变成了“必须用好”的核心组件。
我接触过不少项目,从简单的EEPROM读写到复杂的多路数据采集系统,深刻体会到对SPI控制器底层机制理解不透彻带来的麻烦——数据错位、DMA传输卡死、功耗异常,排查起来往往耗时费力。QuadSPI并非简单的SPI四线模式,它是一个集成了标准SPI模式、连续时钟模式、专用串行闪存模式以及丰富中断/DMA机制的复杂外设。理解其工作原理,特别是连续时钟(Continuous SCK)的适用场景与陷阱、中断与DMA的协同策略,以及串行闪存模式下的高效数据搬运机制,是稳定驱动高速外设、优化系统性能的关键。本文将结合手册细节和实际调试经验,深入解析这些核心功能,帮你避开我当年踩过的那些坑。
2. 连续时钟模式:为特殊从设备提供稳定时序基石
标准SPI通信中,时钟信号(SCK)仅在数据传输期间有效,每个数据帧之间SCK会停止。这对大多数设备来说没有问题。但有些特殊的从设备,例如某些高精度ADC、数字传感器或带有内部PLL的通信芯片,需要主机提供一个持续、稳定的时钟信号,以维持其内部电路的同步或作为参考时钟源。QuadSPI的连续时钟模式正是为此而生。
2.1 连续时钟模式的工作原理与配置要点
启用连续时钟模式非常简单,只需将QuadSPI模块配置寄存器(QSPI_MCR)中的CONT_SCKE位置1即可。一旦启用,无论是否有数据传输,SCK引脚都会持续输出时钟信号,其频率和极性由当前有效的时钟与传输属性寄存器(CTAR)决定。
这里有几个至关重要的配置细节,手册里提到了,但经验告诉我们必须高度重视:
第一,相位限制。手册明确指出,连续时钟模式仅支持CPHA=1(即数据在时钟的第二个边沿采样,第一个边沿捕获)。如果你配置为CPHA=0,即使CONT_SCKE位被设置,控制器也会忽略此设置,或者可能导致不可预测的行为。在实际操作中,我建议在初始化阶段就明确检查并设置CPHA=1,避免后续调试时出现诡异的时序问题。
第二,CTAR的使用规则。在连续时钟模式下,所有传输初始化时都使用CTAR0。只有当一个新的SPI帧传输开始时,如果该帧指定的CTAS(选择使用哪个CTAR)与当前不同,才会切换到相应的CTAR。这意味着,如果你在连续传输过程中需要改变波特率或时钟极性,必须通过在新帧的指令中指定不同的CTAR来实现。但手册也给出了一个强烈建议:在使用连续SCK时,所有传输最好使用相同的波特率。因为在不同帧之间切换时钟极性(CPOL)可能会导致传输错误。我曾在一个项目中,试图在连续时钟下动态切换速率与从设备通信,结果出现了零星的数据错位,最后统一速率后问题消失。
第三,时序参数的固定化。启用连续SCK后,片选信号到SCK的延迟(PCS to SCK delay)会被禁用,而传输后延迟(TDT)则被固定为一个SCK时钟周期。这意味着你无法再通过调整这些精细的延迟来匹配特定从设备的建立/保持时间要求。因此,在决定使用连续时钟模式前,务必确认你的从设备在固定的TDT=1周期下能正常工作。
注意:连续时钟模式与低功耗模式存在冲突。当QuadSPI进入停止模式(Stop Mode)或模块禁用模式(Module Disable Mode)时,连续SCK操作无法保证。这意味着,如果你的系统需要间歇性进入低功耗状态,那么使用连续时钟驱动的外设可能需要额外的时钟源,或者在进入低功耗前妥善结束连续时钟传输。
2.2 连续选择与潜在的数据损坏风险
除了CONT_SCKE,另一个相关的位是CONT(连续选择),它位于TX FIFO条目中。当CONT=1时,片选信号(PCS)在两次传输之间保持有效(拉低)。结合连续SCK,这可以实现背靠背(back-to-back)的无缝连续传输。
然而,这里隐藏着一个危险的陷阱,手册用了一小段描述,但实际危害很大:在某些条件下,即使PCS保持有效,SCK继续运行,但主机的数据输出线(SO)可能没有数据移出(处于高阻或上拉至高电平)。这会导致从设备采样到错误的数据(全1或不确定电平)。
这些条件包括:
- 启用了连续SCK且CONT位被设置,但TX FIFO中没有数据。
- 启用了连续SCK且CONT位被设置,同时QuadSPI进入了STOPPED状态。
- 启用了连续SCK且CONT位被设置,同时进入了停止模式或模块禁用模式。
我的实操心得是:在启用连续SCK和CONT位进行长时间流传输时,必须确保TX FIFO的填充机制(无论是DMA还是CPU)足够稳健,绝不能断流。同时,在计划停止传输或进入低功耗前,必须有明确的流程先终止CONT模式,确保最后一帧数据完整送出,再让SCK停止。一个可靠的实践是在发送队列的最后一个命令字中清除CONT位,并利用“队列结束”中断来安全地执行后续操作。
3. SPI模式下的中断与DMA请求机制精解
高效管理SPI数据传输,中断和DMA是关键。QuadSPI在SPI模式下提供了六种可触发中断或DMA请求的条件,理解它们之间的区别和协作方式是实现高效、低CPU占用率通信的核心。
3.1 六种中断/DMA条件详解
下表概括了这六种条件及其属性:
| 条件 | 状态标志位 (QSPI_SPISR) | 可触发中断 | 可触发DMA请求 | 说明 |
|---|---|---|---|---|
| 队列结束 (EOQ) | EOQF | 是 | 否 | 当执行的SPI命令中EOQ位被置位时触发,标志一个传输队列的结束。 |
| TX FIFO 填充 (TFFF) | TFFF | 是 | 是 | 当TX FIFO未满(有空位)时触发,用于通知主机可以写入更多发送数据。 |
| 传输完成 (TCF) | TCF | 是 | 否 | 每一帧串行数据传输完成时触发,适用于帧级别的同步处理。 |
| TX FIFO 下溢 (TFUF) | TFUF | 是 | 否 | 仅在SPI从模式下有效。当TX FIFO为空且外部SPI主机发起传输时触发,表示从机未能及时提供数据。 |
| RX FIFO 排空 (RFDF) | RFDF | 是 | 是 | 当RX FIFO非空(有数据)时触发,用于通知主机可以读取接收到的数据。 |
| RX FIFO 溢出 (RFOF) | RFOF | 是 | 否 | 当RX FIFO和移位寄存器已满,但仍有新的传输试图写入时触发,意味着数据丢失。 |
关键点解析:
- TFFF与RFDF的灵活性:这两个标志是唯一可以配置为触发中断或DMA请求的。通过设置QSPI_SPIRSER寄存器中的TFFF_DIRS和RFDF_DIRS位来选择。这是实现DMA自动搬运数据的核心。
- EOQ的用途:它并非指物理队列结束,而是指命令字中的EOQ位。通常用于在DMA传输链中,标记一组关联传输的结束,以便软件切换队列或进行后续处理。
- 溢出与下溢:RFOF和TFUF是错误状态标志。特别是RFOF,一旦发生,意味着接收数据丢失。寄存器中的ROOE位决定了溢出时新数据是被忽略还是覆盖移位寄存器中的数据。在要求数据完整性的��景,必须使能此中断并严格处理。
3.2 中断与DMA的协同编程策略
在实际项目中,如何搭配使用这些中断和DMA?
策略一:纯DMA驱动的大数据量传输。这是最理想的性能模式。配置步骤通常如下:
- 初始化DMA控制器,为TX和RX通道分别配置描述符(源/目标地址、传输量等)。
- 在QuadSPI中,使能TFFF和RFDF的DMA请求(设置TFFF_DIRS和RFDF_DIRS)。
- 启动DMA传输。当TX FIFO有空位时,自动触发DMA将数据从内存搬至TX FIFO;当RX FIFO有数据时,自动触发DMA将数据从RX FIFO搬至内存。
- 在传输队列的最后一个命令字中设置EOQ位。
- 使能EOQF中断。当整个队列传输完毕,触发EOQF中断,在中断服务程序中,可以安全地关闭DMA、处理接收完成的数据缓冲区或启动下一轮传输。
这种模式下,CPU几乎不参与数据传输过程,效率极高。
策略二:中断辅助的混合传输。适用于数据量不大或传输间隔不规律的场景。例如:
- 使用TCF(每帧完成)中断来处理需要逐帧响应的协议。
- 使用RFDF中断来读取不定长数据。当RFDF触发时,在中断服务程序中读取QSPI_POPR寄存器,直到RXCNT为零。
- 对于发送,可以使用TFFF中断来填充TX FIFO,实现“即用即发”,避免一次性写入大量数据占用内存。
避坑指南:
- 中断使能顺序:建议先清除所有状态标志,再使能中断请求位。避免一上电就因为残留状态误触发中断。
- DMA与中断的互斥:对于TFFF和RFDF,同一时刻只能选择一种请求方式(DMA或中断)。如果同时使能了中断和DMA使能位,行为是未定义的。
- 处理RFOF:一旦发生RX FIFO溢出,后续数据可能错位。一个稳健的做法是,在RFOF中断服务程序中,清空RX FIFO(CLR_RXF),并重置通信状态,必要时向上层报告错误。
4. 串行闪存模式:高效访问外部存储的核心
QuadSPI的串行闪存模式是其区别于普通SPI控制器的最大亮点。它专为连接外部Quad-SPI Flash(支持四线数据IO的串行闪存)而设计,通过指令、地址、模式和数据的灵活组合,以及多达4条双向数据线,实现了远超标准SPI的读写带宽。
4.1 SFM命令的两种触发方式:IP命令与AHB命令
访问外部闪存,本质上是向QuadSPI模块提交一个“命令”。手册介绍了两种触发方式,理解它们的区别对软件架构设计很重要。
IP命令(寄存器接口触发):这种方式通过直接配置一组专用寄存器来发起命令,完全由软件控制,灵活性最高。
- 写地址:将目标闪存地址写入QSPI_SFAR寄存器。
- 写指令选项:将指令相关的参数(如数据阶段大小、地址宽度、指令码本身等)写入QSPI_ICR寄存器的ICO字段。
- 触发执行:最后,将具体的指令码写入QSPI_ICR寄存器的IC字段。注意:对IC字段的写操作必须是整个命令配置序列的最后一步,因为它会立即触发命令执行。也可以将ICO和IC字段合并为一次32位写操作。
AHB命令(内存映射访问触发):这种方式将外部闪存映射到处理器的内存地址空间。软件只需要像访问普通内存一样进行读操作,QuadSPI硬件会自动在后台完成闪存读取、缓存到内部AHB Buffer等一系列动作,对软件透明,使用最简便。
- 配置预取:通过QSPI_ACR寄存器设置预取参数(如预取大小)。
- 内存读触发:当CPU或DMA访问被映射的闪存地址空间时,如果所需数据不在内部的AHB Buffer中,QuadSPI自动发起一次AHB命令,从闪存读取一整块数据到AHB Buffer。
- 直接读取:后续对同一块数据的访问,直接从AHB Buffer中读取,速度极快。
选择建议:
- 对随机、小规模的读写操作(如读取状态寄存器、写使能、擦除扇区),使用IP命令,因为它更直接,开销小。
- 对大规模的顺序数据读取(如执行代码XIP,或读取大块数据文件),使用AHB命令配合内存映射模式,可以极大简化软件设计,并利用硬件的预取和缓存机制提升性能。
4.2 闪存编程与读取的实操流程
手册给出了Flash编程和读取的步骤,但有些细节需要结合实践来理解。
Flash编程(写操作)流程:
- 确保TX Buffer为空:检查QSPI_SFMSR[TXNE]位。如果不为空,需要向QSPI_MCR[CLR_TXF]写1来清除TX Buffer。这是一个关键检查点,我曾在连续写入时忽略它,导致数据覆盖错误。
- 设置目标地址:将编程地址写入QSPI_SFAR。
- 填充数据到TX Buffer:通过写QSPI_TBDR寄存器,将至少一个字(Word)的数据写入TX Buffer。这是一个深度为15个字的循环FIFO。
- 配置指令参数:将本次编程操作的数据长度等选项写入QSPI_ICR[ICO]。
- 触发IP命令:写入指令码到QSPI_ICR[IC],编程操作立即开始。
- 持续填充数据:QuadSPI会从TX Buffer中不断取出数据发送给闪存。软件需要监控QSPI_TBSR[TRCTR]字段(已写入字数),并及时向QSPI_TBDR写入后续数据,避免TX Buffer下溢(Underrun)。下溢会导致命令异常终止。
重要提示:当QuadSPI模块发送完所有数据后,其状态会从“忙”变为“空闲”。但这仅仅意味着数据已全部发送至闪存芯片。闪存芯片内部的编程(电荷注入)过程仍在进行,可能需要几毫秒。软件必须通过读取闪存的状态寄存器,来确认编程真正完成,才能进行下一步操作(如读验证或擦写其他扇区)。忽略这一步是导致数据写入失败的最常见原因。
Flash读取流程:读取分为两步:QuadSPI从闪存读到内部Buffer,然后主机从Buffer中读取。
- IP命令读取:类似于编程,设置地址(QSPI_SFAR)和读指令参数/指令码(QSPI_ICR),触发后数据被读入RX Buffer。软件可以通过轮询QSPI_SFMSR[RXNE]标志或使能RBDF中断/DMA请求来获取数据。
- AHB命令读取(内存映射):这是最常用的方式。配置好QSPI_ACR后,直接对映射的内存地址进行读操作。如果数据不在AHB Buffer中,硬件自动触发预取。这里有个性能技巧:AHB Buffer就像一个单行缓存。顺序访问时效率最高。如果随机跳跃访问,会导致频繁的缓存未命中,性能下降。
主机读取Buffer数据的方式:
- 标志位轮询:读QSPI_ARDB地址,并通过写1到QSPI_SFMFR[RBDF]位来移动读指针。
- DMA读取:将QSPI_ARDB配置为DMA源地址,可以实现数据从RX Buffer到系统内存的全自动搬运。
- 直接寄存器访问:直接读QSPI_RBDR0~14寄存器。但手册不推荐连续读取超过15个字时使用此法,因为缺乏清晰的FIFO指针管理,容易出错。
- 内存映射访问:直接读映射的闪存地址。这是AHB命令的读取方式,最简单。
4.3 SFM模式下的中断与Buffer管理
SFM模式有自己独立的一套中断标志,集中在QSPI_SFMFR寄存器中。
| 条件 | 标志位 | 说明 |
|---|---|---|
| TX Buffer 填充 (TBFF) | TBFF | TX Buffer有空位,可写入新数据。用于编程时驱动数据流。 |
| RX Buffer 排空 (RBDF) | RBDF | RX Buffer有数据可读。可配置为中断或DMA请求。 |
| TX Buffer 下溢 (TBUF) | TBUF | 编程时TX Buffer已空,但模块仍需数据,命令终止。 |
| RX Buffer 溢出 (RBOF) | RBOF | RX Buffer已满,但仍有数据要写入,数据丢失。 |
| AHB Buffer 溢出 (ABOF) | ABOF | AHB Buffer已满,预取数据丢失。 |
| 指令码错误 (ICEF) | ICEF | 发送了不支持的指令码。 |
| 事务完成 (TFF) | TFF | 当前SFM命令执行完毕。 |
Buffer管理心得:
- TX Buffer下溢是编程失败主因:务必确保数据供给速度大于QuadSPI的发送速度。使用TBFF中断或DMA来及时填充数据是最佳实践。
- 利用TFF中断:对于擦除、写使能等不带数据阶段或数据阶段很短的操作,使能TFF中断可以精确知道操作何时被闪存接收,便于安排后续操作。
- AHB Buffer溢出的预防:在内存映射读取时,如果CPU/DMA读取速度远低于QuadSPI预取速度,可能发生ABOF。优化软件读取逻辑,或调整QSPI_ACR中的预取大小(ARSZ),使其与典型读取块大小匹配。
5. 低功耗模式下的注意事项与初始化实践
嵌入式设备常需低功耗,QuadSPI提供了停止模式、模块禁用模式等节能策略。
5.1 进入与退出低功耗模式的时序约束
无论是通过软件写MDIS位,还是硬件信号ipg_stop/ipg_doze请求进入低功耗模式,QuadSPI都不会立即响应。它会等待当前操作完成:
- SPI模式:等待到达帧边界。
- SFM模式:等待当前执行的SFM命令完成。
这意味着,在请求进入低功耗到实际时钟关闭的窗口期内,以及从时钟恢复到完全可操作的窗口期内,某些操作是非法的。手册特别强调,在SFM模式下,从请求进入停止/模块禁用模式开始,到完全退出该模式为止,禁止发起新的SFM命令或新的AHB请求。违反此规定可能导致总线挂起或数据损坏。
实践建议:在进入低功耗的软件流程中,先确保所有QuadSPI传输已完成(查询BUSY位),再发起低功耗请求。退出低功耗后,稍作延时(几个时钟周期)再访问QuadSPI模块,确保其内部逻辑已稳定。
5.2 队列切换与模块初始化的标准流程
手册第30.6.1节给出了一个在SPI模式下切换队列的示例流程,这是一个非常经典的模块再初始化和资源清理过程,值得牢记:
- 设置队列结束标志:在旧传输队列的最后一个命令字中设置EOQ位。
- 等待队列结束:EOQF标志置位,QuadSPI进入STOPPED状态。
- 禁用DMA:在DMA控制器中禁用与TX FIFO和RX FIFO关联的DMA通道请求。这一步必须在清空FIFO之前进行,否则DMA可能在你清理过程中又写入新数据。
- 排空RX FIFO:通过读取QSPI_POPR,或检查RFDF标志,确保所有已接收数据都已从RX FIFO转移到内存。
- 更新DMA描述符:为新的数据传输队列配置DMA描述符。
- 清空FIFO:写1到QSPI_MCR[CLR_TXF]和CLR_RXF位,清空发送和接收FIFO。
- 重置传输计数:可以通过设置新队列第一个命令字的CTCNT位,或者直接写QSPI_TCR寄存器中的SPI_TCNT字段来实现。
这个流程的核心思想是:安全地停止旧任务 -> 彻底清理硬件状态 -> 无误地配置新任务。在SFM模式下进行类似的操作(如切换读写命令)时,也应遵循类似的“查询状态 -> 停止 -> 清理 -> 重配”的原则,这是保证QuadSPI长期稳定工作的关键。