
很多第一次接触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.5VFlash大概率读不到稳定高电平更别提后面跑高速了。反过来也一样VCCO是3.3VFlash是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 1b0; rst_n 1b0; end else begin rst_n_r1 1b1; 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_propertyset_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”把下面三件事核对完再继续写逻辑每个设计端口是否有对应的物理引脚每个引脚的IOSTANDARD是否都写了没有写的话工具默认可能使用LVCMOS或者LVTTL不一定是你想要的你使用的引脚是否落在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输出到FlashREAD_ID连续24个CLK上升沿在每个上升沿采样MISO方向变成Flash输出到FPGADONECS_N拉高输出ID数据回到IDLE一个周期脉冲这里要注意SPI协议中数据在上升沿采样、下降沿切换是绝大多数Flash芯片的默认行为也就是CPOL0、CPHA0。写状态机时我习惯把MOSI和MISO的变化都放在CLK下降沿之后而在每个CLK上升沿完成采样这样时序余量最大。如果你在仿真或者上板时发现读到的数据整体错位一位多半就是沿搞反了。一段可综合的顶层结构如下状态机内部的细节可以根据你的芯片手册去填localparam IDLE 3d0; localparam SEND_CMD 3d1; localparam READ_ID 3d2; localparam DONE 3d3; 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 3d7) state READ_ID; end READ_ID: begin if (bit_cnt 3d7) state DONE; end DONE: state IDLE; endcase end end4.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”。如果这一步都失败不要慌按下面的顺序排查不要跳步确认程序真的下载进去了。这听起来像废话但调试时经常出现“下载器连不上”“配置完成后bit根本没加载”的情况。先看FPGA的DONE引脚是否有指示或者通过Vivado Hardware Manager看一下program状态。用万用表或示波器量Flash供电。Flash的VCC引脚是否有3.3V或对应电压GND是否真正连通。有些开发板排针供电接触不良会导致Flash部分上电失败。量CS_N在指令期间是否确实拉低。很多情况下不是Flash不回数据而是片选根本没低下去。量MISO在读ID期间是否有时钟沿对应的电平变化。如果没有变化检查MISO是否因为Bank电压不匹配而被钳位到固定电平。对比原理图和约束文件。如果上面的电平都正常那大概率是引脚绑错了。重新核对一遍你的XDC/QSF文件和原理图网络名。我在实验室带过几个本科实习生他们在第4步卡住的最多。原因是原理图上Flash的MISO引脚名是DOFPGA引脚约束也绑了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#复用为IO2HOLD#复用为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读写控制器往里面填了。