QSPI 协议帧格式:命令、地址、数据模式及操作码

要玩转 QSPI,首先得懂它的协议帧格式。

说白了,就是主机往总线上扔一串比特,告诉从机:「我要干嘛、对哪干、怎么干、以及数据在哪儿。」

这四部分——命令、地址、数据模式、操作码,就是 QSPI 通信的最小公约数。我刚开始接触时也被一堆时序图绕晕了,后来发现只要抓住帧结构,其他的都是套路。

帧结构:四段式

QSPI 的每一笔传输,本质上可以拆成四个阶段:

  • 命令阶段:发送操作码,告诉从机执行什么操作(读、写、擦除等)。
  • 地址阶段:发送目标存储器地址(24 位、32 位甚至更长)。
  • 模式/空操作阶段:某些指令需要插入的 Dummy 周期(也叫等待周期)。
  • 数据阶段:实际的读写数据传输。

嗯,这里要注意:并不是所有指令都有四个阶段。像 Write Enable (WREN) 这种,只有一个命令阶段,没有地址和数据。

核心原则:命令阶段始终使用单线(即 SIO0/SI 线),但地址和数据阶段可以配置为 1-4 条线同时传输。这就是 QSPI 比普通 SPI 快 4 倍的原因。

时钟极性与相位

QSPI 和 SPI 一样,由四个模式决定采样沿:

模式CPOLCPHA数据采样沿
模式 000第一个时钟上升沿
模式 101第一个时钟下降沿
模式 210第一个时钟上升沿
模式 311第一个时钟下降沿

我个人习惯用模式 0(CPOL=0, CPHA=0)。大多数 Flash 芯片默认就是这个,省心。但有些高速 QSPI 器件会要求模式 3,记得看数据手册。

操作码归纳总结

操作码是 QSPI 通信的「指令集」。这里我把常用的操作码分成了三类,这张表你最好存下来:

类别操作码字节说明
基本操作WREN0x06写使能,任何写操作前必须发
基本操作WRDI0x04写禁止
基本操作RDSR0x05读状态寄存器,我常用来查 WIP 位
读操作READ0x03普通读,单线模式,速度慢但兼容性最好
读操作FAST_READ0x0B快读,包含 8 个 Dummy 周期
读操作4READ0x13?四线快读,XIP 模式的核心指令
写操作PP0x02页编程,一次最多写一页(通常 256 字节)
写操作QPP0x32四线页编程,比 PP 快 4 倍
擦除操作SE0x20扇区擦除(4KB)
擦除操作BE0xC7批量擦除,全片清零

你看,FAST_READ(0x0B)和 READ(0x03)的区别就在于多了一个 Dummy 字节。我以前踩过坑——用 READ 指令跑高速时钟,结果读出来的数据全是乱的。排查了半天才发现,是没加 Dummy 周期,Flash 内部跟不上。

避坑指南:Dummy 周期的数量不是固定的。有些 Flash 要求 2 个 Dummy,有些要求 8 个。我建议在初始化阶段通过读取状态寄存器或 SFDP 来动态确定,不要硬编码。

数据线模式:何时用 1 线?何时用 4 线?

QSPI 的数据线可以动态切换。命令阶段固定走 1 线,地址和数据阶段可以:

  • 1-1-1:标准 SPI 模式,所有东西都走单线。
  • 1-1-4:命令走 1 线,地址走 1 线,数据走 4 线。这叫 QPI 读。
  • 1-4-4:命令走 1 线,地址和数据都走 4 线。这就是真正的 Quad 读。
  • 4-4-4:命令、地址、数据全走 4 线。这种模式需要先通过指令切换到 QPI 模式,XIP 通常用这个。

你想想看,为什么 XIP 要用 4-4-4?因为 CPU 每取一条指令都要发一次命令+地址,如果命令部分只走 1 线,那带宽就浪费了 75%。

一个典型的高速读帧

这是我在项目中常用的 FAST_READ Quad Output(1-1-4 模式)的帧结构:


// CS# 拉低
// 命令字节: 0x0B (8 个 SCLK)
// 地址字节: 24 位地址 (8*3=24 个 SCLK)
// Dummy 周期: 8 个 SCLK (Dummy 字节)
// 数据输出: 连续输出直到 CS# 拉高 (每 4 个 SCLK 输出 4 bit)
// CS# 拉高

如果用 1-4-4 模式,地址也会走 4 线,24 位地址只需要 6 个 SCLK 就传完了。省下的时间就是性能提升的来源。

