QSPI 读写时序详解

1. 时钟相位:CPOL 与 CPHA

做 QSPI 驱动,第一步就是把时钟极性和相位搞明白。
我遇到过不少工程师,明明代码逻辑都对,数据就是错位,最后发现是时钟相位设反了。

QSPI 的时钟由两个参数决定:

  • CPOL(Clock Polarity):空闲时时钟是高电平还是低电平。CPOL=0 表示空闲低,CPOL=1 表示空闲高。
  • CPHA(Clock Phase):数据在时钟的哪个边沿采样。CPHA=0 表示第一个边沿采样,CPHA=1 表示第二个边沿采样。

组合起来有四种模式,但实际 QSPI 器件最常用的是 模式0(CPOL=0, CPHA=0) 和 模式3(CPOL=1, CPHA=1)。为什么?因为这两种模式的数据采样边沿都在时钟的上升沿,主控和从机容易对齐。

模式CPOLCPHA空闲电平采样边沿
000低上升沿
101低下降沿
210高下降沿
311高上升沿
我曾经踩过的坑: 项目里用了一个国产 Flash 芯片,手册写的“模式0”,实际需要模式3才能正确读数据。后来我用逻辑分析仪抓时序才发现,芯片内部采样边沿比标准慢半个时钟。所以千万别只看手册上的模式编号,一定先发个 0xFF 读状态寄存器验证一下。

2. 单线模式(Standard SPI)

单线模式就是传统 SPI。一条 MOSI 发命令,一条 MISO 回数据,一个时钟周期传 1 bit。

命令帧结构很简单:

| 8-bit 指令 | 24/32-bit 地址 | …… | 8-bit 数据(写) / 等待+读(读) |

写操作:主控在 MOSI 上依次发出指令、地址、数据。每个 bit 都在时钟的采样边沿被从机锁存。
读操作:主控发出指令和地址后,需要插入一个空周期让总线方向切换(从机从接收转为发送),然后从机在 MISO 上逐位输出数据。

我个人习惯在单线模式下把时钟尽量压低到 20 MHz 以下,因为线太长的话信号反射会导致采样错误。如果要用高速,记得加串联电阻匹配阻抗。

3. 双线模式(Dual SPI)

双线模式很有意思。命令阶段仍然用单线,但数据阶段同时使用两根线(IO0 和 IO1)传输,每个时钟传 2 bit。

帧结构变成:

| 8-bit 指令(单线) | 24/32-bit 地址(单线) | 空周期 | 数据(双线,每时钟2bit) |

双线的地址依旧走单线,因为器件在开始阶段还不知道要切换成双线。指令里有一个 “Quad Enable” 位,告诉 Flash 后续阶段用双线还是四线。注意:地址阶段不能切换到双线,否则主控和从机对不齐。

特性单线双线四线
数据带宽1 bit/时钟2 bit/时钟4 bit/时钟
命令阶段线宽1 线1 线1 线 / 4 线(取决于指令)
地址阶段线宽1 线1 线1 线 / 4 线
数据阶段线宽1 线2 线4 线
典型读指令0x030xBB (Fast Read Dual)0xEB (Fast Read Quad)
在实际项目中,双线模式常作为“过渡方案”:当主控的 QSPI 控制器不支持四线 DMA 时,双线可以提升约 1.8 倍读速度(比单线快),同时引脚占用少。我有一块 STM32H7 的板子,Flash 支持 Quad,但 PCB 只连了双线 —— 嗯,设计没规划好,那就用 Dual 模式顶一下。

4. 四线模式(Quad SPI)

四线才是真正的“QSPI”。数据阶段用四条线(IO0~IO3),一个时钟传 4 bit,速度直接翻倍。配合 DMA 时,读性能可以接近理论极限。

典型四线读指令(0xEB)的帧结构:

| 8-bit 指令 (单线) | 24-bit 地址 (单线) | 8-bit 空周期 | 数据 (四线,每时钟4bit) |

