FPGA贪吃蛇项目实战:从Verilog状态机到VGA显示的完整设计
2026/9/2 10:22:11 网站建设 项目流程

简介:本资源是一份面向电子类专业本科生及FPGA初学者的课程设计实践套件,聚焦数字系统综合应用能力培养,完整实现基于FPGA的VGA显示贪吃蛇游戏系统。资源包含详实的课程设计报告(含摘要、系统分析、VGA/PS2模块设计原理、状态机建模与关卡逻辑说明)及可在Quartus II 9.1环境下直接编译下载的Verilog HDL工程源码,覆盖VGA时序驱动、PS2键盘解码、蛇身坐标管理、苹果随机生成、碰撞检测、动态积分与关卡切换等核心功能。压缩包共404个文件,以44个.v源文件(含GAME_STATE、PS2_CTL、SNAKE_ROM等关键模块)、67个.cdb工程数据库文件、33个.jpg设计图及19个.html报告页面为主,辅以.hex、.bin、qpf等配置与烧录文件,总大小9.44MB。已有2153人学习下载,内容结构清晰、注释充分,适合作为数字逻辑、EDA技术或嵌入式系统课程的实验参考、课程设计范例及FPGA入门项目实战素材。

1. 项目概述:当贪吃蛇遇上FPGA

最近在整理资料时,翻出了一个压箱底的课程设计项目——“基于FPGA的贪吃蛇小游戏”。这个项目当年花了不少心思,从Verilog代码编写、状态机设计,到最后的板级调试和VGA显示驱动,完整地走了一遍FPGA开发的全流程。项目包里包含了完整的Quartus II 9.1工程源码、设计报告,算是一个比较经典的FPGA入门到进阶的练手案例。今天,我就以这个项目为引子,和大家深入聊聊如何用FPGA来实现一个实时交互的小游戏,这不仅仅是写个游戏那么简单,更是对数字逻辑设计、时序控制、外设驱动等核心硬件设计能力的一次综合考验。

对于电子、微电子或计算机相关专业的学生来说,用FPGA做课程设计是个非常好的选择。它不像纯软件编程那样抽象,你能实实在在地看到代码如何变成硬件电路,如何驱动一个个像素点亮起来,如何响应按键控制蛇的移动。这个贪吃蛇项目,麻雀虽小五脏俱全,它涉及了时钟分频、按键消抖、游戏逻辑状态机、VGA显示时序生成、帧缓冲管理、随机数生成等多个关键模块。无论你是想入门FPGA,还是想找一个综合性的项目来巩固Verilog和数字电路知识,这个案例都有很高的参考价值。接下来,我将拆解整个项目的设计思路、核心模块的实现细节,并分享一些在调试过程中踩过的坑和总结的经验。

2. 整体设计与核心思路拆解

2.1 为什么选择FPGA实现贪吃蛇?

很多人第一个疑问可能是:贪吃蛇用单片机(比如STM32)甚至Python都能轻松实现,为什么非要上FPGA?这个问题直指核心。用FPGA来实现,其目的远非“做一个能玩的游戏”那么简单。核心价值在于实践基于硬件描述语言(HDL)的同步数字系统设计方法

在单片机中,游戏逻辑通常由一个主循环(while(1))和若干中断服务程序构成,程序的执行是顺序的、时间片轮转的。而在FPGA中,我们设计的是并行的硬件电路。蛇的移动、食物的生成、碰撞检测、VGA时序控制、按键扫描这些功能,在理想情况下是同时在硬件中运行的。这要求设计者必须具备清晰的“硬件思维”,即思考如何用寄存器、组合逻辑、状态机等基本元件来构建一个高效、可靠的并行系统。例如,蛇的移动不能像软件那样用一个数组循环移位来实现,而需要设计一个专门管理蛇身坐标的存储与控制逻辑,其移动操作在一个时钟周期内并行更新所有节点。这正是FPGA设计的精髓所在。

2.2 系统顶层架构设计

