FPGA实战:从原理图到工程搭建,QSPI Flash读写入门
QSPI公开课做到第三期前面一直在讲协议、讲Flash存储原理很多同学已经按捺不住想碰板子了。这一期正好进入正题原理图解读与工程搭建。我把课程里的内容整理成文字版手把手带你把一块开发板上的QSPI Flash电路从原理图到工程完整地跑一遍。这篇内容不挑板子黑金、正点原子、自己画的板子思路都一样只是引脚号不同学会了方法换块板子照样能拿下。这个阶段适合谁已经会点灯、跑过流水灯、知道always块怎么写但对“原理图怎么看”“引脚怎么分配”“一个完整工程怎么从零搭起来”还不太熟的同学。这期把这些事情讲透以后你再拿到一块陌生板子就不会傻眼了。工程已经建好代码也没几行但这节课最关键的不是代码本身而是让你明白代码里的每个信号是怎么跟原理图上的每一个引脚对应起来的。这个意识一旦建立FPGA开发你就入门了。1. 为什么FPGA课程要从原理图讲起很多初学者有个误区觉得FPGA开发就是打开软件写Verilog写完一编译一下载板子就该按自己的想法动了。实际上FPGA和单片机最大的区别在于单片机引脚功能往往已经固定好而FPGA的引脚是可编程的甚至同一个引脚可以用作普通IO、时钟输入、差分对、专用配置引脚。那FPGA怎么知道哪个信号应该从哪个引脚出来全靠你在工程里做引脚分配而这个分配的依据就是原理图。1.1 原理图是RTL设计和硬件之间的桥梁打个比方你写Verilog相当于画了一张交通指挥图板子上的FPGA芯片相当于一个大型十字路口而原理图就是告诉你这个路口每个车道朝向哪里的那张地图。你不看地图凭感觉指挥车辆结果八成是撞车。具体到QSPI接口你想让FPGA去读写板上的Flash代码里一定会有qspi_cs、qspi_clk、qspi_dq这些端口。这些端口最终要绑定到FPGA芯片的物理引脚上比如PIN_M2、PIN_N4。这串引脚号从哪里来只能从原理图上看。芯片手册只会告诉你这个引脚是什么Bank的、能支持什么电平标准不会告诉你它在你的板上连到哪颗Flash。我见过不少同学代码写得没毛病波形仿真也过了下载到板子上就是没反应最后发现是引脚分配错了。这类问题在FPGA调试里占的比例相当高绕开它的唯一办法就是从原理图这一步走扎实。1.2 QSPI在FPGA工程里的两种角色在开始看原理图之前先分清QSPI在FPGA工程里扮演的角色这会影响你怎么理解原理图上的连线。第一种是作为配置接口。很多FPGA开发板使用QSPI Flash存储比特流文件上电后FPGA从Flash加载配置这种方式叫SPI Flash配置模式。此时QSPI接口是FPGA配置电路的一部分通常在原理图里紧挨着FPGA的专用配置引脚。这种场景下你关注的重点是串行配置芯片的型号、供电电压、上拉电阻是否完整。第二种是作为通用外设接口。FPGA上电后通过QSPI接口主动去读写Flash用来存数据、存字库、存固件。这种场景下你需要完全自己实现QSPI协议时序时钟频率、时序约束、IO标准全部自己负责代码里要完整实现读写Flash的指令序列。我们的课程主要围绕第二种场景展开这也是实际工程中最常用的方式。但不管哪种角色原理图都是第一步。2. QSPI原理图解读从一颗Flash看懂整块板拿到一块开发板的原理图别急着看全图先定位QSPI相关电路。一般来说原理图会用网络标号把同一根信号连接关系标出来比如FPGA侧一个引脚标着QSPI_CSFlash芯片侧也会有一个引脚标着QSPI_CS二者通过这个网络标号连接。2.1 一张典型QSPI Flash原理图长什么样我以手头这块板子为例它上面用的Flash芯片是Winbond的W25Q64一颗64Mbit的串行NOR Flash这是QSPI教学最常用的芯片。打开原理图找到这颗芯片之后整个QSPI电路通常由以下几部分组成。电源部分Flash芯片的供电引脚VCC通常接3.3V旁边一定有0.1uF的去耦电容。别小看这个电容Flash在上电瞬间和擦写操作时电流变化很大没有去耦电容滤波可能导致供电电压跌落造成读写数据错误。我甚至见过有些低成本板子省掉了这个大电容Flash写数据时偶尔就丢字节。时钟和复位QSPI接口没有单独的复位引脚Flash的复位动作靠掉电完成。如果你想做硬件复位需要在代码里控制CS引脚做一次“空操作”来重新初始化。时钟信号CLK在原理图上一般直接连到FPGA的普通IO没有特殊处理。数据接口QSPI的核心是4根数据线DQ0到DQ3加上一根时钟CLK、一根片选CS。原理图上这些信号会统一标注成QSPI_DQ0、QSPI_DQ1、QSPI_DQ2、QSPI_DQ3、QSPI_CLK、QSPI_CS读起来很清晰。我一般会把QSPI相关电路整理成表格方便实际做引脚分配时对照。你可以用这个表作为模板。信号Flash引脚FPGA引脚FPGA Bank电压标准备注QSPI_CSCS#PIN_AB19Bank 33.3V LVCMOS片选QSPI_CLKCLKPIN_AA20Bank 33.3V LVCMOS时钟QSPI_DQ0IO0PIN_AB21Bank 33.3V LVCMOS数据0QSPI_DQ1IO1PIN_AA21Bank 33.3V LVCMOS数据1QSPI_DQ2IO2PIN_AB22Bank 33.3V LVCMOS数据2QSPI_DQ3IO3PIN_AA22Bank 33.3V LVCMOS数据32.2 由原理图到引脚分配表拿到原理图上FPGA引脚的连接关系后接下来就是把它们变成引脚分配表。这张表是整个工程搭建的核心依据。实际操作时我习惯先看FPGA芯片周围那几页原理图找到Flash芯片之后沿着它的IO0到IO3、CLK、CS引脚往回追看它们连到FPGA的哪个引脚。原理图软件一般是OrCAD或者Altium Designer直接搜索网络标号就能跳转。需要注意的是FPGA是分Bank的每个Bank的供电电压决定了这个Bank的IO可以支持什么电平标准。比如引脚所在的Bank供电是3.3V那这个引脚的IO Standard就只能选3.3V LVCMOS如果Bank供电是1.8V你还选2.5V下载后信号电平就不对Flash完全不工作这种问题用万用表都测不出来因为万用表量的是通断量不了电平匹配。所以做引脚分配表时一定要把每个信号所在Bank的电压标准标出来。这也是为什么我强调原理图上不仅要看信号连到哪还要看FPGA旁边的电源网络标号。2.3 原理图上容易忽略的三个细节原理图读到这里外行人觉得可以了但我们做工程的人知道还有三个细节不看仔细后面调试一样踩坑。第一个是上拉电阻。很多Flash芯片的CS引脚和HOLD引脚W25Q系列里对应WP#和HOLD#内部有弱上拉有些没有。如果原理图上CS引脚悬空或者没有上拉上电瞬间FPGA还没配置好IO引脚处于高阻态CS信号就是浮空的Flash可能误判为片选有效进入一种异常状态。好的原理图设计会在这几个关键引脚上加10K上拉。第二个是配置模式引脚。如果你的FPGA是Xilinx那重点看MODE引脚设置如果是IntelAltera重点看MSEL引脚。这些引脚的电平组合决定FPGA上电时从什么介质加载配置。如果你的板子设计是QSPI Flash启动那MSEL或MODE引脚的接法必须和原理图一致否则会出现“明明Flash里烧了程序上电就是跑不起来”的问题。第三个是时钟来源。FPGA系统时钟和Flash时钟是两码事Flash时钟由FPGA IO口产生但系统时钟决定FPGA内部状态机的运行速度。原理图上要找到板载晶振是多大频率、连到FPGA哪个时钟引脚这个时钟引脚在引脚分配时要设置为“时钟输入”用途代码里也要用它作为全局时钟。如果时钟连错引脚代码里用了别的引脚做时钟综合器会报错或者性能下降。3. 工程搭建用最小系统验证QSPI通路原理图看明白了接下来就是工程搭建。FPGA开发软件有好几家Intel平台用QuartusXilinx平台用Vivado操作界面不一样但核心流程一致新建工程、添加设计文件、引脚约束、综合、布局布线、生成比特流、下载。这节课我以Quartus为例讲Vivado用户照着思路换成对应操作即可。3.1 新建工程时容易忽略的三个设置新建工程看似简单但有几个细节决定了后续开发是否顺利。工程名称和路径不要用中文、不要带空格。这不是迷信Quartus和Vivado对中文路径的支持这些年虽然好了点但偶尔还是会出现莫名其妙的编译错误一旦遇到重开一个工程很浪费时间。我自己的习惯是D:/fpga_prj/qspi_lesson3一目了然。器件型号必须和板子上的芯片完全一致。很多人图省事随便选个相近型号综合时倒是能过引脚分配就麻烦了因为你选的芯片引脚数量或者封装如果和板子不一样Pin Planner里根本找不到原理图上标的那串引脚号。新建工程时器件型号一定要对着原理图上的FPGA型号逐字核对比如EP4CE10F17C8还是XC7A35TFTG256差一个字母都不行。工程建立之后第一件事不是写代码而是先把原理图里整理出的引脚分配表填进Pin Planner。你可以先把引脚分配做好再开始写Verilog这样写完端口定义之后编译一次就能直接对应上后面调试链路会顺很多。3.2 引脚分配实操跟着Pin Planner走一遍在Quartus里从菜单栏Assignments找到Pin Planner打开之后左侧是FPGA芯片的封装视图右侧是引脚列表。这个时候你只需要按照之前整理好的引脚分配表一项一项填进去。具体操作很简单在Location列输入或选择引脚号在I/O Standard列选择3.3V LVCMOS根据你原理图上对应的Bank电压来定然后保存。如果你还想让某些引脚在配置期间有确定的电平状态可以设置Reserved Pin配置不过对于QSPI教学这个环节默认设置就够了。填完之后顺手做两件事第一用菜单里的Report检查有没有引脚功能冲突比如某个引脚是不是被系统保留用于JTAG或配置了第二检查是否有引脚漏掉IO Standard这个检查项在编译时可能会被当警告放过去但它恰恰是最容易导致实际电路不工作的原因之一。Vivado用户在IO Constraints窗口里操作类似选择引脚号、设置IO Standard然后保存为XDC文件。3.3 约束文件的另一种玩法直接用代码分配引脚除了图形界面Quartus支持把引脚分配直接写进QSF文件Vivado对应的是XDC文件。这种方式批量操作更快也更容易维护。举个例子QSF文件里这几行就是引脚分配set_location_assignment PIN_AB19 -to qspi_cs set_location_assignment PIN_AA20 -to qspi_clk set_location_assignment PIN_AB21 -to qspi_dq[0] set_location_assignment PIN_AA21 -to qspi_dq[1] set_location_assignment PIN_AB22 -to qspi_dq[2] set_location_assignment PIN_AA22 -to qspi_dq[3] set_instance_assignment -name IO_STANDARD 3.3-V LVCMOS -to qspi_cs注意信号名一定要和顶层模块的端口名完全一致大小写也不能错否则会报找不到信号。我自己更习惯用QSF文件管理引脚因为板子升级或者换板时只需要改这一个文件比在图形界面里一个个点快得多。XDC文件里对应的是set_property PACKAGE_PIN AB19 [get_ports {qspi_cs}] set_property IOSTANDARD LVCMOS33 [get_ports {qspi_cs}]4. 手写代码让QSPI Flash为数码管供数据工程搭建完成接下来就是今天的重头戏手写代码。为了让结果看得见摸得着我设计的这个实验目标是从QSPI Flash里读出一组数据然后把数据显示到开发板的4位数码管上。这样做的意义在于既验证了QSPI接口的读通路又让结果直观显示比单纯用逻辑分析仪抓波形更贴近实战。4.1 顶层模块架构设计这个工程我打算分成三个模块来写QSPI控制器、数据控制逻辑、数码管驱动。QSPI控制器负责产生QSPI时序发送指令、接收数据数据控制逻辑决定什么时候发起读取、读取哪个地址的数据、把读上来的数据送给数码管数码管驱动则把数据转换成段码并动态刷新显示。模块之间用简单的握手信号交互不搞复杂协议初学者看起来不会晕。这个架构的核心思路是通过一个地址常数和读指令从Flash固定地址读4个字节将第一个字节高位和第二个字节低位拼成一个数码管显示值。这样读出来是什么就能直接看到什么调试起来非常直观。4.2 QSPI读控制状态机从发指令到收数据QSPI控制器的核心是一个状态机它的状态转移大概是这样的空闲状态等待触发触发后进入发送状态把8位指令和24位地址发出去然后切到读数据状态不停接收数据直到数量足够最后回到空闲状态。代码里最需要关注的是时序参数。QSPI时钟是FPGA通过IO输出的跑多快完全自己定。在这个实验里我用的系统时钟是50MHzQSPI时钟用分频的方式降到12.5MHz左右也就是对系统时钟4分频。为什么不全速跑一是因为Flash芯片的规格书上最大时钟频率有限制二是因为刚上电时FPGA和Flash都还不稳定慢速跑能减少调试干扰。等代码稳定了再提频不迟。下面给出读操作状态机的主要代码结构注意这里不追求最高性能但求稳定可靠module qspi_reader ( input wire clk, input wire rst_n, input wire trigger, output reg [15:0] data_out, output reg done ); localparam IDLE 3d0; localparam SEND_CMD 3d1; localparam SEND_ADDR 3d2; localparam READ_DATA 3d3; localparam FINISH 3d4; reg [2:0] state; reg [5:0] bit_cnt; reg [31:0] shift_reg; // 伪代码示意状态机主体 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; end else begin case (state) IDLE: if (trigger) state SEND_CMD; SEND_CMD: begin // 发送0x03命令Read Data时序按QSPI协议 if (bit_cnt 7) state SEND_ADDR; end SEND_ADDR: begin // 发送24位地址例如读地址0x000000 if (bit_cnt 23) state READ_DATA; end READ_DATA: begin // 接收16位数据组合成data_out if (bit_cnt 15) state FINISH; end FINISH: begin done 1b1; state IDLE; end endcase end end endmodule细说几个容易写错的地方。发送数据时数据要提前一个时钟放在IO上等QSPI时钟上升沿采样接收数据时FPGA在QSPI时钟的下降沿采样数据这样数据有半个周期稳定时间。这个细节如果搞反了读上来的数据往往是乱的。另外一个常见问题是读数据时高字节和低字节的顺序Flash是大端还是小端要看芯片手册王保的W25Q系列读出来是按字节顺序的如果后面对不上交换一下高低位就行。4.3 数码管动态显示把数据变成肉眼可见的结果QSPI控制器把16位数据读回来之后下一步就是显示。数码管驱动模块的核心是动态扫描因为4位数码管共用段选线每次只能点亮一位所以必须在极短的时间内轮流刷新每一位利用人眼视觉暂留效应看起来就像4位同时亮。我的数码管驱动模块接口这样设计module seg_driver ( input wire clk, input wire rst_n, input wire [15:0] data_in, output reg [3:0] seg_sel, output reg [7:0] seg_data );段码表用case语句实现0到9分别对应共阳或共阴数码管的段码。这里有一个特别容易踩的坑共阳和共阴数码管的段码是反的你得看你板子上数码管原理图那里面有标是共阳还是共阴不能凭感觉来。动态扫描频率一般取1kHz到2kHz也就是每位刷新间隔0.5ms到1ms这样动态刷新不会闪。如果闪了多半是刷新频率太低。数据拼接方面我读出来的是16位数据在高4位显示低字节的高4位其他位显示低字节的低4位这样对初学者来说最容易对应上。4.4 顶层连线与整体调试顶层模块做的事情就是把这三个模块连接起来系统时钟和复位连到所有模块按键作为trigger信号QSPI控制器的data_out连到数码管模块的data_in。全编译一次下载到板子上按一下按键如果Flash读出来的数据和预期一致数码管就会显示对应的数字。我这个实验用的Flash读地址是0x000000Flash出厂时这个区域默认是0xFF所以正常情况你会看到数码管显示FFFF。如果你事先用烧录器在Flash的这个地址写入了别的数那显示出来的就是那个数。这个实验的意义在于你能看到一个跟Flash内容相关的现象说明你的QSPI读通路已经打通了。哪怕报错或者显示不对那也是正常的学习过程下面一节专门讲排查方法。5. 实际调试中遇到的坑与排查方法这部分是我最想写的。这些坑不是我一次踩完的而是带了很多学生之后总结出来的高频问题。照着这个表去排查至少能帮你省掉两三天调板时间。5.1 引脚分配后烧录无反应现象程序下载成功但板子上没有任何反应数码管不亮IO口电压量不到变化。排查路径首先看引脚分配打开Pin Planner把原理图上的引脚号和QSF里的分配对照一遍重点看有没有因为看花眼把PIN_M2填成PIN_N2。其次看IO Standard如果报了warning说某些引脚没有设置回去补上。最后看时钟确认板子上晶振频率和代码里的频率参数一致如果代码写的是50MHz板子上实际是20MHz时序就会不对。5.2 QSPI读出来全是0xFF或0x00现象能读回数据但数据永远是全1或者全0。全0xFF一般说明Flash根本没被选中或者数据线没有真正连上。先查CS引脚的上拉再用示波器或者万用表量CS信号在操作期间有没有拉低。CS一直为高Flash永远不响应。全0x00一般是时钟问题。QSPI时钟没有产生或者产生但相位不对。检查时钟分频的逻辑很多同学在状态机里用时钟的边沿来采样数据但发送时钟却是另外生成的结果数据和时钟不同步。我的建议是发送时钟和数据都用同一个时钟域下的寄存器输出用分频时钟的下降沿打数据这样时序比较稳。5.3 Flash能读但数码管显示不对现象读上来的数据不是预期的内容但Flash确实工作了。这种情况先别怀疑QSPI时序大概率是段码表或者高低位拼接错了。如果你做的是共阴数码管却用了共阳段码显示出来的数字会看着像倒过来的。程序里做个最简单的自测把数码管显示值固定为4hA看显示的不是A就说明段码表方向不对。高低位拼接也值得检查尤其是当你后续要读的位置地址跨度比较大的时候地址的高位和地位顺序容易搞反。对着原理图看Flash的DQ0到DQ3接到FPGA的哪几个引脚实际上W25Q的DQ编号跟引脚顺序不一定一样如果你把DQ2和DQ3接反了代码里又是按顺序发送的读出来就是乱的。5.4 复位信号与初始状态问题还有一个坑是复位。很多开发板的复位按键是低有效但有些是反的如果你把复位极性搞错整个模块会一直被复位着看起来就是“死”的。判断方法很简单看原理图上复位按键连接的是高电平还是低电平。按键不按时是低电平、按下是高电平那复位信号就要取反。有些情况更隐蔽复位释放之后模块一开始工作正常但跑一段时间就出问题。我遇到过第一次按键能正常读Flash第二次按键就卡住不动的案例最后发现是状态机在FINISH状态释放done信号之后没有等busy信号确认就收到了新的trigger导致状态机被二次触发乱套了。解决办法是在状态机里做去抖保证一次trigger处理完再去响应下一次。5.5 不同开发工具的工程兼容性最后说一句工具链。如果你的板子用的是Xilinx芯片上面讲的流程在Vivado里对应换成创建工程、添加约束XDC、生成比特流、下载这几步核心思路完全不变。很多人卡在“我用的Vivado怎么办”其实一旦理解了官方工具的通用流程换工具只是界面上找按钮的事。顺带提一句Quartus和Vivado的许可证问题别忽视很多教学板自带的免费版本已经够用没必要费劲去找盗版免费版对于这种级别的教学工程绰绰有余。6. 课程总结与下一步方向这一节课的内容实际做下来我从拿到一块新板子到数码管显示出Flash里的数据整个流程大概需要一小时其中看原理图占掉了一半时间。这不是浪费时间而是FPGA开发必须养成的习惯先看硬件再写代码最后才是调程序。把这节课的核心步骤刻在脑子里拿到板子先打开原理图找到目标芯片整理出引脚表再打开工程EDA工具填引脚约束然后才写代码。顺序反了后面全是痛苦。下一步如果还想继续深挖有两个方向可以走。一个方向是真正实现QSPI Flash的写操作在现在的读通路基础上加擦除和编程指令然后把写入的数据断点回读验证。另一个方向是把QSPI时钟频率往上提研究高速接口下的时序约束怎么做这是你从FPGA入门迈向实际项目开发必须要迈过的一道坎。我个人在实际带项目的过程中最大的感受是很多工程上的问题从原理图入手基本都能找到答案。代码只是把你的设计思想翻译成硬件语言而原理图才是整个设计的地基。地基打牢了上面盖多高的楼都不用慌。下一节课我会把QSPI的写操作和擦除操作完整写一遍到时候直接在这节课的工程上改你就能感受到一个完整的Flash读写流程是怎么跑的。先把这个读流程调通写操作的代码看起来会非常顺。