QSPI Flash原理图解读与FPGA工程搭建:从信号链到管脚约束
2026/9/16 11:10:38 网站建设 项目流程

很多第一次接触FPGA开发的人,拿到开发板之后都会有一种错觉:项目最难的部分是Verilog或者VHDL的语法。实际干过几个带Flash的板级项目你就明白,真正卡你一两周的问题,往往是从原理图到工程转换的那道坎。尤其像QSPI这种带方向复用、有时序关系的接口,如果你不从原理图上把每一个信号的角色、每一个引脚的电气属性搞清楚就直接写代码,后面排错是真的会怀疑人生。这一讲是QSPI公开课里比较“硬核基建”的一节,我带你把开发板原理图中与QSPI-Flash相关的部分逐行拆掉,再手把手把顶层RTL和管脚约束建起来,为后续实现真正的QSPI读写控制器打一个不会塌的地基。

1. 从原理图上把这根QSPI链路彻底拆开

1.1 先认Flash芯片,再认网络标号

打开开发板原理图,第一件事不要急着看FPGA引脚连到了哪里,而是先看板载Flash的型号。国内最常见的板载Flash是Winbond的W25Q系列,比如W25Q64、W25Q128,少数高速开发板会用ISSI或者Macronix的芯片。芯片型号决定了后面所有时序参数:指令集、页大小、扇区大小、最大时钟频率,这些都会写进数据手册,而你写控制器时每一步都要对着它来。

原理图上W25Q128通常会画成一个带引脚序号的矩形符号,角落里标注“U3”这类位号。你先注意看引脚名称的写法:CS#(片选)、CLK(时钟)、DI/DQ0(数据输入/IO0)、DO/DQ1(数据输出/IO1)、WP#/IO2(写保护/IO2)、HOLD#/IO3(保持/IO3),另外还有VCC和GND。这里面的“#”号表示低电平有效,有的原理图也写成CS_N、nCS、HOLD_N,都是一个意思。

这里有个新手容易忽略的点:原理图上的网络标号不一定和芯片引脚名一致。比如Flash的DI引脚在原理图里可能连着名为SPI_MOSI的网线,而DO引脚连着SPI_MISO。这是硬件工程师按照“FPGA视角”命名的,很正常。但如果你以为DI就真的叫DI,回头去找顶层模块里一个叫DI的输出端口,那就找不到了。所以阅读时要做一次“翻译”:芯片引脚名 ↔ 原理图网络标号 ↔ 你RTL里的端口名。

看芯片引脚的时候顺手把VCC和GND也圈出来。Flash的供电通常是3.3V,个别低功耗板子可能拉到1.8V,这个电压等级不能猜,必须看原理图上标注的电源网络名,比如VCC3V3、VCC18,或者看Flash周围的去耦电容耐压。

1.2 FPGA侧的IO分组与Bank电压,决定了能不能直接通讯

看完Flash侧,再把视线转到FPGA侧。原理图上连接Flash的那几个FPGA引脚,通常会被标注成“PIN_xx”或者“AA1”这类位号。但位号本身不重要,重要的是这个位号落在FPGA的哪个IO Bank,以及这个Bank的VCCO电压是多少。

为什么要这么较真?因为FPGA引脚不是你想当然地“接上就能通”的。每一组IO Bank都有独立的供电引脚VCCO。如果你的Flash工作在3.3V,而它连接的FPGA Bank的VCCO被设计成2.5V,那么FPGA引脚的输出高电平最高也只有2.5V,Flash大概率读不到稳定高电平,更别提后面跑高速了。反过来也一样,VCCO是3.3V,Flash是1.8V的应用,必须加电平转换芯片,不然Flash迟早烧掉。

所以,我建议你读图时不要只在脑子里过一遍,直接拿笔或者用表格记录:

信号用途网络标号Flash引脚名FPGA引脚位号FPGA BankBank电压
片选SPI_CS_NCS# / Pin1AA1Bank 14VCCO 3.3V
时钟SPI_CLKCLK / Pin6AA2Bank 14VCCO 3.3V
写数据SPI_MOSIDI / DQ0 / Pin5AB1Bank 14VCCO 3.3V
读数据SPI_MISODO / DQ1 / Pin2AB2Bank 14VCCO 3.3V