整个系统的顶层模块(Top Module)就像一个总指挥,负责将所有子模块实例化并正确连接。基于典型的DE2-115或类似开发板(带VGA接口和按键),我们的系统架构可以分解为以下几个核心部分:

  1. 时钟与复位管理模块:开发板提供50MHz的主时钟。我们需要将其分频,产生用于VGA时序的25MHz像素时钟(以驱动640x480@60Hz标准分辨率),以及用于游戏逻辑更新的较低频率时钟(例如每秒更新10次,用于控制蛇的移动速度)。
  2. 输入处理模块:负责扫描四个方向按键。由于机械按键存在抖动,必须集成按键消抖电路,确保每次有效的按键按下只产生一个稳定的控制信号。
  3. 游戏核心逻辑模块:这是整个系统的“大脑”。它包含:
    • 游戏状态机:定义游戏的状态,如“初始化”、“运行”、“暂停”、“游戏结束”、“重新开始”等。
    • 蛇身控制单元:存储蛇的每一节坐标(通常用两个寄存器数组分别存储X和Y坐标),并根据当前移动方向、游戏时钟和碰撞检测结果,更新蛇头位置并移动蛇身。
    • 食物生成单元:在游戏区域内随机生成一个食物坐标。在FPGA中实现真随机数比较困难,通常采用一个线性反馈移位寄存器(LFSR)来生成伪随机数序列。
    • 碰撞检测单元:实时判断蛇头是否撞墙(超出边界)或者撞到自己的身体。同时,也要判断蛇头是否吃到了食物。
  4. 图像生成与VGA控制模块:这是“渲染引擎”。它根据游戏逻辑模块提供的蛇身坐标、食物坐标、游戏状态等信息,结合当前VGA扫描的像素位置(由VGA时序发生器提供),实时决定当前像素点应该显示什么颜色(蛇身色、食物色、背景色、边框色等),并输出对应的RGB信号。
  5. VGA时序发生器模块:严格按照VGA显示标准,生成行同步(HSYNC)和场同步(VSYNC)信号,并输出当前有效的像素坐标(pixel_x, pixel_y)。

这些模块通过顶层模块的连线(wire)和寄存器(reg)有机地结合在一起,形成一个协同工作的片上系统(SoC)。

注意:在划分模块时,要遵循“高内聚、低耦合”的原则。例如,VGA时序发生器只关心如何产生标准的同步信号和像素坐标,它不应该知道屏幕上要画什么。图像生成模块则根据这个坐标去查询“画什么”。这样划分有利于单独调试和代码复用。

3. 核心模块详解与实现要点

3.1 游戏逻辑状态机设计

状态机是游戏控制的核心,它决定了游戏在不同输入和条件下如何流转。一个典型的状态机可以设计如下:

localparam S_IDLE = 3'b000; // 空闲/初始化 localparam S_PLAY = 3'b001; // 游戏进行中 localparam S_PAUSE = 3'b010; // 暂停 localparam S_OVER = 3'b011; // 游戏结束 localparam S_NEW_GAME= 3'b100; // 准备新游戏(清空数据) reg [2:0] current_state, next_state; // 状态转移逻辑(组合逻辑部分) always @(*) begin next_state = current_state; case (current_state) S_IDLE: if (start_game) next_state = S_PLAY; S_PLAY: begin if (pause_pressed) next_state = S_PAUSE; else if (collision_detected) next_state = S_OVER; end S_PAUSE: if (pause_pressed) next_state = S_PLAY; S_OVER: if (restart_pressed) next_state = S_NEW_GAME; S_NEW_GAME: next_state = S_PLAY; // 自动转入游戏状态 default: next_state = S_IDLE; endcase end // 状态寄存器更新(时序逻辑部分) always @(posedge clk or posedge rst) begin if (rst) current_state <= S_IDLE; else current_state <= next_state; end

实现要点与避坑指南

  • 明确状态编码:使用独热码(One-Hot)还是二进制码?对于状态数较少(如少于8个)的情况,二进制码更节省寄存器资源。独热码虽然占用更多触发器,但译码逻辑简单,在高速设计中有时更有优势。本项目状态少,用二进制码即可。
  • 状态输出定义:每个状态应该输出哪些控制信号?例如,在S_PLAY状态,游戏时钟(game_clk)才有效,蛇身才能移动;在S_OVER状态,可以输出一个标志位用于在屏幕上显示“Game Over”字样。
  • 避免锁存器(Latch):在描述组合逻辑的状态转移和输出时,必须确保所有条件分支都已覆盖,或者有默认赋值(next_state = current_state;),否则综合工具会推断出锁存器,这通常是设计错误和不可靠性的来源。

