QSPI 协议帧格式:命令、地址、数据模式及操作码
要玩转 QSPI,首先得懂它的协议帧格式。
说白了,就是主机往总线上扔一串比特,告诉从机:「我要干嘛、对哪干、怎么干、以及数据在哪儿。」
这四部分——命令、地址、数据模式、操作码,就是 QSPI 通信的最小公约数。我刚开始接触时也被一堆时序图绕晕了,后来发现只要抓住帧结构,其他的都是套路。
帧结构:四段式
QSPI 的每一笔传输,本质上可以拆成四个阶段:
- 命令阶段:发送操作码,告诉从机执行什么操作(读、写、擦除等)。
- 地址阶段:发送目标存储器地址(24 位、32 位甚至更长)。
- 模式/空操作阶段:某些指令需要插入的 Dummy 周期(也叫等待周期)。
- 数据阶段:实际的读写数据传输。
嗯,这里要注意:并不是所有指令都有四个阶段。像 Write Enable (WREN) 这种,只有一个命令阶段,没有地址和数据。
核心原则:命令阶段始终使用单线(即 SIO0/SI 线),但地址和数据阶段可以配置为 1-4 条线同时传输。这就是 QSPI 比普通 SPI 快 4 倍的原因。
时钟极性与相位
QSPI 和 SPI 一样,由四个模式决定采样沿:
| 模式 | CPOL | CPHA | 数据采样沿 |
|---|---|---|---|
| 模式 0 | 0 | 0 | 第一个时钟上升沿 |
| 模式 1 | 0 | 1 | 第一个时钟下降沿 |
| 模式 2 | 1 | 0 | 第一个时钟上升沿 |
| 模式 3 | 1 | 1 | 第一个时钟下降沿 |
我个人习惯用模式 0(CPOL=0, CPHA=0)。大多数 Flash 芯片默认就是这个,省心。但有些高速 QSPI 器件会要求模式 3,记得看数据手册。
操作码归纳总结
操作码是 QSPI 通信的「指令集」。这里我把常用的操作码分成了三类,这张表你最好存下来:
| 类别 | 操作码 | 字节 | 说明 |
|---|---|---|---|
| 基本操作 | WREN | 0x06 | 写使能,任何写操作前必须发 |
| 基本操作 | WRDI | 0x04 | 写禁止 |
| 基本操作 | RDSR | 0x05 | 读状态寄存器,我常用来查 WIP 位 |
| 读操作 | READ | 0x03 | 普通读,单线模式,速度慢但兼容性最好 |
| 读操作 | FAST_READ | 0x0B | 快读,包含 8 个 Dummy 周期 |
| 读操作 | 4READ | 0x13? | 四线快读,XIP 模式的核心指令 |
| 写操作 | PP | 0x02 | 页编程,一次最多写一页(通常 256 字节) |
| 写操作 | QPP | 0x32 | 四线页编程,比 PP 快 4 倍 |
| 擦除操作 | SE | 0x20 | 扇区擦除(4KB) |
| 擦除操作 | BE | 0xC7 | 批量擦除,全片清零 |
你看,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 控制器并发送 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 跑飞。
我的建议:写代码前先画一张这样的帧结构草图,标出每个阶段的长度和线宽。养成习惯后,几乎不会犯配置错误。