news 2026/9/7 6:06:08

FPGA实战:用Verilog实现贪吃蛇的完整设计思路与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA实战:用Verilog实现贪吃蛇的完整设计思路与避坑指南

简介:这份源码包基于Verilog硬件描述语言实现经典贪吃蛇游戏,面向FPGA入门与进阶学习者、数字逻辑课程设计者,可在VGA显示器上运行,完整演示状态机控制、像素绘制、键盘输入等数字系统设计要点。压缩包共196个文件,大小5.38MB,其中包含25个.v源文件、4个.tdf以及Quartus工程配置(.qpf/.qsf)、初始化数据(.mif)和可烧录文件(.sof/.pof),既适合在软件中仿真学习时序逻辑,也适合直接下载到FPGA开发板运行。已有1168人学习/下载。代码按蛇移动、游戏逻辑、VGA同步、开始/结束画面控制等模块划分,并配有readme说明;借助真实的键盘扫描、DPRAM/BRAM存储蛇身、行场同步信号生成等功能,可以直观理解Verilog状态机设计、异步复位同步释放、跨时钟域处理及VGA显示协议,是课程设计或竞赛练手的完整参考方案。 讲到FPGA学习路上的经典项目,用Verilog写一个贪吃蛇源码,绝对排得上号。不过我要先说一句:这个“贪吃蛇(verilog)”跟你在C语言课设里写的那种终端刷屏版本,压根不是一个难度量级。它等于把状态机设计、异步按键处理、VGA显示时序、伪随机数生成这几个硬核方向全部揉在一个小游戏里,一锅端。

我第一次做这个项目的时候,刚把Modelsim仿真跑通,觉得自己已经明白“时序逻辑”了,结果上板之后要么蛇不动,要么VGA画面整个偏到一边去,要么一吃东西就判定死亡。反反复复折腾了好几个晚上才理清楚。做完之后回头看,这项目虽然名字叫“贪吃蛇”,但对综合能力、仿真思路、时序意识的训练价值,比很多看起来高大上的通信协议模块还要扎实。

这篇文章我就完整记录一下在FPGA上实现Verilog贪吃蛇的思路和踩坑过程,从模块划分、显示方案、蛇身存储,到状态机和仿真验证一步一步说清楚。准备上手的朋友可以直接照着搭,卡在半路的也能对照排查。

1. 项目定位与整体架构:一次练到几个硬技能?

1.1 这个项目到底在练什么

很多人以为贪吃蛇是“入门级小项目”,其实这话只说对了一半。如果只是把C语言那套逻辑搬过来,写个行为级仿真,那确实不难;但一旦你要把游戏真正跑到FPGA板子上,用VGA显示器输出画面,问题就全来了。

首先是状态机。游戏从待机、运行、暂停到死亡,每个状态之间的跳转条件要清晰,而且和蛇的移动节拍、按键输入必须严格同步。其次是异步输入处理。板载按键是物理器件,按下的一瞬间会有抖动,不消抖的话方向会乱跳,经常出现“我按了左键它往上走”这种鬼畜行为。再往下是显示时序。VGA对行场同步信号的时序要求非常严苛,参数差几个时钟周期,画面就会错位或者直接黑屏。最后还有资源意识。你写的每一个寄存器、每一段组合逻辑,最终都会变成芯片上实实在在的LUT和触发器,写代码时就得想着怎么省。

这几块东西,任何一个单独拿出来都能写一篇文章。贪吃蛇这个项目妙就妙在,它把四者天然地串在了一条链路上:按键产生方向,方向驱动状态机,状态机更新蛇身坐标,坐标决定VGA每个像素的颜色。链路不长,但每一环都不能含糊。

1.2 顶层模块划分与数据流

我习惯把整个工程分成五个模块,每个模块职责单一,这样仿真和调试都能分开进行。

模块核心职责关键输出
clk_div板载时钟分频,生成VGA像素时钟和游戏节拍clk_pix、clk_game
key_top按键消抖、方向互斥仲裁key_pulse、dir
game_ctrl游戏主状态机、蛇身环形缓冲、碰撞检测、食物生成grid_ram状态、分数
vga_ctrl生成640x480@60Hz时序,逐像素查询网格状态输出RGBhsync、vsync、rgb
seg_disp把分数显示到板载数码管seg_cs、seg_data