3.2 蛇身移动与存储策略

蛇身本质上是一个坐标队列。最直观的存储方式是使用两个数组(或寄存器组)分别存储X和Y坐标。

// 假设蛇最大长度为256,游戏区域坐标用10位表示(0-1023) reg [9:0] snake_x [0:255]; reg [9:0] snake_y [0:255]; reg [7:0] snake_length; // 当前蛇的长度

移动算法:当游戏时钟有效时,蛇需要向前移动一格。在硬件中,高效的实现不是将整个数组顺序移动,而是采用“头部增长,尾部跟进”的策略,但这需要额外的逻辑来管理尾部。更简单且硬件友好的方法是整体移位

  1. 从蛇尾到蛇头的第二个节点开始,将前一节节点的坐标赋值给后一节(snake_x[i] <= snake_x[i-1])。
  2. 根据当前方向,计算新的蛇头坐标,并赋值给snake_x[0]snake_y[0]

吃到食物的处理:当检测到蛇头与食物坐标重合时,snake_length加1。此时,新的“蛇尾”实际上就是原来最后一节的位置,我们不需要立即为新的节点分配一个坐标,只需要在下一次移动时,保持原来的最后一节不动(即不进行移位覆盖),这样就实现了“增长”。这需要在移位逻辑中增加对索引和长度的判断。

实操心得:蛇身坐标的存储深度(数组大小)决定了游戏的最大长度。要合理估计,避免资源浪费。同时,计算新蛇头坐标时,一定要进行边界检查,防止坐标溢出导致显示错乱或逻辑错误。例如,如果游戏区域是32x24个格子(每个格子16x16像素),那么蛇头坐标的计算应该是模运算:new_head_x = (current_head_x + delta_x + GRID_WIDTH) % GRID_WIDTH;

3.3 VGA显示驱动与图像合成

VGA显示是项目的视觉输出核心,也是最容易出问题的地方。

VGA时序发生器:这部分有严格的标准。以640x480@60Hz为例,你需要精确计算并生成行时序和场时序。通常我们会用一个像素时钟计数器(pixel_counter)和一个行计数器(line_counter)来实现。

// 像素时钟计数器 always @(posedge pix_clk or posedge rst) begin if (rst) pixel_counter <= 0; else if (pixel_counter == H_TOTAL - 1) pixel_counter <= 0; else pixel_counter <= pixel_counter + 1; end // 行计数器 always @(posedge pix_clk or posedge rst) begin if (rst) line_counter <= 0; else if (pixel_counter == H_TOTAL - 1) begin if (line_counter == V_TOTAL - 1) line_counter <= 0; else line_counter <= line_counter + 1; end end // 生成同步信号 assign h_sync = (pixel_counter < H_SYNC_PULSE) ? 1'b0 : 1'b1; assign v_sync = (line_counter < V_SYNC_PULSE) ? 1'b0 : 1'b1; // 生成有效显示区域坐标 assign valid_area = (pixel_counter >= H_SYNC_PULSE + H_BACK_PORCH) && (pixel_counter < H_SYNC_PULSE + H_BACK_PORCH + H_DISPLAY) && (line_counter >= V_SYNC_PULSE + V_BACK_PORCH) && (line_counter < V_SYNC_PULSE + V_BACK_PORCH + V_DISPLAY); assign pixel_x = valid_area ? (pixel_counter - H_SYNC_PULSE - H_BACK_PORCH) : 0; assign pixel_y = valid_area ? (line_counter - V_SYNC_PULSE - V_BACK_PORCH) : 0;

图像生成模块:这个模块根据pixel_x,pixel_y和游戏逻辑模块输出的物体坐标,决定RGB颜色。

