简介:基于FPGA开发平台的按键控制弹球小游戏工程,面向数字逻辑、EDA或计算机组成原理等课程的本科学生与FPGA入门开发者。工程围绕VGA显示输出设计,完整演示了按键输入处理、游戏逻辑状态机、像素绘制与VGA时序同步信号的配合方法,并涵盖模块划分、时序约束与综合调试等关键环节,适合作为课程设计或综合实验的参考项目。压缩包共有115个文件,大小约3.79MB,包含Verilog源文件与工程备份,还提供Quartus工程文件、编译综合报告、仿真数据库以及可直接烧录到开发板的配置文件,目录结构清晰,便于定位代码、报表与约束设置。目前已有1230人学习,读者可通过完整工程快速复现弹球游戏效果,并在此基础上修改按键逻辑或新增难度等级,从而深入理解VGA显示驱动与有限状态机的实际应用。 最近在FPGA开发板上折腾一个弹球小游戏,用按键控制挡板,VGA输出到显示器。东西本身不复杂,但做下来发现从时序到逻辑一堆细节,踩的坑比预期多不少。这篇就按我自己的思路记录下来,从系统架构到每个模块的实现,再到调试心得,希望能给刚接触FPGA、想做VGA显示或者按键控制逻辑的新手一些参考。
整个项目做完,核心收获是搞懂了三大块:VGA时序怎么产生、按键输入怎么稳定采样、游戏状态怎么用状态机管理。这三块几乎是FPGA图形交互类项目的通用底座,做完这个小游戏,再去看别的显示类项目会顺很多。
1. 项目概述与整体设计思路
1.1 这个项目到底在做什么
先把这个项目拆开看,硬件其实就三部分:FPGA开发板、按键、VGA接口的显示器。开发板上的按键负责控制挡板左右移动,FPGA内部产生VGA信号把球和挡板实时画到屏幕上。游戏规则和最早的打砖块一样:挡板接住弹球,球在屏幕内弹来弹去,掉到底部就结束。
这里面没有用到任何CPU软核,也不跑操作系统,纯逻辑电路实现。FPGA的工作就是每秒钟生成60帧画面,同时不停读取按键状态,更新球的位置和速度,最后把像素颜色通过VGA接口送出去。整个过程是硬件并行在跑,和软件写游戏完全是两种思路。
1.2 为什么选这套方案练手
弹球游戏选FPGA做,而不是单片机和开发板,核心原因是它把FPGA最典型的技术点全串起来了。按键属于异步输入,要处理跨时钟域和抖动;游戏逻辑是典型的状态机和计数器;VGA输出需要精确的行场时序。这三个问题合在一起,就是一个非常完整的数字逻辑设计练习。
另外这个项目的反馈非常直观。写一段代码,烧到板子里,马上就能在显示器上看到效果,成就感比仿真波形强太多。如果之前只做过流水灯或者简单接口实验,这个项目能让你真正体会到“我在用硬件做东西”的感觉。
1.3 开发环境与板卡选型
我用的板子是常见的Artix-7系列开发板,芯片型号为XC7A35T,自带两个按键、四个拨码开关、一个VGA接口。这类板子市面上很常见,如果手上是 cyclone IV或者别的型号,逻辑同理,只需要改引脚约束。
开发工具是Vivado 2019.1,仿真用自带的Vivado Simulator。如果用的是Quartus,操作流程也一样的,只是约束文件格式不同。这里我不太推荐一开始就上复杂的仿真流程,直接上板调,配合逻辑分析仪(ILA)观察关键信号,效率反而更高。
2. 系统架构与核心模块拆解
2.1 顶层框架与信号走向
整个系统在顶层模块内部按功能分成几个子模块,信号走向非常清晰:
- 时钟分频模块:把开发板上的50MHz或100MHz系统时钟,分频出VGA需要的像素时钟
- VGA时序模块:产生行同步、场同步信号,同时输出当前像素坐标
- 按键消抖模块:对按键输入做消抖和边沿检测
- 游戏逻辑模块:维护球的位置、速度、挡板位置,实时判断碰撞
- 显示绘制模块:根据当前像素坐标和游戏状态,输出RGB颜色信号
有人可能觉得一个简单游戏不需要分这么多模块,但FPGA开发最忌把所有逻辑堆在一个always块里。分模块的好处有两个:一是每个模块可以单独仿真,出问题能快速定位;二是后面想加计分显示、加速、关卡,往对应模块里加逻辑就行,不用大改。
2.2 时钟分频:VGA像素时钟的计算
VGA的时序标准里,分辨率640x480@60Hz是比较经典的一档,很多入门项目都用它。这个模式需要的像素时钟是25.175MHz,实际项目中不会那么精确,用25MHz也完全没问题,只是实际帧率会比60Hz稍微低一点点。
我板子上的主时钟是50MHz,所以需要做一个二分频。最简单的写法是:
// 50MHz -> 25MHz像素时钟 reg pix_clk = 0; always @(posedge clk_50m) begin pix_clk <= ~pix_clk; end这里有个关键点:VGA时序模块的所有逻辑都应该跑在pix_clk域里,包括坐标计数器和RGB输出,这样才能保证输出信号的时序一致性。如果混用时钟域,VGA图像会出现撕裂或者闪烁。
2.3 按键消抖与单脉冲生成
按键按下和松开的时候,机械触点会产生几毫秒到几十毫秒的抖动,如果不处理,一个按键会被识别成几十次按下。最常见的消抖方式是延迟采样,等信号稳定后再读取。
我的做法是:按键信号进来先打两拍同步到系统时钟域,然后用一个10ms的计数器做消抖,消抖结束后检测下降沿,输出一个单时钟周期的脉冲。这样做的效果是:每次按下只产生一个有效的“按键事件”,游戏逻辑模块收到这个脉冲后移动挡板,不会出现一次按下挡板跳好几格的问题。
这里建议不要用延时等待的方式消抖,也就是检测到低电平后死等10ms再读一次,因为这种写法会让按键扫描阻塞住其他逻辑。用计数器做“持续采样”的方式,消抖和游戏逻辑并行跑,才是FPGA的思维。
2.4 游戏逻辑:球速、挡板与碰撞判定
游戏逻辑是核心,我用状态机加计数器实现。球的运动用X坐标、Y坐标和两个方向的速度来描述,每个VGA帧(60Hz)更新一次位置。为了方便尺寸计算,我把整个游戏区域的坐标映射到640x480的VGA有效区域:
- 球:8x8像素的方块
- 挡板:80x8像素的长条,固定在屏幕底部
- 挡板只会水平移动,Y坐标固定
球的初始方向是斜上方45度,也就是X方向和Y方向每帧各移动2个像素。碰撞判定逻辑简化成:
- 碰到左右边界,X方向速度取反
- 碰到上边界,Y方向速度取反
- 和挡板水平位置重叠并且Y方向到达挡板高度,Y方向速度取反
- 球Y坐标超过屏幕底部,游戏结束
挡板的移动速度设置为每帧4个像素,比球稍微快一点,操作起来才跟得上。移动太快会不好控制,太慢又接不住球,这个值我反复调过几次,4像素每帧比较合适。
3. VGA时序与显示驱动
3.1 640x480@60Hz的标准时序参数
VGA显示的本质是逐行扫描:电子束从左到右扫一行,然后回到下一行开头,扫完整个屏幕后再从左上角重新开始。FPGA要做的事情就是准确产生行同步和场同步信号,并在有效显示区域输出正确的RGB数据。
标准时序参数如下表:
| 参数 | 像素/行数 | 说明 |
|---|---|---|
| 整行像素 | 800 | 包含消隐和同步 |
| 有效显示 | 640 | 实际可见区域 |
| 行同步脉冲 | 96 | HSYNC低电平持续时间 |
| 行前肩 | 16 | 同步结束到有效数据开始 |
| 行后肩 | 48 | 有效数据结束到同步开始 |
| 整场行数 | 525 | 包含消隐和同步 |
| 有效显示行 | 480 | 实际可见区域 |
| 场同步脉冲 | 2 | VSYNC低电平持续时间 |
| 场前肩 | 10 | 同步结束到有效数据开始 |
| 场后肩 | 33 | 有效数据结束到同步开始 |
代码里核心就是两个计数器,一个数像素、一个数行。我习惯把计数器的取值范围写成0到799和0到524,然后在特定区间拉低同步信号、在特定区间标记有效区域,这样逻辑比较直观。
3.2 RGB颜色输出与绘制逻辑
VGA接口的RGB信号在FPGA上一般是几位就接几位,常见的有3位、6位或者8位。我板子上是RGB各2位,所以总共有64种颜色,做个小游戏颜色完全够用。
绘制逻辑的核心思路是“每个像素点决定自己是什么颜色”。VGA时序模块输出当前像素坐标后,绘制模块在有效区域内比较坐标:
- 如果坐标落在球的位置范围内,输出白色的RGB值
- 如果坐标落在挡板位置范围内,输出蓝色的RGB值
- 如果坐标在底部还有一个“游戏结束”的红色区域标志,输出红色
- 其他情况输出黑色
理解这个思路很重要:FPGA没有“画图”的概念,它只是高速地逐个像素判断当前点该是什么颜色。整个画面60帧每秒刷新,人眼看起来就是一个动态画面。
4. 实操过程与代码细节
4.1 顶层模块与引脚约束
顶层模块的端口定义比较直接:
module pong_top( input wire clk_50m, input wire btn_left, input wire btn_right, output wire vga_hsync, output wire vga_vsync, output wire [1:0] vga_r, output wire [1:0] vga_g, output wire [1:0] vga_b );约束文件里需要把网表和板子的引脚对应起来。如果你用的是和我不同的板子,要重点查一下VGA引脚是直接连到FPGA引脚还是经过了电平转换芯片,这个会直接影响引脚约束的写法。我在这一步花的时间其实比写代码还多,建议先跑一个纯色输出的测试代码,确认VGA通路没问题再做游戏逻辑。
4.2 游戏逻辑的核心Verilog代码
游戏逻辑的代码写出来不长,但有几个地方很容易踩坑。先看主要的更新逻辑:
// 每帧更新一次球的位置 always @(posedge pix_clk) begin if (frame_vsync) begin ball_x <= ball_x + ball_vx; ball_y <= ball_y + ball_vy; // 左右边界 if (ball_x <= 0 || ball_x >= 632) ball_vx <= -ball_vx; // 上边界 if (ball_y <= 0) ball_vy <= -ball_vy; // 挡板碰撞:坐标重叠且到达挡板行位置 if (ball_y >= 464 && ball_y <= 472) begin if (ball_x + 8 >= paddle_x && ball_x <= paddle_x + 80) ball_vy <= -ball_vy; end // 出界判定 if (ball_y > 480) game_over <= 1; end end这里提醒一个容易出的问题:如果直接在if里把ball_vx取反,可能会因为每帧多次命中条件导致速度反复翻转。解决办法是加一个“碰撞处理标志”,每帧只处理一次相同类型的碰撞。
4.3 帧同步信号的处理
代码里用到frame_vsync,这个信号怎么产生要特别小心。如果直接把VGA模块的vsync接过来用,因为vsync低电平持续2行时间,会出现一帧里多个周期都被判定为“帧开始”,导致球的位置更新多次。
我这里用的是vsync上升沿检测后产生一个单周期脉冲,确保每个VGA帧只更新一次游戏状态。这个细节我在仿真时没发现,上板后球的运动明显异常,用ILA抓了一下才发现是这里的问题。
看过很多初学FPGA的代码,大家经常忽略“脉冲”和“电平”的区别。VGA的时序信号是电平,游戏逻辑需要的是事件,中间必须加一个边沿检测转成单脉冲。
5. 调试过程与常见问题排查
5.1 调试图文:上板必踩的几个坑
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 显示器没有画面 | 同步时序不对或RGB引脚未接 | 先输出纯色图像测试VGA通路 |
| 画面偏移、左右滚动 | 行同步或前后肩参数不对 | 严格对照标准参数表,逐项检查计数器阈值 |
| 球移动速度过慢 | 球的位置在像素时钟下每个时钟都更新,导致极快而非极慢,需要检查是否用了帧脉冲 | 确认更新逻辑只在帧脉冲到来时触发 |
| 按键一次触发多次 | 消抖时间不足 | 加长消抖时间到10ms以上 |
| 画面有重影 | 像素时钟频率不匹配 | 检查分频配置,确认是25MHz |
这个表格是我调试过程中真实遇到的问题汇总,其中最麻烦的是第一个:显示器完全没画面,因为问题根源可能在行时序、场时序、RGB任意一环,排查起来没有头绪。我的建议是写一个测试模块,让全屏输出固定颜色,比如绿色,然后从行时序一路往下查,到RGB输出逐段确认。
5.2 按键手感与游戏体验的调优
游戏逻辑调通之后,真正花时间的是手感调试。最开始我把挡板速度设为1像素每帧,结果球速稍微快一点就根本来不及反应。后来改成4像素每帧,又发现按键轻轻一碰挡板就甩到边上。最后在中间做了个折中,并且加上了一个小技巧:根据按键按下的帧率来做倍速移动,就是说按住按键时,每两帧移动一次,移动速度就降下来了。
还有按键消抖时间和游戏响应之间的平衡。消抖时间太长,按键按下到反应有明显延迟;太短,又会出现一次按键触发多次。我用10ms消抖时间,实际体验下来延迟基本感觉不到。
如果你想让游戏更有意思,可以试试给球增加一个“挡板碰撞点决定反弹角度”的机制:球撞到挡板的不同位置,X方向的速度取不同的值,这样玩家可以通过移动挡板来控制球的角度,游戏一下子就有了操控感。我听群里的朋友说,基于这个思路加计分、加砖块、加关卡,已经是另一个量级的项目了。
5.3 仿真与上板调试的效率对比
最后聊一下调试效率的问题。VGA这种图形项目,纯看波形仿真其实很难判断画面是否正确,你只能确认同步信号的周期对不对,但看不出实际图像。我的经验是:先用仿真确认计数器边界值、消抖逻辑、碰撞逻辑没有明显错误,然后直接上板看效果,配ILA观测关键信号。
ILA用好了效率远超仿真。比如球的位置异常,你可以在ILA里触发观察ball_x和ball_y在每个帧脉冲下的变化轨迹,快速定位是坐标更新逻辑出错,还是碰撞判定条件写错了。这种“仿真大概看一下,上板重点调”的方式,是目前我做FPGA项目比较顺手的工作流。
写在最后的体会
这个项目做完之后,最大的感受是:FPGA写游戏,本质上写的是一个并行运行的实时控制系统,和软件里用循环、事件队列的思路完全不同。球的位置更新、VGA扫描、按键检测,三个事情同时在进行,你需要协调的是时间关系和数据通路,而不仅仅是逻辑对不对。
如果接下来想继续深入,可以尝试在这个框架上加一个数码管计分模块,游戏结束用串口发个提示到上位机,或者把球的运动加上重力加速度模拟真实弹跳。每加一个功能,你对FPGA并行设计的理解都会深一层。
最后再分享一个实用的小技巧:调试VGA图像问题的时候,手里准备一张纯色测试图,就是全屏显示红绿蓝三种颜色来回切换,很多显示问题能立刻分辨出来是硬件通路还是逻辑时序的锅。这个技巧帮我省了很多排查时间,也分享给你。
本文还有配套的精品资源,点击获取