FPGA实战:基于Verilog的七人抢答器完整设计流程

FPGA实战:基于Verilog的七人抢答器完整设计流程 简介基于DE2-115开发板用Verilog编写的七人抢答器项目是一份适合FPGA入门与进阶学习者的完整工程资料。资源围绕抢答器核心逻辑提供seven.v、CLK_KEY.v、KEY_DOWN.v等源码以及Quartus 17工程配置、引脚约束与综合报告便于在DE2-115板上直接下载验证。压缩包共102个文件除源码外还包含cdb、hdb、rpt、qsf、qpf等大量工程生成文件可辅助理解Quartus编译流程、时序分析与硬件调试信息。资源包大小3.1MB适合电子类专业学生、竞赛选手及FPGA自学者参考。已有853人学习下载。通过研究此工程可以学习七路独立抢答信号处理、按键输入检测与时钟控制模块的写法熟悉从编写Verilog代码、配置工程到烧录上板的完整流程是一份不错的FPGA实战训练素材。 如果你在FPGA学习路上想找一个“把分频、消抖、状态机、显示驱动串起来”的完整小项目基于DE2-115开发板用Verilog写一个七人抢答器是个很合适的练手目标。这个题目看起来简单——不就是几个按键加数码管吗实际动手后你会发现按键消抖、优先级仲裁、锁存、数码管扫描、蜂鸣器控制每一个环节都有值得琢磨的细节。这篇文章我会把整个项目的设计思路、硬件接入方案、Verilog核心代码、仿真调试方法以及我实际踩过的坑完整写出来适合刚学完Verilog语法、想上手FPGA开发板做综合练习的同学参考。1. 项目定位与整体方案设计1.1 为什么在FPGA上做七人抢答器抢答器的核心需求其实很明确多个参赛者同时按下按键系统要准确判断“谁先按下的”并且把结果锁存起来后续按键不能覆盖先前的记录。这个需求天然适合用FPGA的状态机来实现因为它需要毫秒级甚至微秒级的响应速度还要处理按键抖动带来的误判问题。用DE2-115开发板做这件事还有一个现实原因这块板子资源非常丰富板上自带了按键、拨码开关、LED、七段数码管和蜂鸣器几乎不需要外接任何电路就能把整个系统搭起来。相比单片机方案FPGA方案的并行特性也更有意思——七路按键的检测状态在同一个时钟沿被采样不存在顺序扫描带来的时间差这正好符合抢答器“公平判断”的核心诉求。另外从学习角度来说七人抢答器覆盖了Verilog设计中几个非常基础又非常重要的知识点时钟分频、按键消抖、边沿检测、状态机设计、优先编码器、数码管动态扫描。这几个模块单独拿出来都很简单但组合在一起就是一个完整的数字系统设计流程。这也是很多高校数字逻辑课程和FPGA实验课选这个题目的原因。1.2 整体架构与模块划分我在设计这个项目的时候没有把全部逻辑堆在一个顶层模块里而是按功能拆成了五个独立模块这样仿真和调试都方便很多。模块清单如下模块功能关键输出clk_div将板载50MHz时钟分频生成各模块所需时钟1kHz扫描时钟、1MHz测试时钟debounce对七路按键进行消抖处理七路稳定后的按键电平rush_lock核心抢答状态机完成仲裁和锁存胜出选手编号、LED状态seg_display数码管动态扫描显示位选信号、段选信号beep_ctrl抢答成功提示音控制蜂鸣器驱动信号顶层模块负责例化这几个子模块并把它们连接起来。这种模块化拆分的好处是调试消抖模块的时候不需要关心显示模块怎么工作单独给一个testbench就能验证后期想扩展功能比如加倒计时、加违规抢答检测也只需要在对应模块上做增量修改不影响整体结构。时钟分频这里需要特别说一下。DE2-115板载晶振是50MHz这个频率对于人手的按键操作来说太快了直接用来采样按键信号可能存在亚稳态问题处理起来很麻烦。所以我把时钟分频成两个频率使用一个1MHz用于按键输入同步和消抖计数一个1kHz用于数码管扫描刷新。分频计数器用简单的计数清零方式实现50MHz到1MHz是50分频到1kHz是50000分频。2. 硬件外设规划与引脚分配2.1 七路按键输入到底怎么接DE2-115板上自带的按键只有四个KEY0到KEY3而题目要求七路抢答所以按键输入必须想办法扩展。我实际试过两种方案各有取舍。第一种方案是使用GPIO扩展接口外接七个小按键。DE2-115的GPIO接口引出大量IO引脚直接把七路按键一端接GPIO引脚另一端接地内部通过上拉电阻读取电平就行。按键按下时引脚被拉低未按下时保持高电平。这个方案的优点是符合真实抢答器的使用场景按键可以按实际座位布局摆放缺点是需要在面包板上跳线稍微麻烦一点。第二种方案是板上的四个KEY按键加三个拨码开关临时充数。KEY按键按下为低电平拨码开关拨上去也是低电平逻辑上和按键一样可以检测。这个方案适合快速验证功能不需要任何外部接线但拨码开关没有“自动弹起”的特性演示效果会差一些。我最终采用的是GPIO接外部按键的方案因为做的是七人抢答器项目总得有点抢答器的样子。如果你只是想在宿舍里快速看看效果直接用KEY加SW就行代码完全不用改只要引脚分配上对应调整即可。2.2 显示与提示外设的规划结果显示我用的是DE2-115板上的七段数码管。这块板子有8个七段数码管我只需要显示两位数字就足够了——十位显示0个位显示选手编号1到7比如选手5抢答成功就显示“05”。数码管驱动需要注意一点DE2-115的8个数码管是动态扫描方式工作的所有数码管的段选信号a到dp共用一组IO通过位选信号来控制当前点亮哪一个。人眼有视觉暂留效应只要扫描刷新频率足够快一般大于60Hz看起来所有数码管就是同时点亮的。我用1kHz时钟做扫描8个数码管轮流点亮一圈只需要8ms视觉上非常稳定看不到闪烁。蜂鸣器提示用的就是板载的蜂鸣器。DE2-115的蜂鸣器是无源蜂鸣器需要给一定频率的方波才能发声。我在抢答成功的瞬间让蜂鸣器输出一个2kHz方波持续0.5秒后自动关闭。这个提示音能让人明显感觉到“系统做出了判定”实际使用体验比单纯看指示灯好很多。2.3 引脚分配参考下面是我实际使用的引脚分配表如果你用GPIO外接按键可以直接参考这个映射关系信号名DE2-115引脚说明clk_50mPIN_Y2板载50MHz晶振输入rst_nPIN_AB22KEY0作为复位键sys_rstPIN_AC28SW0作为主持人复位开关key_in[0]GPIO_0_D0选手1按键key_in[1]GPIO_0_D1选手2按键key_in[2]GPIO_0_D2选手3按键key_in[3]GPIO_0_D3选手4按键key_in[4]GPIO_0_D4选手5按键key_in[5]GPIO_0_D5选手6按键key_in[6]GPIO_0_D6选手7按键hex0_out[6:0]PIN_AE26等个位数码管段选hex1_out[6:0]PIN_Y26等十位数码管段选ledr_out[6:0]PIN_AA24等七路指示灯buzzer_outPIN_AE26蜂鸣器驱动注意以上GPIO引脚编号是DE2-115板载丝印标号实际使用时建议对照板卡用户手册确认不同批次的板子可能有细微差异。上板前用万用表量一下引脚电平确认接线无误再下载程序避免因接线错误烧坏IO口。3. Verilog核心逻辑实现3.1 顶层模块搭建方式顶层模块是所有子模块的连接中枢我先把接口定义清楚再逐层实现内部逻辑。顶层模块的端口包括时钟、复位、七路按键输入、七段数码管显示输出、LED输出和蜂鸣器输出。module quiz_top ( input wire clk_50m, input wire rst_n, input wire sys_rst, input wire [6:0] key_in, output wire [6:0] led_out, output wire [6:0] hex0_data, output wire [6:0] hex1_data, output wire buzzer );这里我把按键低电平有效作为设计约定即按键未按下时输入为高电平按下时为低电平。整个系统的复位也统一为低电平复位这是FPGA设计里最常见的习惯。顶层模块内部实例化各子模块并连接好时钟信号和按键信号。3.2 按键消抖模块的实测参数按键消抖是整个项目第一个让人“翻车”的地方。机械按键按下和释放的时候触点会来回跳动几毫秒到几十毫秒如果不做消抖处理一次按键会被状态机认为是多次触发抢答结果完全不可控。我采用的消抖策略是“连续计数法”当输入电平发生变化后开始计数如果计数达到设定阈值时电平仍未变化就认为电平稳定输出新电平如果中途电平又变了计数器清零重新开始。在50MHz时钟下设置20ms计数窗口需要计数的次数是50MHz乘以0.02秒等于1000000次。module debounce #(parameter DELAY 1_000_000) ( input wire clk, input wire rst_n, input wire key_in, output reg key_out ); reg [19:0] cnt; reg [1:0] sync; always (posedge clk or negedge rst_n) begin if (!rst_n) sync 2b11; else sync {sync[0], key_in}; end always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 20d0; key_out 1b1; end else if (sync[1] ! key_out) begin if (cnt DELAY - 1) begin key_out sync[1]; cnt 20d0; end else cnt cnt 1; end else cnt 20d0; end endmodule这个消抖模块的关键在于先用两级同步器把按键输入同步到时钟域消除亚稳态风险。同步输出后的信号再送入计数器判断稳定状态。这里有一个参数选择的心得20ms是根据常见机械按键抖动时间通常不超过10ms留了一倍余量得出的实际测试中效果很好。如果按键抖动特别厉害可以适当把DELAY调大比如调到40ms但不要超过100ms否则抢答时会感觉延迟明显。3.3 抢答状态机与优先级仲裁抢答器最核心的逻辑就是状态机和优先级仲裁。我的状态机只有两个状态空闲和锁定。空闲状态下等待七路按键输入任何一路按键有效触发就进入锁定状态同时记录该路选手编号锁定状态下忽略所有后续按键输入直到主持人按复位键才回到空闲状态。这个设计用一个优先级编码器判断具体是哪一路选手抢先从七号开始往下判断哪个有效就输出对应的选手编号。module rush_lock ( input wire clk, input wire rst_n, input wire sys_rst, input wire [6:0] key_db, output reg [6:0] led_out, output reg [3:0] winner, output reg lock_flag ); parameter IDLE 1b0; parameter LOCKED 1b1; reg state; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; winner 4d0; led_out 7d0; lock_flag 1b0; end else if (sys_rst) begin state IDLE; winner 4d0; led_out 7d0; lock_flag 1b0; end else begin case (state) IDLE: begin if (|key_db) begin if (key_db[6]) winner 4d7; else if (key_db[5]) winner 4d6; else if (key_db[4]) winner 4d5; else if (key_db[3]) winner 4d4; else if (key_db[2]) winner 4d3; else if (key_db[1]) winner 4d2; else if (key_db[0]) winner 4d1; led_out key_db; lock_flag 1b1; state LOCKED; end end LOCKED: begin // 保持锁定等待主持人复位 end endcase end end endmodule这段代码里有一个容易被忽略的设计点winner寄存器的更新必须在一个时钟周期内完成不能分成多个时钟周期去判断。因为抢答要求并行比较七路输入用组合逻辑判断优先级再在时钟沿锁存结果才是正确的做法。如果写成顺序判断后面的选手可能因为前面的分支条件判断耗时太长而产生延迟差。3.4 数码管扫描显示与蜂鸣器控制显示模块负责把winner值换算成七段码并通过动态扫描方式点亮数码管。这里有一个七段码查表的问题我用了一个组合逻辑case语句实现数字到段码的转换显示“0”到“7”的段码分别是0对应0x401对应0x792对应0x243对应0x304对应0x195对应0x126对应0x027对应0x78这里使用的是共阴极数码管的低有效段码。扫描显示的核心逻辑是按1kHz时钟轮流选中个位和十位数码管先把要显示的数字放到数据总线上再拉高对应位选信号。两个数码管轮流点亮各自占空比约50%看起来就是常亮状态。蜂鸣器控制比较简单抢答锁定后拉高beep_en信号蜂鸣器驱动逻辑产生一个2kHz方波reg [24:0] beep_cnt; reg beep_en; always (posedge clk or negedge rst_n) begin if (!rst_n) begin beep_cnt 25d0; beep_en 1b0; end else if (lock_flag) begin if (beep_cnt 25d1000000) begin // 0.5s 50MHz beep_cnt beep_cnt 1; beep_en 1b1; end else begin beep_cnt 25d0; beep_en 1b0; end end end assign buzzer beep_en ? ~clk_50m : 1b0; // 2kHz需额外分频此处示意要注意DE2-115的蜂鸣器是无源蜂鸣器需要驱动方波才能发声不能直接给直流电平。我在实际实现里用了一个8位计数器对50MHz时钟做分频产生约2kHz的方波信号然后与beep_en做与门输出。4. 仿真验证与上板调试4.1 用Modelsim仿真时的testbench设计写完核心代码之后别急着上板先跑仿真能省掉大量调试时间。我用Modelsim对三个核心模块分别写了testbench这里重点说rush_lock状态机的仿真验证方法。testbench里要构造几个关键场景一是单个按键按下验证状态从空闲跳转到锁定winner值正确二是多个按键同时按下验证优先级仲裁结果是否符合预期三是锁定状态下再按其他按键确认状态不跳变四是复位后状态回到空闲所有输出清零。initial begin clk 0; forever #10 clk ~clk; // 50MHz end initial begin rst_n 0; sys_rst 0; key_db 7d0; #100 rst_n 1; // 测试场景1选手3按下 key_db 7b000_0100; #20; // 检查 winner 3 // 测试场景2选手3和选手5同时按下 key_db 7b001_0100; #20; // 期望 winner 5优先级从高到低 sys_rst 1; #50 sys_rst 0; $stop; end仿真时有一个容易踩的坑消抖模块的输出key_db是已经消抖后的稳定电平状态机里直接拿它判断即可。但如果你在testbench里直接对原始按键信号赋值出来的波形会因为消抖延时而与预期不一致所以仿真时要分模块验证不能一上来就全部连起来跑。Modelsim里我习惯用“单步仿真加波形观察”的方式先在lock_flag信号拉高的沿附近放大波形确认winner数据在同一个时钟沿被稳定捕获。如果发现winner出现毛刺或不确定值优先检查优先级编码的组合逻辑是否有分支遗漏。4.2 上板后遇到的几个典型问题第一个典型问题是数码管显示乱码。代码仿真完全正常但上板后数码管显示的数字不对而且时而正常时而乱跳。排查后发现是数码管扫描频率太高导致的——我用50MHz时钟直接做扫描每个数码管点亮时间只有几百纳秒亮度不够而且会有串扰。解决办法是把扫描时钟降到1kHz也就是每个数码管点亮1ms肉眼观察效果才稳定。第二个典型问题是抢答灵敏度太高手指还没按到底就触发了。这个本质上不是问题而是消抖时间参数没调好。机械按键的抖动通常在5到10ms如果消抖窗口只有1ms抖动期间的电平跳变会被误判为有效按键。我把消抖DELAY从10000对应0.2ms调整为1000000对应20ms之后误触发问题彻底消失。第三个问题是主持人复位不可靠。刚开始我把sys_rst设计成脉冲信号需要在抢答结束后按一下SW0再把SW0拨回去。实际使用中发现主持人经常忘记拨回导致下一次抢答时一直处于复位状态。后来我改成了电平触发复位只要SW0拨到高电平就保持复位拨回来才允许下一次抢答操作逻辑清晰很多。4.3 调试工具和流程建议上板调试时我会同时用SignalTap和LED指示灯两个工具。SignalTap是Quartus自带的逻辑分析仪它可以实时抓取FPGA内部信号波形对于排查状态机跳转问题非常好用。我在rush_lock模块里把state、winner、key_db这几个信号引到SignalTap的探针上上板后复现抢答操作就能看到这些信号在真实时钟下的变化过程。LED指示灯则用来做快速粗略判断。我把七路LED直接接到消抖后的按键信号上按键按下对应LED点亮这样能快速判断消抖工作是否正常。如果按下按键LED马上亮说明消抖模块可能没有生效如果按下按键LED延迟约20ms才亮说明消抖正常。5. 常见问题速查与扩展方向5.1 上板常见问题排查汇总我在整个调试过程中整理了一个问题排查表按照“现象、原因、解决办法”的格式列出来方便以后直接对照现象可能原因解决办法数码管显示乱码扫描刷新频率过高使用1kHz扫描时钟按键无响应GPIO引脚分配错误对照用户手册检查引脚按键一次触发多次消抖时间过短增大DELAY参数到20ms抢答锁定后无法复位复位信号设计为脉冲改为电平触发复位蜂鸣器无声无源蜂鸣器需要方波驱动增加2kHz方波生成逻辑多个按键同时按下时判断错误优先级编码逻辑有漏检查组合逻辑分支覆盖下载程序后板子无反应FPGA配置失败检查JTAG连接和bitstream这个表格不一定能覆盖所有问题但基本上涵盖了按键类项目最常见的故障点。遇到问题的时候先按“输入—逻辑—输出”三个环节去排查不要一上来就怀疑代码写错了。5.2 这个项目还能怎么升级七人抢答器做完之后我给它加了一个“五秒倒计时”功能主持人按复位后系统自动进入计时状态数码管倒数5、4、3、2、1倒计时结束后允许抢答。这样更接近真实知识竞赛节目的效果。还有一个比较实用的小功能是违规抢答检测。在主持人发出“开始”指令之前如果已经有选手按下按键系统会记录该选手编号并显示违规提示同时蜂鸣器发出急促的报警音。这个功能实现思路也不复杂只需在状态机里增加一个“预抢答检测”状态在计时开始前检测按键输入即可。如果你对这个项目的代码风格和扩展逻辑感兴趣可以试试用Chisel这类高层描述语言重新写一遍——用Chisel方式生成RTL和原生Verilog开发的区别在于Chisel的模块化和参数化能力更强比如要扩展到十人抢答器只需要修改一个参数和相应的位宽不用像原生Verilog那样大改状态机和优先级判断逻辑。我在做这个项目时的最大体会是一个看似简单的“七人抢答器”实际上把数字系统设计里最核心的几个知识点都涵盖了。你把它完整做下来按键消抖、状态机设计、模块划分、仿真验证、上板调试这些能力都会有质的提升。如果做的时候卡在哪一步了建议先画一画状态转移图理清楚信号方向再动手写代码比闷头改代码有效得多。本文还有配套的精品资源点击获取