always @(posedge pix_clk) begin if (!valid_area) begin rgb <= 12'h000; // 非显示区,输出黑色 end else begin // 将像素坐标转换为游戏网格坐标 grid_x = pixel_x >> 4; // 除以16,假设每个格子16像素宽 grid_y = pixel_y >> 4; // 优先级判断:食物 > 蛇身 > 背景/边框 if ((grid_x == food_x) && (grid_y == food_y)) begin rgb <= 12'hF00; // 食物为红色 end else if (is_snake_body(grid_x, grid_y)) begin rgb <= 12'h0F0; // 蛇身为绿色 end else if (grid_x == 0 || grid_x == GRID_WIDTH-1 || grid_y == 0 || grid_y == GRID_HEIGHT-1) begin rgb <= 12'hFFF; // 边框为白色 end else begin rgb <= 12'h000; // 背景为黑色 end end end

关键调试技巧

  • 先调时序,再调内容:务必先确保VGA时序发生器能输出稳定的图像(哪怕全屏是一种颜色)。可以用SignalTap II逻辑分析仪抓取HSYNC、VSYNC和RGB信号,对照时序图检查。更简单的方法是,先让整个有效显示区域显示一种颜色(如红色),看显示器是否能正确满屏显示红色,没有滚动或偏移。
  • 坐标映射是关键pixel_x/pixel_ygrid_x/grid_y的转换必须准确。一个常见的错误是转换逻辑错误,导致蛇或食物的显示位置偏移。建议先用一个固定的图案(比如在屏幕中心画一个方块)来测试坐标映射是否正确。
  • 颜色深度:根据开发板VDA DAC的位数选择RGB位宽。常见的有12位(4位/色)或24位(8位/色)。代码中的12'hF00表示12位颜色,红色全亮,绿蓝为0。

4. 工程实现与调试全流程

4.1 Quartus II 9.1 工程搭建要点

虽然现在Quartus Prime是主流,但许多老教程和学校实验室仍在使用Quartus II 9.1/13.1等版本。其核心流程是一致的。

  1. 创建工程:指定工程目录、顶层实体名(如snake_game_top)、目标器件型号(如Cyclone IV EP4CE115F29C7)。
  2. 添加设计文件:将编写好的所有.v.vhd文件添加到工程中。
  3. 引脚分配:这是硬件连接的关键一步。根据开发板原理图,将顶层模块的输入输出信号分配到具体的物理引脚上。
    • clk_50m-> 连接到板载50MHz晶振的引脚(如PIN_Y2)。
    • rst_n-> 连接到复位按键的引脚(注意是高电平复位还是低电平复位)。
    • key_up, key_down, key_left, key_right-> 连接到四个方向按键的引脚。
    • vga_hsync, vga_vsync-> 连接到VGA接口的行、场同步引脚。
    • vga_r[3:0], vga_g[3:0], vga_b[3:0]-> 连接到VGA接口的RGB颜色引脚。

    重要提示:务必仔细核对原理图,错误的引脚分配会导致板子无任何反应,这是新手最常遇到的问题之一。可以将分配好的引脚保存为.qsf文件或.tcl脚本,方便下次重用。

  4. 编译设置:在“Settings”中,可以设置编译优化策略。对于这个规模的设计,选择“Balanced”即可。确保“Verilog HDL Input”版本设置正确。

4.2 功能仿真与Modelsim使用

在烧录到板子前,用Modelsim进行仿真能提前发现大量逻辑错误。

  1. 编写Testbench:创建一个顶层模块的Testbench文件(tb_snake_game_top.v)。在Testbench中,你需要:
    • 实例化被测设计(DUT)。
    • 生成时钟和复位信号。
    • 模拟按键输入序列(例如,上电复位后,等待一段时间,然后模拟按下“上”键若干周期)。
    • 可以将一些内部信号(如蛇头坐标、游戏状态、VGA同步信号)添加到波形窗口中观察。
  2. 运行仿真:在Quartus中设置好仿真工具为Modelsim,然后运行“RTL Simulation”。观察波形,检查:
    • 复位后,状态机是否进入S_IDLE
    • 按下开始键后,是否进入S_PLAY
    • 游戏时钟game_clk的周期是否正确?
    • 模拟蛇头碰到边界时,collision_detected信号是否拉高?状态机是否跳转到S_OVER
    • VGA的hsyncvsync信号波形是否符合标准时序?

