
简介本资源是一套完整的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显示的本质是时序控制。以640x48060Hz模式为例我们需要一个约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} 3b111; // 白色球 else if (in_paddle_l || in_paddle_l) // 右球拍判断类似 {r, g, b} 3b110; // 黄色球拍 else if (x H_DISP/2 y[3:0] 8) // 画虚线中线 {r, g, b} 3b101; else {r, g, b} 3b000; // 黑色背景 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_reg1向右-1向左dy_reg1向下-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 20d1_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 1b1) (key_in_r1 1b0); 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的乒乓球游戏机项目就像一把钥匙帮你打开了数字逻辑设计的大门。从理解时序、编写状态机、处理外设接口到最后的系统调试每一步都充满了工程师解决问题的乐趣。当你第一次在屏幕上看到自己设计的球按照预想的物理规律弹跳并且能用按键自如操控时那种成就感是无可替代的。希望这份详细的拆解能帮助你少走弯路顺利实现自己的“乒乓梦想”。本文还有配套的精品资源点击获取