注意那个 空周期(Dummy Cycles)。为什么需要它?

  • 给 Flash 内部地址译码和读操作留时间。
  • 更重要的是:让总线从“输出方向”(主控发地址)切换到“输入方向”(从机发数据)。如果省略空周期,数据的前几个 bit 可能被主控自己的地址信号冲掉。

空周期的数量由 Flash 芯片决定,常见是 4~8 个时钟。你在 DMA 配置里要精确设置 dummy 个数,多一个或少一个都会导致整帧错位。

// QSPI 读命令配置示例(以 STM32 为例)
qspi.CommandInstruction  = 0xEB;          // Fast Read Quad
qspi.CommandMode          = QSPI_CMD_ONE_LINE;
qspi.AddressSize          = QSPI_ADDRESS_24_BIT;
qspi.AddressMode          = QSPI_ADDR_ONE_LINE;
qspi.AlternateByteMode    = QSPI_ALTERNATE_BYTES_NONE;
qspi.DataMode             = QSPI_DATA_FOUR_LINES;
qspi.DummyCycles          = 6;            // 与 Flash 手册一致
qspi.SIOOMode             = QSPI_SIOO_INST_EVERY_CMD;
我遇到过最隐晦的 Bug: dummy 周期设为 8,但 Flash 实际需要 6。结果每次读取前 2 个字节都错,后面又正常。因为错位后从第 3 个字节开始对齐了?不是的,是假象!你用 DMA 连续读大块数据时,错位会导致整块数据偏移一个字节,非常难排查。所以务必用示波器或逻辑分析仪确认 dummy 个数。

5. 时序全貌对比

下面这张图用 SVG 画出了单线、双线、四线模式下的帧结构对比,帮你一眼看清区别:

三种 QSPI 模式帧结构对比 单线模式 (0x03) 指令 8-bit 地址 24-bit 空周期 数据 (1线/时钟) 时钟方向 双线模式 (0xBB) 指令 8-bit 地址 24-bit 空周期 数据 (2线/时钟) 四线模式 (0xEB) 指令 8-bit 地址 24-bit 空周期 (6-8) 数据 (4线/时钟) 关键说明: 1. 所有模式的指令和地址阶段通常使用单线传输(部分芯片支持四线地址)。 2. 空周期(Dummy)用于总线方向切换和内部延时,数量由 Flash 手册决定。 3. 双线/四线模式的数据吞吐率分别为单线的 2 倍和 4 倍,但需要更多引脚和 DMA 通道配合。 4. 实际项目中 Flash 的 QE(Quad Enable)位必须提前使能,否则四线读会失败。

6. 避坑指南 · 经验总结

  • 时钟相位一定要验证:我建议用 SPI Flash 的“读状态寄存器”命令(0x05)先发再读,看返回值是不是 0x00。如果读出来全是 0xFF 或 0x00,大概率是相位搞反了。
  • 空周期别想当然:有些国产 Flash 的 dummy 周期会随工作电压变化。我曾经在 3.3V 下设置 6 个 dummy 正常,换到 2.8V 就必须改成 8 个。所以产品量产前要做电压拉偏测试。
  • 四线模式先读 ID:每次初始化先发 0x9F 读 JEDEC ID,用单线模式读。如果 ID 正确,再使能 QE 位切换到四线。这个顺序能帮你区分是硬件连线问题还是协议问题。
  • DMA 配置不要漏 dummy:DMA 的字节计数必须包含 dummy 周期占用的时钟数。例如用 6 个 dummy,QSPI 控制器会在数据阶段前自动插入 6 个时钟,你传给 DMA 的数据缓存起始地址要跳过 dummy 占用的时间窗口。具体怎么做?看控制器的参考手册的“Dummy Cycles”寄存器。
最后分享一个小技巧:调试时序时,把时钟降到 1 MHz,用逻辑分析仪抓完整帧,然后手动数一下时钟边沿和数据位 —— 虽然原始,但能 100% 确认对齐关系。等跑通了再把时钟提上去。

掌握了单线、双线、四线的时序差异和避坑要点,你再去配置 QSPI DMA 就会清晰很多。剩下的就是根据 Flash 手册塞对寄存器值了。