FPGA实战:从零构建Verilog抢答器系统(Quartus Prime 18.1全流程指南)

当我在大学第一次接触FPGA时,完全被这种可编程逻辑器件的灵活性震撼了。不同于传统单片机固定架构,FPGA让你能够真正"用硬件思维"设计电路。今天要分享的抢答器项目,正是绝佳的FPGA入门实践——它不仅涵盖状态机设计、时钟分频等核心概念,还能让你体验从仿真到硬件部署的完整开发流程。

1. 开发环境配置与工程创建

工欲善其事,必先利其器。我们选择Intel Quartus Prime 18.1作为开发环境,这是目前高校和企业最主流的FPGA工具链之一。安装时注意勾选以下组件:

  • Quartus Prime (包含ModelSim Starter Edition)
  • Device support for Cyclone IV E系列(适配常见的EP4CE6开发板)
  • Nios II工具链(可选,用于后续SOPC开发)

安装完成后,新建工程时需特别注意器件选择。以EP4CE6E22C8为例,关键参数如下:

参数项设置值
器件系列Cyclone IV E
封装EQFP144
速度等级8
核心电压1.0V

提示:开发板型号不匹配会导致后续引脚分配错误。若使用其他型号,请查阅对应手册修改器件参数。

新建Verilog文件时,推荐采用模块化设计结构。我们的抢答器系统将包含以下核心模块:

module responder(
    input clk,         // 50MHz系统时钟
    input rst_n,       // 低电平复位
    input [3:0] key_in,// 按键输入[主持人,选手C,选手B,选手A]
    output [7:0] dig_sel, // 数码管位选
    output [7:0] dig_seg, // 数码管段选
    output [7:0] led_o    // LED状态指示
);
// 子模块实例化将在此添加
endmodule

2. 抢答器核心逻辑设计

抢答器的本质是一个带有优先级的状态机。我们需要处理几个关键逻辑:

  1. 主持人控制权:只有主持人按下开始键后,选手按键才有效
  2. 互斥锁定:任一选手抢答成功后,其他选手按键立即失效
  3. 倒计时机制:10秒内无抢答则自动锁定系统
  4. 状态显示:通过数码管实时显示倒计时和抢答结果

2.1 中央控制模块设计

中央控制器是整个系统的大脑,采用三段式状态机实现:

// 状态编码(独热码)
parameter IDLE   = 3'b001;
parameter COUNT  = 3'b010;
parameter LOCK   = 3'b100;

always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        current_state <= IDLE;
    end else begin
        current_state <= next_state;
    end
end