数据流的走向是这样的:玩家按键触发key_top,输出一个稳定的方向脉冲;game_ctrl在每个游戏节拍里读取方向,按状态机逻辑移动蛇头、回收蛇尾、更新真彩网格;vga_ctrl在逐行扫描时根据当前像素坐标反算出它属于哪个游戏格子,查表输出对应颜色;seg_disp则独立把分数刷在数码管上。

我第一次做的时候没想这么细,把按键逻辑、游戏逻辑、显示逻辑全写在一个always块里,结果稍微一动就到处是编译错误,改了A又坏B。后来老老实实拆开,每个模块单独验证,进度反而快了很多。所以这个模块划分不是形式主义,是真的能救命的。

2. 显示、蛇身与食物:三个核心设计决策

2.1 VGA显示方案:网格化与位截断

VGA输出的分辨率我选择标准的640x480@60Hz,像素时钟约25MHz。50MHz板载晶振正好二分频,省去PLL配置的麻烦。把屏幕逻辑上切成格子,每个格子16x16像素,这样整个棋盘就是40列x30行,视觉上蛇身、食物和边框都清晰可辨,而且16正好是2的4次方,像素坐标到格子坐标的换算可以完全用位截断实现,不需要任何除法器。

// 像素坐标 -> 格子坐标:直接右移4位 wire [9:0] grid_col = pixel_x[9:4]; // 0~39 wire [9:0] grid_row = pixel_y[9:4]; // 0~29 wire [9:0] grid_idx = grid_row * 40 + grid_col;

这里有个很关键的思路:显示模块永远不关心蛇身坐标。它只做一件事:根据当前扫描到的像素坐标,算出它在棋盘上的格子编号,然后去查一张“格子状态表”grid_ram,按表里的状态输出颜色。状态的更新完全由game_ctrl负责,显示和逻辑彻底解耦。这样VGA部分就变得非常单纯,几乎不会出bug。

为什么不用8x8像素的小格子?格子越小,屏幕上的分辨率越高,蛇看起来越细腻,但网格数量会变成80x60=4800个,状态表容量和判断逻辑都翻四倍。对于练手项目,16x16已经足够,而且蛇身的“颗粒感”反而让操作判断更明确。49寸大屏上看起来就像经典街机版,很有味道。

2.2 蛇身存储:为什么不用移位寄存器

新手最容易想到的方案是“每拍把整条蛇的坐标全部往后搬一格”,就像C语言里数组整体移动一样。这在Verilog里不是不能写,而是代价很高:蛇身长度假设是64,每拍就要把64个坐标寄存器全部串行移位,逻辑深度随蛇长线性增长,综合后时序很容易恶化,而且FF用量白白浪费。

我采用的方案是环形缓冲区,只更新头尾两个指针。具体做法是定义一组坐标数组,用head_ptr指向蛇头所在的位置,tail_ptr指向蛇尾。每移动一拍,新蛇头坐标写入head_ptr的下一个位置,头指针后移;如果没吃到食物,尾指针也后移,相当于把蛇尾的格子“回收”。

localparam MAX_LEN = 64; // 最大蛇长,可按板子资源调 reg [5:0] body_x [0:MAX_LEN]; // 蛇身X坐标 reg [5:0] body_y [0:MAX_LEN]; // 蛇身Y坐标 reg [6:0] head_ptr; // 头指针 reg [6:0] tail_ptr; // 尾指针 reg [6:0] snake_len;

这个方案的时间复杂度是O(1),无论蛇长多少,每拍只写两个寄存器。代价是需要想清楚指针的回绕问题,也就是数组写满后怎么回到开头。我的做法是限制最大蛇长为64,指针到64就归零,数组深度开MAX_LEN,保证头尾追尾时不会越界。游戏里蛇长超过64基本已经占了大半个屏幕,继续玩下去的难度也极高,所以这个上限完全够用。

2.3 食物生成:LFSR伪随机与冲突兜底

伪随机数在FPGA里最经典的做法是LFSR线性反馈移位寄存器。不需要真正均匀分布的随机数,游戏食物只需要“看起来不规律”就行。我用一个32位的LFSR,每拍移位一次,取其中两段位分别作为食物的X和Y候选。

// LFSR反馈多项式,常见写法,32位最大周期 always @(posedge clk_game or negedge rst_n) begin if (!rst_n) lfsr_q <= 32'hDEADBEEF; else lfsr_q <= {lfsr_q[30:0], lfsr_q[31] ^ lfsr_q[21] ^ lfsr_q[1]}; end // 取模映射到棋盘内部区域 food_x <= (lfsr_q[9:4] % COL) + 1; food_y <= (lfsr_q[19:14] % ROW) + 1;

