🏠 课程总览 > 第08课
第08课: 贪吃蛇
40×30网格贪吃蛇,吃食物变长+撞墙检测
🏆 蛇吃食物变长
✅ Verilator仿真验证通过
📖 核心概念
- 蛇数据结构:蛇由最多16个坐标对组成:snake_x[0..15], snake_y[0..15]。snake_x[0]是蛇头
- 移动算法:蛇头按方向移动1格,身体每节跟随前一节。不需要移动每一节,只需将头部插入,尾部删除
- 食物生成:使用LFSR伪随机数生成器在新位置放置食物。食物不能出现在蛇身上
💡 关键思路:本课的核心是蛇数据结构——蛇由最多16个坐标对组成:snake_x[0..15], snake_y[0..15]。snake_x[0]是蛇头
💻 Verilog设计代码
设计模块源码——这是你真正要理解的硬件逻辑:
// 第08课: 贪吃蛇 - 蛇吃食物变长
// 第8课: 贪吃蛇 - 蛇吃食物变长
module snake (
input wire clk,
input wire rst_n,
input wire [1:0] dir,
output reg [3:0] snake_len,
output reg [5:0] snake_x [0:15],
output reg [5:0] snake_y [0:15],
output reg [5:0] food_x,
output reg [5:0] food_y,
output reg game_over
);
reg [15:0] tick;
reg [5:0] new_head_x, new_head_y;
reg [7:0] lfsr;
wire feedback = lfsr[7] ^ lfsr[5] ^ lfsr[4] ^ lfsr[3];
integer i;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
snake_len <= 4'd3;
snake_x[0] <= 6'd20; snake_y[0] <= 6'd15;
snake_x[1] <= 6'd19; snake_y[1] <= 6'd15;
snake_x[2] <= 6'd18; snake_y[2] <= 6'd15;
snake_x[3] <= 6'd0; snake_y[3] <= 6'd0;
snake_x[4] <= 6'd0; snake_y[4] <= 6'd0;
snake_x[5] <= 6'd0; snake_y[5] <= 6'd0;
snake_x[6] <= 6'd0; snake_y[6] <= 6'd0;
snake_x[7] <= 6'd0; snake_y[7] <= 6'd0;
snake_x[8] <= 6'd0; snake_y[8] <= 6'd0;
snake_x[9] <= 6'd0; snake_y[9] <= 6'd0;
snake_x[10] <= 6'd0; snake_y[10] <= 6'd0;
snake_x[11] <= 6'd0; snake_y[11] <= 6'd0;
snake_x[12] <= 6'd0; snake_y[12] <= 6'd0;
snake_x[13] <= 6'd0; snake_y[13] <= 6'd0;
snake_x[14] <= 6'd0; snake_y[14] <= 6'd0;
snake_x[15] <= 6'd0; snake_y[15] <= 6'd0;
food_x <= 6'd30;
food_y <= 6'd10;
game_over <= 0;
tick <= 0;
lfsr <= 8'hAC;
end else if (!game_over) begin
tick <= tick + 1;
lfsr <= {lfsr[6:0], feedback};
if (tick == 5000) begin
tick <= 0;
case (dir)
2'b00: begin new_head_x = snake_x[0]; new_head_y = snake_y[0] - 1; end
2'b01: begin new_head_x = snake_x[0]; new_head_y = snake_y[0] + 1; end
2'b10: begin new_head_x = snake_x[0] - 1; new_head_y = snake_y[0]; end
2'b11: begin new_head_x = snake_x[0] + 1; new_head_y = snake_y[0]; end
default: begin new_head_x = snake_x[0] + 1; new_head_y = snake_y[0]; end
endcase
if (new_head_x >= 40 || new_head_y >= 30) begin
game_over <= 1;
end else begin
for (i = 0; i < 16; i = i + 1) begin
if (i < snake_len && snake_x[i] == new_head_x && snake_y[i] == new_head_y)
game_over <= 1;
end
if (!game_over) begin
for (i = 15; i > 0; i = i - 1) begin
if (i <= snake_len) begin
snake_x[i] <= snake_x[i-1];
snake_y[i] <= snake_y[i-1];
end
end
snake_x[0] <= new_head_x;
snake_y[0] <= new_head_y;
if (new_head_x == food_x && new_head_y == food_y) begin
if (snake_len < 15)
snake_len <= snake_len + 1;
food_x <= lfsr[5:0] % 40;
food_y <= lfsr[4:0] % 30;
end
end
end
end
end
end
endmodule
🧪 测试平台(Testbench)
testbench = 你的"手柄+屏幕",模拟输入、验证输出:
/* verilator lint_off WIDTHEXPAND */
/* verilator lint_off WIDTHTRUNC */
/* verilator lint_off UNOPTFLAT */
/* verilator lint_off WIDTHEXPAND */
/* verilator lint_off WIDTHTRUNC */
/* verilator lint_off UNOPTFLAT */
module tb;
reg clk, rst_n;
reg [1:0] dir;
snake uut (
.clk(clk), .rst_n(rst_n), .dir(dir),
.snake_len(),
.snake_x(),
.snake_y(),
.food_x(),
.food_y(),
.game_over()
);
always clk = #10 ~clk;
integer i;
initial begin
$dumpfile("sim.vcd"); $dumpvars(0, tb);
clk = 0; rst_n = 0; dir = 2'b11;
repeat(5) @(posedge clk); rst_n = 1;
$display("=== 贪吃蛇仿真 ===");
$display("蛇吃食物变长");
$display("");
// Test 1: Initial state
$display("--- 测试1: 初始状态 ---");
repeat(10) @(posedge clk);
$display(" 蛇长度=%0d, 头(%0d,%0d)", uut.snake_len, uut.snake_x[0], uut.snake_y[0]);
if (uut.snake_len == 3) $display(" ✅ 蛇初始长度=3");
else $display(" ❌ 蛇初始长度=%0d(期望3)", uut.snake_len);
// Test 2: Snake moves
$display("");
$display("--- 测试2: 蛇移动 ---");
dir = 2'b11;
uut.tick = 4999;
repeat(10) @(posedge clk);
$display(" 蛇头X=%0d", uut.snake_x[0]);
$display(" ✅ 蛇按方向移动");
// Test 3: Eat food
$display("");
$display("--- 测试3: 吃食物变长 ---");
uut.snake_x[0] = 25; uut.snake_y[0] = 15;
uut.food_x = 26; uut.food_y = 15;
dir = 2'b11;
uut.tick = 4999;
repeat(10) @(posedge clk);
$display(" 吃食物后: 蛇长度=%0d", uut.snake_len);
if (uut.snake_len == 4) $display(" ✅ 蛇吃食物后长度+1(4)");
else $display(" ❌ 蛇长度=%0d(期望4)", uut.snake_len);
// Test 4: Eat another food
$display("");
$display("--- 测试4: 再次吃食物 ---");
uut.food_x = uut.snake_x[0] + 1; uut.food_y = uut.snake_y[0];
uut.tick = 4999;
repeat(10) @(posedge clk);
$display(" 再次吃后: 蛇长度=%0d", uut.snake_len);
if (uut.snake_len >= 4) $display(" ✅ 蛇持续吃食物变长");
// Test 5: Direction change
$display("");
$display("--- 测试5: 方向改变 ---");
dir = 2'b01;
uut.tick = 4999;
repeat(10) @(posedge clk);
$display(" 向下移动后: 头(%0d,%0d)", uut.snake_x[0], uut.snake_y[0]);
$display(" ✅ 蛇可以改变方向");
// Test 6: Wall collision
$display("");
$display("--- 测试6: 墙壁碰撞 ---");
uut.snake_x[0] = 39; uut.snake_y[0] = 15;
dir = 2'b11;
uut.tick = 4999;
repeat(10) @(posedge clk);
if (uut.game_over) $display(" ✅ 撞墙游戏结束");
else $display(" ⏳ 游戏继续");
$display("");
$display("✅ 蛇吃食物变长验证通过!");
$display("🏆 成就解锁: 蛇吃食物变长!");
$finish;
end
endmodule
✅ 仿真输出
运行 verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace --build -j 4 -o sim 后的输出:
=== 贪吃蛇仿真 ===
蛇吃食物变长
--- 测试1: 初始状态 ---
蛇长度=3, 头(20,15)
✅ 蛇初始长度=3
--- 测试2: 蛇移动 ---
蛇头X=21
✅ 蛇按方向移动
--- 测试3: 吃食物变长 ---
吃食物后: 蛇长度=4
✅ 蛇吃食物后长度+1(4)
--- 测试4: 再次吃食物 ---
再次吃后: 蛇长度=5
✅ 蛇持续吃食物变长
--- 测试5: 方向改变 ---
向下移动后: 头(27,16)
✅ 蛇可以改变方向
--- 测试6: 墙壁碰撞 ---
✅ 撞墙游戏结束
✅ 蛇吃食物变长验证通过!
🏆 成就解锁: 蛇吃食物变长!
- tb.sv:89: Verilog $finish
🔧 编译和运行
# 编译
verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace \
--build -j 4 -o sim \
-Wno-WIDTHEXPAND -Wno-WIDTHTRUNC -Wno-UNOPTFLAT \
-Wno-TIMESCALEMOD -Wno-STMTDLY -Wno-WIDTH \
-Wno-UNSIGNED -Wno-SELRANGE -Wno-BLKLOOPINIT
# 运行
./obj_dir/sim
# 查看波形(可选)
gtkwave sim.vcd
🎮 实战步骤
1
蛇身移动:每5000个时钟周期移动一次。先计算新头部位置,然后将身体坐标向后复制
2
食物碰撞:新头部位置与食物位置匹配时,snake_len+1(不移除尾部=蛇变长)
3
墙壁碰撞:新头部x>=40或y>=30则游戏结束。注意网格坐标从0开始
4
自身碰撞:新头部位置与蛇身任一节重叠则游戏结束。遍历检查snake_x/y数组
🎮 游戏开发知识
贪吃蛇起源:1976年Gremlin的Blockade是最早的蛇形游戏。1998年诺基亚手机内置版让贪吃蛇风靡全球
链表实现:C语言常用链表实现蛇身,每个节点存储坐标和指向下一节点的指针
游戏节奏:5000周期=50μs(100MHz)太快。实际游戏约100-200ms一步,需要更大的tick阈值
🏆
蛇吃食物变长
✅ Verilator仿真验证通过
🧠 知识扩展
贪吃蛇起源:1976年Gremlin的Blockade是最早的蛇形游戏。1998年诺基亚手机内置版让贪吃蛇风靡全球
链表实现:C语言常用链表实现蛇身,每个节点存储坐标和指向下一节点的指针
游戏节奏:5000周期=50μs(100MHz)太快。实际游戏约100-200ms一步,需要更大的tick阈值
⚡ 性能提示
• 使用--trace选项生成VCD波形文件,用GTKWave查看
• 使用-j 4选项并行编译,加快构建速度
• 使用--build选项让Verilator自动调用make
• 大量$display输出会拖慢仿真速度,验证通过后可以减少打印频率