简介基于DE2-115开发板的Verilog七人抢答器项目面向FPGA初学者与数字逻辑课程设计人群演示多路按键输入消抖、时钟分频与抢答优先级判定的综合实现。资源包共102个文件约3.1MB核心为seven.v、CLK_KEY.v、KEY_DOWN.v三个Verilog源码以及Quartus 17的.qpf/.qsf工程配置其余为编译生成的.qdb、.rpt、.summary等报告与数据库文件便于对照分析综合与布局布线结果。已有854人学习下载。通过该项目可掌握DE2-115开发板按键、LED等外设的使用方式理解抢答器中关键的时钟同步和按键消抖逻辑并学会用Quartus完成从编写、综合到下载验证的完整流程。代码结构清晰适合作为课程设计或竞赛练习的基础模板也可据此扩展为更多路数的抢答装置。1. 抢答器为什么用FPGA而不是单片机一个并行锁存需求引发的课程设计第一次在DE2-115开发板上写七人抢答器是在某高校EDA课程设计的验收前一周。A同学最开始想用单片机做理由是“抢答器不就是读七个按键再显示编号吗轮询几行代码就搞定了”。结果联调时翻车了七个选手几乎同时按下按键单片机主频有限等程序扫到后按键的GPIO时第一个按下的人已经被后面的人“顶”掉了。FPGA方案的核心优势正好卡在这个需求上七路按键信号是物理并行的用Verilog写一个状态机加锁存器能在纳秒级把第一个有效按键钉死在寄存器里后续按键一概不认。这篇笔记把整个工程的模块划分、状态机设计、按键消抖、数码管扫描、管脚约束和调试坑完整拆开讲适合刚学完基础语法但没完整跑过时序系统的FPGA初学者也适合课程设计答辩前想快速复现一套可演示代码的人。2. 模块化设计把抢答流程拆成四个可单独测试的Verilog模块抢答器功能上看是个小系统但直接写一个巨型always块把所有逻辑塞进去综合也能过。问题在于出bug之后面对的是几百行的黑匣子只能靠仿真波形一遍遍猜。我习惯把工程切成四个模块按键消抖、赛制状态机、数码管显示、蜂鸣器驱动。每个模块都能单独仿真和上板验证最后顶层只负责连线。这样分工明确调试时也能定位到具体模块。2.1 顶层互联一张表看懂谁管什么先把赛制捋清楚主持人按下“开始”键后七位选手才能抢答第一位有效按键被锁存数码管显示其编号同时蜂鸣器响一声提示此后任何选手按键都无效主持人按下“复位”键进入下一轮。这个流程决定了顶层模块的接口。模块名输入输出职责key_debounce原始按键、时钟消抖后按键电平消除机械抖动与亚稳态rush_fsm消抖后按键、主持人控制胜者编号、锁定标志、蜂鸣器脉冲赛制核心状态机seg_display胜者编号、锁定标志段码、位选数码管动态扫描显示buzzer_drv蜂鸣器脉冲、时钟蜂鸣器管脚驱动产生约1kHz提示音顶层模块将上述模块实例化并连接。工程命名为rush_answer_top顶层源码如下// 七人抢答器顶层负责例化四个子模块并把信号连起来 module rush_answer_top #( parameter CLK_FREQ 50_000_000 )( input wire clk_50m, // DE2-115 板载 50MHz 时钟 input wire rst_n, // 复位键低有效 input wire host_start, // 主持人“开始”键高有效 input wire host_reset, // 主持人“复位”键高有效 input wire [6:0] player_key, // 七个选手按键按下为高 output wire [6:0] seg_data, // 数码管段码 a~g output wire [7:0] seg_sel, // 数码管位选低有效 output wire buzzer // 蜂鸣器驱动 ); // 消抖后的内部信号 wire [6:0] key_clean; wire host_start_clean; wire host_reset_clean; // 状态机输出 wire [2:0] winner_id; // 胜者编号1~7 wire lock_flag; // 1 表示本轮已锁定 wire buzz_pulse; // 锁定瞬间产生一个脉冲 // 七路选手按键各自做消抖 genvar i; generate for (i 0; i 7; i i 1) begin : player_db_gen key_debounce #( .WAIT_CYCLES(CLK_FREQ / 1000 * 20) // 20ms 消抖窗口 ) u_player_db ( .clk (clk_50m), .rst_n (rst_n), .key_in (player_key[i]), .key_out(key_clean[i]) ); end endgenerate // 主持人两个按键同样需要消抖 key_debounce #( .WAIT_CYCLES(CLK_FREQ / 1000 * 20) ) u_host_start_db ( .clk (clk_50m), .rst_n (rst_n), .key_in (host_start), .key_out(host_start_clean) ); key_debounce #( .WAIT_CYCLES(CLK_FREQ / 1000 * 20) ) u_host_reset_db ( .clk (clk_50m), .rst_n (rst_n), .key_in (host_reset), .key_out(host_reset_clean) ); // 赛制状态机 rush_fsm u_fsm ( .clk (clk_50m), .rst_n (rst_n), .host_start(host_start_clean), .host_reset(host_reset_clean), .player_key(key_clean), .winner_id (winner_id), .lock_flag (lock_flag), .buzz_pulse(buzz_pulse) ); // 数码管动态扫描 seg_display u_display ( .clk (clk_50m), .rst_n (rst_n), .winner_id(winner_id), .lock_flag(lock_flag), .seg_data (seg_data), .seg_sel (seg_sel) ); // 蜂鸣器驱动 buzzer_drv u_buzzer ( .clk (clk_50m), .rst_n (rst_n), .buzz_pulse(buzz_pulse), .buzzer (buzzer) ); endmodule逻辑说明顶层把所有按键先送进消抖模块得到统一的干净信号后再进状态机。这样做的目的是让状态机看到的输入是“稳定后的真实按键”而不是带毛刺的物理电平。player_key位宽是7位对应第1到第7号选手winner_id只有3位因为最多显示数字7二进制位数够用即可。seg_sel设计成低有效这与DE2-115上多数数码管位选管脚的电平极性一致。参数说明CLK_FREQ定义了板载时钟频率用于计算消抖等待周期WAIT_CYCLES。CLK_FREQ / 1000 * 20在50MHz下等于1,000,000即20ms内50MHz时钟跑了100万个周期。这个时间窗口对机械按键足够因为常见按键抖动一般不超过10ms留一倍余量更稳妥。generate-for循环是Verilog-2001语法Quartus Prime完全支持比手写七个实例化更简洁改位数时只需调整player_key位宽和循环边界。2.2 赛制状态机空闲、允许、锁定三个状态如何切换状态机是整个抢答器的灵魂。我把它设计成三段式状态机三个状态分别为IDLE等待主持人喊开始、READY允许抢答、LOCKED已锁定胜者。状态转移逻辑和输出逻辑分开写后续要加超时、犯规等状态时不需要动顶层。// 赛制状态机负责抢答判定、锁存与主持人复位 module rush_fsm ( input wire clk, input wire rst_n, input wire host_start, // 主持人开始脉冲有效 input wire host_reset, // 主持人复位脉冲有效 input wire [6:0] player_key, // 消抖后的选手按键高有效 output reg [2:0] winner_id, // 胜者编号1~7 output reg lock_flag, // 锁定标志 output reg buzz_pulse // 蜂鸣器触发脉冲持续约1个时钟周期 ); // 状态编码用独热码方便综合器优化 localparam IDLE 3b001; localparam READY 3b010; localparam LOCKED 3b100; reg [2:0] state, next_state; reg [2:0] winner_r; // 第一段状态寄存 always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else state next_state; end // 第二段组合逻辑决定下一状态 always (*) begin next_state state; case (state) IDLE: begin if (host_start) next_state READY; // 主持人没按开始停留在空闲 end READY: begin // 有人抢答立即锁存 if (player_key ! 7b0000000) next_state LOCKED; // 主持人也能取消本轮 else if (host_reset) next_state IDLE; end LOCKED: begin // 主持人复位进入下一轮 if (host_reset) next_state IDLE; end default: next_state IDLE; endcase end // 第三段输出寄存同时提取胜者编号 integer j; always (posedge clk or negedge rst_n) begin if (!rst_n) begin winner_id 3d0; lock_flag 1b0; buzz_pulse 1b0; end else begin // 默认保持 buzz_pulse 1b0; lock_flag (state LOCKED); if (state READY (player_key ! 7b0000000)) begin // 用 for 循环从低位到高位扫描找到第一个 1 的编号 winner_r 3d0; for (j 0; j 7; j j 1) begin if (player_key[j]) winner_r j 1; // 越靠后的循环赋值会覆盖前面的 end winner_id winner_r; lock_flag 1b1; buzz_pulse 1b1; // 锁定瞬间给蜂鸣器一个脉冲 end end end endmodule逻辑说明状态机三段式写法将状态寄存、次态组合逻辑、输出寄存严格分开。IDLE状态下只有主持人按下host_start才进入READY否则就算选手疯狂按键也没什么反应这一步就完成了“禁止抢跑”的赛制要求。READY状态检测到player_key任意一位为1立即跳到LOCKED同时记录胜者编号。这里用for循环从低位到高位扫描winner_r最终会被最高位覆盖也就是说如果两个人几乎同时按下最后编号大的胜出这实际是一种“高优先级覆盖低优先级”的编码方式具体规则可自行调整。参数说明winner_r是阻塞赋值在同一个always块里先清零再循环最终赋值的结果就是最高位有效键的编号加1。buzz_pulse设计为单个时钟周期的高电平脉冲蜂鸣器驱动模块收到这个脉冲后会自动播放一段固定时长的提示音避免了在状态机里做耗时计数。2.3 按键消抖20ms连续稳定计数器的两种写法按键消抖看起来是基本功但抢答器里它直接决定赛制公平性。20ms消抖窗口意味着按键必须保持稳定高电平20ms以上才会被认为有效这能有效过滤掉按下瞬间的机械抖动。代码里还要做两级同步防止外部异步信号进入FPGA内部时产生亚稳态。// 按键消抖模块连续高电平超过 WAIT_CYCLES 个时钟周期才输出有效 module key_debounce #( parameter WAIT_CYCLES 1_000_000 // 默认 20ms 50MHz )( input wire clk, input wire rst_n, input wire key_in, output reg key_out ); reg [19:0] counter; reg [1:0] sync_ff; // 第一级打两拍同步消除亚稳态 always (posedge clk or negedge rst_n) begin if (!rst_n) begin sync_ff 2b00; end else begin sync_ff[0] key_in; sync_ff[1] sync_ff[0]; end end // 第二级连续高电平计数输出对应电平 always (posedge clk or negedge rst_n) begin if (!rst_n) begin counter 20d0; key_out 1b0; end else if (sync_ff[1]) begin if (counter WAIT_CYCLES) begin counter counter 1b1; key_out 1b0; // 还未稳定输出保持低 end else begin key_out 1b1; // 持续高电平超过门限判定有效 end end else begin // 检测到低电平立刻清零计数器 counter 20d0; key_out 1b0; end end endmodule逻辑说明sync_ff打两拍的做法是异步信号处理的常规手段——第一拍寄存原始输入第二拍寄存第一拍结果这样即使外部信号发生亚稳态也能在两个周期内稳定下来。计数逻辑只在sync_ff[1]为高时累加一旦检测到低电平立即清零所以只有“连续”高电平达到门限才会输出高。这个消抖策略对高有效按键有效如果板子上按键是低有效需要把判断条件取反。参数说明WAIT_CYCLES的位宽要跟计数器匹配。50MHz下20ms对应1,000,000需要20位宽如果把门限改成5ms就是250,00019位也能放下但我习惯统一保留20位避免改参数时忘记改位宽导致综合时报截断警告。这个消抖逻辑的边界在于如果选手快速连击每次按下时间小于20ms模块不会输出有效高电平。这是合理设计因为赛制要求选手“按稳”而不是“碰一下就算”。另一个细节是复位时key_out强制为0避免上电瞬间误判抢答。3. 工程落地DE2-115管脚约束、数码管扫描与综合烧录写Verilog只是走了一半路另一半在Quartus工程里。DE2-115开发板上有丰富的外设但每个外设都对应固定的管脚号不写约束文件综合器根本不知道clk_50m这个信号要接到板子上哪个物理引脚。数码管也不是直接接7个段码就能用的必须做动态扫描否则8位数码管要占几十个管脚。这一章讲工程落地时的三个关键点。3.1 数码管动态扫描为什么看起来同时在亮DE2-115上有8个共阳极数码管如果每个数码管独立接段码需要7×856个管脚这在教学板上根本不可能。实际板子的做法是所有数码管的段码线并联公共端分别引出依靠“位选”信号轮流点亮每一个数码管。只要轮流速度足够快人眼会因为视觉暂留效应看到所有数码管“同时”亮起。// 数码管扫描模块轮流点亮两位数码管用来显示胜者编号 module seg_display ( input wire clk, input wire rst_n, input wire [2:0] winner_id, // 胜者编号 1~7 input wire lock_flag, // 锁定标志 output reg [6:0] seg_data, // 段码低有效 output reg [7:0] seg_sel // 位选低有效 ); reg [15:0] scan_cnt; reg scan_pos; // 0 显示个位1 显示十位 reg [6:0] seg_tmp; // 段码表共阳极数码管低电平点亮 always (*) begin case (winner_id) 3d1: seg_tmp 7b1111001; // 1 3d2: seg_tmp 7b0100100; // 2 3d3: seg_tmp 7b0110000; // 3 3d4: seg_tmp 7b0011001; // 4 3d5: seg_tmp 7b0010010; // 5 3d6: seg_tmp 7b0000010; // 6 3d7: seg_tmp 7b1111000; // 7 default: seg_tmp 7b1111111; // 全灭 endcase end // 扫描时钟对50MHz分频得到约500Hz的位选切换 always (posedge clk or negedge rst_n) begin if (!rst_n) begin scan_cnt 16d0; scan_pos 1b0; end else begin if (scan_cnt 16d49_999) scan_cnt scan_cnt 1b1; else begin scan_cnt 16d0; scan_pos ~scan_pos; // 每1ms切换一次位选 end end end // 输出段码和位选先关位选再切段码避免拖影 always (*) begin seg_sel 8b1111_1111; // 默认全部熄灭 seg_data 7b111_1111; if (scan_pos 1b0) begin // 第一位显示胜者编号 seg_sel[0] 1b0; // 点亮最右侧数码管 if (lock_flag) seg_data seg_tmp; end else begin // 第二位锁定后显示“—”未锁定时熄灭 seg_sel[1] 1b0; if (lock_flag) seg_data 7b0000110; // “—”的段码 end end endmodule逻辑说明scan_cnt计数到50,000后翻转一次scan_pos对应1ms翻转一次。两个数码管轮流点亮每个人看到的是一号管亮1ms、二号管亮1ms刷新率500Hz人眼完全不会感到闪烁。seg_tmp用一个组合逻辑case把3位的胜者编号转换成7段码低电平点亮。位选信号seg_sel[0]为0时最右侧数码管公共端被拉低此时段码数据只对这个数码管生效另一位选为1时该数码管熄灭不会看到错误的显示。参数说明分频计数器宽度是16位最大计数6553550,000远小于这个值合理。如果板子上数码管数量多扫描频率可以提高到1kHz避免闪烁但要注意分频数越低位选切换越快留给段码建立的时间越短。实际测下来500Hz对8位数码管也够用显示稳定没有拖影。另一个容易被忽略的细节是段码表对应共阳极数码管换成共阴极板子需要把所有7位取反这是DE2-115和其他开发板不兼容的常见原因。3.2 QSF管脚约束写错一个PIN板子就“装死”Quartus工程里综合之前的最后一步是写QSF约束文件。管脚约束分三部分引脚位置、IO电平标准、时序约束。DE2-115大部分外设是3.3V LVTTL时钟管脚比较特殊需要单独声明。# 时钟DE2-115 板载 50MHz 有源晶振 set_location_assignment PIN_AF14 -to clk_50m set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to clk_50m # 复位键使用 KEY[0] set_location_assignment PIN_M23 -to rst_n set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to rst_n # 主持人开始键使用 KEY[1] set_location_assignment PIN_M21 -to host_start set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to host_start # 主持人复位键使用 KEY[2] set_location_assignment PIN_M19 -to host_reset set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to host_reset # 七个选手按键测试阶段建议用拨码开关代替 set_location_assignment PIN_AB27 -to player_key[0] set_location_assignment PIN_AC28 -to player_key[1] set_location_assignment PIN_AC27 -to player_key[2] set_location_assignment PIN_AD27 -to player_key[3] set_location_assignment PIN_AB26 -to player_key[4] set_location_assignment PIN_AD26 -to player_key[5] set_location_assignment PIN_AC25 -to player_key[6] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to player_key[0] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to player_key[1] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to player_key[2] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to player_key[3] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to player_key[4] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to player_key[5] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to player_key[6] # 数码管段码 seg_data[0]~[6] 对应 HEX0 的 a~g set_location_assignment PIN_AB17 -to seg_data[0] set_location_assignment PIN_AC18 -to seg_data[1] set_location_assignment PIN_AC19 -to seg_data[2] set_location_assignment PIN_AE20 -to seg_data[3] set_location_assignment PIN_AF20 -to seg_data[4] set_location_assignment PIN_AF19 -to seg_data[5] set_location_assignment PIN_AE18 -to seg_data[6] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to seg_data[0] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to seg_data[1] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to seg_data[2] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to seg_data[3] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to seg_data[4] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to seg_data[5] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to seg_data[6] # 数码管位选 seg_sel[0] 对应 HEX0 公共端seg_sel[1] 对应 HEX1 公共端 set_location_assignment PIN_AA17 -to seg_sel[0] set_location_assignment PIN_AA16 -to seg_sel[1] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to seg_sel[0] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to seg_sel[1]逻辑说明QSF里每组约束都包含位置和电平标准两行。位置写法是PIN_XX -to 信号名一个物理管脚对应一个信号不能重复占用。IO电平标准统一设成“3.3-V LVTTL”这是DE2-115上绝大多数GPIO的默认电平。seg_sel[7:2]没有用到的位在工程里保持悬空Quartus会按默认策略处理不影响功能。参数说明这里给出的管脚号是按DE2-115手册整理的命名不同批次板卡丝印可能有差异上板前务必对照手头手册核对一遍尤其时钟管脚写错会导致整个设计无法工作。测试阶段用拨码开关代替选手按键是个实用技巧拨码开关电平稳定不需要消抖能先验证状态机逻辑再接物理按键验证消抖模块分步排错会快很多。3.3 综合与烧录检查顺序RTL视图、Pin Planner与SignalTap工程配置完成后我习惯按固定顺序做检查而不是直接点编译按钮等结果。第一步检查RTL视图。Quartus综合完成后在“Tools - Netlist Viewers - RTL Viewer”里看模块连接图重点确认状态机和顶层连线是否和设计一致。这一步能发现例化时端口接错位的问题比上板测试花的时间少得多。第二步打开Pin Planner确认每个信号都分配到了正确管脚没有出现两个信号争抢同一个管脚的情况。第三步查看编译报告里的“Fitter Settings”确认没有未分配管脚的警告。第四步下载.sof文件到板子先跑最简单的“全灭测试”——把数码管全部点亮确认硬件通路没问题再做功能测试。SignalTap是调试最后的后悔药。把lock_flag和winner_id加进逻辑分析仪触发条件设为lock_flag上升沿就能看到抢答瞬间选手按键和状态机的真实时序关系。硬件调试中很多“灵异现象”最后都是靠SignalTap抓到信号真相而不是靠眼睛看数码管猜。4. 避坑实录七人抢答器调试中遇到的五个“玄学”问题这章写的是调这个工程时真实踩过的坑。每一条在当时都让我怀疑是板子坏了或者芯片体质差最后定位到原因后发现全是设计问题。4.1 复位后还没按开始一号键就先锁存了现象上电复位完成后没有人碰选手按键数码管直接显示“7”蜂鸣器响了一声。原因状态机的READY状态检测到player_key非零就锁存但复位瞬间player_key信号处于不确定状态。如果拨码开关或按键悬空FPGA输入引脚的电平会被外部噪声扰动几毫秒内出现一次高电平毛刺正好被状态机当成抢答信号。解决把复位逻辑做成“安全进入IDLE”复位信号释放后强制等待至少10ms再检测按键同时对player_key信号做同步打拍确保进入状态机的是两级寄存器同步后的信号。我在实际工程里是在顶层把按键从input直接打两拍再送进消抖模块这样即使外部有毛刺也要连续两拍保持高电平才可能通过同步链概率骤降。4.2 七个人同时按下出来的总是小号选手但按规则应该是大号胜出现象用拨码开关模拟七人同时抢答数码管稳定显示编号7而测试人员坚持说最先按下的是4号。原因状态机里的for循环从低位向高位扫描末尾的赋值会覆盖前面的赋值所以编号7天然具有最高优先级。如果规则要求“先按先答”而硬件无法判断绝对时间先后这种静态优先级编码就是不公平的。解决课程设计要求不高时明确规则为“同时按下时编号大者优先”并在答辩PPT里注明这一条状态机逻辑不需要改动。如果真要按时间区分需要给每个按键通道加一个时间戳寄存器用计数器记录按键边沿到达时刻然后比较时间戳大小那是另一个复杂度等级的设计。我当时选的是第一种方案先把规则讲清楚再展示功能。4.3 数码管闪烁切换到第二位数码管时出现拖影现象两次数码管显示时第二位偶尔会出现前一位的残影特别是胜者编号从暗到亮切换的瞬间。原因位选和段码切换发生在同一个时钟沿seg_sel关闭前一个数码管和seg_data切换段码是同时发生的导致前一个数码管在新段码生效前短暂显示了错误数据人眼看到的就是拖影。解决标准做法是先关位选再切段码中间留一个时钟周期的空档。我在seg_display模块里用的组合逻辑写法已经保证了先置位选为全1全灭再给seg_data赋值新段码。如果后续改成时序逻辑输出顺序要写成三段熄灭、切段码、点亮这个顺序不能反过来。4.4 主持人按复位键状态机像被“点穴”有时按好几下才生效现象抢答结束后按复位键数码管不回到全灭要用力多按几次才能恢复下一轮。原因host_reset信号直接连到状态机的host_reset输入没有做边沿检测。主持人按住不放时状态机虽然已经回到IDLE但只要松手瞬间出现电平抖动状态机又会被抖进别的状态消抖模块输出的是持续电平不是“一次有效脉冲”导致一次按键被解释成多次操作。解决在状态机输入端把host_reset改成“上升沿脉冲提取”检测到消抖后信号从0变1时输出一个单周期脉冲状态机只认这个脉冲。实现方式是保存上一拍的消抖信号当前拍为1且上一拍为0时产生host_reset_pulse。这个改动让主持人按键行为变成“按一次只生效一次”无论按多久都不会重复触发。4.5 烧录完板子没反应综合报告却一点报错都没有现象.sof文件下载成功但数码管全灭蜂鸣器不响按键无任何反应像是板子没通电。原因QSF约束没写全。顶层模块里的seg_data[7]、seg_sel[7:2]等未使用信号没有对应管脚Quartus默认将未分配管脚设置为“作为输入且三态”但如果顶层声明中某个输出信号在QSF里找不到对应位置编译会直接把这个信号优化掉板子上对应的物理管脚自然没有驱动。解决编译完成后打开编译报告搜索“warning about pins”看是否有信号没有分配。另一个常见原因是时钟管脚方向写反把input写成output综合器会报错误但不中断现象同样是板子没反应。后来我每次工程建完第一件事就是把QSF文件里所有管脚和顶层端口名逐一对应建一个Excel表格打勾确认再也不靠“应该没问题”来赌运气。5. 扩展技巧把抢答器升级成带倒计时和计分的答题系统抢答器验证通过后我顺手把它扩展成了课堂答题计时系统主持人按开始后数码管从30秒开始倒计时选手抢答时倒计时暂停并显示剩余秒数答对加分答错扣分分数同样显示在额外两位数码管上。这个扩展只需要在现有状态机上增加两个状态而不需要改顶层架构。新增状态设计为COUNTDOWN和SCORE_UPDATE。从READY进入倒计时倒计时结束无人抢答则超时判负有人抢答则锁存并冻结剩余时间。计时模块用10Hz脉冲做递减因为50MHz计数到5,000,000正好是1秒再分频得到0.1秒精度数码管刷新仍然走原来的扫描逻辑。// 倒计时模块产生1Hz脉冲并维护剩余秒数 module countdown_timer #( parameter CLK_FREQ 50_000_000, parameter INIT_SEC 30 )( input wire clk, input wire rst_n, input wire start, // 开始倒计时脉冲 input wire freeze, // 抢答锁存后冻结 output reg [5:0] remain_sec, // 剩余秒数 output reg timeout // 超时标志 ); reg [25:0] clk_cnt; reg sec_tick; // 分频产生1秒脉冲 always (posedge clk or negedge rst_n) begin if (!rst_n) begin clk_cnt 26d0; sec_tick 1b0; end else if (clk_cnt CLK_FREQ - 1) begin clk_cnt clk_cnt 1b1; sec_tick 1b0; end else begin clk_cnt 26d0; sec_tick 1b1; // 每秒产生一个单周期脉冲 end end // 倒计时递减 always (posedge clk or negedge rst_n) begin if (!rst_n) begin remain_sec INIT_SEC; timeout 1b0; end else if (start) begin remain_sec INIT_SEC; timeout 1b0; end else if (sec_tick !freeze) begin if (remain_sec 1) remain_sec remain_sec - 1b1; else begin remain_sec 6d0; timeout 1b1; // 超时 end end end endmodule逻辑说明CLK_FREQ - 1是分频器的模值计数到50,000,000 - 1刚好耗时1秒。sec_tick只在最后一个周期拉高一拍作为递减时钟。freeze信号接自抢答状态机的lock_flag锁存后sec_tick继续产生但剩余秒数不再递减实现了“抢答成功后时间冻结”。INIT_SEC是参数化初始值改成10、15都可以现场配置。参数说明remain_sec是6位最大支持63秒倒计时如果题目需要60秒以上要扩到7位。CLK_FREQ用了50MHz整数所以分频计数没有精度误差如果时钟是33.333MHz之类非整数频率推荐先做一个全局1Hz基准模块再在这里用标志位避免重复分频导致累积误差。这套扩展没有改任何已有模块的接口只是新增了一个countdown_timer实例并把timeout信号接回状态机作为超时跳转条件。从那以后我每次写时序控制都会先把状态转移画成表格写明每个状态在什么输入下跳到哪里、输出什么再动手写Verilog。状态机不是玄学把规则列清楚代码只是翻译。希望帮到你。本文还有配套的精品资源点击获取