第08课: 贪吃蛇

40×30网格贪吃蛇,吃食物变长+撞墙检测

🏆 蛇吃食物变长 ✅ Verilator仿真验证通过

📖 核心概念

💡 关键思路:本课的核心是蛇数据结构——蛇由最多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输出会拖慢仿真速度,验证通过后可以减少打印频率