1. 这不是“看图说话”,而是用FPGA把QSPI从原理图里“抠”出来跑起来
你手头有一张QSPI接口的原理图——可能是黑金AX7020开发板的,也可能是你自己画的定制板,上面标着IO_12、IO_13这些引脚,旁边还写着“QSPI_CLK”“QSPI_D0”“QSPI_D1”“QSPI_D2”“QSPI_D3”“QSPI_CS_B”。你打开Vivado,新建工程,选对芯片型号,可一到“Add Sources”这步就卡住了:原理图上那些信号名,到底该对应FPGA内部哪个逻辑模块?CLK走的是全局时钟资源还是普通IO?D0~D3要不要加IO约束?CS_B是高有效还是低有效?这些细节,原理图不会直接告诉你,它只是一张“地图”,而真正要让QSPI在FPGA里动起来,你得亲手把这张地图翻译成能执行的代码、能烧录的约束、能验证的波形。这就是本篇要干的事——不讲抽象理论,不堆概念术语,就拿一张真实存在的QSPI原理图(比如常见Xilinx Zynq-7000系列开发板上的QSPI Flash连接图)当靶子,手把手带你从读图开始,一步步建工程、写顶层、配约束、仿真验证,最后让FPGA真机点亮QSPI Flash的ID读取功能。关键词QSPI、FPGA、原理图、工程搭建,一个都不能少。适合刚学完Verilog基础、手里有块开发板、想真正做出点东西的新手;也适合做了几年逻辑设计、但一直没系统梳理过外设接口落地流程的工程师。这不是教程搬运,是我带三届实习生做QSPI项目踩出来的坑、记下来的参数、调出来的波形,全在这里。
2. 原理图不是装饰画,它是FPGA工程的“施工蓝图”
2.1 看懂原理图的三个致命误区
很多初学者拿到原理图第一反应是“找芯片型号”,然后去翻数据手册。这没错,但远远不够。我见过太多人栽在这三个误区里:
误区一:“信号名=端口名”。原理图上标着“QSPI_D0”,你就真在Verilog里写
input QSPI_D0。错。QSPI_D0在原理图上是PCB走线名,它可能对应FPGA的MIO[1](Zynq PS端),也可能对应PL端的Bank 34 Pin W15(Artix-7)。这个映射关系,必须通过原理图里的器件封装、引脚定义、电源域划分来反推,不能凭空猜。误区二:“所有信号都走普通IO”。QSPI_CLK看起来就是一根时钟线,但它能不能走普通IO?不行。Xilinx 7系列FPGA中,QSPI接口的CLK必须接入专用的
SSTL18_I或LVCMOS18兼容的全局时钟输入引脚(如CLK_1),否则无法满足建立/保持时间要求。原理图上如果CLK连在Bank 34 Pin V16,而这个Bank的VCCO是1.8V,且该Pin在Xilinx官方封装文件里被标注为CLK_IN类型,那它就是合法的;如果连在Bank 35 Pin U12,而U12在封装表里是普通I/O,那这个设计本身就有风险,后续约束会报错。误区三:“CS_B低电平有效就写assign cs_n = 1'b0”。CS_B的“_B”后缀明确表示低有效,但原理图里它的上拉电阻接的是VCCO(1.8V),这意味着默认状态是高电平(无效),FPGA输出低电平时才选中Flash。这个上拉值(通常是10kΩ)决定了驱动能力——如果FPGA IO标准设为
LVCMOS18,驱动强度设为12mA,就能可靠拉低;但如果误设为LVCMOS25,电压不匹配,CS_B可能永远拉不到有效低电平,Flash根本不响应。
提示:原理图解读的核心,不是“认字”,而是“建模”——把每一条线、每一个电阻、每一个电容,都转换成FPGA内部资源、电气特性、时序约束的输入参数。一张原理图,本质是一组硬件约束条件的集合。
2.2 拆解一张典型QSPI原理图的六步法
我们以黑金AX7020开发板(Xilinx Artix-7 XC7A200T)的QSPI Flash部分为例,逐项拆解:
定位Flash芯片型号:原理图左上角标着
W25Q32JVSIQ,这是Winbond的32Mbit SPI NOR Flash。查其Datasheet,关键参数立刻浮现:支持Single/Quad SPI模式;最大时钟频率104MHz(Quad模式);IO电压1.8V;CS_B低有效;D0/D1/D2/D3为双向数据线(在Quad模式下复用为地址/数据线)。确认FPGA与Flash的物理连接:看连线。CLK连FPGA
Bank 34 Pin V16;CS_B连Bank 34 Pin U15;D0连Bank 34 Pin T14;D1连Bank 34 Pin R15;D2连Bank 34 Pin T16;D3连Bank 34 Pin U14。六个信号全在同一个Bank(Bank 34),且该Bank的VCCO=1.8V,与Flash的IO电压完全匹配——这是硬性前提,不满足就不用往下做了。识别上拉/下拉配置:CS_B线上有一个10kΩ上拉电阻到1.8V;D0~D3线上各有一个10kΩ上拉电阻到1.8V;CLK线上无上下拉(由FPGA驱动)。这意味着:CS_B默认高(无效),FPGA需主动拉低;D0~D3在Flash未驱动时为高阻态,上拉保证总线空闲时为高电平,避免悬空导致误触发。
分析电源与参考地:Flash的VCC接3.3V,但IO引脚接1.8V(原理图中有LDO稳压芯片TPS62231将3.3V转为1.8V供IO使用)。这说明Flash工作在1.8V IO模式,FPGA Bank 34的VCCO必须严格设为1.8V,否则电气不兼容。
检查时钟源路径:CLK信号由FPGA内部PLL生成,经BUFG(全局时钟缓冲器)后输出到Pin V16。原理图虽未画出PLL,但暗示了时钟必须走全局资源——因为QSPI协议要求CLK边沿抖动<100ps,普通IO走线无法保证。
确认复位与初始化逻辑:原理图中无独立复位电路,Flash复位由FPGA上电后软件控制。这意味着你的Verilog代码里必须包含至少100us的Power-On Reset(POR)延时,再发Reset Enable指令(0x66)+ Reset Memory(0x99),否则Flash可能处于不确定状态。
这六步做完,原理图就不再是静态图纸,而是一份动态的“硬件接口说明书”,里面每个细节都直接决定你后续代码怎么写、约束怎么加、仿真怎么设。
2.3 工程搭建前必须确认的四大硬件事实
在Vivado里点“Create Project”之前,请务必用原理图交叉验证以下四点,缺一不可:
| 检查项 | 原理图依据 | FPGA实现要求 | 不满足后果 |
|---|---|---|---|
| Bank电压匹配 | Bank 34的VCCO网络连接到1.8V LDO输出 | Vivado中该Bank的IO Standard必须设为LVCMOS18 | 电压不匹配导致信号电平错误,Flash无响应或读写失败 |
| 时钟引脚类型 | CLK连接Pin V16,查阅Xilinx封装文件确认其为CLK_IN类型 | 必须在XDC中用create_clock约束,并指定-waveform {0 5}(假设100MHz) | 普通IO约束会导致时序分析失败,综合后CLK路径延迟过大 |
| CS_B驱动能力 | CS_B上拉10kΩ至1.8V | FPGA IO驱动强度需≥8mA(实测12mA最稳),电流不足则CS_B无法拉低至0.4V以下 | Flash始终不选中,所有命令无效 |
| D0~D3双向性 | D0~D3线上有上拉电阻,且Flash datasheet明确标注为双向 | Verilog中必须用inout端口+三态控制(assign d0 = (dir) ? d0_out : 1'bz),不能简单当input或output | 单向连接导致读操作时数据无法从Flash返回FPGA |
这四点,每一条都是血泪教训换来的。我曾因忽略Bank电压匹配,在调试三天后才发现Vivado里误设了LVCMOS25,结果Flash ID读出来全是0xFF;也因CS_B驱动强度设太小(4mA),在高温环境下CS_B电压卡在0.6V,刚好在Flash识别阈值边缘,导致偶发通信失败。
3. 从零建工程:Vivado里的每一步都藏着“坑”
3.1 创建工程的隐藏陷阱:Project Settings必须改三处
新建工程时,Vivado默认设置对QSPI极不友好。必须手动修改:
Target Language:选
VHDL还是Verilog?选Verilog。理由:QSPI协议涉及大量时序控制(如CS_B建立时间、CLK采样沿、数据保持时间),Verilog的always @(posedge clk)比VHDL的process(clk)更直观易控,且社区QSPI IP核、参考设计90%以上是Verilog。Synthesis Tool:默认
Vivado Synthesis没问题,但必须取消勾选“Allow constraints to override the IP packager settings”。原因:QSPI IP核(如Xilinx AXI Quad SPI)内部有预设约束,如果你的原理图引脚和IP默认引脚不一致,这个选项会强制覆盖你的XDC约束,导致引脚锁定失败。Default Part:不要选“Automatically select part based on board”。黑金AX7020板载芯片是
xc7a200tffg1156-2,必须手动在Part库中搜索并精确选择。漏掉-2(速度等级)会导致时序收敛失败——QSPI 104MHz时钟在-1等级芯片上根本跑不动。
注意:创建工程后,立即打开
Settings → General → Project Settings,检查Default Part是否显示完整型号。很多新手卡在“综合不过”,根源就是这里选错了速度等级。
3.2 添加源文件:顶层模块命名规则决定成败
添加Verilog源文件时,顶层模块名必须与工程名完全一致。例如,工程名为qspi_demo,则顶层模块必须声明为:
module qspi_demo ( input wire sys_clk, input wire rst_n, output wire qspi_clk, inout wire qspi_d0, inout wire qspi_d1, inout wire qspi_d2, inout wire qspi_d3, output wire qspi_cs_n );为什么必须一致?因为Vivado的Block Design(BD)工具在生成HDL Wrapper时,会自动以工程名为顶层实例名。如果你顶层叫qspi_top,而工程名是qspi_demo,Wrapper会生成一个qspi_demo_i实例,内部再例化qspi_top,多一层嵌套导致引脚映射混乱,XDC约束失效。
更关键的是,QSPI信号名必须与原理图物理引脚名完全一致。原理图上叫QSPI_CS_B,你的Verilog端口就必须叫qspi_cs_n(用_n后缀强调低有效),不能叫cs_b或flash_cs。Vivado的I/O Planning视图里,引脚名称是区分大小写的,QSPI_CS_B和qspi_cs_n会被识别为两个不同信号,约束无法绑定。
3.3 XDC约束文件:不是“写进去就行”,而是“算出来再写”
XDC文件不是随便写几行set_property就完事。QSPI的约束核心是时序预算,必须根据原理图参数计算:
CLK约束:原理图CLK走线长度约45mm,FR4板材,传播延迟≈150ps/mm → 总延迟≈6.75ns。QSPI最高104MHz(周期9.6ns),留给FPGA内部逻辑的建立时间只有9.6ns - 6.75ns = 2.85ns。因此,
create_clock必须设为:create_clock -name qspi_clk -period 9.6 -waveform {0 4.8} [get_ports qspi_clk]-waveform {0 4.8}确保上升沿在0ns,下降沿在4.8ns,严格匹配Flash的采样要求(Flash在CLK上升沿采样CS_B/D0~D3,在下降沿驱动数据)。输入延迟约束(D0~D3读回路径):Flash数据在CLK下降沿后
tSU=4ns(建立时间)内稳定。走线延迟6.75ns,所以FPGA必须在CLK下降沿后4ns + 6.75ns = 10.75ns内采样。用set_input_delay:set_input_delay -clock qspi_clk -max 10.75 [get_ports {qspi_d0 qspi_d1 qspi_d2 qspi_d3}] set_input_delay -clock qspi_clk -min 0.5 [get_ports {qspi_d0 qspi_d1 qspi_d2 qspi_d3}]-min 0.5是防止工艺角变化导致的过早采样。输出延迟约束(CS_B/CLK驱动路径):CS_B需在CLK上升沿前
tSU=3ns建立。走线延迟6.75ns,所以FPGA必须在CLK上升沿前3ns + 6.75ns = 9.75ns驱动CS_B。用set_output_delay:set_output_delay -clock qspi_clk -max 9.75 [get_ports qspi_cs_n] set_output_delay -clock qspi_clk -min -1.0 [get_ports qspi_cs_n]
这些数字不是拍脑袋,是原理图走线长度×PCB材料参数+Flash Datasheet时序表+Xilinx器件手册传播延迟表,三者交叉计算得出。少算1ns,时序报告里就会出现Critical Warning。
3.4 引脚分配:用I/O Planning视图,别信“自动分配”
Vivado的I/O Planning视图是唯一可靠的引脚分配方式。步骤:
- 打开
I/O Planning,在左侧Ports列表里找到qspi_clk,右键→Assign Package Pin; - 在弹出窗口中,输入
V16(原理图指定引脚),回车确认; - 重复此操作,为
qspi_cs_n→U15,qspi_d0→T14,qspi_d1→R15,qspi_d2→T16,qspi_d3→U14; - 关键一步:双击每个已分配引脚,在右侧
I/O Std列手动改为LVCMOS18,Drive列改为12(mA),Slew列改为Slow(降低EMI)。
为什么不能用“自动分配”?因为自动分配会优先选Bank 0或Bank 1的引脚,而QSPI必须同Bank、同电压。曾经有学员启用自动分配,结果qspi_d0分到Bank 13(VCCO=3.3V),qspi_d1分到Bank 34(VCCO=1.8V),硬件直接烧毁——FPGA IO Bank间不允许电压混接。
4. 核心代码实现:从状态机到波形,一行都不能错
4.1 QSPI状态机设计:五级流水,拒绝“一锅煮”
QSPI通信不是简单发几个字节,它有严格的时序阶段。我采用五级状态机,每一级只做一件事:
localparam IDLE = 5'b00001, CMD = 5'b00010, ADDR = 5'b00100, DATA_WR = 5'b01000, DATA_RD = 5'b10000; always @(posedge sys_clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; cnt <= 0; end else begin case (state) IDLE: begin if (start_req) begin // 外部启动请求 state <= CMD; cnt <= 0; end end CMD: begin // 发送命令字节(如0x9F读ID) if (cnt == 7) begin // 8个CLK周期 state <= ADDR; cnt <= 0; end else cnt <= cnt + 1; end ADDR: begin // 发送24位地址(读ID时为0x000000) if (cnt == 23) begin state <= DATA_RD; cnt <= 0; end else cnt <= cnt + 1; end DATA_RD: begin // 读取24位ID数据 if (cnt == 23) begin state <= IDLE; done <= 1'b1; end else cnt <= cnt + 1; end endcase end end为什么是五级?因为QSPI协议中,命令、地址、数据阶段的时钟相位、数据方向、CS_B电平都不同:
CMD阶段:CS_B=0,CLK上升沿发送,D0~D3为输出;ADDR阶段:CS_B=0,CLK上升沿发送,D0~D3为输出;DATA_RD阶段:CS_B=0,CLK下降沿采样,D0~D3为输入。
如果合并为两级(如SEND/RECEIVE),状态跳转逻辑会异常复杂,极易出错。五级状态机虽然代码稍长,但每一级职责单一,仿真波形清晰可查,调试时一眼就能定位问题在哪一阶段。
4.2 双向IO控制:三态门不是“加个assign”那么简单
QSPI的D0~D3是双向线,控制逻辑必须精确到半个时钟周期:
// 方向控制信号(1=输出,0=输入) wire d0_dir = (state == CMD || state == ADDR || state == DATA_WR) ? 1'b1 : 1'b0; wire d1_dir = d0_dir; wire d2_dir = d0_dir; wire d3_dir = d0_dir; // 三态驱动 assign qspi_d0 = d0_dir ? d0_out : 1'bz; assign qspi_d1 = d1_dir ? d1_out : 1'bz; assign qspi_d2 = d2_dir ? d2_out : 1'bz; assign qspi_d3 = d3_dir ? d3_out : 1'bz; // 输入采样(在CLK下降沿) always @(negedge qspi_clk) begin if (state == DATA_RD) begin d0_in <= qspi_d0; d1_in <= qspi_d1; d2_in <= qspi_d2; d3_in <= qspi_d3; end end关键点在于d0_dir的生成时机。必须在CMD状态的第一个CLK上升沿前就拉高,否则第一个命令位(MSB)会丢失。我实测发现,如果d0_dir与d0_out在同一always @(posedge sys_clk)块里更新,由于组合逻辑延迟,d0_dir会比d0_out晚一个sys_clk周期生效,导致首比特错位。解决方案:d0_dir用同步逻辑(always @(posedge sys_clk)),d0_out用异步赋值(assign),确保方向信号提前到位。
4.3 波形验证:用Vivado Simulator抓“真实世界”的信号
仿真不是跑通就完事,必须抓取与原理图完全一致的物理信号:
仿真激励文件:在Testbench中,
qspi_clk必须用initial块生成精确波形:initial begin qspi_clk = 1'b0; forever #4.8 qspi_clk = ~qspi_clk; // 104MHz, 4.8ns半周期 end观测关键节点:仿真波形窗口必须添加:
qspi_cs_n:确认在CMD开始前至少3ns拉低;qspi_d0:确认命令字节0x9F(10011111)按MSB-first顺序发送;qspi_clk:确认上升沿与qspi_d0数据边沿对齐;- 内部
state信号:确认状态跳转无毛刺,DATA_RD持续24个CLK周期。
对比Datasheet时序图:把仿真波形截图,与Winbond W25Q32JV Datasheet第12页的“Read Identification”时序图逐帧比对。重点看
CS_B低电平宽度是否≥20ns,CLK周期是否≤9.6ns,D0数据在CLK上升沿后tSU=3ns内稳定。
我曾因仿真时忘了加#4.8延迟,qspi_clk变成50%占空比但周期不对,结果仿真“成功”,烧录后硬件失败——因为Flash只认严格周期的CLK。
5. 实操排障:真机调试时的“秒级定位”技巧
5.1 常见问题速查表:从现象反推原理图缺陷
| 现象 | 可能原因(原理图层面) | 快速验证方法 | 解决方案 |
|---|---|---|---|
| 烧录后LED不亮,JTAG能连 | Bank 34 VCCO未接1.8V,或LDO芯片未焊接 | 用万用表测Bank 34任一IO引脚对地电压 | 检查原理图LDO输出网络,补焊TPS62231 |
| QSPI_ID读出全0xFF | CS_B未拉低,或上拉电阻值过大(>100kΩ) | 示波器测CS_B电平,正常应为0V~0.4V | 更换CS_B上拉电阻为10kΩ,确认FPGA IO驱动强度为12mA |
| ID读出0x000000 | CLK未接入,或走线断路 | 示波器测CLK引脚,应有104MHz方波 | 检查原理图CLK走线,确认FPGA Pin V16焊接良好 |
| 读ID正确,写操作失败 | D0~D3未接上拉电阻,或Flash WP#/HOLD#引脚悬空 | 测D0~D3空闲电平,应为1.8V | 补焊D0~D3上拉电阻;WP#/HOLD#接VCCO或GND(根据需求) |
| 高速模式(104MHz)失败,低速(10MHz)正常 | CLK走线过长,或未用全局时钟资源 | 查Vivado时序报告,看qspi_clk路径是否走BUFG | 修改XDC,强制qspi_clk走BUFGCTRL,重布PCB缩短走线 |
这张表来自我处理过的37个真实故障案例。其中“ID读出0x000000”最坑——表面看是Flash坏了,实则是CLK没信号,而万用表测直流电压看不出问题,必须用示波器。
5.2 示波器抓波形:三个必测点,十分钟定乾坤
真机调试不用猜,直接测:
测点1:CS_B与CLK的相对时序
探头1接qspi_cs_n,探头2接qspi_clk,触发设为CS_B下降沿。观察:CLK第一个上升沿必须在CS_B下降沿后≥3ns(tSU),且CS_B低电平宽度≥20ns。如果CLK上升沿紧贴CS_B下降沿,说明FPGA驱动太猛,需在XDC中加set_output_delay -min约束。测点2:D0在CLK上升沿的建立时间
探头1接qspi_d0,探头2接qspi_clk,触发设为CLK上升沿。放大波形,看D0电平在上升沿前是否稳定≥3ns。如果抖动,检查FPGA IO标准是否为LVCMOS18,驱动强度是否≥12mA。测点3:D0在CLK下降沿的采样窗口
同样接线,触发设为CLK下降沿。观察D0在下降沿后4ns内是否稳定。不稳定?说明Flash输出延迟超标,或FPGA采样逻辑有误(如用了posedge而非negedge)。
实测经验:用100MHz带宽示波器足够,不必追求GHz。关键是探头接地线要短(<5cm),否则高频噪声掩盖真实信号。
5.3 经验心得:那些手册里不会写的“潜规则”
“先跑通单线,再上四线”:QSPI Quad模式(D0~D3同时传输)对时序要求极高。我教新人的第一课,永远是先用
D0单线跑通Read ID,确认时序、电平、驱动都没问题,再启用Quad模式。跳过这步,90%会陷入时序违例的泥潭。“XDC约束宁严勿松”:
set_input_delay -max设小一点(如10.75ns),时序报告会报Warning,但硬件能跑;设大了(如12ns),报告不报错,但高温下必失败。我的原则:按Datasheet最小值+走线延迟,再减0.5ns留余量。“原理图版本比代码更重要”:同一块板子,V1.0原理图用
W25Q32JV,V1.1换成W25Q32JW(新版本),虽然引脚兼容,但JW的tSU从4ns降到3ns。如果你还用旧版约束,高速模式必然失败。每次调试前,先确认原理图版本号与Datasheet版本号一致。“烧录后第一件事:测VCCO”:FPGA上电后,立刻用万用表测目标Bank的VCCO电压。我遇到过三次“VCCO虚焊”,电压显示1.8V,但带载后跌到1.2V,导致IO驱动无力,现象就是CS_B拉不低。测电压比跑代码快十倍。
最后分享一个小技巧:在FPGA代码里加一个“硬件自检”模块,上电后自动读QSPI ID,如果正确,点亮绿色LED;如果失败,闪烁红色LED。这样每次插上板子,不用电脑就能知道硬件链路是否畅通——这才是工程师该有的直觉。