接触FPGA一段时间的工程师应该都有同感:写Verilog本身不是最劝退的,最劝退的是拿到一块陌生板子、一张原理图,完全不知道从哪里下手。我这套QSPI公开课讲到这里正好是第三讲,主题也从"怎么握笔"变成了"怎么读图、怎么开工"。这一篇要解决的是两件很具体的事:第一,把QSPI Flash相关的原理图吃透,搞清楚每个引脚连到哪里、为什么这样连;第二,在Quartus里从头搭起一个可用工程,把原理图上的网络名翻译成FPGA的物理引脚约束。适合已经有Verilog基础、但对"从图纸到工程"这条链路还不熟的朋友,这一篇可以当作一份完整的操作笔记来跟。
1. 从一张原理图说起:QSPI Flash在FPGA系统里到底是干什么的
1.1 为什么FPGA需要外挂一颗Flash
常规FPGA(比如Altera的Cyclone系列、Xilinx的Artix系列)核心是基于SRAM工艺的,SRAM的特点是读写快、但掉电即失。所以FPGA上电后第一件事,就是从外部非易失存储介质里把配置文件读进来,然后才能进入用户逻辑工作状态。这颗外部存储介质,绝大多数低成本板子选的就是QSPI Flash,也就是一颗Nor Flash芯片,容量从8Mbit到256Mbit不等,常见的如W25Q16、W25Q64、W25Q128。
它承担两个角色:一是存放FPGA的bitstream配置文件,掉电不丢;二是在系统运行起来之后,剩余空间还可以用来存日志、存校准参数、存用户自定义数据。这就意味着你写的QSPI控制器,既可能用在"上电自加载"阶段,也可能用在"运行时任意读写"阶段。两者的时序要求不太一样,自加载阶段由FPGA内部硬件逻辑自动完成,不需要用户操心;但运行时读写,就需要你自己实现一套可读、可写、可擦除的控制器逻辑。这也是为什么这个系列第四讲开始就是手写代码,第三讲把它之前的原理图工作和工程搭建先讲透。
1.2 QSPI和普通SPI的差别在哪里
很多初学者会把QSPI直接当成"快一点的SPI",其实不够准确。标准SPI是四根线的通信:CS片选、SCK时钟、MOSI主出从入、MISO主入从出,一个时钟沿只能传1bit,属于单通道传输。QSPI里的"Q"是Quad的意思,它把数据通道扩成了四根:DQ0、DQ1、DQ2、DQ3,在时钟沿上可以一次传输4bit。同样是50MHz的时钟,QSPI在x4模式下的理论吞吐率能达到标准SPI的四倍。这个速度差距,在加载几十Mbit的bitstream时非常明显。
但注意一个细节:QSPI不是"改变物理引脚数量",而是复用引脚。以常见的SOIC-8封装的W25Q Flash为例,引脚功能是这样的:
| 引脚序号 | 标准SPI功能 | QSPI功能 |
|---|---|---|
| 1 | CS_n 片选 | CS_n |
| 2 | DO 数据输出 | DQ1 |
| 3 | WP_n 写保护 | DQ2(复用) |
| 4 | GND | GND |
| 5 | DI 数据输入 | DQ0 |
| 6 | CLK 时钟 | CLK |
| 7 | HOLD_n 保持 | DQ3(复用) |
| 8 | VCC | VCC |
也就是说,如果只做普通SPI读,WP_n和HOLD_n可以直接拉高或拉低,不占FPGA的IO;但如果要用QSPI的x4模式,这两个引脚就必须被FPGA控制,因为它们在x4命令下变成了数据线DQ2、DQ3。这个判断会直接影响你对原理图的理解——当你看到原理图上WP_n和HOLD_n接的是10k电阻拉到电源,基本可以确定设计者当初只打算用标准SPI或者x1模式;如果看到这两个引脚直接连到FPGA的普通IO上,那说明这个设计预留了x4高速读写的可能。我在实际项目里就吃过这上面的亏,误判了连接关系,导致引脚约束少分配了两根,后面返工换板才解决。
2. 原理图重点信息提取:先看芯片再看网络,最后看电源
2.1 芯片型号与封装后缀:这一步不能跳过
解读原理图,第一步不是看信号走线,而是确认芯片具体型号和完整丝印。原理图上通常只会标一个元件符号,旁边写型号,比如W25Q128JVSIQ。这个型号的每一段都有含义:W25Q128代表容量是128Mbit(也就是16MB);JV代表工作电压是3.3V,JW则是1.8V;S代表SOIC封装;I代表工业级温度范围;尾缀Q代表新的封装版本。电压版本不同,IO电平就可能完全不同,这会直接影响FPGA侧的IO标准选择。
有些国产替代芯片型号规则不一样,比如GD25Q128、XM25QH128,前段数字一样是128Mbit,电压和封装的后缀各有差异。拿到原理图后最好养成一个习惯:把Flash的完整型号抄下来,去官网下载对应的数据手册,翻到"DC Characteristics"和"Pin Configuration"这两页,确认工作电压和引脚定义。原理图本身有时候会画错,数据手册才是最终依据。这一步花十分钟,能省掉后面排查电平不匹配的几天时间。
2.2 连接关系:片选、时钟、四根数据线的来龙去脉
在原理图中找到Flash芯片后,沿着每一根信号网络去追,看它最终到达FPGA的哪个引脚。最典型的连接结构包括:
- CS_n片选:通常网名是
FLASH_CS_n、QSPI_CS或SPI_CS,连到FPGA的某个bank的普通IO。原理图上有时会在CS_n上画一个上拉电阻到VCC,这是为了防止FPGA未初始化时Flash被误选中。 - SCK时钟:网名通常是
FLASH_CLK、QSPI_SCK,同样连到FPGA IO。有些设计会在CLK线上串联一个22欧姆或33欧姆的匹配电阻,目的是减小信号反射。这个电阻对FPGA内部的逻辑没有影响,但看原理图时心里要有数。 - DQ0/DQ1/DQ2/DQ3:网名五花八门,常见有
FLASH_DQ0到FLASH_DQ3,也有按SPI习惯写成FLASH_DI/FLASH_DO/FLASH_WP/FLASH_HOLD。如果是后一种命名,说明画图的人按标准SPI思维画的,你要自己意识到后两根在QSPI模式下会被复用。
这里最容易出差错的是复用引脚的最终去向。我看到过不少板子,DQ0和DQ1确实连到了FPGA,但DQ2和DQ3只接了上拉电阻,没有走线到FPGA。这种板子从硬件上就不支持x4模式,软件写得再好也没用。所以你在解读原理图时,一定要把四根数据线逐一追完,确定它们都到了FPGA的IO,才可以在代码里启用Quad模式。
2.3 电源与去耦电容:三个不起眼但致命的细节
Flash的电源引脚通常网名叫FLASH_VCC或者和板上3.3V主电源共用,旁边并联着0.1uF和10uF的去耦电容。这里有几个容易被忽略的点:
第一,Flash供电必须和所在FPGA bank的供电电压保持一致。如果Flash是3.3V供电,那它接的那个FPGA bank的VCCIO也必须是3.3V,IO标准才能设置为LVCMOS33。如果两者不一致,轻则逻辑电平不识别,重则长时间工作后IO损坏。原理图上一般会标注bank电压,比如标注"BANK3_VCCIO_3V3",这时候就能确定IO标准了。
第二,去耦电容的位置。对于100Mbit以上的Flash,数据翻转时瞬间电流比较大,如果去耦电容距离VCC引脚太远,或者容值不足,高速读写时可能出现电压跌落,表现就是偶发性读错误。原理图设计阶段很难改,但你在做板级验证时心里要有这根弦。
第三,某些板子会把Flash的VCC和FPGA配置相关的专用电源(如VCCIO_PLL)分得很细。如果原理图上有跳线帽或者磁珠隔离,先确认跳线帽默认状态是否让电源真正到达了Flash。我遇到过一块板子,Flash单独用一路LDO供电,LDO输出默认是关断的,要软件拉高使能脚才有电。这种"隐性开关"在原理图上往往只有一行标注,不仔细看就会漏掉。
3. 从原理图到引脚约束:把图纸上的网络名翻译成FPGA管脚
3.1 为什么必须核对Bank电压与IO标准
FPGA的IO口是按bank分组的,每个bank有独立的供电引脚VCCIO,同一个bank里所有IO只能使用同一种IO电平标准。QSPI Flash信号如果接在3.3V的bank上,那这些引脚的IO标准就必须是3.3V LVCMOS(或者3.3V LVTTL),不能在一个bank里混用3.3V和2.5V。这个规则看似基础,但实际项目里翻车率极高,因为有些FPGA开发板为了兼容各种外设,会把Flash信号分散在不同bank。
另外,还要注意not all good pins。FPGA的很多引脚有多重复用功能,比如专用时钟引脚(CCLK)、配置状态引脚(nSTATUS、nCONFIG)、JTAG引脚(TMS、TCK),这些引脚尽管也可以当作普通用户IO用,但有条件限制,比如必须经过配置或禁止用于某些功能。如果你的QSPI Flash信号恰好占用了这些特殊引脚,Pin Planner里会给出警告,严重时编译都过不了。所以最稳妥的做法是:先打开Quartus的Pin Planner,导入原理图上的引脚分配,然后让软件自己跑一次引脚合法性检查。
3.2 引脚分配实操:Quartus Pin Planner与qsf文件写法
引脚分配有两种方式,工程小的时候直接图形界面操作,工程大了或者要版本管理,直接写qsf文件更高效。所谓qsf文件就是Quartus工程设置文件,里面每一行是一条设置命令。
典型的QSPI约束片段如下:
set_location_assignment PIN_AB23 -to qspi_cs_n set_location_assignment PIN_AA25 -to qspi_clk set_location_assignment PIN_AC24 -to qspi_dq[0] set_location_assignment PIN_AD25 -to qspi_dq[1] set_location_assignment PIN_AE26 -to qspi_dq[2] set_location_assignment PIN_AF25 -to qspi_dq[3] set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to qspi_cs_n set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to qspi_clk set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to qspi_dq[0] set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to qspi_dq[1] set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to qspi_dq[2] set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to qspi_dq[3]这里的PIN_AB23是一个示例,实际操作时以你的原理图为准。注意qspi_dq[3:0]做成总线,比分开取名干净很多,后续在Verilog代码里用dq[3:0]直接索引,qsf和RTL模块的端口命名保持完全一致,能少掉很多低级错误。
从原理图反查引脚时,我建议按表格整理一份中间清单,包含四列:网络名(原理图上的)、FPGA引脚编号(原理图上标注的)、FPGA所在Bank、IO电平标准。整理完再往qsf里抄,而不是看一眼原理图抄一个引脚,那样容易漏。
3.3 容易被忽略的配置脚冲突
在Altera/Intel FPGA里,AS配置接口和QSPI Flash关系密切。特别是当Flash承担着FPGA配置功能时,DCLK、DATA0、CS_n这几个信号在原理图上的命名经常是AS_DCLK、AS_DATA0、AS_CS_n,它们不仅要连到Flash,还要连到FPGA的专用配置引脚。这种情况下,如果用户代码里想复用同一颗Flash做数据存储,读写引脚配置就要特别小心。
我的经验是:配置用的Flash,它的DCLK和DATA0引脚在用户设计里尽量不要当作普通IO去读,因为FPGA配置完成后这些引脚可能仍处于专用模式,或者受nCEO、MSEL等状态影响。如果你想做的产品既要配置又要运行时读,硬件设计时就应该把Flash放在普通IO挂载,配置则通过其他方式(比如主动串行),或者干脆选用支持"配置+数据分时复用"的专用Flash控制器方案。这些在原理图阶段就要想清楚,而不是等到代码阶段再改。
4. 工程搭建全流程:Quartus里从零建起一个可综合的项目
4.1 新建工程的器件选择与EDA设置
工程搭建这一步,不同版本的Quartus界面略有差异,但核心流程一致:File -> New Project Wizard,先设置工程目录和工作目录,然后选择器件。器件选择时,如果你手头是Cyclone系列,最好按具体型号、封装、速度等级来选,比如EP4CE15F17C8,选错封装后续引脚分配会对不上。
工程目录方面,我的习惯是新建一个主文件夹,下面分src(RTL源文件)、sim(仿真文件)、con(约束文件)、doc(原理图、数据手册)四个子目录。这样工程一多不会乱,而且Quartus工程文件(.qpf、.qsf)放在根目录,生成的编译产物不要混在src里。有些初学者喜欢把所有文件堆在一个文件夹,吃过几次版本混乱的亏后会明白这个习惯有多重要。
工程向导里还有EDA Tool Settings。如果你装了ModelSim或QuestaSim,可以在这里指定仿真工具和语言标准(Verilog建议选SystemVerilog或者Verilog 2001)。不指定也可以,后面每次手动打开仿真器也行,但指定了Quartus能在编译后一键启动仿真,省时间。语言标准这里有个坑:选Verilog-1995会不支持一些现代语法(比如always_comb、带符号的$signed),所以尽量选Verilog-2001以上。
4.2 顶层模块端口设计:先画好模块框图再写代码
工程建好之后,别急着敲代码。我强烈建议先在纸上或者任意画图工具里画出顶层模块的框图:左边是外部输入,右边是外部输出,中间是你的控制器。针对QSPI Flash,顶层端口通常是下面这些:
module qspi_ctrl_top ( input wire clk, // 系统时钟,例如50MHz input wire rst_n, // 异步复位,低有效 // 用户接口 input wire rd_start, // 读请求 input wire [23:0] rd_addr, // 读地址 output wire [31:0] rd_data, // 读数据 output wire rd_done, // 读完成 // 物理接口 output wire qspi_cs_n, output wire qspi_clk, inout wire [3:0] qspi_dq );这里qspi_dq[3:0]是三态总线,内部必须有一个输出使能信号控制何时把数据驱动到总线上、何时释放总线让Flash输出。很多新手在这里犯迷糊,以为inout就是双向引脚,直接赋值就行。实际上三态总线的驱动逻辑是:
assign qspi_dq[0] = dq_oe ? tx_data : 1'bz; assign rx_data = qspi_dq[0];读Flash时dq_oe拉低,把FPGA输出置为高阻,让Flash驱动总线;写命令和写地址时dq_oe拉高,FPGA把数据推出去。这个三态切换是QSPI控制器的灵魂,后面写代码时几乎每个状态都在处理它。
4.3 例化与约束:让Quartus认识你的QSPI引脚
顶层模块写好后,把它设为工程顶层实体:右键工程 -> Set as Top-Level Entity。然后打开Pin Planner(Assignments -> Pin Planner),会看到左边列出了你顶层模块的端口名,右边是器件封装图。根据qsf文件里的位置约束,软件会自动把端口和封装引脚对应起来;如果没有写qsf,也可以在Pin Planner里手动点击一个引脚然后分配。
这一步有个验证技巧:先编译一次,看Fit/Place & Route有没有报引脚不合法。Quartus的Fitter会检查你的引脚是否与专用功能冲突、IO标准是否和bank电压一致。如果有错误,会明确告诉你"Pin XX is reserved for JTAG"或者"IO standard LVCMOS18 is not supported on this bank",这些都是宝贵的信息,比你自己瞎猜快得多。
约束文件在Quartus里就是qsf,默认在工程根目录。你既可以通过Pin Planner图形操作,也可以直接用文本编辑器打开qsf追加设置,两种方式等效。但要注意,qsf文件的端口名必须和顶层模块端口名完全一致,多了或少了一个[0]都不行。我有一次把qspi_dq[0]写成了qspi_dq0,编译直接报端口不存在,排查了半天,非常低级的错误。
5. 读写流程的思路:状态机比你想的更简单
5.1 QSPI Flash的指令集:读、写、擦除、状态寄存器
原理图和工程都搞定后,接下来就是为第四讲手写代码做逻辑准备。QSPI Flash的控制并不复杂,核心是掌握几条常用命令:
| 命令 | 操作码 | 功能说明 |
|---|---|---|
| 读JEDEC ID | 0x9F | 读回厂商ID和器件ID,上板后第一个要跑的命令 |
| 写使能 | 0x06 | 把状态寄存器WEL位(bit1)置1,写、擦除前必须发 |
| 读状态寄存器 | 0x05 | 轮询bit0(BUSY位)判断是否忙 |
| 读数据 | 0x03 | 普通读,x1模式,一次可读任意字节 |
| 快速读 | 0x0B | 带dummy周期,速度更快 |
| 扇区擦除 | 0x20 | 按4KB扇区擦除,把数据位从0变1 |
| 页编程 | 0x02 | 按页写,一页通常256字节,只能把1变0 |
命令格式基本都是:先发1字节操作码,再发3字节地址(高字节在前),然后是需要的数据。写入前必须先擦除,因为Flash的物理特性决定了编程操作只能把1写成0,只有擦除操作能把0恢复成1。这是一个反复被忽视的细节。
5.2 状态机框架:发送命令、地址、数据的字节序设计
QSPI控制器的常见实现方式是Mealy型状态机,状态跳转围绕着"当前发的是什么内容"来组织。最简单的一组状态包括:
- IDLE:等待用户发起读写请求,同时监控
rd_start或wr_start信号。 - SEND_CMD:把操作码按位送到DQ上,一个时钟发1bit(x1模式)或4bit(x4模式)。这里用到的移位逻辑就是最核心的部分。
- SEND_ADDR:按地址从高位到低位逐bit/逐4bit发送。
- TRANSFER_DATA:读写数据阶段。
- WAIT_IDLE:发完命令后轮询状态寄存器的BUSY位,直到Flash空闲。
以x1模式写使能为例,指令是单字节0x06,状态机核心发送逻辑大致是这样:
// 8bit命令逐位发送 reg [2:0] bit_cnt; wire [7:0] cmd = 8'h06; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin bit_cnt <= 3'd0; dq_oe <= 1'b0; end else if (state == SEND_CMD) begin dq_oe <= 1'b1; qspi_dq[0] <= cmd[7 - bit_cnt]; if (bit_cnt == 3'd7) begin bit_cnt <= 3'd0; state <= IDLE; end else begin bit_cnt <= bit_cnt + 1'b1; end end end这里要注意的是数据方向:每bit数据需要在时钟的某个边沿驱动出去。QSPI Flash一般在时钟上升沿采样输入数据,所以我们可以在上升沿到来之前把数据准备好,或者用下降沿更新数据。这个"对准沿"的问题如果在仿真里发现数据错位,优先查这里。
5.3 关于时钟频率和分频的选择
QSPI Flash的SCK最高频率和具体的操作模式相关。W25Q128在普通读0x03模式下最高50MHz,在快速读0x0B模式下最高133MHz(不同厂商有差异),但工程上不要顶着极限跑,我一般选择把系统时钟分频后给SCK。比如系统是50MHz,Flash规格是104MHz,SCK直接给50MHz就完全够用;如果是100MHz系统时钟,Flash跑50MHz模式,那SCK就要二分频。
分频在RTL里实现很简单,但有个关键点:SCK和系统时钟的相位关系。如果你用系统时钟做状态机,然后输出SCK=系统时钟的二分频,那么发送数据时一定要保证状态机的信号变化发生在SCK的下降沿附近,给Flash留出足够的数据建立时间。一个偷懒且稳定的做法是:让状态机在系统时钟下降沿更新数据,SCK用系统时钟上升沿产生。这样数据在SCK上升沿到来前就已经稳定了。
6. 上板调试实录:我踩过的三个典型坑
6.1 引脚分对了,但读回全FF
这是QSPI调试最典型的现象:ID读不回来,读什么都是0xFF。我遇到过几次,原因各不相同,但排查顺序高度一致。第一步,用万用表和示波器确认芯片供电正常,特别是确认LDO或者DC-DC是否真的输出3.3V。第二步,检查片选信号——CS_n必须严格低有效,而且整个通信期间要保持低,通信结束再拉高。如果状态机在发送命令和地址之间不小心把CS_n拉高了,Flash会认为这次通信已经终止,后面的数据全部无效。第三步,检查数据线是否正确连到Flash对应引脚,尤其是DQ0和DQ1有没有接反。很多原理图上Flash引脚标的是DO(pin2)和DI(pin5),在做QSPI约束时容易把DO和DQ1对应错,其实DO就是DQ1(复用),DI才是DQ0。
6.2 写数据后读出来不对,问题出在擦除时序
写操作失败特别容易让人崩溃,因为现象千奇百怪:写入0x5A读出来是0x5B,某几个位不对;或者写入后立刻读是对的,过一会儿再读又不对。说到底都是同一个根本原因:没有先擦除,或者擦除后没有等待BUSY位清零。Nor Flash固化写入时只能将1编程为0,如果你在已有数据的扇区上直接编程,有些1无法变成0或者保持不想要的0,读出来就是错乱的。正确流程是:发写使能0x06、发扇区擦除0x20加地址、轮询状态寄存器直到BUSY位为0、再发写使能、发页编程0x02加地址加数据、再轮询BUSY位。这串流程少了任何一步,结果都不保证。
我在自己项目里还碰到过一种特殊情况:擦除命令发出去后,状态寄存器BUSY位确实在变,但等待时间不够又继续读状态,导致读到的是一个中间的0x06值,误判为"busy已结束"。解决方法是读状态后不要急着判断,连续读两次,确认两次结果一致再继续下一步。
6.3 用SignalTap抓内部信号是最高效的排错方式
很多FPGA工程师调试时过度依赖板级示波器和逻辑分析仪,其实Altera/Intel的SignalTap II(逻辑分析仪IP核)比外部仪器好用得多,因为它直接挂在内部逻辑节点上,能看到的是RTL内部信号的真实时序。配置SignalTap时,需要加入想要观察的信号列表,添加触发条件,然后重新全编译并下载到板子。比较常用的触发方式是:state信号等于某个关键状态,或者rd_start上升沿。抓到的波形可以导出成VCD文件,在ModelSim里和仿真波形对比,定位是哪一步状态跳转出了问题。
用SignalTap有一个注意点:它会占用片内RAM资源,信号数量最好控制在32个以内,采样深度也不用设太大,2K或4K一般够用。抓QSPI时序时,把SCK、CS_n、状态机状态、数据字节都加进去,一次抓完整个读操作流程,基本能看出所有时序问题。
在这里分享一个个人习惯:拿到新板子的第一件事,不要写复杂的读写控制器,而是先写一个最简单的状态机,只发送读JEDEC ID命令(0x9F)并把读到的3字节数据回传到LED或串口。这个功能跑通了,就证明原理图连接、引脚约束、工程配置、时钟复位全链路都是通的。后续再开发页编程、扇区擦除、x4快速读这些高级功能,都建立在这个小功能的地基上。很多新手一上来就写完整读写器,出问题了都不知道是底层连接的问题还是逻辑实现的问题,排查成本反而更高。