简介:本资源是一套完整的FPGA数字系统设计实践项目,面向电子工程、集成电路与数字逻辑课程学习者及FPGA初学者,解决数字电路综合应用能力训练问题。项目基于Altera Cyclone II EP2C5T144C8芯片,采用Verilog HDL实现乒乓球游戏机功能,涵盖球拍控制、球轨迹计算、碰撞检测、得分显示等核心逻辑,在Quartus II中完成编译、仿真与硬件下载调试。压缩包含251个文件,总计1.61MB,其中69个.cdb(编译数据库)、67个.hdb(层次化数据库)、21个.tdf(时序数据文件)支撑工程构建与时序分析,7个.v文件为关键模块源码,另有.bak备份、.sof配置文件、.qpf工程文件及.doc设计报告,结构完整便于理解FPGA开发全流程。已有2148人学习下载,配套报告详述设计原理、状态机划分与引脚约束,源码注释清晰,适合开展课程实验、课程设计或自学验证。
1. 项目概述:从零到一,用FPGA复刻经典乒乓
最近在整理硬盘里的老项目,翻出来一个大学时期做的FPGA乒乓球游戏机设计,源码和报告都还在。这个项目虽然不大,但麻雀虽小五脏俱全,从需求分析、状态机设计、外设驱动到最后的板级调试,完整走了一遍FPGA开发流程。对于刚接触Verilog和FPGA的朋友来说,这是一个绝佳的练手项目,它不涉及复杂的算法,却能让你深刻理解数字系统设计的核心思想——用硬件描述语言去“构造”一个并行的、确定性的系统。简单来说,这个项目就是用一块FPGA开发板,配合几个按键和VGA显示器,实现一个两人对打的乒乓球游戏。球拍由玩家控制上下移动,球在碰到拍子或边界时会反弹,漏接则对方得分。听起来简单,但里面关于时钟域、状态机、视频时序生成、消抖处理等知识点,都是FPGA工程师的必修课。
2. 核心设计思路与架构拆解
2.1 需求分析与功能定义
一个基本的乒乓球游戏需要哪些功能?我们首先得把它拆解成硬件能理解的任务。
- 图像显示:需要在VGA显示器上稳定地显示游戏画面,包括球台中线、两个球拍、乒乓球以及双方比分。
- 玩家输入:两个玩家分别控制左右球拍的上下移动,通常使用按键或拨码开关。这里必须处理机械按键的抖动问题。
- 游戏逻辑:这是核心,包括乒乓球的运动轨迹计算(位置、速度、方向)、与球拍及边界的碰撞检测、得分判定与计分逻辑。
- 系统控制:控制游戏开始、暂停、复位等状态。
在FPGA中,这些功能将被映射为不同的模块,并行执行。整个系统的顶层架构可以这样规划:一个顶层的模块(Top Module)负责例化和连接所有子模块,而主时钟则驱动着整个系统的同步运转。
2.2 系统顶层架构设计
基于上述需求,我们可以设计出如下模块化架构:
- 时钟管理模块(clk_gen):将板载晶振时钟(如50MHz)分频或通过PLL产生系统所需的各种时钟,例如VGA像素时钟(25.175MHz或相近频率)、游戏逻辑更新时钟(如60Hz)等。稳定的时钟是数字系统的基石。
- VGA显示控制模块(vga_ctrl):根据VGA时序标准,生成行同步(HSYNC)、场同步(VSYNC)信号以及当前像素的有效坐标(x_cnt, y_cnt)。同时,它也是一个“裁判”,根据当前像素坐标和游戏状态(球拍位置、球位置、分数)来输出对应的RGB颜色值,决定屏幕上每个点该显示什么。
- 按键消抖与输入处理模块(key_debounce):机械按键在按下和弹起时会产生一段时间的抖动,直接读取会导致误触发。这个模块通过采样和滤波(如20ms延时检测)来获取稳定的按键状态,并将其转换为清晰的“上移”、“下移”等控制信号。
- 游戏逻辑核心模块(game_logic):这是整个设计的“大脑”。它接收处理后的玩家输入,在游戏时钟的节拍下更新游戏状态:
- 球拍控制:根据按键信号更新左右球拍的垂直坐标,并限制其移动范围不超出屏幕上下边界。
- 乒乓球运动:用一个寄存器记录球的当前位置(x, y)和速度方向(dx, dy,通常用+1/-1表示)。每个游戏时钟周期,球的位置按速度更新。碰撞检测就是判断新位置是否碰到边界(顶部、底部)或球拍(判断球的x坐标在球拍x范围内,且y坐标在球拍的矩形区域内)。碰到边界则dy取反,碰到球拍则dx取反,并且可以稍微随机化反弹后的dy,增加趣味性。
- 得分与状态机:如果球运动到左/右屏幕外且未被球拍拦截,则判定对方得分,分数寄存器加一,然后重置球到屏幕中心并发球。游戏状态可以用一个简单状态机管理,如
IDLE(等待开始)、PLAYING(游戏中)、GOAL(得分后短暂停顿)等。
这些模块通过顶层模块的连线(wire)和寄存器(reg)连接起来,形成一个完整的数据流和控制流。
3. 关键模块的Verilog实现细节
3.1 VGA显示模块:如何“画”出游戏画面
VGA显示的本质是时序控制。以640x480@60Hz模式为例,我们需要一个约25MHz的像素时钟。模块内部用两个计数器(h_cnt, v_cnt)来追踪当前扫描的位置。
// 简化的时序参数定义 parameter H_DISP = 640; parameter H_FP = 16; parameter H_SYNC = 96; parameter H_BP = 48; parameter H_TOTAL = H_DISP + H_FP + H_SYNC + H_BP; // 800 parameter V_DISP = 480; parameter V_FP = 10; parameter V_SYNC = 2; parameter V_BP = 33; parameter V_TOTAL = V_DISP + V_FP + V_SYNC + V_BP; // 525在每个像素时钟上升沿,水平计数器h_cnt加1。当h_cnt计满H_TOTAL时归零,同时垂直计数器v_cnt加1。根据h_cnt和v_cnt的值,产生HSYNC和VSYNC信号(在同步脉冲期间置低有效),并判断当前是否处于有效显示区域(h_cnt < H_DISP && v_cnt < V_DISP)。
在有效显示区域内,模块根据输入的ball_x,ball_y,paddle_l_y,paddle_r_y,score_l,score_r等游戏状态信号,判断当前像素点(x, y)应该属于背景、球台中线、球拍、乒乓球还是数字字符,并输出对应的RGB值。例如:
// 判断是否在左球拍区域内(假设球拍宽10像素,高80像素) wire in_paddle_l = (x >= PADDLE_L_X && x < PADDLE_L_X + 10 && y >= paddle_l_y && y < paddle_l_y + 80); // 判断是否在乒乓球区域内(假设球半径为4像素) wire in_ball = ((x - ball_x)*(x - ball_x) + (y - ball_y)*(y - ball_y)) <= 16; // 距离平方判断 always @(*) begin if (in_ball) {r, g, b} = 3'b111; // 白色球 else if (in_paddle_l || in_paddle_l) // 右球拍判断类似 {r, g, b} = 3'b110; // 黄色球拍 else if (x == H_DISP/2 && y[3:0] < 8) // 画虚线中线 {r, g, b} = 3'b101; else {r, g, b} = 3'b000; // 黑色背景 end显示数字比分稍微复杂,需要一个小型的字符点阵ROM,根据当前像素坐标和要显示的数字索引,输出该点是否亮起。
注意:VGA时序必须精确,否则显示器可能无法同步。不同分辨率/刷新率的时序参数不同,需要查阅标准。在实际工程中,常使用开发板厂商提供的VGA控制器IP核或经过验证的代码。
3.2 游戏逻辑模块:状态机与碰撞的舞蹈
游戏逻辑模块是纯数字逻辑的精彩体现。它通常由一个状态机(FSM)和一系列组合逻辑、时序逻辑构成。
状态机设计:可以设计为三个状态。
S_IDLE:上电或复位后的状态,显示欢迎画面,等待开始键。S_PLAY:游戏进行状态。在此状态下,每个游戏时钟周期(例如每秒60次)更新球的位置和检测碰撞。S_SCORE:一方得分后的状态。在此状态停留约1秒,显示得分,然后自动跳回S_PLAY状态并发球。
碰撞检测逻辑:这是游戏真实性的关键。球的运动用寄存器ball_x_reg,ball_y_reg和方向dx_reg(+1向右,-1向左),dy_reg(+1向下,-1向上)表示。
// 每个游戏时钟周期更新 always @(posedge game_clk) begin if (state == S_PLAY) begin // 预计算下一位置 next_ball_x = ball_x_reg + dx_reg; next_ball_y = ball_y_reg + dy_reg; // 边界碰撞(顶部和底部) if (next_ball_y <= 0 || next_ball_y >= (V_DISP-1)) dy_next = ~dy_reg + 1; // 取反,即dy = -dy else dy_next = dy_reg; // 球拍碰撞检测(以左球拍为例) // 判断球在水平方向即将进入球拍区域,且垂直方向在球拍高度范围内 if (dx_reg == -1 && // 球向左运动 next_ball_x <= PADDLE_L_X + PADDLE_WIDTH && next_ball_x >= PADDLE_L_X && ball_y_reg >= paddle_l_y_reg && ball_y_reg < paddle_l_y_reg + PADDLE_HEIGHT) begin dx_next = 1; // 反弹向右 // 可选:根据击中球拍的位置,微调垂直速度,增加策略性 if (ball_y_reg < paddle_l_y_reg + PADDLE_HEIGHT/3) dy_next = -1; // 击中上部,向上弹 else if (ball_y_reg > paddle_l_y_reg + 2*PADDLE_HEIGHT/3) dy_next = 1; // 击中下部,向下弹 end // 右球拍碰撞检测逻辑类似... // 得分检测 if (next_ball_x < 0) begin // 球出左边界,右方得分 score_r_next = score_r_reg + 1; state_next = S_SCORE; end else if (next_ball_x > H_DISP) begin // 球出右边界,左方得分 score_l_next = score_l_reg + 1; state_next = S_SCORE; end else begin // 正常更新球位置和方向 ball_x_next = next_ball_x; ball_y_next = next_ball_y; dx_next = dx_next; dy_next = dy_next; state_next = S_PLAY; end end end在S_SCORE状态,可以利用一个计数器实现短暂延时,然后重置球的位置和发球方向(通常发球给上一回合的失分方),再回到S_PLAY状态。
3.3 输入处理模块:让操作跟手的关键
按键消抖的算法很多,这里介绍一种简单可靠的“计数器法”。其原理是:当检测到按键电平变化后,启动一个计数器(例如对应20ms),在此期间持续采样按键电平。如果20ms后电平保持稳定,则认为这是一个有效的按键动作。
module key_debounce ( input wire clk, // 系统时钟(如50MHz) input wire key_in, // 原始按键输入,低电平有效 output reg key_out // 消抖后输出,一个时钟周期高脉冲 ); parameter CNT_MAX = 20'd1_000_000; // 50MHz * 0.02s = 1,000,000 reg [19:0] cnt; reg key_in_r0, key_in_r1; // 两级同步寄存器,消除亚稳态 reg key_stable; always @(posedge clk) begin key_in_r0 <= key_in; key_in_r1 <= key_in_r0; // 同步化 if (key_in_r1 != key_stable) begin // 检测到变化 cnt <= CNT_MAX; end else if (cnt != 0) begin cnt <= cnt - 1; end else begin key_stable <= key_in_r1; // 稳定后更新 end // 检测下降沿(按下事件),产生单周期脉冲 key_out <= (key_stable == 1'b1) && (key_in_r1 == 1'b0); end endmodule将处理后的key_out脉冲信号输入到游戏逻辑模块,用于控制球拍移动。通常,按一次上键,球拍坐标paddle_y_reg减去一个固定步长(如5像素);按下键则增加。同时要限制paddle_y_reg的范围在[0, V_DISP-PADDLE_HEIGHT]之间。
4. 系统集成与板级调试实战
4.1 顶层模块设计与引脚约束
顶层模块(top_pong)的任务是把所有子模块像搭积木一样连接起来,并映射到FPGA的实际物理引脚。
module top_pong ( input wire sys_clk, // 系统时钟(50MHz) input wire rst_n, // 复位按键,低有效 input wire key_start, // 开始按键 input wire key_up_l, // 左玩家上 input wire key_down_l, // 左玩家下 input wire key_up_r, // 右玩家上 input wire key_down_r, // 右玩家下 output wire vga_hsync, // VGA行同步 output wire vga_vsync, // VGA场同步 output wire [2:0] vga_r, // VGA红色,3位 output wire [2:0] vga_g, // VGA绿色,3位 output wire [2:0] vga_b // VGA蓝色,3位 ); // 内部信号声明 wire clk_vga; // VGA像素时钟 wire clk_game; // 游戏逻辑更新时钟 wire [9:0] pixel_x, pixel_y; // 当前像素坐标 wire [9:0] ball_x, ball_y; wire [8:0] paddle_l_y, paddle_r_y; wire [3:0] score_l, score_r; wire key_up_l_db, key_down_l_db, ...; // 消抖后的按键信号 // 模块实例化 clk_gen u_clk_gen(.sys_clk(sys_clk), .clk_vga(clk_vga), .clk_game(clk_game)); key_debounce u_debounce_l_up(.clk(sys_clk), .key_in(key_up_l), .key_out(key_up_l_db)); // ... 其他按键消抖实例化 game_logic u_game_logic( .clk(clk_game), .rst_n(rst_n), .key_up_l(key_up_l_db), // ... 其他输入 .ball_x(ball_x), .ball_y(ball_y), .paddle_l_y(paddle_l_y), .paddle_r_y(paddle_r_y), .score_l(score_l), .score_r(score_r) ); vga_ctrl u_vga_ctrl( .clk(clk_vga), .rst_n(rst_n), .ball_x(ball_x), .ball_y(ball_y), .paddle_l_y(paddle_l_y), .paddle_r_y(paddle_r_y), .score_l(score_l), .score_r(score_r), .hsync(vga_hsync), .vsync(vga_vsync), .pixel_x(pixel_x), .pixel_y(pixel_y), .vga_r(vga_r), .vga_g(vga_g), .vga_b(vga_b) ); endmodule编写完顶层代码后,最关键的一步是编写引脚约束文件(.xdc for Vivado/.qsf for Quartus)。你需要根据开发板的原理图,将sys_clk、rst_n、各个key_*信号、vga_*信号分配到FPGA芯片对应的物理引脚上。例如:
# 时钟引脚 set_property PACKAGE_PIN “E3” [get_ports sys_clk] set_property IOSTANDARD LVCMOS33 [get_ports sys_clk] # 复位按键 set_property PACKAGE_PIN “N4” [get_ports rst_n] set_property IOSTANDARD LVCMOS33 [get_ports rst_n] # VGA HSYNC引脚 set_property PACKAGE_PIN “G13” [get_ports vga_hsync] set_property IOSTANDARD LVCMOS33 [get_ports vga_hsync]引脚分配错误是导致板级调试失败的最常见原因,务必反复核对原理图。
4.2 仿真验证:Modelsim中的虚拟对战
在烧录到板子之前,必须进行充分的仿真测试。我们可以编写一个测试平台(Testbench)来模拟输入,并观察内部信号和输出波形。
`timescale 1ns / 1ps module tb_pong(); reg sys_clk_tb; reg rst_n_tb; // ... 其他输入信号 // ... 输出信号 top_pong uut (.*); // 实例化待测设计 // 生成50MHz时钟 always #10 sys_clk_tb = ~sys_clk_tb; initial begin // 初始化 sys_clk_tb = 0; rst_n_tb = 0; key_start_tb = 1; key_up_l_tb = 1; // ... #100; rst_n_tb = 1; // 释放复位 #200; key_start_tb = 0; #100000; key_start_tb = 1; // 模拟按下开始键 #1000000; // 模拟左玩家按上键 key_up_l_tb = 0; #1000000; key_up_l_tb = 1; #5000000; $stop; // 停止仿真 end endmodule在Modelsim中运行仿真,你可以观察:
- 复位后,游戏状态机是否进入
S_IDLE。 - 按下开始键后,是否进入
S_PLAY,球是否开始运动。 - 模拟按键后,
paddle_l_y信号是否按预期变化。 - 当球运动到边界时,
dx_reg/dy_reg是否正确取反。 - 当球运动出界时,
score_l或score_r是否加一,状态机是否跳转到S_SCORE。 - VGA时序信号
hsync和vsync的波形是否符合标准(周期、脉冲宽度)。
实操心得:仿真时,可以把游戏逻辑时钟(
clk_game)的频率设得比实际高很多(比如用系统时钟驱动),这样能快速模拟完几十秒的游戏过程,节省仿真时间。同时,学会使用Modelsim的“Memory List”或“Waveform”窗口添加内部信号进行观察,这是调试复杂逻辑的利器。
4.3 上板调试与问题排查
当仿真通过后,就可以进行综合、实现、生成比特流并下载到FPGA开发板了。上板后可能遇到以下典型问题及排查思路:
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| VGA无显示 | 1. 时钟频率不准。 2. 时序参数错误。 3. RGB引脚未正确赋值或电平标准不对。 4. 连接线或显示器问题。 | 1. 用示波器或逻辑分析仪测量clk_vga频率和hsync/vsync波形,对照VGA标准检查。2. 检查代码中的时序参数。 3. 确保在非显示区域RGB输出为0。检查引脚约束的电平标准(通常是LVCMOS33)。 4. 换线、换显示器或测试VGA接口的+5V电源。 |
| 画面撕裂或错位 | 游戏逻辑时钟与VGA像素时钟不同步,导致在帧中间更新了显示缓冲区。 | 确保VGA模块只从游戏逻辑模块读取球拍、球的位置和分数,而游戏逻辑的更新必须与VGA扫描异步。一个稳健的方法是:游戏逻辑在每帧开始(vsync上升沿)时,将计算好的新位置更新到一组“显示寄存器”中,VGA模块在整个帧期间都读取这组稳定的寄存器值。这避免了读写冲突。 |
| 按键不灵敏或连击 | 1. 消抖参数不合适(时间太短或太长)。 2. 按键检测逻辑有误(边沿检测还是电平检测)。 | 1. 调整消抖计数器的值。20ms是经验值,可根据实际按键特性微调。 2. 确认代码是检测稳定的下降沿后产生单周期脉冲,而不是持续的低电平。脉冲驱动球拍移动一次,长按不会连续移动,这需要额外的“连按加速”逻辑来实现。 |
| 球速过快或过慢 | 游戏逻辑更新频率(clk_game)设置不当。 | clk_game的频率直接决定球速。例如,60Hz的更新率,球每帧移动若干像素。如果觉得球速不合适,调整clk_game的分频系数即可。一个技巧是:可以将球速做成可调的,通过板载拨码开关选择不同的分频比,方便调试和适配不同难度。 |
| 资源使用超限 | 设计过于复杂,超出了FPGA的逻辑单元或存储器资源。 | 在综合实现后查看资源利用率报告。对于这个简单游戏,通常不会超限。如果超了,可以考虑优化:用查找表(LUT)代替小容量ROM存储字符;减少颜色位宽(如用8色代替全彩);简化碰撞检测算法。 |
调试王牌工具:嵌入式逻辑分析仪(ILA)。对于像VGA信号这种高速且复杂的调试,片上逻辑分析仪是神器。以Vivado的ILA为例,你可以在设计中插入ILA IP核,将hsync、vsync、pixel_x[0]、ball_x等关键信号连接到探针上。下载包含ILA的比特流后,在Vivado硬件管理器中设置触发条件(如vsync上升沿),然后捕获波形。这样你就能在真实硬件运行中看到这些信号的时序关系,对于排查显示不同步、图像错位等问题有奇效。
5. 项目进阶与扩展思路
完成基础版本后,这个项目还有巨大的扩展空间,可以让你深入学习更多FPGA知识:
- 增加音频输出:使用PWM(脉宽调制)或Delta-Sigma调制,通过一个IO口驱动无源蜂鸣器或外接音频模块。在击球、得分时播放简单的音效。这涉及到数模转换和音频信号生成的基本概念。
- 实现电脑AI对手:让右侧球拍自动跟随球运动,实现单人游戏模式。AI的难度可以通过增加反应延时、设置移动速度上限、甚至引入预测算法来调整。这能练习更复杂的状态机和算法在硬件中的实现。
- 使用PS/2键盘或游戏手柄输入:替换简单的按键,使用标准接口的输入设备。你需要编写PS/2或SPI/I2C协议的解码模块,这能让你深入理解外设通信协议。
- 增加图形特效:例如,球在击打时闪烁,得分时屏幕闪烁,或者绘制更精美的球拍和球(使用小尺寸位图)。这需要更多的显示缓冲资源或更复杂的图形生成逻辑。
- 联网对战(高阶):通过板载的UART、以太网或Wi-Fi模块,将两台FPGA开发板连接起来,实现真正的远程对战。这将涉及网络协议栈、数据同步、延迟处理等高级主题,是一个完整的系统级挑战。
这个基于FPGA的乒乓球游戏机项目,就像一把钥匙,帮你打开了数字逻辑设计的大门。从理解时序、编写状态机、处理外设接口到最后的系统调试,每一步都充满了工程师解决问题的乐趣。当你第一次在屏幕上看到自己设计的球按照预想的物理规律弹跳,并且能用按键自如操控时,那种成就感是无可替代的。希望这份详细的拆解,能帮助你少走弯路,顺利实现自己的“乒乓梦想”。
本文还有配套的精品资源,点击获取