简介:基于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_50m | PIN_Y2 | 板载50MHz晶振输入 |
| rst_n | PIN_AB22 | KEY0作为复位键 |
| sys_rst | PIN_AC28 | SW0作为主持人复位开关 |
| 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_out | PIN_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 <= 2'b11; else sync <= {sync[0], key_in}; end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 20'd0; key_out <= 1'b1; end else if (sync[1] != key_out) begin if (cnt == DELAY - 1) begin key_out <= sync[1]; cnt <= 20'd0; end else cnt <= cnt + 1; end else cnt <= 20'd0; 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 = 1'b0; parameter LOCKED = 1'b1; reg state; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; winner <= 4'd0; led_out <= 7'd0; lock_flag <= 1'b0; end else if (sys_rst) begin state <= IDLE; winner <= 4'd0; led_out <= 7'd0; lock_flag <= 1'b0; end else begin case (state) IDLE: begin if (|key_db) begin if (key_db[6]) winner <= 4'd7; else if (key_db[5]) winner <= 4'd6; else if (key_db[4]) winner <= 4'd5; else if (key_db[3]) winner <= 4'd4; else if (key_db[2]) winner <= 4'd3; else if (key_db[1]) winner <= 4'd2; else if (key_db[0]) winner <= 4'd1; led_out <= key_db; lock_flag <= 1'b1; state <= LOCKED; end end LOCKED: begin // 保持锁定,等待主持人复位 end endcase end end endmodule这段代码里有一个容易被忽略的设计点:winner寄存器的更新必须在一个时钟周期内完成,不能分成多个时钟周期去判断。因为抢答要求并行比较七路输入,用组合逻辑判断优先级再在时钟沿锁存结果,才是正确的做法。如果写成顺序判断,后面的选手可能因为前面的分支条件判断耗时太长而产生延迟差。
3.4 数码管扫描显示与蜂鸣器控制
显示模块负责把winner值换算成七段码,并通过动态扫描方式点亮数码管。这里有一个七段码查表的问题,我用了一个组合逻辑case语句实现数字到段码的转换,显示“0”到“7”的段码分别是:0对应0x40,1对应0x79,2对应0x24,3对应0x30,4对应0x19,5对应0x12,6对应0x02,7对应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 <= 25'd0; beep_en <= 1'b0; end else if (lock_flag) begin if (beep_cnt < 25'd1000000) begin // 0.5s @50MHz beep_cnt <= beep_cnt + 1; beep_en <= 1'b1; end else begin beep_cnt <= 25'd0; beep_en <= 1'b0; end end end assign buzzer = beep_en ? ~clk_50m : 1'b0; // 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 = 7'd0; #100 rst_n = 1; // 测试场景1:选手3按下 key_db = 7'b000_0100; #20; // 检查 winner == 3 // 测试场景2:选手3和选手5同时按下 key_db = 7'b001_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那样大改状态机和优先级判断逻辑。
我在做这个项目时的最大体会是:一个看似简单的“七人抢答器”,实际上把数字系统设计里最核心的几个知识点都涵盖了。你把它完整做下来,按键消抖、状态机设计、模块划分、仿真验证、上板调试这些能力都会有质的提升。如果做的时候卡在哪一步了,建议先画一画状态转移图,理清楚信号方向再动手写代码,比闷头改代码有效得多。
本文还有配套的精品资源,点击获取