取模运算在Verilog里如果除数是常数,综合器能优化成乘加结构,不会真的生成一个笨重的除法器,这点可以放心。但随机坐标有可能刚好落在蛇身上,这是食物生成最容易忽略的坑。我的兜底策略是:生成后和蛇身坐标做一次组合比较,如果发现冲突,就统一加一个固定偏移值,比如坐标整体加8后再判断一次;如果还冲突,就再加。虽然不保证绝对完美,但工程上足够用了。

仿真阶段就不要再依赖LFSR了,否则每次跑出来的食物位置都不一样,出问题很难复现。后面我在仿真部分会说一个很实用的技巧:用force命令把食物坐标固定住。

3. 核心实现:游戏状态机与蛇身更新

3.1 游戏主状态机

游戏的状态切换用标准三段式状态机来实现。五个状态:IDLE待机、READY准备、RUN运行、PAUSE暂停、DEAD死亡。按键start作为统一的状态跳转触发,游戏运行中检测到碰撞则进入DEAD。

localparam IDLE = 3'd0; localparam READY = 3'd1; localparam RUN = 3'd2; localparam PAUSE = 3'd3; localparam DEAD = 3'd4; reg [2:0] game_state; always @(posedge clk_sys or negedge rst_n) begin if (!rst_n) begin game_state <= IDLE; end else begin case (game_state) IDLE: if (key_start) game_state <= READY; READY: if (key_start) game_state <= RUN; RUN: begin if (key_start) game_state <= PAUSE; else if (hit_wall || hit_self) game_state <= DEAD; end PAUSE: if (key_start) game_state <= RUN; DEAD: if (key_start) game_state <= READY; endcase end end

这里有个细节很多人会忽略:如果不加READY准备状态,玩家一上电直接进RUN,蛇会立刻开始动,根本没有时间观察初始画面。加了READY之后,第一次按start只是“摆好棋盘等开跑”,再按一次才真正开始,整体的使用体验要合理很多。

3.2 蛇头移动、蛇尾回收与撞墙检测

这部分是整个项目的核心,也是最容易写出错的地方。完整的蛇身更新逻辑长这样:

// 方向向量:0上 1右 2下 3左 wire signed [5:0] dir_dx = (dir == 1) ? 1 : (dir == 3) ? -1 : 0; wire signed [5:0] dir_dy = (dir == 2) ? 1 : (dir == 0) ? -1 : 0; wire [5:0] next_x = head_x + dir_dx; wire [5:0] next_y = head_y + dir_dy; wire eat = (next_x == food_x) && (next_y == food_y); always @(posedge clk_game or negedge rst_n) begin if (!rst_n) begin head_ptr <= 0; tail_ptr <= 0; snake_len <= 3; end else if (game_state == RUN) begin // 未吃到食物时,尾巴回收 if (!eat) tail_ptr <= (tail_ptr == MAX_LEN) ? 7'd0 : tail_ptr + 7'd1; // 头指针后移并写入新蛇头 head_ptr <= (head_ptr == MAX_LEN) ? 7'd0 : head_ptr + 7'd1; body_x[(head_ptr == MAX_LEN) ? 7'd0 : head_ptr + 7'd1] <= next_x; body_y[(head_ptr == MAX_LEN) ? 7'd0 : head_ptr + 7'd1] <= next_y; if (eat) begin snake_len <= snake_len + 1; // 这里交给食物生成模块刷新 food_x/food_y end end end

核心逻辑其实就三步:先算新蛇头坐标,然后根据是否吃到食物决定蛇尾是否移动,最后把新蛇头写入缓冲区的下一个位置。头尾指针的回绕条件必须一致,否则蛇长超过数组深度时,头尾会互相覆盖,表现出来就是蛇身莫名其妙“断成两截”。

撞墙检测很简单,只要判断next_x和next_y是否落在棋盘边界外就可以。注意边界本身我是在grid状态里当作墙壁绘制的,所以格子坐标1~38才是可移动区域:

wire hit_wall = (next_x == 0) || (next_x == COL-1) || (next_y == 0) || (next_y == ROW-1);

蛇身自碰检测稍微麻烦一点,需要遍历蛇身数组,看新蛇头坐标是否和已存在的某一节身体重合。我直接用组合逻辑遍历整个数组:

reg hit_self; integer i; always @(*) begin hit_self = 1'b0; for (i = 0; i <= MAX_LEN; i = i + 1) begin if ((i != head_ptr) && (body_x[i] == next_x) && (body_y[i] == next_y)) hit_self = 1'b1; end end

这种写法综合成硬件会有大约64个比较器并行工作,不算小,但在现代FPGA上完全能吃下。如果你追求极致资源优化,可以维护一个格子占用表grid_ram,直接查目标格子的状态是否为蛇身,一步到位。不过这个优化会引入额外的状态维护逻辑,代码复杂度上升,练手阶段先用遍历方案更直观。

3.3 方向仲裁与按键消抖

按键消抖我采用计数法:按键输入为低(按下)时开始计数,持续约20ms仍然为低才认为按键有效。20ms是一个经验值,太短防不住抖动,太长会让人感觉按键“延迟”。这里把四个按键做成4路脉冲输出key_pulse。

方向仲裁最关键的一点是禁止反向掉头。蛇正在向右走,玩家按左键,这个方向必须被忽略,否则蛇头会直接穿进身体里。实现上就是比较当前方向寄存器的值:

always @(posedge clk_sys or negedge rst_n) begin if (!rst_n) dir <= RIGHT; // 初始向右 else if (game_state == RUN) begin if (key_pulse[0] && dir != RIGHT) dir <= LEFT; else if (key_pulse[1] && dir != LEFT) dir <= RIGHT; else if (key_pulse[2] && dir != DOWN) dir <= UP; else if (key_pulse[3] && dir != UP) dir <= DOWN; end end

还有一个容易被忽略的时序问题:方向寄存器的更新是在系统时钟域,蛇头移动是在游戏节拍时钟域。如果玩家在一个游戏节拍内快速按了两下不同方向,理论上蛇应该在下一个节拍只移动一次,但方向寄存器已经变了两次,导致蛇“瞬移转向”。我的解决办法是把方向寄存器改成“下一拍生效”,即key_pulse产生后先打一拍寄存,等到clk_game上升沿来时再统一采样方向。这样即便节拍内多次按键,最终蛇头移动方向只采用节拍到达瞬间的方向值,不会出现穿模。

4. 仿真、上板与问题排查实录

4.1 用testbench分阶段验证

整个项目直接烧板子调试是最低效的方式,因为一旦出错,你根本不知道是状态机问题、VGA时序问题还是按键问题。我在Modelsim里把验证拆成了三个阶段。

第一个阶段只验证VGA时序。写一个专门的testbench,例化vga_ctrl模块,检查hsync和vsync的极性、周期、脉宽是否严格符合640x480@60Hz标准,并定时采样有效区域的像素坐标。没画蛇没关系,只要能看到整屏底色输出,说明显示通路已经通了。

第二个阶段验证游戏逻辑。这里有个很实用的技巧:把游戏节拍在仿真时加速到和系统时钟一致。比如真实游戏中蛇每0.2秒移动一格,仿真时直接把分频参数设成1,这样每个时钟周期蛇都走一步,原本要几分钟才能吃到的食物,在波形里几十个时钟周期就能看到完整的状态流转。验证完逻辑再改回真实分频上板,速度非常快。

第三个阶段针对食物随机问题。LFSR每次仿真生成的坐标不同,不利于复现问题。testbench里直接用force命令把食物坐标固定:

module snake_tb; reg clk = 0; reg rst_n = 0; always #10 clk = ~clk; // 50MHz snake_top u_top(.clk(clk), .rst_n(rst_n)); initial begin #1000; rst_n = 1; #2000; force u_top.food_x = 6'd20; force u_top.food_y = 6'd12; #100000; $finish; end endmodule

force命令可以把内部寄存器强制设为指定值,绕过LFSR的逻辑。等需要验证随机数模块自身的时候,再取消force,让LFSR正常跑。这样能把“食物生成”和“蛇身更新”两个问题域彻底分开。

4.2 高频问题排查速查表

把实际调试中遇到的典型问题和排查思路整理成一张表,遇到类似现象可以照着查。

