项目复盘:基于Verilog与Quartus II的四路抢答器模块化设计与仿真调试

1. 项目背景与需求分析

四路抢答器是数字电路课程的经典实践项目,也是FPGA入门者从理论走向实战的绝佳练手案例。去年我在指导本科生做课程设计时,发现很多同学虽然能写出基本功能代码,但在模块化设计和仿真调试环节频频踩坑。这次就以Quartus II 15.0和ModelSim 10.4b环境为例,分享一个工业级标准的实现方案。

这个项目的核心需求其实很生活化——想象你正在主持一场知识竞赛:

  • 主持人按下Start键后,60秒倒计时开始
  • 四位选手谁先按下抢答键,数码管立即锁定显示其编号(01-04)
  • 若倒计时结束无人抢答,系统自动复位
  • 所有状态都要通过七段数码管清晰展示

实际开发中我发现三个关键痛点:机械按键抖动导致的误触发、多模块协同时的时序冲突、仿真时的"error load design"报错。接下来就逐个击破这些难点。

2. 模块化设计实战

2.1 系统架构设计

采用自顶向下的设计思想,将系统拆解为九个功能模块。就像搭积木一样,每个模块独立开发测试,最后通过顶层模块组装。下图是经过三次迭代优化的系统框图:

[信号输入层] └─ 按键消抖模块(key_debounce) [逻辑控制层] ├─ 控制模块(responder_control) ├─ 倒计时模块(responder_count) └─ 优先编码器(responder_encode) [显示输出层] ├─ 译码模块(responder_decode) └─ 动态扫描模块(responder_scan) [支撑层] ├─ 分频模块(responder_clk) └─ 顶层连线(responder)

这种分层结构的好处是:当数码管显示异常时,可以快速定位是译码问题还是扫描时序问题。我在第一次实现时把所有功能写在一个模块里,调试时简直痛不欲生。

2.2 关键模块代码解析

按键消抖模块的玄机藏在时钟周期里。机械按键按下会产生5-10ms的抖动,我们的策略是连续检测3个时钟周期的稳定信号:

module key_debounce( input clk, input [4:1] key, output [4:1] key_debounce ); reg [4:1] key_r, key_rr, key_rrr; always @(posedge clk) begin key_rrr <= key_rr; // 三级寄存器级联 key_rr <= key_r; key_r <= key; end assign key_debounce = key_rrr & key_rr & key_r; // 三拍稳定信号相与 endmodule

控制模块采用有限状态机(FSM)实现,这是整个系统的大脑。定义三个状态:

  • WAIT:等待开始信号
  • COUNT:倒计时进行中
  • LOCK:抢答结果锁定
parameter WAIT = 2'b00, COUNT = 2'b01, LOCK = 2'b10; always @(posedge clk or negedge rst_n) if (!rst_n) CS <= WAIT; else CS <= NS; always @(*) begin case(CS) WAIT: NS = start ? COUNT : WAIT; COUNT: begin if (touch) NS = LOCK; else if (zero_flag) NS = COUNT; else NS = WAIT; end LOCK: NS = LOCK; endcase end

倒计时模块的难点在于BCD码转换。我最初直接用二进制计数,结果数码管显示乱码。改进方案是用两个4位寄存器分别存储十位和个位:

`define TEN 4'b0110 // 十进制6 `define ONE 4'b1001 // 十进制9 always @(posedge clk or negedge rst_n) begin if (!rst_n) ten <= `TEN; else if (en_count) begin if (one==0 && ten>0) ten <= ten - 1; // 个位归零时十位减1 end else ten <= `TEN; // 复位 end

3. Quartus II 开发全流程

3.1 工程创建与配置

新建工程时有个容易踩的坑:器件选择必须与开发板匹配。比如DE2-115开发板对应Cyclone IV EP4CE115F29C7。我曾因选错器件导致引脚分配后无法编程。

推荐设置:

  • 添加所有Verilog文件时勾选"Add to Project"
  • 在Analysis & Synthesis Settings中设置"Verilog-2001"标准
  • 启用"Smart Compilation"加速后续编译

3.2 功能仿真技巧

Testbench的编写要覆盖边界条件,这是我的测试场景:

  1. 正常抢答流程
  2. 两人同时抢答(测试优先级)
  3. 倒计时结束无人抢答
  4. 主持人中途复位
initial begin // 基础时钟生成 forever #10 clk_sig = ~clk_sig; // 测试场景1:B选手成功抢答 #100 start_sig = 1; #200 start_sig = 0; #300 b_sig = 1; #350 b_sig = 0; // 测试场景2:A和D同时抢答 #500 a_sig = 1; d_sig = 1; #550 a_sig = 0; d_sig = 0; end

在ModelSim中运行仿真时,建议将关键信号分组显示:

  • 控制信号组:start, rst_n, lock_flag
  • 选手输入组:a, b, c, d
  • 显示输出组:an, data

4. 调试经验与性能优化

4.1 常见错误解决方案

Error: Error loading design这个问题折磨了我整整两天,最终发现三个常见诱因:

  1. 顶层模块端口声明与实例化不匹配
    • 检查信号位宽是否一致
    • 确认所有子模块已添加到工程
  2. Testbench时钟信号未初始化
    • 添加clk_sig = 0;初始赋值
  3. 仿真库未正确映射
    • 在Quartus中执行"Tools -> Launch Simulation Library Compiler"

数码管显示残影这是动态扫描时序问题,解决方案:

  • 将扫描时钟频率提高到1kHz以上
  • 确保位选信号an的变化与段选信号data同步
  • 添加消隐代码防止切换时的鬼影:
always @(posedge clk_scan) begin an <= 8'b1111_1111; // 先关闭所有数码管 #1; // 短暂延时 case(scan_state) // 更新位选和段选 S1: an <= 8'b1111_1110; data <= seg_one; ... endcase end

4.2 资源优化策略

通过Quartus的Compilation Report发现:

  • 原始设计消耗了128个LE(逻辑单元)
  • 优化后降至89个LE

关键优化手段:

  1. 共用分频器:将原本四个独立分频器合并,用使能信号控制
  2. 状态机编码:使用Gray码减少状态切换时的毛刺
  3. 常量替换:用`define宏替代直接数值,方便综合器优化

5. 项目总结与扩展思考

这个项目最让我惊喜的是模块化设计带来的可扩展性。最近在原有基础上增加了这些功能:

  • 通过PWM调节数码管亮度
  • 添加蜂鸣器提示音效
  • 扩展为八路抢答系统

对于想深入学习的同学,建议尝试:

  1. 改用VHDL实现相同功能
  2. 添加分数统计功能
  3. 移植到其他FPGA开发板

最后提醒几个工程实践要点:

  • 每完成一个模块就立即仿真验证
  • 使用Git进行版本控制(我吃过没备份的亏)
  • 编写详细的注释——三个月后再看自己的代码可能会怀疑人生