注意:进入 4-4-4 模式后,后续所有的命令和地址也必须走 4 线。此时如果主机没有正确配置引脚方向,会导致信号冲突。我就见过一例——MCU 引脚在切换模式时没改方向,结果 Flash 的输出和 MCU 的输出怼上了,烧了一个 IO 口。

Dummy 周期:决定时钟的天花板

Dummy 周期不只是「等一会儿」那么简单。它决定了你能跑多高的时钟。Flash 内部的存储阵列读取速度是有极限的,Dummy 周期就是给内部电荷泵和敏感放大器留出建立时间。

我做过一次对比实验:

  • Dummy=6 时,最高时钟 83 MHz,超过就出错。
  • Dummy=8 时,可以跑到 104 MHz。
  • Dummy=10 时,120 MHz 稳如狗。

所以,如果你发现 Flash 在高频下时不时返回错误,先别急着调时序,试试增加 Dummy 周期。

QSPI 协议帧格式结构图

下面这张图总结了 QSPI 帧的完整结构和数据线切换点:

QSPI 协议帧格式结构图 命令阶段 固定 1 线 地址阶段 1/2/4 线可选 模式/空操作 Dummy 周期 数据阶段 1/2/4 线可选 常见数据线配置模式 1-1-1:命令/地址/数据 均走 1 线(标准 SPI) 1-1-4:命令走 1 线,地址走 1 线,数据走 4 线(FAST_READ Quad Output) 1-4-4:命令走 1 线,地址走 4 线,数据走 4 线 4-4-4:命令/地址/数据 均走 4 线(QPI 模式,XIP 常用) 关键提醒 • Dummy 周期数决定最大时钟频率,不同 Flash 厂家要求不同 • 进入 4-4-4 模式后,一切传输均按 4 线处理 • 片选 CS 每次拉高拉低代表一帧的起始和结束

代码实战:初始化 QSPI 控制器并发送 FAST_READ

这里是基于 STM32H7 系列的 QSPI 初始化片段,我实际在项目里跑过:


// 设置 QSPI 为 4 线模式,FAST_READ Quad Output
qspi_handle.Instance = QUADSPI;
qspi_handle.Init.ClockPrescaler = 4;           // 分频,获得 100MHz
qspi_handle.Init.FlashSize = POSITION_VAL(0x2000000) - 1; // 32MB Flash
qspi_handle.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_NONE;
qspi_handle.Init.FlashID = QSPI_FLASH_ID_1;
qspi_handle.Init.ChipSelectHighTime = 1;        // CS 高电平保持时间

if (HAL_QSPI_Init(&qspi_handle) != HAL_OK) {
    Error_Handler();
}

// 发送 FAST_READ 命令:1-1-4 模式
QSPI_CommandTypeDef cmd;
cmd.Instruction = 0x0B;                         // FAST_READ
cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE;  // 命令走 1 线
cmd.Address = 0x000000;                         // 起始地址
cmd.AddressSize = QSPI_ADDRESS_24_BITS;         // 24 位地址
cmd.AddressMode = QSPI_ADDRESS_1_LINE;          // 地址走 1 线
cmd.DummyCycles = 8;                            // 8 个 Dummy 周期
cmd.DataMode = QSPI_DATA_4_LINES;               // 数据走 4 线
cmd.NbData = 256;                               // 读 256 字节

你看,代码里明确了每个阶段的线宽配置。如果 DummyCycles 设少了,Flash 跟不上时钟,读出来的数据就会错位。我建议你第一次调试时,先把 Dummy 设大一点(比如 10),确认通讯没问题后再试着减小,找到性能和稳定性的平衡点。

实际项目经验谈

最后分享我自己的一个教训。

有一次调试 XIP,CPU 执行到某个函数时总是跳飞到未定义区。我用逻辑分析仪抓了 QSPI 总线,发现 Flash 在返回数据时,前两个字节是对的,后面全乱套。检查后发现:

  • Dummy 周期设为了 6,但 Flash 要求至少 8(数据手册第 27 页小字部分)
  • 地址模式配成了 QSPI_ADDRESS_4_LINES,但 Flash 当时还在 1-1-4 模式

这两个问题导致 Flash 内部状态机紊乱,返回的数据从第三个字节开始错位。

从那以后,我每次写 QSPI 驱动,都会在调试阶段打印出配置值,和数据手册逐条核对。你想想看,一个小数点就看错,可能让 CPU 跑飞。

我的建议:写代码前先画一张这样的帧结构草图,标出每个阶段的长度和线宽。养成习惯后,几乎不会犯配置错误。