2026/9/7 7:29:18

FPGA实战:VGA弹球游戏中的时序、消抖与状态机设计

FPGA实战:VGA弹球游戏中的时序、消抖与状态机设计 简介基于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的时序标准里分辨率640x48060Hz是比较经典的一档很多入门项目都用它。这个模式需要的像素时钟是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 640x48060Hz的标准时序参数VGA显示的本质是逐行扫描电子束从左到右扫一行然后回到下一行开头扫完整个屏幕后再从左上角重新开始。FPGA要做的事情就是准确产生行同步和场同步信号并在有效显示区域输出正确的RGB数据。标准时序参数如下表参数像素/行数说明整行像素800包含消隐和同步有效显示640实际可见区域行同步脉冲96HSYNC低电平持续时间行前肩16同步结束到有效数据开始行后肩48有效数据结束到同步开始整场行数525包含消隐和同步有效显示行480实际可见区域场同步脉冲2VSYNC低电平持续时间场前肩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图像问题的时候手里准备一张纯色测试图就是全屏显示红绿蓝三种颜色来回切换很多显示问题能立刻分辨出来是硬件通路还是逻辑时序的锅。这个技巧帮我省了很多排查时间也分享给你。本文还有配套的精品资源点击获取