FPGA新手必看:用Verilog在Quartus Prime 18.1上实现抢答器(附完整源码解析)
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. 抢答器核心逻辑设计
抢答器的本质是一个带有优先级的状态机。我们需要处理几个关键逻辑:
- 主持人控制权:只有主持人按下开始键后,选手按键才有效
- 互斥锁定:任一选手抢答成功后,其他选手按键立即失效
- 倒计时机制:10秒内无抢答则自动锁定系统
- 状态显示:通过数码管实时显示倒计时和抢答结果
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引脚 | 开发板对应接口 |
|---|---|---|
| clk | PIN_23 | 50MHz晶振 |
| rst_n | PIN_24 | KEY0 |
| key_in[0] | PIN_25 | KEY1(主持人) |
| key_in[1] | PIN_26 | KEY2(选手A) |
| dig_sel[0] | PIN_100 | DIG1位选 |
| 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开发的完整流程。当看到自己设计的电路在开发板上完美运行时,那种成就感是无可替代的。建议在此基础上尝试扩展更多功能,比如:
- 增加抢答违规检测
- 实现无线抢答器功能
- 添加语音提示模块
- 支持多轮比赛积分统计
更多推荐



所有评论(0)