仿真调试心得:对于游戏逻辑,仿真时可以将游戏时钟频率大幅提高(比如将分频系数改小),这样能在短时间内模拟较长的游戏时间,快速验证吃到食物、撞墙等逻辑。对于VGA时序,由于仿真像素时钟太快,不适合在波形中完整观看一帧,主要检查同步脉冲的宽度和周期是否正确即可。

4.3 板级调试与SignalTap II实战

当仿真通过后,就可以进行全编译并下载到开发板了。如果屏幕上没有预期图像,就需要板级调试。Quartus自带的SignalTap II Logic Analyzer是强大的片上调试工具,相当于一个“数字示波器”。

  1. 插入SignalTap II:在Quartus中新建一个SignalTap II文件(.stp)。
  2. 添加探测信号:将需要观察的内部信号添加进来。对于贪吃蛇项目,建议添加:游戏状态current_state、蛇头坐标snake_x[0]/snake_y[0]、食物坐标、按键消抖后的信号key_pressed、游戏时钟game_clk、碰撞信号collision,以及VGA的hsyncvsync
  3. 设置采样时钟和深度:采样时钟通常选择系统主时钟(50MHz)或像素时钟(25MHz)。深度要足够,以便能捕获到关键事件(如一次完整的按键过程或一次状态转移)。
  4. 触发条件设置:这是高效调试的关键。例如,你可以设置触发条件为collision信号上升沿,这样一旦发生碰撞,逻辑分析仪就会捕获并显示碰撞前后一段时间所有信号的变化,方便你分析碰撞检测逻辑是否在正确的时机被触发。
  5. 编译与下载:将包含SignalTap II的工程重新编译,下载到FPGA。在SignalTap II界面中运行,等待触发条件满足。

踩坑记录:我曾遇到一个Bug,蛇偶尔会“穿墙”。通过SignalTap II抓取发现,在蛇头移动到边界最后一格的同一个时钟沿,由于按键信号和游戏状态更新的时序竞争,导致蛇头坐标在边界检查前就已经被更新为非法值(越界)。解决方法是在计算新蛇头坐标的组合逻辑路径中插入寄存器进行打拍,确保边界检查逻辑有稳定的输入,或者在状态机中确保坐标更新和碰撞检查在明确的先后顺序。

5. 常见问题排查与优化技巧

5.1 典型问题速查表

问题现象可能原因排查思路与解决方案
屏幕无显示或显示不稳定(滚动、闪烁)1. VGA时序参数错误。
2. 像素时钟(pix_clk)不稳定或频率不准。
3. 引脚分配错误(特别是时钟和同步信号)。
1. 用SignalTap抓取hsyncvsync波形,与标准VGA时序图对比。
2. 检查时钟分频模块,确保分频逻辑正确,无毛刺。
3. 双重检查引脚分配文件,确认连接到显示器接口的引脚正确。
蛇或食物显示位置错乱1. 像素坐标到网格坐标的转换公式错误。
2. 蛇身/食物坐标寄存器位宽不足,计算时溢出。
3. 图像生成模块中,判断“是否蛇身”的函数is_snake_body逻辑有误。
1. 在图像生成模块中,将grid_xgrid_y直接赋值给RGB输出(如rgb = {grid_x[3:0], 4'b0, 4'b0}),在屏幕上观察颜色渐变,验证转换是否正确。
2. 检查坐标寄存器的定义,确保能覆盖整个游戏区域。
3. 简化测试:固定蛇身坐标,看方块是否显示在预期位置。
按键控制不灵敏或连击1. 按键消抖模块未工作或参数设置不当(消抖时间太短或太长)。
2. 按键扫描频率与游戏逻辑时钟不匹配。
3. 按键信号被多个时钟域采样,未做同步处理。
1. 增加消抖计数器位数,延长消抖时间(如20ms)。用SignalTap观察原始按键信号和消抖后信号。
2. 确保游戏逻辑只在game_clk的上升沿响应一次按键,避免在一个游戏周期内多次响应。
3. 如果按键扫描时钟与游戏时钟不同源,需在游戏时钟域对按键信号进行两级寄存器同步。
游戏运行速度异常快或慢game_clk分频系数计算错误。计算目标速度对应的分频比。例如,系统时钟50MHz,希望蛇每秒移动10次,则game_clk周期应为0.1秒,即分频计数器需计数50M * 0.1 = 5,000,000次。检查分频计数器逻辑。
吃到食物后蛇长度不变或显示异常1. 食物碰撞检测信号eat_food未正确产生或持续太短。
2. 蛇身增长逻辑有缺陷,新节点坐标未正确初始化或移位逻辑覆盖了它。
3.snake_length计数器溢出或更新逻辑错误。
1. 用SignalTap抓取eat_food、蛇头坐标、食物坐标,看三者是否在同一个时钟周期内对齐。
2. 在增长逻辑处设置断点或添加调试信号,跟踪长度增加时,蛇身坐标数组的变化。
3. 检查snake_length的位宽,确保其最大值不超过数组深度。