这张表看起来简单,但它就是你后面写管脚约束文件的唯一依据。我见过太多人跳过了这个步骤,结果建工程时把两个信号位置写反,板子上电后要么读不到数据,要么把IO口驱动得过热。

1.3 从原理图提取一张“信号-管脚”对照表

再往下走一步,把原理图里有关于QSPI的所有信息汇总成一个专属于这块板子的对照表。除了上面那些,还要额外记录几个东西:

  • 复位相关的网络,比如FPGA的复位按钮接到哪个引脚,是低有效还是高有效
  • 时钟源的来源与频率,是板上晶振直接给出,还是通过PHY芯片分频产生
  • 给Flash供电的电源网络名,以及Flash周围是否有串联电阻、上拉电阻

串联电阻这件事要特别说一句。很多原理图里,Flash的CLK、MOSI线上会串一个22Ω或者33Ω的电阻,有的会标注“0Ω”默认不贴。这是为了匹配阻抗、减少信号反射。你看原理图时如果发现有串联电阻,先记下阻值;如果自己画板子,也建议在时钟和数据线上留一个0402封装的串联电阻位。这个对高速QSPI的稳定性帮助很大,尤其是在杜邦线实验或者飞线调试时。

把这些信息理顺,你在心里对整个链条就有了一个三维图像:Flash在左侧,FPGA在右侧,中间是四条信号线加电源网络。接下来读时钟和复位时,就不会觉得它们是孤立的引脚了。

2. 时钟、复位和电源:原理图里三个最容易漏掉的细节

2.1 SPI时钟是FPGA发还是外部来,决定引脚怎么选

在一般开发板上,QSPI-Flash是挂在FPGA的普通IO口上的,时钟也是由FPGA内部逻辑产生再通过IO输出到Flash。这种情况下,Flash的CLK引脚连到的FPGA引脚不需要是专用时钟引脚,普通的用户IO就行。

但如果你做的是更复杂的设计,比如用FPGA给外部SPI设备做主机,或者FPGA本身作为从机接收外部主机的SPI数据,那就要注意了:外部来的SPI时钟如果进入FPGA内部逻辑,最好接到专用全局时钟引脚上,通过BUFG进时钟树,否则时序会很难收敛,板子上随机出现采样错误。

怎么从原理图判断这个时钟是输出还是输入?最简单的方法就是看网络名和连接方向。如果Flash的CLK引脚连接到的那个FPGA引脚,同时还连着另一端到板载晶振的输出脚,那说明是FPGA作为从机,外部时钟进入FPGA。如果CLK引脚只是FPGA的一根普通输出口,那时钟就是FPGA内部逻辑生成后输出的。

还有一个很小的细节,很多人读图时会漏:SPI从设备在时钟下降沿采样还是上升沿采样,虽然是由数据手册决定,但很多开发板的原理图上会通过不同的命名暗示时序方向,比如标“SPI_CLK_OUT”和“SPI_CLK_IN”。你的顶层RTL需要把方向搞清楚,否则后面读ID死活都是0xFF。

2.2 复位小圆圈和“N后缀”,背后是一套同步处理逻辑

原理图上的复位信号,最常见的写法是RST_N、RESET_B、nRST,反斜杠或字母N都表示低有效。在FPGA工程里,这个信号一般直接连到按键或者上电复位芯片,按下时拉低,松开时被电阻上拉到高电平。

很多教程告诉你“复位信号进来之后要打两拍同步”,这句话在异步信号跨时钟域时是必须的,但在简单的开发板demo里,很多人直接把它当作普通信号用。我的建议是,如果你的FPGA工程里有多个时钟域,或者复位按钮按下瞬间会有抖动,就老老实实做一个异步复位同步释放模块:

module rst_sync ( input wire clk, input wire rst_n_raw, output reg rst_n ); reg rst_n_r1; always @(posedge clk or negedge rst_n_raw) begin if (!rst_n_raw) begin rst_n_r1 <= 1'b0; rst_n <= 1'b0; end else begin rst_n_r1 <= 1'b1; rst_n <= rst_n_r1; end end endmodule