现象可能原因排查方向
整屏黑屏或花屏VGA行场同步极性反了,像素时钟分频不对检查hsync/vsync的极性,对照VGA标准时序逐项比对
画面偏左或偏右行同步参数中的back porch不对核对行周期的前肩、同步、后肩计数
蛇几乎不动游戏节拍分频信号没正确产生仿真查看clk_game波形,确认是否有周期性的脉冲
按键方向不受控消抖时间太短,或方向互斥没做调大消抖计数,检查方向仲裁逻辑
一吃东西就死食物比较与更新时序错位食物坐标统一打一拍寄存器后用于比较
食物卡在蛇身体里LFSR生成坐标与蛇身冲突没有兜底增加冲突偏移逻辑,检查生成后比较的时机
综合后资源爆炸蛇身数组过大或有大量全数组遍历调小MAX_LEN,避免在always里写太多并行比较

这里单独说一下“一吃东西就死”这个坑。我第一次实现时,食物坐标在吃到食物的同一拍就立即更新,但碰撞检测用的是组合逻辑立即判定的旧坐标,导致蛇头明明已经踩到食物格子,吃食物标志却要等到下一拍才拉高。这时蛇头可能刚好在棋盘边界,下一拍判定撞墙,表现就是“吃了就死”。解决方法是:食物更新的时机必须和蛇头移动错开一拍,或者干脆把所有和食物相关的比较统一使用同一组寄存器值,不要用组合逻辑边算边更新。

4.3 上板前资源与时序检查

上板之前还有几件事值得花十分钟确认,能省下很多现场排障的时间。

第一,打开综合报告,看逻辑单元和触发器使用率。蛇身数组占用的寄存器数量很大,MAX_LEN设得过大在资源受限的小型FPGA上很危险。我刚开始设成128,在某个小芯片上综合直接爆了,调成64才通过。第二,检查复位电路。FPGA上电后如果没有一个可靠的复位信号,状态机和指针初始值不确定,表现就是整个画面乱闪。我习惯用异步复位同步释放,电路简单且能避免复位释放时的亚稳态。第三,时钟约束。这个项目系统时钟只有50MHz,逻辑路径又不长,一般不需要特别约时序,但如果你在工程里配置了PLL,最好给所有时钟加上create_clock约束,让时序报告有参考依据。

如果你用的是带有HDMI输出或者特殊VGA接口的开发板,还要确认连接器的引脚映射和板卡手册一致。有一条信号反了画面就会偏色或者不同步,这种问题查起来最费时间。

如果让我给第一次做这个项目的朋友提一个建议:不要沉迷在“必须一步到位做出完整可玩游戏”的目标里。先把VGA显示跑通,再让蛇动起来,再吃食物,再考虑暂停和死亡界面。我当初就是贪多,想一口气全做出来,结果卡在VGA显示上耗了大半天,后面所有逻辑都没法验证。后来老老实实一步步来,整个项目反而两晚上就收工了。

最后再分享一个小技巧:仿真阶段把游戏节拍加速到和系统时钟一样,原本要跑很久才能触发的食物刷新和碰撞判定,在波形里一个tick就能看到。这个项目真正做完之后,你回头再看状态机、再看Modelsim的波形、再看综合报告,都会有一种“原来如此”的通透感。贪吃蛇看似小,里头的门道真的不少。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 6:05:28

FanControl 风扇控制指南:10分钟画出一条安静的转速曲线

FanControl 风扇控制指南&#xff1a;10分钟画出一条安静的转速曲线 【免费下载链接】FanControl.Releases This is the release repository for Fan Control, a highly customizable fan controlling software for Windows. 项目地址: https://gitcode.com/GitHub_Trending/…

作者头像 李华
网站建设 2026/9/7 6:04:33

Buzz 离线语音转文字:Whisper 本地音频转录,4 步导出字幕

Buzz 离线语音转文字&#xff1a;Whisper 本地音频转录&#xff0c;4 步导出字幕 【免费下载链接】buzz Buzz transcribes and translates audio offline on your personal computer. Powered by OpenAIs Whisper. 项目地址: https://gitcode.com/GitHub_Trending/buz/buzz …

作者头像 李华
网站建设 2026/9/7 6:03:23

秋叶ComfyUI旗舰版:全中文AI绘画工具本地部署指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 6:03:08

ODAS开源嵌入式听觉系统:从原理到板级实作全攻略

简介&#xff1a;ODAS&#xff08;开放分布式音频系统&#xff09;是一套专为嵌入式设备设计的开源实时音频处理框架&#xff0c;面向机器人、无人机、自动驾驶及智能硬件开发者&#xff0c;解决资源受限环境下声源定位、音频跟踪、波束形成与声音分离等复杂听觉问题。随包共33…

作者头像 李华