5.2 性能与资源优化技巧

虽然贪吃蛇项目对现代FPGA资源消耗很小,但养成优化习惯对复杂设计至关重要。

  1. 逻辑优化

    • 减少关键路径延迟:对于从按键输入到蛇头坐标更新这条长组合逻辑路径,可以通过插入流水线寄存器来切割,提高系统最高运行频率。
    • 使用case代替多层if-else:在描述多路选择时,case语句通常比多层嵌套的if-else语句综合出的电路更规整,速度可能更快。
    • 避免在循环中使用阻塞赋值:在always块中描述移位等操作时,注意Verilog的语义。对于寄存器更新,应使用非阻塞赋值(<=)。
  2. 存储优化

    • 选择合理的存储单元:蛇身坐标数组会被综合成寄存器(Register)还是块存储器(BRAM)?对于小容量数组,工具通常用寄存器实现,速度快但耗逻辑资源。如果蛇身很长(如超过64节),可以考虑用FPGA内部的BRAM来存储,但这需要设计专门的读写地址生成逻辑。本项目蛇身不长,用寄存器即可。
    • 坐标编码:如果游戏区域是32x24=768个格子,直接用10位宽的X和Y坐标存储(共20位/节点)有些浪费。可以采用更紧凑的编码,例如将X和Y合并成一个12位的地址(因为2^12=4096>768),但会增加编解码逻辑的复杂性。需要权衡。
  3. 功耗考虑

    • 时钟门控:对于game_clk这种低频时钟,当其无效时(如游戏暂停状态),可以关闭其翻转,以降低动态功耗。但在小设计中收益不明显,且会增加设计复杂度。
    • 减少不必要的信号翻转:确保在非显示区域,RGB输出为固定值(如0),而不是随着内部逻辑变化而变化,可以减少输出驱动器的功耗。

5.3 功能扩展思路

基础功能实现后,可以尝试以下扩展,让项目更具挑战性和学习价值:

  1. 增加难度等级:通过拨码开关选择不同等级,等级越高,game_clk频率越快(蛇移动越快),或者初始蛇身更长。
  2. 添加音效:利用开发板上的蜂鸣器或音频编解码器,在吃到食物、撞墙、游戏结束时发出不同频率的声音。这需要设计一个简单的PWM或SPI/I2C音频控制模块。
  3. 显示分数与最高分:在屏幕角落用字符或数字显示当前分数和历史最高分。最高分可以存储在FPGA的掉电非易失存储器(如果支持)中,或者每次上电重置。
  4. 更复杂的游戏元素:增加“墙壁”障碍物、随机出现的“减速”或“加速”道具等。
  5. 使用PS/2键盘或UART控制:替换简单的按键,学习更复杂的外设通信协议。

这个基于FPGA的贪吃蛇项目,从单纯的逻辑设计到包含人机交互的完整系统,是一个非常好的数字系统设计教学案例。它强迫你从并行硬件思维的角度去思考问题,将抽象的游戏规则转化为具体的电路行为。调试过程中,从仿真到板级,从SignalTap波形分析到肉眼观察屏幕,这种软硬件协同调试的经验尤为宝贵。希望这份详细的拆解和记录,能为你打开FPGA项目实践的大门,或者在你遇到类似问题时,提供一些排查的思路。工程源码中的每一行,都对应着硬件上的一根连线或一个寄存器,这种掌控感,正是硬件设计的魅力所在。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询