拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FPGA数字逻辑设计实战:从零实现经典乒乓球游戏

简介这是一份面向FPGA初学者与数字电路课程设计者的完整乒乓球游戏机实现方案基于Verilog HDL在Altera Cyclone II EP2C5T144C8芯片上完成硬件逻辑设计解决从算法建模、时序控制到外设驱动的全流程开发问题。压缩包共251个文件包含69个.cdb编译数据库、67个.hdb仿真波形数据、21个.tdf时序分析文件、7个.v核心Verilog源码及.qpf/.sof/.pof等工程配置与烧录文件辅以详细设计报告.doc和readme说明总大小仅1.61MB结构紧凑、模块清晰便于理解乒乓运动逻辑、球拍控制、碰撞检测与LED/数码管显示驱动等关键模块。已有2148人学习下载资源提供可直接编译下载的Quartus II工程、带注释的源码、仿真测试激励及完整设计报告特别适合数字系统课程实践、FPGA入门项目复现与竞赛备赛参考。1. 项目概述从零到一用FPGA复刻经典乒乓如果你对数字电路设计感兴趣或者正在学习FPGA和Verilog那么“用FPGA实现一个乒乓球游戏机”绝对是一个经典且极具价值的练手项目。这不仅仅是一个简单的游戏它几乎涵盖了数字逻辑设计的核心要素时钟分频、状态机、计数器、显示驱动、人机交互。当你亲手在开发板上看到两个光点来回跳动并通过按键控制“球拍”时那种成就感是看多少遍理论都无法比拟的。这个项目非常适合电子工程、计算机科学相关专业的学生以及希望从理论转向实践的FPGA初学者。通过它你能深刻理解硬件描述语言如何转化为实际的硬件行为掌握从设计、仿真到上板调试的完整流程。2. 核心设计思路与架构拆解2.1 需求分析与功能定义一个基础的乒乓球游戏机其核心功能可以拆解为以下几个部分球体运动一个光点代表球在屏幕上沿对角线运动遇到边界或球拍时反弹。球拍控制两个可由玩家控制的“球拍”通常是一列或几列LED灯通过按键上下移动。碰撞检测实时判断球是否与球拍或屏幕边界发生碰撞并改变其运动方向。计分系统当一方未能接到球时对方得分并在数码管或LED上显示当前比分。游戏状态控制包括游戏开始、暂停、复位、球速调节等。基于FPGA的实现意味着我们需要用Verilog代码来描述上述所有逻辑并将其映射到开发板上的具体资源按键作为输入LED点阵或VGA接口作为显示输出数码管或LED灯显示分数。2.2 系统顶层模块划分一个清晰、模块化的顶层设计是项目成功的关键。通常我们可以将整个系统划分为以下几个主要模块Top_Pong_Game ├── clk_gen (时钟分频与生成模块) ├── debounce (按键消抖模块) ├── game_control (游戏核心控制与状态机模块) ├── ball_movement (球体运动逻辑模块) ├── paddle_control (球拍控制模块) ├── collision_detect (碰撞检测模块) ├── score_counter (计分模块) └── display_driver (显示驱动模块如LED矩阵驱动或VGA驱动)为什么这样划分解耦与复用每个模块功能单一。例如debounce模块可以复用于所有机械按键clk_gen可以为不同速度需求的模块提供时钟。便于调试在仿真时可以单独测试每个模块的功能例如单独验证碰撞检测逻辑是否正确再集成到顶层。可扩展性如果想增加功能如增加音效、改变游戏模式只需修改或增加对应模块而不必重写整个工程。2.3 核心器件选型与接口定义对于这个项目一块基础的FPGA开发板如Xilinx的Basys3、Artix-7系列或Altera/Intel的Cyclone IV/V系列就足够了。我们需要确认开发板上的资源输入至少4个独立按键两个玩家上下控制一个复位一个开始/暂停。如果资源允许可以增加拨码开关调节球速。输出方案ALED点阵使用8x8或16x16的LED点阵屏来显示球和球拍。优点是直观、驱动相对简单缺点是分辨率低显示效果较粗糙。方案BVGA显示通过VGA接口连接显示器。优点是显示效果极佳可以绘制更精细的图形和文字缺点是驱动逻辑更复杂需要生成符合VGA时序的同步信号和RGB数据。计分显示通常使用板载的数码管或额外的LED灯来表示分数。我的选择与考量对于初学者我强烈建议先从LED点阵方案入手。它的驱动核心是一个行列扫描电路逻辑清晰能让你快速聚焦于游戏逻辑本身而不是复杂的视频时序。当你成功在点阵上跑通游戏后再挑战VGA版本会更有成就感也能更深刻地理解时序电路设计。3. 关键模块的Verilog实现详解3.1 时钟管理与按键消抖这是所有FPGA数字系统稳定运行的基石却最容易被新手忽略。时钟分频模块 (clk_gen) FPGA外部晶振通常提供很高的频率如100MHz。我们的游戏逻辑如球每0.2秒移动一格和扫描显示如LED点阵的刷新率需要60Hz以上需要更低的时钟。这就需要通过计数器进行分频。module clk_gen ( input wire clk_100m, // 100MHz主时钟 input wire rst_n, // 低电平复位 output reg clk_game, // 游戏逻辑时钟如1Hz output reg clk_scan // 显示扫描时钟如1kHz ); // 分频计数器参数 parameter GAME_DIV 100_000_000; // 100MHz / 1Hz 100,000,000 parameter SCAN_DIV 100_000; // 100MHz / 1kHz 100,000 reg [31:0] cnt_game, cnt_scan; always (posedge clk_100m or negedge rst_n) begin if (!rst_n) begin cnt_game 0; cnt_scan 0; clk_game 0; clk_scan 0; end else begin // 游戏时钟分频 if (cnt_game GAME_DIV/2 -1) begin cnt_game 0; clk_game ~clk_game; end else begin cnt_game cnt_game 1; end // 扫描时钟分频 if (cnt_scan SCAN_DIV/2 -1) begin cnt_scan 0; clk_scan ~clk_scan; end else begin cnt_scan cnt_scan 1; end end end endmodule注意这里使用了clk_game ~clk_game;来生成占空比50%的时钟。对于简单的使能信号更常见的做法是生成一个周期性的高脉冲使能信号而不是生成一个新的时钟域以避免跨时钟域问题。但对于这个入门项目直接分频时钟更直观。按键消抖模块 (debounce) 机械按键在按下和弹起时会产生持续数毫秒的抖动直接读取会导致多次误触发。消抖逻辑的核心是检测到按键状态变化后等待一段时间如20ms如果状态保持稳定则输出新的状态。module debounce ( input wire clk, // 扫描时钟如1kHz input wire button_in, // 原始按键输入 output reg button_out // 消抖后稳定输出 ); parameter DEBOUNCE_MS 20; // 消抖时间 parameter CLK_FREQ_HZ 1000; // 时钟频率1kHz localparam COUNTER_MAX DEBOUNCE_MS * CLK_FREQ_HZ / 1000; reg [15:0] counter; reg button_reg; always (posedge clk) begin button_reg button_in; // 同步打拍 if (button_reg ^ button_out) begin // 检测到输入与输出不同有变化 if (counter COUNTER_MAX) begin button_out button_reg; // 稳定后更新输出 counter 0; end else begin counter counter 1; end end else begin counter 0; // 状态稳定计数器清零 end end endmodule实操心得消抖时间DEBOUNCE_MS通常取10~20ms。时钟clk不宜直接用高速主时钟否则计数器会非常大。用一个1kHz1ms周期的分频时钟来驱动消抖模块计数器只需计数20左右逻辑更简洁。3.2 游戏核心状态机与运动逻辑这是整个游戏的“大脑”我习惯用一个顶层状态机来协调所有行为。游戏控制状态机 (game_control)module game_control ( input wire clk_game, input wire rst_n, input wire start_btn, // 开始/暂停 input wire miss_left, // 左方失球 input wire miss_right,// 右方失球 output reg game_active, // 游戏进行中标志 output reg [1:0] state // 状态输出可用于调试 ); // 定义游戏状态 localparam IDLE 2b00; // 待机 localparam PLAY 2b01; // 进行中 localparam PAUSE 2b10; // 暂停 localparam GOAL 2b11; // 得分状态短暂显示 reg [1:0] current_state, next_state; reg [19:0] goal_timer; // 得分显示计时器 // 状态寄存器更新 always (posedge clk_game or negedge rst_n) begin if (!rst_n) current_state IDLE; else current_state next_state; end // 下一状态逻辑 always (*) begin next_state current_state; case (current_state) IDLE: if (start_btn) next_state PLAY; PLAY: begin if (start_btn) next_state PAUSE; else if (miss_left || miss_right) next_state GOAL; end PAUSE: if (start_btn) next_state PLAY; GOAL: if (goal_timer 20d999_999) next_state PLAY; // 显示约1秒后回到PLAY default: next_state IDLE; endcase end // 输出逻辑与计时器 always (posedge clk_game or negedge rst_n) begin if (!rst_n) begin game_active 1b0; goal_timer 0; end else begin case (current_state) PLAY: game_active 1b1; GOAL: begin game_active 1b0; if (goal_timer 20d999_999) goal_timer goal_timer 1; else goal_timer 0; end default: game_active 1b0; endcase end end assign state current_state; endmodule球体运动模块 (ball_movement) 球的位置用二维坐标(ball_x, ball_y)表示方向用两个标志位dir_x, dir_y表示1为增0为减。在game_active有效且每个clk_game上升沿球根据方向移动一格。碰撞检测模块会修改dir_x和dir_y。module ball_movement ( input wire clk_game, input wire rst_n, input wire game_active, input wire collide_x, // X方向碰撞左右边界或球拍 input wire collide_y, // Y方向碰撞上下边界 output reg [3:0] ball_x, // 假设X坐标范围0-15 output reg [3:0] ball_y // 假设Y坐标范围0-7 ); reg dir_x; // 1:向右0:向左 reg dir_y; // 1:向下0:向上 always (posedge clk_game or negedge rst_n) begin if (!rst_n) begin ball_x 4d8; // 初始位置居中 ball_y 4d4; dir_x 1b1; // 初始向右 dir_y 1b1; // 初始向下 end else if (game_active) begin // 处理碰撞改变方向 if (collide_x) dir_x ~dir_x; if (collide_y) dir_y ~dir_y; // 根据方向更新位置 ball_x dir_x ? ball_x 1 : ball_x - 1; ball_y dir_y ? ball_y 1 : ball_y - 1; end end endmodule3.3 碰撞检测与球拍控制碰撞检测模块 (collision_detect) 这是游戏逻辑的精度所在。需要检测四种碰撞上下边界ball_y到达0或最大值。左右球拍ball_x到达最左或最右同时ball_y在对方球拍的高度范围内。module collision_detect ( input wire [3:0] ball_x, ball_y, input wire [3:0] paddle_left_y, // 左球拍中心Y坐标 input wire [3:0] paddle_right_y,// 右球拍中心Y坐标 input wire [2:0] paddle_height, // 球拍高度如3表示3格 output reg collide_x, collide_y, output reg miss_left, miss_right ); parameter X_MAX 15; parameter Y_MAX 7; // 计算球拍上下边界 wire [3:0] left_paddle_top paddle_left_y paddle_height/2; wire [3:0] left_paddle_bot paddle_left_y - paddle_height/2; wire [3:0] right_paddle_top paddle_right_y paddle_height/2; wire [3:0] right_paddle_bot paddle_right_y - paddle_height/2; always (*) begin collide_y 1b0; collide_x 1b0; miss_left 1b0; miss_right 1b0; // 上下边界碰撞 if (ball_y 0 || ball_y Y_MAX) collide_y 1b1; // 左右边界及球拍碰撞 if (ball_x 0) begin // 到达左边界 // 判断是否在左球拍范围内 if (ball_y left_paddle_top ball_y left_paddle_bot) begin collide_x 1b1; // 击中球拍反弹 end else begin miss_right 1b1; // 左方未击中右方得分 end end else if (ball_x X_MAX) begin // 到达右边界 // 判断是否在右球拍范围内 if (ball_y right_paddle_top ball_y right_paddle_bot) begin collide_x 1b1; end else begin miss_left 1b1; // 右方未击中左方得分 end end end endmodule关键细节这里用组合逻辑always (*)实现碰撞检测确保响应是即时性的。球拍位置paddle_left_y等需要由球拍控制模块根据按键输入实时更新。球拍控制模块 (paddle_control) 该模块读取消抖后的按键信号在游戏进行状态game_active下控制球拍位置的上下移动并确保球拍不超出屏幕范围。module paddle_control ( input wire clk_game, input wire rst_n, input wire game_active, input wire btn_up_left, // 左方上移 input wire btn_down_left, // 左方下移 input wire btn_up_right, // 右方上移 input wire btn_down_right,// 右方下移 output reg [3:0] paddle_left_y, output reg [3:0] paddle_right_y ); parameter Y_MAX 7; parameter PADDLE_SPEED 1; // 每次移动的格数 always (posedge clk_game or negedge rst_n) begin if (!rst_n) begin paddle_left_y 4d4; paddle_right_y 4d4; end else if (game_active) begin // 控制左球拍 if (btn_up_left paddle_left_y Y_MAX) begin paddle_left_y paddle_left_y PADDLE_SPEED; end else if (btn_down_left paddle_left_y 0) begin paddle_left_y paddle_left_y - PADDLE_SPEED; end // 控制右球拍 if (btn_up_right paddle_right_y Y_MAX) begin paddle_right_y paddle_right_y PADDLE_SPEED; end else if (btn_down_right paddle_right_y 0) begin paddle_right_y paddle_right_y - PADDLE_SPEED; end end end endmodule3.4 显示驱动与系统集成LED点阵驱动模块 (led_matrix_driver) 这是将游戏逻辑状态“可视化”的关键。我们以8x8点阵为例采用经典的“行列扫描”法来驱动。module led_matrix_driver ( input wire clk_scan, // 扫描时钟如1kHz input wire rst_n, input wire [3:0] ball_x, ball_y, input wire [3:0] paddle_left_y, paddle_right_y, input wire [2:0] paddle_height, output reg [7:0] row_sel, // 行选信号低电平有效 output reg [7:0] col_data // 列数据高电平点亮 ); reg [2:0] scan_cnt; // 扫描计数器0-7循环 wire [7:0] row_mask; // 当前扫描行的掩码 // 根据扫描计数生成行选信号例如共阴极低电平选中一行 always (posedge clk_scan or negedge rst_n) begin if (!rst_n) scan_cnt 0; else scan_cnt scan_cnt 1; end always (*) begin row_sel 8b1111_1111; row_sel[scan_cnt] 1b0; // 选中当前行 end // 生成当前行需要点亮的列数据 always (*) begin col_data 8b0000_0000; // 点亮球 if (ball_y scan_cnt) begin col_data[ball_x] 1b1; end // 点亮左球拍位于X0列 if (scan_cnt paddle_left_y - paddle_height/2 scan_cnt paddle_left_y paddle_height/2) begin col_data[0] 1b1; end // 点亮右球拍位于X7列 if (scan_cnt paddle_right_y - paddle_height/2 scan_cnt paddle_right_y paddle_height/2) begin col_data[7] 1b1; end end endmodule重要提示实际硬件连接时务必查阅开发板原理图确认LED点阵是共阴极还是共阳极以及行列信号是高电平有效还是低电平有效。上述代码以共阴极、行选低有效、列数据高有效为例。驱动能力不足时可能需要三极管扩流。顶层模块集成 (top_pong_game) 最后我们将所有模块像搭积木一样连接起来。module top_pong_game ( input wire clk_100m, input wire rst_n_in, // 外部复位按键 input wire btn_start, input wire btn_up_l, btn_down_l, input wire btn_up_r, btn_down_r, output wire [7:0] led_row, output wire [7:0] led_col, output wire [3:0] score_left, // 左方分数接数码管译码器 output wire [3:0] score_right // 右方分数 ); // 内部信号声明 wire clk_game, clk_scan; wire btn_start_db, btn_up_l_db, btn_down_l_db, btn_up_r_db, btn_down_r_db; wire game_active; wire [3:0] ball_x, ball_y; wire [3:0] paddle_left_y, paddle_right_y; wire collide_x, collide_y, miss_left, miss_right; // 模块实例化 clk_gen u_clk_gen (.clk_100m(clk_100m), .rst_n(rst_n), ...); debounce u_deb_start (.clk(clk_scan), .button_in(btn_start), .button_out(btn_start_db)); // ... 实例化其他按键的消抖模块 game_control u_game_ctrl (.clk_game(clk_game), .rst_n(rst_n), .start_btn(btn_start_db), ...); paddle_control u_paddle_ctrl (.clk_game(clk_game), .rst_n(rst_n), .game_active(game_active), ...); ball_movement u_ball (.clk_game(clk_game), .rst_n(rst_n), .game_active(game_active), ...); collision_detect u_collide (.ball_x(ball_x), .ball_y(ball_y), ...); score_counter u_score (.clk_game(clk_game), .rst_n(rst_n), .miss_left(miss_left), .miss_right(miss_right), ...); led_matrix_driver u_display (.clk_scan(clk_scan), .rst_n(rst_n), .ball_x(ball_x), ...); endmodule4. 仿真、调试与上板实战全记录4.1 测试平台的搭建与关键仿真写Verilog代码不仿真就等于闭着眼睛开车。我们需要一个测试平台Testbench来验证每个模块乃至整个系统的行为。以game_control状态机为例timescale 1ns / 1ps module tb_game_control(); reg clk, rst_n, start_btn, miss_left, miss_right; wire game_active; wire [1:0] state; game_control uut (.*); // 实例化被测模块 // 生成时钟周期20ns对应50MHz这里简化 always #10 clk ~clk; initial begin // 初始化 clk 0; rst_n 0; start_btn 0; miss_left 0; miss_right 0; #100 rst_n 1; // 释放复位 // 测试按下开始键进入PLAY状态 #20 start_btn 1; #40 start_btn 0; // 观察game_active是否变高state是否变为PLAY // 测试在PLAY状态下模拟右方得分 #200 miss_right 1; #40 miss_right 0; // 观察是否进入GOAL状态game_active变低约1秒后回到PLAY // 测试在PLAY状态下暂停 #200 start_btn 1; #40 start_btn 0; // 观察是否进入PAUSE状态game_active变低 #1000 $finish; end endmodule在Modelsim或Vivado Simulator中运行观察波形图确认状态转换、game_active信号和计时器逻辑是否符合预期。仿真要点分模块仿真先单独仿真debounce、collision_detect等关键模块。关键场景覆盖仿真要覆盖边界情况如球刚好擦着球拍边沿通过、球拍移动到屏幕最顶端时按下上键等。查看中间信号把模块内部的重要寄存器如球坐标、方向、计数器也拉到波形图中观察。4.2 上板调试的“血泪”经验仿真通过只是万里长征第一步。上板调试才是真正的挑战。以下是我踩过的一些坑现象LED点阵显示闪烁、重影或亮度不均。排查首先检查clk_scan频率。刷新率太低如低于60Hz会看到闪烁太高可能超过LED的响应速度或扫描逻辑的稳定时间。1kHz左右是个不错的起点。其次检查行列信号的驱动能力如果电流不够LED会暗淡。最后仔细核对原理图确认你的代码中的行列有效电平与硬件匹配。共阴/共阳弄反整个显示就是反的。现象球或球拍移动不流畅有跳帧或卡顿。排查检查clk_game的频率。这是游戏的主时钟决定了球移动的速度。如果频率太低球移动慢如果太高球速过快难以操作。可以通过一个拨码开关连接分频计数器的参数实现球速可调。另外确保game_active信号在正确的时间被置位/清零。现象按键响应迟钝或连击。排查99%的问题出在消抖。检查消抖模块的时钟clk是否确实连接到了1kHz左右的时钟而不是100MHz主时钟。检查消抖时间常数是否合理20ms。可以在代码里添加一个LED用消抖后的信号去点亮它直观测试按键响应。现象碰撞检测不准球有时会穿墙或穿拍。排查这是逻辑错误。回到collision_detect模块打印或通过LED指示球坐标和球拍边界坐标在极限位置如ball_y0, ball_x0单步调试。特别注意比较运算符是、、还是一个等号的差别会导致完全不同的行为。确保你的碰撞判断条件覆盖了所有像素点。综合与实现警告工具如Vivado可能会报告“时序未满足”、“ latch推断”等警告。latch推断通常是因为在组合逻辑always (*)块中if或case语句没有写全所有分支导致电路在某种条件下需要“保持”之前的值从而生成了锁存器。在FPGA设计中应避免非预期的锁存器。务必为所有输出信号在所有分支下都分配一个值。时序违规如果系统时钟很高而组合逻辑路径太长比如碰撞检测逻辑非常复杂可能导致建立/保持时间违例。可以通过打拍插入寄存器来分割组合逻辑路径。4.3 功能扩展与优化思路当基础版本稳定运行后你可以尝试以下扩展让项目更出彩VGA显示学习VGA时序用FPGA生成RGB信号在显示器上绘制更精美的球拍、球、边界线和分数。这会涉及帧缓冲、图形生成等更复杂的概念。音效输出利用PWM脉宽调制驱动一个无源蜂鸣器在击球、得分时发出不同频率的声音。智能对手实现一个简单的AI控制右方球拍自动追踪球的Y坐标做成单人游戏模式。多球模式增加一个球并处理球与球之间的碰撞可选。七段数码管显示分数编写一个二进制到BCD码转换器再连接数码管动态扫描驱动模块专业地显示两位分数。5. 设计报告撰写核心要点一份好的设计报告不仅是代码的说明更是你设计思路的体现。它应该包含设计目标与要求清晰描述你要实现的功能和性能指标。系统总体设计用框图展示顶层模块划分和信号连接关系。分模块详细设计对每个模块如消抖、状态机、运动、碰撞、显示进行说明包括端口定义、功能描述、状态转移图如有、关键代码片段及注释。仿真测试与分析展示关键模块和顶层系统的仿真波形图并解释波形如何验证了功能。硬件测试与结果描述使用的硬件平台、引脚分配表Pin Assignment并附上实际上板运行的照片或视频。总结与心得体会回顾整个项目过程遇到了哪些问题如何解决的有什么收获。这部分最能体现你的个人思考。从构思到实现一个完整的FPGA乒乓球游戏你会经历从模块设计、代码编写、功能仿真、综合实现到上板调试的全过程。每一个环节的坑踩过去你对数字系统设计的理解就会深一层。这个项目麻雀虽小五脏俱全它带给你的绝不仅仅是一个会动的游戏更是解决复杂工程问题的思维方式和动手能力。当你看到自己设计的游戏在开发板上流畅运行时那种硬件被你的代码赋予生命的喜悦是软件编程难以比拟的。动手去做吧从第一个分频器开始。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门