硬核 FPGA:用 Verilog 搓一个贪吃蛇游戏

前言:软件工程师不懂的浪漫

对于软件工程师来说,写一个贪吃蛇也就是几百行 Python/C++ 的事情。
但对于硬件工程师来说,这意味着:

  • 没有操作系统帮你管理内存。
  • 没有显卡驱动帮你画图,每一个像素点的 RGB 信号都要你亲自控制。
  • 没有 CPU 帮你顺序执行指令,所有的逻辑都是并行发生的。

今天,我们要用 Verilog HDL (Hardware Description Language) 在一块普通的 Cyclone IV FPGA 开发板上,实现经典的贪吃蛇游戏,并通过 VGA 接口显示在显示器上。

这将是一场关于时序、状态机和逻辑门电路的硬核探险。

1. VGA 显示原理:电子束的独舞

要显示图像,首先得搞定 VGA 时序。
VGA 标准(这里以 640x480 @ 60Hz 为例)要求我们控制两个核心信号:

  • HSYNC (行同步): 告诉显示器换行了。
  • VSYNC (场同步): 告诉显示器这一帧画完了,回到左上角。

根据标准,我们需要一个 25.175 MHz 的像素时钟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
module vga_driver(
input wire clk, // 25MHz Pixel Clock
input wire rst_n,
output wire hsync,
output wire vsync,
output wire [9:0] pixel_x,// 当前扫描到的 x 坐标
output wire [9:0] pixel_y,// 当前扫描到的 y 坐标
output wire video_on // 是否在显示区域(非消隐区)
);

// 640x480 时序参数
localparam H_DISPLAY = 640;
localparam H_FRONT = 16;
localparam H_SYNC = 96;
localparam H_BACK = 48;
localparam H_TOTAL = 800;

// 省略 V 参数定义...

reg [9:0] h_cnt;
reg [9:0] v_cnt;

// 行计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
h_cnt <= 0;
else if (h_cnt == H_TOTAL - 1)
h_cnt <= 0;
else
h_cnt <= h_cnt + 1;
end

// 场计数器逻辑...

assign hsync = (h_cnt >= H_DISPLAY + H_FRONT) && (h_cnt < H_DISPLAY + H_FRONT + H_SYNC) ? 0 : 1;
// ...
endmodule

看,硬件就是这么直接,你是在用计数器直接去控制显示器的电子枪!

2. 游戏核心逻辑:有限状态机 (FSM)

贪吃蛇的逻辑本质上是一个状态机。

stateDiagram-v2
    [*] --> IDLE
    IDLE --> PLAY: Start Button
    PLAY --> DIE: Hit Wall/Body
    DIE --> IDLE: Retry Button
    
    state PLAY {
        axis_x: Update Snake Position
        axis_y: Check Collision
        axis_z: Render Apple
    }

蛇身的存储

蛇身怎么存?在 CPU 上我们会用 LinkedList。但在 FPGA 上,动态内存分配是不存在的。
我们通常用 Register Array (寄存器数组)

1
2
3
4
parameter MAX_LENGTH = 16;
reg [5:0] snake_x [0:MAX_LENGTH-1]; // 蛇身每节的 X 坐标
reg [5:0] snake_y [0:MAX_LENGTH-1]; // 蛇身每节的 Y 坐标
reg [4:0] length; // 当前长度

移动逻辑

蛇的移动,其实就是:

  1. i 节的位置变成第 i-1 节的位置(从尾巴开始循环)。
  2. 蛇头(第 0 节)根据当前方向(上/下/左/右)更新坐标。

这在软件里是一个 for 循环。但在 Verilog 里,这是一个并在的赋值,在一个时钟周期内完成!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
always @(posedge logic_clk) begin
if (state == PLAY) begin
// 更新身体位置 (移位寄存器)
for (i = MAX_LENGTH-1; i > 0; i = i - 1) begin
if (i <= length) begin
snake_x[i] <= snake_x[i-1];
snake_y[i] <= snake_y[i-1];
end
end

// 更新蛇头
case (direction)
UP: snake_y[0] <= snake_y[0] - 1;
DOWN: snake_y[0] <= snake_y[0] + 1;
LEFT: snake_x[0] <= snake_x[0] - 1;
RIGHT: snake_x[0] <= snake_x[0] + 1;
endcase
end
end

注意:这里不能直接用 VGA 的像素时钟 (25MHz),蛇跑得太快了人眼看不见。我们需要一个分频后的 logic_clk,比如 5Hz(每秒走 5 格)。

3. 图像渲染:即时合成

FPGA 没有显存(或者说片上 Block RAM 很贵,存不下一整帧 640x480 的图)。
所以我们通常采用 即时合成 (Just-In-Time Rendering) 的策略。

VGA 驱动告诉你当前扫描到 (pixel_x, pixel_y) 了,你就在下一刻告诉它这个点是什么颜色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
wire is_snake_body;
wire is_apple;

// 判断当前扫描点是不是蛇身
// 这里需要遍历所有蛇身节点,生成一个组合逻辑信号
// 可能会导致时序违例 (Timing Violation),实际要优化
assign is_snake_body = (snake_x[0] == pixel_x[9:4]) && (snake_y[0] == pixel_y[9:4]) || ...

// 颜色输出选择器
always @* begin
if (!video_on)
rgb = 12'h000; // 黑屏
else if (is_wall)
rgb = 12'hFFF; // 墙是白色
else if (is_apple)
rgb = 12'hF00; // 苹果是红色
else if (is_snake_body)
rgb = 12'h0F0; // 蛇是绿色
else
rgb = 12'h000; // 背景黑
end

这种方式不需要 Frame Buffer,极其节省内存,但对逻辑延迟要求很高。如果判断逻辑太复杂,赶不上像素时钟,画面就会抖动。

4. 总结:并行思维的力量

写完这个 FPGA 贪吃蛇,你最大的感触会是思维方式的转变。
在 CPU 上,我们思考的是步骤:先做A,再做B。
在 FPGA 上,我们思考的是时空:在时钟上升沿到来那一瞬间,所有信号同时改变。

FPGA 让我们可以用积木搭建起属于自己的简易计算架构。虽然用来写贪吃蛇有点杀鸡用牛刀,但同样的逻辑,改一改就能用来做 高频交易系统的行情解析 或者 AI 推理的矩阵由速器。这就是硬件的魅力。

下一篇,我们将深入计算机体系结构,去探究 CPU 内部是如何通过乱序执行来榨干每一滴性能的。


硬核 FPGA:用 Verilog 搓一个贪吃蛇游戏
https://www.qixyuan.top/2025/05/15/6-fpga-snake-game/
作者
QixYuan
发布于
2025年5月15日
许可协议