1. 项目概述
这个FPGA小游戏项目源于我的数字系统设计课程作业,目标是实现一个经典的"打砖块"游戏。作为一名电子工程专业的学生,我一直对硬件描述语言和FPGA开发充满兴趣。通过这个项目,我不仅巩固了Verilog编程技能,更深入理解了数字系统设计的核心思想。
游戏的基本玩法很简单:玩家控制底部的挡板左右移动,反弹小球击碎上方的砖块。全部砖块消除则胜利,小球落底则失败。虽然概念简单,但实现起来涉及VGA显示控制、状态机设计、碰撞检测等多个关键技术点。
在开发过程中,我遇到了不少意料之外的挑战。最初版本的代码虽然能编译通过,但实际运行时小球会穿透砖块、按键响应不灵敏、游戏状态切换异常等问题频出。经过反复调试和重构,最终实现了一个稳定可玩的版本。下面我将详细分享整个设计过程和踩过的坑。
2. 硬件架构设计
2.1 整体模块划分
系统采用模块化设计,主要分为以下几个功能模块:
-
VGA时序生成模块(vga.v)
负责产生标准的800×525@60Hz VGA时序信号,包括行同步(hsync)和场同步(vsync),同时输出当前像素坐标(xcnt, ycnt)。这是整个显示系统的基础。 -
按键消抖模块(key_deb.v)
对机械按键信号进行滤波处理,消除接触抖动带来的误触发。这是确保游戏操作流畅的关键。 -
游戏主控模块(Simple_VGA.v)
作为顶层状态机,管理游戏的两种状态:IDLE(待机)和GAME(游戏进行中)。负责协调各子模块的工作。 -
小球运动模块(ball_move.v)
根据碰撞信号和当前速度方向,计算下一帧小球的位置。实现了8方向运动的状态机。 -
碰撞检测模块(hit_judge.v)
判断小球与砖块、挡板的碰撞情况,并输出碰撞方向和消除信号。 -
图形绘制模块(draw_vga.v)
根据游戏状态和对象位置,生成对应的RGB像素数据,绘制小球、挡板和砖块。 -
彩色条纹模块(color_bar.v)
在待机状态下显示测试用的彩色条纹,用于验证VGA输出是否正常。
2.2 关键接口信号
各模块通过以下主要信号互联:
- 时钟与复位:50MHz系统时钟(clk)和全局复位信号(reset)
- VGA输出:hsync、vsync同步信号和16位RGB数据
- 用户输入:4个方向按键(up, down, left, right)
- 游戏状态:lose(失败)、win(胜利)标志位
- 对象坐标:小球位置(ball_x, ball_y)、挡板位置(stick_position)
- 碰撞信号:h_collision(水平碰撞)、v_collision(垂直碰撞)
2.3 数据流设计
系统的工作流程如下:
- VGA时序模块持续生成扫描线坐标(xcnt, ycnt)
- 图形绘制模块根据当前游戏对象位置生成像素数据
- 按键消抖模块滤波后的信号控制挡板移动
- 每帧结束时(vsync上升沿),碰撞检测模块计算碰撞情况
- 小球运动模块根据碰撞结果更新速度和位置
- 游戏主控模块检测胜利/失败条件,切换游戏状态
这种流水线式的设计确保了显示刷新与游戏逻辑更新的同步进行。
3. 核心模块实现细节
3.1 VGA时序生成
VGA显示需要严格遵循时序规范。我们的设计针对800×525分辨率,实际可见区域为640×480:
verilog复制// 水平时序参数
parameter H_DISP = 640; // 可见区域
parameter H_FP = 16; // 前沿
parameter H_SYNC = 96; // 同步脉冲
parameter H_BP = 48; // 后沿
parameter H_TOTAL = 800; // 总周期
// 垂直时序参数
parameter V_DISP = 480;
parameter V_FP = 10;
parameter V_SYNC = 2;
parameter V_BP = 33;
parameter V_TOTAL = 525;
时序生成通过两个计数器实现:
verilog复制always @(posedge vga_clk) begin
if (hcnt == H_TOTAL-1) begin
hcnt <= 0;
if (vcnt == V_TOTAL-1) vcnt <= 0;
else vcnt <= vcnt + 1;
end else begin
hcnt <= hcnt + 1;
end
end
// 同步信号生成
assign hsync = (hcnt >= H_DISP+H_FP) && (hcnt < H_DISP+H_FP+H_SYNC);
assign vsync = (vcnt >= V_DISP+V_FP) && (vcnt < V_DISP+V_FP+V_SYNC);
注意:不同显示器对同步信号极性要求可能不同,实际项目中需要根据硬件规格调整hsync和vsync的极性。
3.2 按键消抖设计
机械按键的接触抖动通常持续10-20ms。我们的消抖方案采用20ms滤波窗口:
verilog复制module key_deb(
input clk, // 50MHz时钟
input rst_n, // 低电平复位
input key_in, // 原始按键输入
output reg key_out // 消抖后输出
);
reg [19:0] cnt; // 20位计数器(50MHz下可计1ms)
reg key_reg; // 按键状态寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 0;
key_reg <= 1;
key_out <= 0;
end else begin
if (key_in != key_reg) begin // 检测到变化
cnt <= 0; // 重置计数器
key_reg <= key_in; // 保存新状态
end else if (cnt < 20'd1_000_000) begin // 1ms*1000=20ms
cnt <= cnt + 1;
end
// 稳定20ms后输出
if (cnt == 20'd999_999) key_out <= key_reg;
else key_out <= 0;
end
end
endmodule
这种设计能有效滤除抖动,同时保持按键响应的实时性。实际测试中,20ms的消抖时间在各种机械键盘上表现良好。
3.3 游戏状态机设计
游戏主控采用简单的两状态机:
verilog复制parameter IDLE = 1'b0; // 待机状态
parameter GAME = 1'b1; // 游戏状态
reg status; // 当前状态
reg nxt; // 下一状态
always @(posedge clk or negedge reset) begin
if (!reset) status <= IDLE;
else status <= nxt;
end
always @(*) begin
case (status)
IDLE: nxt = up_key ? GAME : IDLE; // 按上键开始游戏
GAME: nxt = (lose || win) ? IDLE : GAME; // 游戏结束返回待机
endcase
end
状态转换条件清晰明了:
- 待机状态下按"上"键进入游戏
- 游戏过程中检测到胜利(win)或失败(lose)时返回待机
3.4 小球运动物理模型
小球运动采用8方向状态机设计,每个方向对应不同的速度分量:
verilog复制localparam SPEED = 3; // 移动步长
localparam BALL_R = 5; // 小球半径
// 运动方向定义
parameter D0 = 3'd0; // 右上
parameter D1 = 3'd1; // 右下
parameter D2 = 3'd2; // 左下
parameter D3 = 3'd3; // 左上
// 其他方向省略...
reg [2:0] ball_state; // 当前运动方向
reg [9:0] ball_x, ball_y; // 小球坐标
always @(posedge vga_clk or negedge reset_n) begin
if (!reset_n) begin
ball_x <= 320; // 初始居中
ball_y <= 240;
ball_state <= D0;
end else if (vsync) begin // 每帧更新一次
case (ball_state)
D0: begin // 右上方向
ball_x <= ball_x + SPEED;
ball_y <= ball_y - SPEED;
if (ball_x + BALL_R >= 640) ball_state <= D1; // 右边界反弹
else if (ball_y - BALL_R <= 0) ball_state <= D3; // 上边界反弹
else if (h_collision) ball_state <= D1; // 水平碰撞
else if (v_collision) ball_state <= D3; // 垂直碰撞
end
// 其他方向处理类似...
endcase
end
end
这种设计模拟了真实小球的反弹物理,同时通过状态机实现避免了复杂的三角函数计算。
4. 碰撞检测优化
4.1 原始方案的问题
最初采用逐像素比较的碰撞检测方法:
verilog复制if (ball_x == hcnt && ball_y == vcnt && barrier_status[barrier_index])
barrier_status[barrier_index] <= 0;
这种方法存在严重缺陷:
- 只有当扫描线恰好经过小球中心时才检测碰撞,概率极低
- 无法准确判断碰撞方向
- 对高速运动的小球会出现"穿透"现象
4.2 改进的矩形检测法
优化后的方案在每帧结束时(vsync上升沿)统一检测:
verilog复制// 砖块碰撞检测
for (i = 0; i < 8; i = i + 1) begin
if (barrier_status[i]) begin
brick_left = 64 + i*64; // 砖块左边界
brick_right = brick_left + 64; // 砖块右边界
// 小球边界
ball_left = ball_x - BALL_R;
ball_right = ball_x + BALL_R;
ball_top = ball_y - BALL_R;
ball_bot = ball_y + BALL_R;
// 矩形重叠检测
if (ball_right > brick_left && ball_left < brick_right &&
ball_bot > 64 && ball_top < 80) begin
barrier_status[i] <= 0; // 消除砖块
// 粗略判断碰撞方向
if (ball_y < 64 || ball_y > 80) v_collision <= 1; // 垂直碰撞
else h_collision <= 1; // 水平碰撞
end
end
end
这种基于矩形包围盒的检测方法具有以下优点:
- 每帧只计算一次,效率高
- 能处理各种碰撞情况,包括边角碰撞
- 可以区分碰撞的大致方向
- 不会漏检高速运动的小球
4.3 挡板碰撞的特殊处理
挡板碰撞需要更精确的方向计算,以支持不同反弹角度:
verilog复制// 挡板碰撞检测
if (ball_bot >= 460 && ball_bot <= 468 && // 垂直范围
ball_x >= stick_pos - 24 && ball_x <= stick_pos + 40) begin // 水平范围
// 根据击中位置计算反弹角度
hit_pos = ball_x - (stick_pos - 24);
if (hit_pos < 16) begin // 左侧
ball_state <= D3; // 左上方向
end else if (hit_pos > 48) begin // 右侧
ball_state <= D0; // 右上方向
end else begin // 中间
ball_state <= D3; // 默认左上
end
end
这种设计使得挡板不同位置能产生不同反弹效果,增加了游戏的可玩性。
5. 图形渲染优化
5.1 对象绘制策略
图形绘制模块采用分层渲染的方式:
- 背景层:全屏底色
- 砖块层:根据barrier_status绘制砖块
- 小球层:圆形绘制
- 挡板层:矩形绘制
- 状态层:胜利/失败提示
verilog复制// 绘制优先级控制
if (win || lose) begin
rgb <= win ? 3'b100 : 3'b110; // 红/黄全屏
end else if (ball) begin
rgb <= 3'b011; // 青色小球
end else if (stick) begin
rgb <= 3'b101; // 品红挡板
end else if (barrier) begin
rgb <= barrier_status[barrier_index] ? 3'b010 : 0; // 绿色砖块
end else begin
rgb <= 3'b000; // 黑色背景
end
5.2 圆形绘制算法
小球的圆形绘制采用距离比较法:
verilog复制wire ball = ((hcnt - ball_x)*(hcnt - ball_x) +
(vcnt - ball_y)*(vcnt - ball_y)) <= (BALL_R * BALL_R);
这种算法虽然需要乘法运算,但在FPGA上实现效率尚可,且能产生平滑的圆形边缘。
5.3 颜色编码
采用RGB565格式输出颜色:
verilog复制assign disp_rgb = { {5{rgb[2]}}, {6{rgb[1]}}, {5{rgb[0]}} };
这种格式兼容大多数VGA显示控制器,同时节省硬件资源。
6. 系统调试与优化
6.1 常见问题排查
在开发过程中遇到的主要问题及解决方案:
-
小球穿透砖块
- 原因:碰撞检测时机不当
- 解决:改为在vsync上升沿统一检测
-
按键响应不灵敏
- 原因:消抖时间不足
- 解决:增加消抖时间到20ms
-
画面闪烁
- 原因:绘制时序与VGA扫描不同步
- 解决:使用双缓冲机制,在vsync时更新对象位置
-
游戏速度不稳定
- 原因:直接使用系统时钟计数
- 解决:创建专门的游戏时钟分频
6.2 性能优化技巧
-
流水线设计
将碰撞检测、位置计算等耗时操作分散在不同时钟周期完成。 -
资源复用
多个砖块共用同一套碰撞检测逻辑,通过循环遍历实现。 -
状态编码优化
使用one-hot编码表示小球运动方向,减少状态解码时间。 -
并行计算
利用FPGA的并行特性,同时计算多个对象的碰撞情况。
6.3 时序约束设置
为保证稳定运行,需要添加适当的时序约束:
code复制create_clock -name clk -period 20 [get_ports clk]
set_input_delay -clock clk 5 [all_inputs]
set_output_delay -clock clk 5 [all_outputs]
这些约束确保设计能在50MHz时钟下稳定工作。
7. 项目扩展方向
当前实现是一个基础版本,还可以进一步扩展:
-
多关卡设计
通过ROM存储不同关卡的砖块布局,增加游戏挑战性。 -
特效支持
添加粒子效果、音效等增强游戏体验。 -
计分系统
根据击碎砖块数量和连击次数计算得分。 -
难度调节
随游戏进程逐渐提高小球速度。 -
多人对战
添加双人模式,支持两个挡板对抗。
这些扩展需要更多的FPGA资源和更复杂的状态机设计,但基本原理与当前实现类似。
8. 开发心得与建议
通过这个项目,我总结了以下几点经验:
-
模块化设计至关重要
清晰的模块划分和接口定义能大幅降低调试难度。建议在编码前先绘制详细的模块框图。 -
仿真先行
在下载到FPGA前,务必进行充分的仿真测试。ModelSim等工具能快速定位逻辑错误。 -
时序分析不可忽视
FPGA设计必须考虑时序收敛问题,特别是当时钟频率较高时。 -
文档记录很必要
详细记录每个版本的变化和问题修复过程,这对后续维护非常重要。 -
参考优秀开源项目
学习类似项目的实现方式能少走很多弯路。GitHub上有大量优质的FPGA游戏项目可供参考。
对于初学者,我建议从更简单的项目开始,比如LED控制、数码管显示等,逐步掌握Verilog和FPGA开发的基本技能后再尝试游戏这类复杂设计。