这个小模块会让所有使用同一时钟的逻辑,在复位释放时处于同一时刻生效,避免有的寄存器已经醒来,有的还在复位。这个问题在QSPI读写时序里尤其烦人,因为如果复位释放不同步,状态机可能走到一个非法状态。

读原理图时还要留意复位信号是否被“与”到了某个电源监控芯片的输出上。有的板子会有DSP或电源管理芯片,只有当各路电源正常后,复位才释放。这样的设计是好事,但也会导致一个现象:你把FPGA程序下载进去,如果SD卡或者DDR电源有问题,复位一直不释放,板子看起来就是“没反应”。排查时需要看复位网络的电平。

2.3 VCCO、去耦电容和串联电阻,别小看这几个网络

先看VCCO。每个FPGA Bank的VCCO引脚在原理图上会集中画在一起,网络名通常是VCCO_13、VCCO_14这样,也可能统一叫VCCIO。你在1.2节里记录了Flash所连Bank的电压,现在要确认一下这个电压的来源。如果该Bank只接了Flash一个器件,那么规格很简单;如果该Bank还接了DDR、HDMI或者其他外设,就得小心了,因为多个外设共用Bank时不能随意调整电压。

再看去耦电容。其实我让你看这个,不是让你真的去计算电容量,而是判断原理图设计是否成熟。一个设计良好的Flash电源网络,周围至少会有1个0.1uF加1个1uF或更大容值的电容。如果原理图上Flash的VCC引脚附近光秃秃一片,连电容都没有,那你要有思想准备:这板子在特定时钟频率下可能会不稳定,上板调试时要优先怀疑电源噪声导致的信号异常。

最后是串联电阻。前面提过CLK/MOSI线上可能串22Ω或33Ω。如果你用的板子上这些电阻是0Ω(或者说“不焊”),而你又想把QSPI跑高一点,比如50MHz以上,建议在原理图上预留的位置补焊一个合适阻值的电阻,或者退一步降低时钟频率。不要小看这一点,QSPI在读数据时进入高速模式后,波形反射会把本来干净的数据眼图破坏得一塌糊涂。

3. 把原理图翻译成FPGA工程:顶层RTL和约束文件

3.1 顶层模块的对外接口,就是原理图中的一撮网络名

建工程这一步,很多教程直接说“File -> New Project -> 选芯片型号 -> 加文件”,然后就不讲了。但实际上,最容易被卡住的是:顶层模块的端口到底怎么写?其实方法很简单,回到第1节整理的对照表,原理图上一个网络,对应顶层RTL里的一个端口。

拿前面那张表举例,顶层模块就是这个样子:

module qspi_flash_top( input wire sys_clk, input wire sys_rst_n, // QSPI Flash接口,与原理图网络名一一对应 output wire flash_cs_n, output wire flash_clk, output wire flash_mosi, input wire flash_miso, // 用户控制与状态 input wire start, output wire busy, output reg [15:0] flash_id );

这里有一个约定俗成的习惯,就是Flash的CS、CLK、MOSI、MISO这些信号名,尽量保持和原理图网络名一致。原因很简单:以后做管脚约束时,看到Port名字就知道对应原理图哪个网线,排错时少做一次翻译。我知道有人喜欢把MOSI改成sdat,把CLK改成sclk,看起来“高级”,但完全没有必要,直接保持一致比什么都强。

建工程的时候还有几个细节:

  • 器件型号不能选错。选定之前打开芯片正面丝印看一眼,或者去原理图器件型号栏确认,不要只凭记忆选。同一个系列里,XC7A35T和XC7A100T的封装、引脚定义完全不同,选错之后管脚约束满屏报错。
  • 顶层文件一定在新建工程时就指明,后面再改容易乱。
  • 如果原工程是英文环境,端口名不要用中文;描述可以用中文注释,但要确保编辑器编码是UTF-8,否则容易出现乱码注释,虽然不影响综合,但看着难受。

3.2 管脚约束文件的标准写法与常见报错

这一步是“工程搭建”的灵魂。以Xilinx Vivado为例,管脚约束文件后缀是.xdc,内容就是一行行set_property

set_property PACKAGE_PIN AA1 [get_ports {flash_cs_n}] set_property IOSTANDARD LVCMOS33 [get_ports {flash_cs_n}] set_property PACKAGE_PIN AA2 [get_ports {flash_clk}] set_property IOSTANDARD LVCMOS33 [get_ports {flash_clk}] set_property PACKAGE_PIN AB1 [get_ports {flash_mosi}] set_property IOSTANDARD LVCMOS33 [get_ports {flash_mosi}] set_property PACKAGE_PIN AB2 [get_ports {flash_miso}] set_property IOSTANDARD LVCMOS33 [get_ports {flash_miso}]

如果是Altera Quartus,则是.qsf文件:

set_location_assignment PIN_AA1 -to flash_cs_n set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to flash_cs_n

这里最容易报错的情况有两种:

第一种是端口名和Top Module里的名字不匹配。比如RTL里写的端口叫flash_mosi,约束文件里写[get_ports {mosi}],Vivado会提示找不到这个port,生成bit文件失败。第二种是PACKAGE_PIN位号写错了,比如把AA1写成了AA2,把flash_cs_n和flash_clk绑到了同一个引脚上,这会在实现阶段报IO location conflict。平时写的时候手指快一点就容易犯,没有捷径,只能多对几遍原理图。

再补一个比较隐蔽的坑:有的FPGA引脚配置了IOSTANDARD但没有给VCCO上对应的电压。当Bank电压和IOSTANDARD不匹配时,工具不一定报错,但输出高电平会不达标,上板后Flash时序行为飘忽。所以写完约束文件后,不要急着综合,花一分钟原地检查每个Bank的VCCO记录。

3.3 编译后的第一轮检查,不是看时序报告而是看I/O Bank

工程综合完成后,我建议先看“I/O Plan”或者“Report IO”,把下面三件事核对完再继续写逻辑:

  1. 每个设计端口是否有对应的物理引脚;
  2. 每个引脚的IOSTANDARD是否都写了,没有写的话工具默认可能使用LVCMOS或者LVTTL,不一定是你想要的;
  3. 你使用的引脚是否落在Flash所在的Bank,以及这个Bank的VCCO电压是否与IOSTANDARD匹配。

这一步做完,相当于硬件级的“冒烟测试”通过了。随后再写时序逻辑,基本上不会出现“引脚没绑对”这种低级问题。

如果你用的开发板原理图是那种多页文档,比如Cadence或者OrCAD导出的,页码很多,那么做这一步尤其重要。我踩过一回:只用搜索功能找到了flash_miso,以为找到了所有相关网络,结果漏了同一个信号名在第二页还有一个分支连到了排针上。最后测试时,排针上外接的GND把Flash的MISO拉低,整个工程状态全乱,排查了大半天。

4. 配合数据手册写一个最简QSPI读写控制器

4.1 控制器里该有哪些子模块,别一上来就画大饼

工程搭好之后,你大概率迫不及待想写代码。但先停一下,我们把控制器的模块边界想清楚。一个完整的QSPI-Flash控制器,哪怕是“最简版”,也至少要有这几个部分:

  • 时钟分频模块:系统时钟往往太高,Flash工作时钟需要分到几十MHz,需要一个简单的分频器或时钟使能信号;
  • 时序状态机:根据数据手册的指令时序,在IDLE、SEND_CMD、SEND_ADDR、READ_DATA等状态之间跳转;
  • 位移位模块:并行数据转串行发出,以及串行接收转并行;
  • 数据缓冲模块:如果是读写页,还需要一个FIFO或寄存器组来存放要写入/读出的数据。

我第一次写这种控制器的时候,想一口气把所有功能都做了,结果状态机写得又大又乱,仿真通过,上板就翻车。后来我学乖了,先做一个最小的“读ID”功能,只验证信号方向、引脚绑定、时钟沿对没对,全通了再去扩展扇区擦除、页编程。

4.2 把指令时序里的“命令-地址-数据”翻译成状态机

以W25Q128为例,读芯片ID的标准指令是0x9F,发送完命令后,芯片会在MISO上连续输出3个字节的厂商ID和设备ID。整个过程可以用几个状态表示:

状态行为关键点
IDLECS_N保持高电平,等待start信号片选默认释放
SEND_CMDCS_N拉低,在CLK上升沿逐位发出0x9F方向为FPGA输出到Flash
READ_ID连续24个CLK上升沿,在每个上升沿采样MISO方向变成Flash输出到FPGA
DONECS_N拉高,输出ID数据,回到IDLE一个周期脉冲

这里要注意,SPI协议中数据在上升沿采样、下降沿切换,是绝大多数Flash芯片的默认行为,也就是CPOL=0、CPHA=0。写状态机时,我习惯把MOSI和MISO的变化都放在CLK下降沿之后,而在每个CLK上升沿完成采样,这样时序余量最大。如果你在仿真或者上板时发现读到的数据整体错位一位,多半就是沿搞反了。

一段可综合的顶层结构如下,状态机内部的细节可以根据你的芯片手册去填:

localparam IDLE = 3'd0; localparam SEND_CMD = 3'd1; localparam READ_ID = 3'd2; localparam DONE = 3'd3; reg [3:0] state; reg [2:0] bit_cnt; reg [23:0] id_shift; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; end else begin case (state) IDLE: begin if (start) state <= SEND_CMD; end SEND_CMD: begin if (bit_cnt == 3'd7) state <= READ_ID; end READ_ID: begin if (bit_cnt == 3'd7) state <= DONE; end DONE: state <= IDLE; endcase end end

4.3 仿真时关注什么:方向切换、时钟沿和片选电平

仿真这一步很多人会跳过,直接在板子上调,但我的经验是,QSPI这种接口先仿真再上板,能省一半时间。仿真时重点看三个东西。

第一是片选:CS_N在发送指令前必须先拉低,整个指令期间必须保持低,结束后拉高。如果在某个状态CS_N提前拉高,Flash会立刻终止当前操作,这是最常见的错误。

第二是数据方向切换:发送指令阶段,MOSI一直有输出;但进入读ID阶段后,MOSI其实不需要继续给值,而MISO开始有数据。如果你的MOSI线和MISO在FPGA内部是同一个三态端口,那么方向切换的时序要格外注意。很多仿真波形上,方向切换的控制信号晚了一个节拍,导致读到的第一个字节少一位或者多移一位。

第三是时钟沿的一致性:所有状态的采样沿必须是一致的。有些状态里你在上升沿采样,有些状态里不小心在下降沿采样,仿真里可能都能跑出波形,但一到真实芯片就随机出错。所以建议在仿真testbench里严格给定input数据,比对读回的ID是否与你设定的完全一致。

5. 上板调试的实测经验:从读ID失败到稳定读写

5.1 一次读ID失败的全链路排查顺序

读ID是QSPI控制器最简单也最有效的“Hello World”。如果这一步都失败,不要慌,按下面的顺序排查,不要跳步:

  1. 确认程序真的下载进去了。这听起来像废话,但调试时经常出现“下载器连不上”“配置完成后bit根本没加载”的情况。先看FPGA的DONE引脚是否有指示,或者通过Vivado Hardware Manager看一下program状态。
  2. 用万用表或示波器量Flash供电。Flash的VCC引脚是否有3.3V或对应电压,GND是否真正连通。有些开发板排针供电接触不良,会导致Flash部分上电失败。
  3. 量CS_N在指令期间是否确实拉低。很多情况下不是Flash不回数据,而是片选根本没低下去。
  4. 量MISO在读ID期间是否有时钟沿对应的电平变化。如果没有变化,检查MISO是否因为Bank电压不匹配而被钳位到固定电平。
  5. 对比原理图和约束文件。如果上面的电平都正常,那大概率是引脚绑错了。重新核对一遍你的XDC/QSF文件和原理图网络名。

我在实验室带过几个本科实习生,他们在第4步卡住的最多。原因是原理图上Flash的MISO引脚名是DO,FPGA引脚约束也绑了DO,但顶层RTL里端口名用的是flash_miso且方向写成了output。综合工具不会报错,因为flash_miso根本没接到约束文件里,也就没有物理引脚,MISO实际悬空。这一类问题用传统语言查错几乎查不出来,必须回看Input/Output方向。

5.2 用示波器和逻辑分析仪观察QSPI信号的方法

有条件的情况下,示波器看模拟波形,逻辑分析仪看协议解码。两者配合比单纯瞎猜效率高得多。

示波器重点看CLK的上升沿是否干净。如果CLK上升沿上有明显的过冲、振铃或台阶,先怀疑串联电阻和走线阻抗。QSPI时钟跑得不高的话,问题不大;一旦到了DDR模式或者高频读,这种毛刺会导致采样错误,而且错误是偶发的,非常难查。实测时用示波器探头表笔要短地线,不要用长鳄鱼夹,否则探头本身带来的寄生电容会让本来好看的波形多出很多反射。

逻辑分析仪则用来确认协议层面的时序是否符合手册。很多逻辑分析仪软件自带SPI解码器,先设置好CPOL/CPHA,然后抓CS_N、CLK、MOSI、MISO四路信号。留意解码结果里有没有“CRC error”或者“bit order reversed”。注意SPI的数据位序一般是MSB first,但你用的IP核或者自己写的移位逻辑可能写成了LSB first,这种错误在波形上看不出来,但芯片就是不理你。

还有一个我常用的笨办法,但很有效:先写一个固定发送0x9F指令的模块,让CS_N在每个唤醒周期都拉低一次,然后用万用表量MOSI引脚的平均电压。如果MOSI引脚实际有波形,平均电压会接近1.65V左右(3.3V供电时)。如果量出来是0V或者3.3V纹丝不动,说明方向、绑定或逻辑根本没生效。这个方法比示波器快,适合手头没有示波器的时候快速定位。

5.3 开发板原理图上那些默认不体现的“隐藏”问题

你可能以为原理图把一切画清楚了,但实际项目中,真正让人头疼的往往不在原理图明确标注的地方。

第一个隐藏问题是Flash的WP#和HOLD#引脚。标准SPI模式只用到CS#、CLK、DI、DO,但QSPI模式下会把WP#复用为IO2,HOLD#复用为IO3。很多Flash默认出厂时,状态寄存器里的写保护位是1,如果你在原理图上看到WP#的脚没有上拉,而是直接接地了,那后续做页编程就会一直失败。读ID可能没问题,一旦想写Flash,写使能命令发过去,芯片还是拒绝一切写操作。解决办法是调整硬件上拉,或者把WP#接到FPGA的普通IO口,并在RTL中将其驱动为高电平。

第二个隐藏问题是Flash的“QPI模式使能”命令。不是所有Flash都默认开Quad模式,有些芯片要发送0x38或者0x66/0x99这类指令,才能把DO/DQ1、WP#/IO2、HOLD#/IO3切换成四线IO模式。如果你只看了SDR的SPI时序,没看手册里Quad SPI的使能流程,那你的“QSPI控制器”很可能只是“SPI控制器换了个名字”,四根线里只有两根真正在起作用。

第三个隐藏问题和FPGA引脚的电平配置有关。FPGA管脚默认可能带有内部下拉或者弱上拉,上电瞬间的状态可能与原理图预期不一致,导致Flash在上电初始化阶段以为收到了无效片选信号。遇到这种问题,在约束文件里给CS_N引脚加上一个上拉属性,比如Xilinx的PULLUP,可以在FPGA配置期间把CS_N稳定在高电平,避免误触发。

我自己的一个实战经历是:某块板子上,Flash的HOLD#引脚没有做任何处理,悬空。第一次把工程下载进去,读ID正常,页编程偶尔失败。后来用示波器抓HOLD#波形,发现它随着周围信号串扰上下抖动,高频率时甚至出现完整的高低电平翻转,等于是在告诉Flash“暂停当前操作”。把HOLD#用飞线上拉到3.3V之后,问题立刻消失。这个案例我讲给很多朋友听,他们都觉得原理图上没画出来,所以想不到。硬件设计的坑往往就是这样,原理图上清清楚楚,但只有真正跑过代码、踩过坑的人才会注意到。

回到开头说的,QSPI原理图解读和工程搭建,本身不是很难,难的是把原理图里的数字信号翻译成FPGA工程里的管脚约束和顶层逻辑,并且能意识到那些原理图上不会主动强调的电气细节。只要你每拿到一块新板子,都能稳稳地把上面这五步走完,后面无论写SPI还是QSPI控制器,都只是在同一个地基上盖不同的房子而已。这一讲的工程骨架既然立住了,下一步就能安心把手上的QSPI读写控制器往里面填了。

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

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

立即咨询