always @(*) begin
    case(current_state)
        IDLE: next_state = (key_in[0]) ? COUNT : IDLE;
        COUNT: begin
            if(key_in[3:1] != 3'b000 || time_up)
                next_state = LOCK;
            else
                next_state = COUNT;
        end
        LOCK: next_state = (key_in[0]) ? COUNT : LOCK;
        default: next_state = IDLE;
    endcase
end

倒计时模块采用50MHz时钟分频实现精确计时:

// 1秒计数器(50MHz时钟)
reg [25:0] cnt_1s;
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) 
        cnt_1s <= 26'd0;
    else if(current_state == COUNT) 
        cnt_1s <= (cnt_1s >= 26'd49_999_999) ? 26'd0 : cnt_1s + 1'b1;
    else
        cnt_1s <= 26'd0;
end

// 倒计时值更新
reg [3:0] countdown;
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        countdown <= 4'd10;
    else if(current_state == IDLE)
        countdown <= 4'd10;
    else if(cnt_1s == 26'd49_999_999 && countdown != 4'd0)
        countdown <= countdown - 1'b1;
end

2.2 数码管动态显示

数码管显示需要解决两个问题:数字解码和动态扫描。我们采用时间分割复用技术实现四位数码管显示:

// 数码管译码表
localparam 
    NUM_0 = 8'hC0, NUM_1 = 8'hF9, NUM_2 = 8'hA4,
    NUM_3 = 8'hB0, NUM_A = 8'h88, NUM_B = 8'h83;

// 20us刷新计数器(50MHz时钟下计数1000次)
reg [9:0] refresh_cnt;
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        refresh_cnt <= 10'd0;
    else
        refresh_cnt <= (refresh_cnt >= 10'd999) ? 10'd0 : refresh_cnt + 1'b1;
end

// 位选信号轮询
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        dig_sel <= 8'b1111_1110; // 选中第一位
    else if(refresh_cnt == 10'd999)
        dig_sel <= {dig_sel[6:0], dig_sel[7]}; // 循环左移
end

// 根据当前位选输出对应段码
always @(*) begin
    case(dig_sel)
        8'b1111_1110: dig_seg = decode(countdown); // 第一位显示倒计时
        8'b1111_1101: dig_seg = (key_in[1]) ? NUM_A : 8'hFF; // 第二位选手A
        8'b1111_1011: dig_seg = (key_in[2]) ? NUM_B : 8'hFF; // 第三位选手B
        default: dig_seg = 8'hFF; // 其余位熄灭
    endcase
end

3. 按键消抖与系统仿真

机械按键的抖动问题不容忽视。实测显示,普通按键的抖动时间通常在5-20ms之间。我们采用状态机实现硬件消抖:

module key_debounce(
    input clk,
    input key_in,
    output reg key_out
);

parameter DEBOUNCE_TIME = 16'd50_000; // 50MHz时钟下1ms

reg [15:0] counter;
reg [1:0] state;

always @(posedge clk) begin
    case(state)
        2'b00: begin // 等待按键按下
            if(!key_in) begin
                state <= 2'b01;
                counter <= 16'd0;
            end
        end
        2'b01: begin // 消抖计时
            if(counter >= DEBOUNCE_TIME) begin
                key_out <= 1'b0;
                state <= 2'b10;
            end else begin
                counter <= counter + 1'b1;
                if(key_in) state <= 2'b00;
            end
        end
        2'b10: begin // 等待释放
            if(key_in) begin
                state <= 2'b11;
                counter <= 16'd0;
            end
        end
        2'b11: begin // 释放消抖
            if(counter >= DEBOUNCE_TIME) begin
                key_out <= 1'b1;
                state <= 2'b00;
            end else begin
                counter <= counter + 1'b1;
                if(!key_in) state <= 2'b10;
            end
        end
    endcase
end
endmodule

在ModelSim中构建测试平台时,需要模拟真实按键抖动。下面是一个典型的测试用例:

initial begin
    // 初始化
    clk = 0; rst_n = 0; key_in = 4'b1111;
    #100 rst_n = 1;
    
    // 模拟主持人按键(带抖动)
    fork
        begin // 时钟生成
            forever #10 clk = ~clk;
        end
        begin // 测试场景
            // 前抖动阶段
            repeat(15) begin
                #({$random}%50) key_in[0] = ~key_in[0];
            end
            key_in[0] = 0; // 稳定按下
            #1000000; // 保持1ms
            
            // 后抖动阶段
            repeat(15) begin
                #({$random}%50) key_in[0] = ~key_in[0];
            end
            key_in[0] = 1; // 释放
            
            #2000000;
            $stop;
        end
    join
end

仿真时重点关注几个关键信号:

  • 状态机转换是否正确
  • 倒计时是否精确
  • 抢答锁定功能是否正常
  • 显示输出是否符合预期

4. 硬件部署与调试技巧

完成仿真后,进入最令人兴奋的硬件实现阶段。引脚分配需要对照开发板原理图,以下是一个典型配置:

信号名FPGA引脚开发板对应接口
clkPIN_2350MHz晶振
rst_nPIN_24KEY0
key_in[0]PIN_25KEY1(主持人)
key_in[1]PIN_26KEY2(选手A)
dig_sel[0]PIN_100DIG1位选
dig_seg[0]PIN_101段选a

实际调试中可能会遇到以下问题及解决方案:

问题1:数码管显示闪烁或不亮

  • 检查位选信号频率(推荐200-500Hz)
  • 确认共阴/共阳类型匹配
  • 测量段选信号电压是否足够

问题2:按键响应不灵敏

  • 调整消抖时间参数(通常10-20ms)
  • 检查按键硬件电路(上拉电阻是否正常)
  • 验证时钟频率是否正确

问题3:倒计时不准

  • 确认系统时钟频率设置
  • 检查计数器位宽是否足够
  • 使用SignalTap抓取实际计数信号
// SignalTap配置示例
altsyncram syncram_inst (
    .aclr (1'b0),
    .address (trigger_in),
    .clock (clk),
    .data (data_in),
    .wren (1'b1),
    .q (data_out)
);

完成所有调试后,建议进行压力测试:连续快速触发抢答场景,观察系统稳定性。同时记录资源使用情况:

  • 逻辑单元(LE): 约1200/6272 (19%)
  • 寄存器: 86
  • 引脚: 28/92
  • 存储器比特: 0/276480

这个简单的抢答器项目虽然功能基础,但已经包含了FPGA开发的完整流程。当看到自己设计的电路在开发板上完美运行时,那种成就感是无可替代的。建议在此基础上尝试扩展更多功能,比如:

  • 增加抢答违规检测
  • 实现无线抢答器功能
  • 添加语音提示模块
  • 支持多轮比赛积分统计

更多推荐