多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Verilog拔河游戏机设计:按键消抖、状态机与FPGA仿真调试

Verilog拔河游戏机设计:按键消抖、状态机与FPGA仿真调试 简介这是一份基于 FPGA 开发板的 Verilog 拔河游戏机工程设计报告适合数字系统设计课程学生、Verilog HDL 初学者及 FPGA 实践爱好者参考。资源为单个 doc 文档大小约 452KB源自河海大学物联网工程学院课程设计完整覆盖实验要求、方案比选脉冲信号方案与按键消抖模块方案、系统框图、模块划分及代码设计思路。报告重点讲解双按键控制 LED 模拟拔河过程的实现方法包含时钟分频、按键消抖、LED 移动译码等关键模块并详细说明按键按下速度、信号过滤、LED 变化与绳子位置之间的关系以及通过标识符控制比赛开始与结束的逻辑涵盖设计标准文档、功能仿真、约束综合、布局布线、时序仿真与下载验证等完整流程。读者可从中掌握 Verilog HDL 基本语法、模块化设计方法和 FPGA 开发板应用可作为课程设计参考或 Verilog 项目起步学习资料。已有 551 人学习浏览供相关方向同学借鉴。1. Verilog拔河游戏机从按键到胜负的一个完整状态机项目拔河游戏机这个题目在FPGA课设清单里经常被划进“简单项目”但真在板子上把它调到能顺畅比赛至少要处理按键抖动、跨时钟域同步、优势判决、LED显示刷新和胜负锁定五件事。它和流水灯最大的差别在于流水灯是单向状态推进拔河是两路输入同时驱动一个共享状态天然涉及竞争与仲裁。这个项目适合正在从“会写Verilog语法”过渡到“能独立完成一个状态机设计”的人也适合老手用来验证自己对边沿检测、消抖窗口和可综合代码风格的理解。文章整条链路我会按五层模块展开消抖、边沿检测、优势判决、状态机、显示输出最后落到仿真和板级调试。2. 按键消抖与脉冲统计把物理按键变成稳定脉冲2.1 按键电路的默认电平与verilog消抖参数选取开发板上的按键基本都是机械触点按下和释放的瞬间电平会在几十毫秒内反复弹跳。如果拿这个裸信号去做按键次数统计一次按下会被计成好几次拔河比赛的判决就完全失真。所以模块第一件事是把物理按键信号变成一段确认后的稳定电平。我一般用计数式消抖整个项目拆成五个独立文件debounce、edge_detect、tug_arbiter、game_fsm、top。前两个模块负责物理信号到逻辑脉冲的转换中间是比赛规则最后控制显示和流程。消抖的核心参数是确认时间系统时钟100MHzMAX_CNT取250000对应2.5ms的稳定确认窗口。对绝大多数机械按键这个窗口足够覆盖触点弹跳。确认时间太短消不掉抖动太长会拖慢连按节奏。拔河类游戏把消抖窗口压在1~5ms之间比较合适按键反应慢几毫秒完全不影响手感判决的确定性才重要。按键极性也要先查原理图大多数开发板按键按下为低电平、释放为高电平下面的代码按这个极性写。// debounce.v 计数式消抖连续 MAX_CNT 拍不一致才确认新电平 module debounce #( parameter MAX_CNT 250_000 // 100MHz 下 2.5ms )( input wire clk, input wire rst_n, input wire key_in, output reg key_out ); reg [31:0] cnt; reg key_tmp; // 当前被确认的电平 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 32d0; key_tmp 1b1; // 复位按未按下处理 key_out 1b1; end else if (key_in key_tmp) begin cnt 32d0; // 输入与确认值一致计数清零 end else begin if (cnt MAX_CNT - 1) begin key_tmp key_in; // 持续不一致足够久确认新电平 key_out key_in; cnt 32d0; end else begin cnt cnt 1b1; end end end endmodule逻辑说明key_in和key_tmp一致时计数器清零说明当前电平稳定不一致时cnt开始累加直到连续2.5ms都保持新电平才把key_tmp和key_out一起更新到新值。机械抖动时电平快速来回电平每一次弹回原值都会让cnt清零只有真正按下后持续稳定输出才会跟着改变。这个模块的失效模式比较单一按键引脚悬空导致电平浮动这时消抖逻辑怎么调都没用要先用万用表确认引脚外部有上拉电阻。参数上要注意MAX_CNT位宽32位足够覆盖几十毫秒的计数。2.2 边沿检测模块把消抖后的电平变成单拍脉冲消抖后的key_out是一个电平信号按下多久就低多久。拔河判决关心的是“按下的那一下”而不是按住的时间长度所以下一步要做边沿检测把每次按键触发成一个单时钟周期的脉冲信号。检测边沿的标准做法是先把信号打两拍再用前后两拍的电平组合判断跳变方向。// edge_detect.v 两级寄存同步 下降沿检测 module edge_detect ( input wire clk, input wire rst_n, input wire sig_in, // 消抖后的按键电平按下为低 output reg pulse_fall // 单拍下降沿脉冲 ); reg sig_d1, sig_d2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sig_d1 1b1; sig_d2 1b1; pulse_fall 1b0; end else begin sig_d1 sig_in; sig_d2 sig_d1; pulse_fall (~sig_d1) sig_d2; // 下降沿判定 end end endmodule代码说明sig_d2保存的是sig_d1上一拍的值当sig_d1为0且sig_d2为1时说明信号刚刚从高变低此时pulse_fall输出一拍高电平。两级寄存同时完成了跨时钟域同步避免按键信号直接进逻辑时引入亚稳态。如果板子按键按下为高电平把判定表达式改成sig_d1 (~sig_d2)即可。边沿检测之后pulse_fall就是后面判决模块能直接使用的输入。2.3 为什么原始脉冲计数不能直接当比分到这一步不少人会顺手写两个计数器分别统计pulse_a和pulse_b的次数差值当成比分。功能上能跑但有个明显问题谁手速快谁就无限累积优势一局游戏十几秒就一边倒结束缺少拉锯过程。而且手速差会被按键行程放大判决的物理意义不明确。我习惯不在统计端做总比分而是做“优势积分”每次有效按键给当前方向累加一点双方差值累积到阈值就移动一格移动后积分清零。游戏被切成一段段小回合每次移动代表“这一小段时间内谁更连续地按键”相当于一个滑动窗口在判断近期节奏。这个模型下一章展开实现。参数典型值说明系统时钟100MHz开发板主时钟所有模块共用MAX_CNT250000消抖确认时间约2.5ms按键极性按下为低以板卡原理图为准边沿类型下降沿和按键极性配套3. 拔河判决模型阈值积分器怎么实现移动判定3.1 对比边沿触发移位与净优势积分两种判决方案市面上能搜到的拔河机代码里第一种方案是“边沿触发直接移位”检测到甲按键脉冲绳子上的LED就向左移一格检测到乙按键脉冲就向右移一格。这种写法代码最短反馈也直接但游戏体验不好双方手速稍有差异比赛就从第一拍定死了。第二种方案是“净优势积分”甲乙双方的按键脉冲共同驱动一个有符号积分值差值达到阈值才移动一格移动后积分清零。方案实现逻辑优点缺点边沿触发移位谁按键谁移一格代码量少响应直观手速碾压无拉锯游戏结束快净优势积分差值到阈值才移动有节奏感比赛有来回要调阈值多一个状态量第二种更接近拔河的物理含义比的是在一段时间里谁更稳定地持续发力而不是谁手速偶尔快一下。所以项目采用积分判定。注意这个“积分”不是把比分无限制累加而是饱和式积分优势每次被“消耗”掉才产生移动这样不会出现某一方攒了巨大优势然后一波带走的局面。3.2 阈值积分器核心代码与移动输出时序阈值积分器的实现思路是score用有符号数表示净优势甲按键加一、乙按键减一双方同按不变化。score绝对值达到THRESHOLD时输出move脉冲并清零score相当于把这段时间的按键优势兑换成一次移动。这个模型和滑动窗口滤波的等价关系在于滑动窗口维护的是最近N个结算周期内的按键分布阈值积分器维护的是一个被限幅的净计数两者对均匀按键频率的判决结果非常接近但积分器在Verilog里实现更简单时序也更干净。// tug_arbiter.v 按键优势积分与移动判决 module tug_arbiter #( parameter THRESHOLD 4 // 净优势阈值达到即移动一次 )( input wire clk, input wire rst_n, input wire en, // 比赛进行中使能 input wire hit_a, // 甲有效按键脉冲 input wire hit_b, // 乙有效按键脉冲 output reg move, // 移动脉冲1拍 output reg dir // 0 向甲侧1 向乙侧 ); reg signed [3:0] score; // 甲正乙负 always (posedge clk or negedge rst_n) begin if (!rst_n) begin score 4sd0; move 1b0; dir 1b0; end else if (!en) begin score 4sd0; move 1b0; end else begin move 1b0; // 同一拍双方都按键优势抵消不积分 if (hit_a hit_b) begin score score; end else if (hit_a) begin // 甲侧加一达到阈值前触发移动并清零 score (score THRESHOLD - 1) ? 4sd0 : score 1b1; if (score THRESHOLD - 1) begin move 1b1; dir 1b0; end end else if (hit_b) begin score (score -THRESHOLD 1) ? 4sd0 : score - 1b1; if (score -THRESHOLD 1) begin move 1b1; dir 1b1; end end end end endmodule逻辑说明score是4位有符号数范围-8到7。甲连续第4次按键时score从3变到4会溢出所以代码在score等于THRESHOLD-1时直接触发move并写0不做实际累加这样既避免溢出又保证移动和清零在同一拍完成。乙侧逻辑完全镜像。注意同一拍双按时不积分这是判决公平性的关键否则测试时同时注入两个脉冲会让比赛出现偏移。参数调节上THRESHOLD4意味着双方净按键比超过4:1才移动一格一局比赛大概能持续几十秒拉锯感明显。想更快决出胜负或适应展示场合的节奏可以把阈值降到2或3。score位宽与THRESHOLD的匹配关系要检查THRESHOLD最大取6留出符号位余量否则加法溢出后score变成负数游戏会往反方向跑这是最容易忽视的坑。3.3 同一拍双按、阈值溢出与比赛僵局的处理双按处理已经在判决模块内解决同时到来的hit_a和hit_b让score保持不变等效于优势抵消。物理上两个人几乎不可能在同一时钟沿按键但这个保护对测试时同步注入的脉冲很有必要它保证了判决模块在任何输入组合下都不会漂移。判决模块只负责“数学比较”僵局、超时、开局这类事务应该交给状态机去管职责分得越清仿真越容易定位问题。如果两个人都停手score停在某个非零值比赛会僵住。常见做法是在游戏层加一个倒计时开赛60秒内没有产生任何move判定平局并自动回到初始状态。这个超时计数器放在状态机里让tug_arbiter保持纯组合判决和积分逻辑后续要复用这个仲裁器时也不用带着超时包袱。4. 显示驱动与游戏状态机LED移位、边界锁定与顺序控制4.1 LED独热码移位与两端边界检查拔河绳上的每个点用一个LED表示我用独热码而不是二进制编码。独热码的好处是当前绳上位置可以直观地从led_pos寄存器的哪一位为1看出来板级调试时看LED就能验证移位方向仿真时看波形也只需要数位数不用做数值转换。15个LED对应15位寄存器正中间是第7位复位后中点单独点亮。// led_pos 为独热码1所在位置表示当前绳上点位 always (posedge clk or negedge rst_n) begin if (!rst_n) led_pos 15b000_0000_1000_0000; // 中点 else if (state S_PLAY move !dir led_pos ! 15b000_0000_0000_0001) led_pos led_pos 1; // 甲侧移动向左 else if (state S_PLAY move dir led_pos ! 15b100_0000_0000_0000) led_pos led_pos 1; // 乙侧移动向右 end边界判断是关键甲方向移到最左端时led_pos是1此时如果继续右移寄存器会变成全0LED全灭之后比赛无法判定胜负。必须用精确的端点值判断不能在代码里简单写 “大于0就移”。参数化LINE_W时端点值要用表达式生成避免手写不同位宽的字面量。LED闪烁指示胜负时我习惯用一个分频计数器在S_DONE状态里周期性地把led_pos清零再恢复闪烁频率约5Hz即可。4.2 三段式状态机的状态定义与转移条件表整个游戏流程用三段式状态机管理状态只有三个S_IDLE等待开局、S_PLAY比赛进行、S_DONE胜负锁定。按下START键从S_IDLE进入S_PLAYLED到达任一端点进入S_DONES_DONE中按下复位键回到S_IDLE。三段式写法的好处是状态寄存、次态组合、输出逻辑各占一个always块后仿真或板级抓波形时能直接断言state按预期变化。现态触发条件次态输出动作S_IDLE按下START键消抖后脉冲S_PLAY复位比分点亮中点使能判决S_PLAYled_pos到达左端或右端S_DONE停止判决胜负灯常亮S_PLAY60秒无移动S_IDLE平局清除显示S_DONE按下复位键消抖后脉冲S_IDLE清除所有显示// 次态组合逻辑三段式状态机中的转移部分 always (*) begin next_state state; case (state) S_IDLE: if (start_pulse) next_state S_PLAY; S_PLAY: if (left_done || right_done) next_state S_DONE; else if (timeout) next_state S_IDLE; S_DONE: if (rst_pulse) next_state S_IDLE; default: next_state S_IDLE; endcase endstart_pulse和rst_pulse必须来自第2章的消抖与边沿检测链路不能直接把按键引脚接进状态判断。裸按键信号进FSM一次按下会在S_IDLE和S_PLAY之间来回弹跳这是这个项目里最常见的“状态机失灵”原因。输出逻辑里把en和score清零都挂到状态条件上只有S_PLAY状态才允许判决模块积分其余状态强制score保持0。4.3 时钟分频与按键跨时钟域的同步顺序项目里有三档节奏消抖计数、判决时钟、LED闪烁。我习惯把所有分频都做成时钟使能而不是生成新的clk信号。用计数器产生一个clk_out往下传会在FPGA里增加时钟偏移而且组合毛刺可能被当成有效时钟沿。正确做法是全部模块共用同一个系统时钟模块内部用使能信号决定“本拍是否工作”。判决模块需要的1kHz节拍可以在tug_arbiter内部做周期计数器产生en_signal。按键信号进入判决模块的顺序是固定的先消抖再边沿检测然后进FSM和仲裁器。消抖模块天然承担了跨时钟同步的作用因为它的输出只在时钟沿更新等于打了多拍。这个单时钟设计里亚稳态风险不大但保持“所有异步输入先同步再使用”的习惯后面把判决器移植到多时钟系统时会少踩很多坑。5. 仿真验证与故障排除testbench、license报错与波形检查5.1 testbench骨架用task注入不同频率的按键动作testbench的核心任务是模拟出“甲连续快按、乙偶尔慢按”的比赛场景并观察移动方向和频率。用task把“按下-保持-释放”封装成一个动作每次调用产生一次下降沿和一次上升沿对应一次有效拉动。注意按键保持的时长与有效次数无关——拔河比的是连按频率不是按住时长这个区别正好能在仿真里验证清楚。timescale 1ns/1ps module tb_tug; reg clk 0; reg rst_n 0; reg key_a 1, key_b 1; // 默认未按下高电平 wire move, dir; // 例化顶层仿真时把消抖窗口缩短到50个时钟周期 tug_top #( .DEBOUNCE_MAX(50), .THRESHOLD(4) ) dut ( .clk(clk), .rst_n(rst_n), .key_a(key_a), .key_b(key_b), .move(move), .dir(dir) ); always #5 clk ~clk; // 100MHz 时钟 task press_a; // task 封装一次完整按键动作 begin key_a 0; #30_000; // 按下30us远大于消抖确认时间 key_a 1; #20_000; end endtask task press_b; begin key_b 0; #200_000; // 乙按得更久但只产生一次脉冲 key_b 1; #50_000; end endtask initial begin #1000 rst_n 1; repeat (4) press_a; // 甲快按4次首次触发移动 repeat (2) press_b; repeat (4) press_a; // 再次触发移动 #1_000_000; $display(move%b dir%b, move, dir); $finish; end endmodule初始化里rst_n先拉低1000ns再释放复位。甲快按4次时score累计到阈值move应输出一拍脉冲且dir0乙只按2次score回退甲再按4次再触发移动。task里乙的按下时间是200us但按键释放和按下各只有一次边沿所以不会产生两次脉冲这是验证“按压时长不直接产生优势”的关键场景。仿真通过的条件是move恰好出现两次、方向一致。5.2 仿真检查点与波形对照表只看最终胜负很容易漏掉中间故障。仿真时要按信号链逐级检查先从按键物理信号开始确认极性再看消抖输出是否延迟一段后跟随输入再看边沿脉冲是否只有单拍最后看score的加减节奏和move的出现时机。信号正确现象常见错误key_a/key_b按下为低释放为高极性写反消抖后输出一直为0key_out延迟2.5ms后跟随key_in抖动毛刺没有被滤掉pulse_fall每次按键恰好1拍出现多个脉冲或一直为1score甲按加一乙按减一溢出变成负数移动反向move达到阈值时1拍脉冲move持续多拍stateS_IDLE→S_PLAY→S_DONE卡在S_IDLE不动波形里重点看score的联动score累到3次后第4次按键那一拍move同时拉高score下一拍变0。如果move出现了但led_pos没动问题在FSM的使能条件而不是判决模块这种按信号链定位的方法比对着整个系统猜要快得多。5.3 Quartus仿真license报错与iverilog快速回归用Quartus自带仿真器时很多人会碰到一句“17.1 error: failure to obtain a verilog simulation license.”。这是Quartus安装包里仿真组件授权不完整导致的并不是代码错误。常见做法是先检查license环境变量和安装路径如果暂时解决不了逻辑仿真可以直接换用开源工具链iverilog完全不受厂商license限制。尤其在这个项目里所有模块都是纯Verilog没有任何厂商专用原语iverilog编译非常顺畅。iverilog -o tb_tug.vvp tb_tug.v tug_top.v debounce.v edge_detect.v tug_arbiter.v game_fsm.v vvp tb_tug.vvp gtkwave tb_tug.vcd第一行把所有设计文件和testbench一起编译成vvp可执行文件第二行执行仿真并生成vcd波形文件第三行用GTKWave打开波形。整个流程不依赖厂商IDE在vscode里配好Verilog插件就能写代码、跑仿真、看波形适合做每日回归。注意如果一运行就$finish且没有任何波形第一件事检查复位逻辑没有有效复位的话状态机寄存器全是x后续逻辑全部无法推进。提示仿真缩减消抖参数时一定要通过顶层参数向下传递不能用全局宏硬改设计文件否则板级综合时容易忘记还原窗口值导致真实按键抖动滤不掉。6. 参数化与复用把拔河仲裁逻辑搬出游戏6.1 板级调试按键极性、示波器观测与分频策略板级跑通后第一件事不是开比赛而是把消抖后的key_out引到一个空置LED上做链路验证。按下按键灯亮、释放灭快速短按灯不闪烁这一步确认了按键极性、上拉和消抖窗口都正确。如果短按时LED乱闪把MAX_CNT翻倍再试。这个验证把物理链路和逻辑设计分开避免拿整机行为猜按键问题。随后把move和dir临时引到两个LED上手动模拟快按节奏看移动脉冲是否按预期出现。确认板上按键按下为低还是高消抖后信号先单独验证再用LED验证move和dir最后接FSM和LED移位显示6.2 参数化手感调节与拨码开关接口手感调节集中在两个参数THRESHOLD决定需要多少净按键才移动一格超时长度决定僵局等待。把这两个参数从parameter改成端口输入用板上的拨码开关直接控制比赛现场就能边试边调不用重新综合。阈值越大比赛越公平但节奏慢适合双方实力接近的场合阈值小则比赛刺激适合演示。调试时把score做成调试总线引到逻辑分析仪观察优势积分的实时符号和幅度比对着LED瞎猜快得多。6.3 把拔河判决器改造成通用双通道仲裁器把tug_arbiter的端口抽象一层hit_a和hit_b换成两路请求信号move对应grant脉冲dir对应授权方向一个拔河判决器就变成了公平的仲裁器。它不会让高频请求方占死通道因为每次grant后score清零优势被消耗必须再次累积才能获得下一次授权。常见做法是让子域两路DMA请求共用一组阈值积分器保证低频率请求方也能在竞争窗口内获准。用systemverilog断言来验证led_pos的独热性也很直接在仿真中加一句property保证任何时刻led_pos只有一位为1移位越界和全0状态都能被自动捕获。本文还有配套的精品资源点击获取
返回列表