本文介绍基于verilog语言开发的贪吃蛇小游戏,FPGA板卡至少需要900个LC(文中程序需求1700个LC,可通过删减部分代码得到)、4个独立按键以及1个VGA接口。本文使用的板卡配有50MHz晶振,所配备的程序生成800*600*60Hz驱动信号,采用其它晶振频率的板卡需要对VGA驱动渲染模块(本文中为render)进行相应修改,使得刷新脉冲符合显示屏输入信号要求。
注意:本文中使用snack代指snake,这是由于最初的错误累积而成,不得已对其进行忽略。
贪吃蛇工程由7个模块构成,结构层级如图1所示。其中包含1个顶层模块snack、3个功能模块(random_box、render、snack_control)以及3个底层模块(keycheck、random、box_create)。本文设计的结构层次存在一定的瑕疵,其修正方法会在各个模块中提出,但由于本人水平有限,也有部分问题未能解决,欢迎各位能够与我进行探讨。
图1 贪吃蛇工程结构层次
顶层模块主要规定了工程对外的各类接口,以及各个功能模块之间的关系。在本文中,顶层模块调用了三个功能模块(random_box、render、snack_control),作为游戏功能的分区设计。将整个工程所需要实现的功能肢解为随机小盒子(即苹果或被吃物的创建与更新)、VGA驱动脉冲生成与随机小盒子画面渲染、蛇的控制(移动方向、体长、行为检测)。其RTL视图如图2所示。
图2 顶层模块RTL视图
在此处,出现了本文的第一个设计瑕疵:功能划分不清。在设计中,将随机小盒子的画面渲染放到render模块中,却将蛇的渲染放置到snack_control模块中,造成了模块的功能混淆。更加理性地办法是将随机小盒子的渲染放到random_box模块中,render模块只负责VGA驱动信号的生成以及画面总体渲染。这样工程模块的划分也更加清晰,任务也更加明确。
module snack(
input clk,
input rst_n,
input key_r,
input key_l,
input key_u,
input key_d,
output hsync,
output vsync,
output vga_r,
output vga_g,
output vga_b
);
//贪吃蛇游戏分为:box生成模块、蛇身控制模块、画面渲染模块
//蛇:红色
//box:绿色
//背景:蓝色
//--------------------------------------------------------------------------------
//画面渲染位置
wire [9:0]x_pos;
wire [9:0]y_pos;
//box坐标传递
wire[9:0]box_x;
wire[9:0]box_y;
//--------------------------------------------------------------------------------
//蛇身控制模块,蛇是红色的
wire drive;
wire snack_r;
snack_control u1_snack_control(
.clk(clk),
.rst_n(rst_n),
.key_r(key_r),
.key_l(key_l),
.key_u(key_u),
.key_d(key_d),
.box_x(box_x),
.box_y(box_y),
.x_pos(x_pos),
.y_pos(y_pos),
.drive(drive),
.snack_r(snack_r)
);
//--------------------------------------------------------------------------------
//box生成模块
random_box u1_random_box(
.clk(clk),
.rst_n(rst_n),
.drive(drive),//生成驱动信号
.box_x(box_x),//坐标信号
.box_y(box_y)
);
//--------------------------------------------------------------------------------
//画面渲染模块
render u1_render(
.clk(clk),
.rst_n(rst_n),
.box_x(box_x),
.box_y(box_y),
.snack_r(snack_r),
.hsync(hsync),
.vsync(vsync),
.x_pos(x_pos),
.y_pos(y_pos),
.vga_r(vga_r),
.vga_g(vga_g),
.vga_b(vga_b)
);
endmodule
该模块主要负责随机小盒子的生成与更新。盒子的坐标来自于LFSR伪随机码生成器产生的两个相邻数据,为了获取完整的随机坐标,必须等待2个时钟周期。
module random_box(
input clk,
input rst_n,
input drive,
output wire[9:0]box_x,
output wire[9:0]box_y
);
//---------------------------------------------------------------------------------
//随机数生成模块
wire [8:0]rand_num;
random U1_random(
.clk(clk),
.rst_n(rst_n),
// .seed(seed),
//.load(load),
.rand_num(rand_num)
);
//随机盒子创建
wire [9:0]rand_x;
wire [9:0]rand_y;
// wire rand_drive;//随机小方块激励模块
box_create U1_box_create(
.clk(clk),
.rst_n(rst_n),
.rand_num(rand_num),
.rand_drive(drive),
.rand_x(rand_x),
.rand_y(rand_y)
);
assign box_x = rand_x;
assign box_y = rand_y;
endmodule
该模块采用LFSR机制生成伪随机数,其种子已提前预置。本文中采用了9位宽度的设计,使得生成的伪随机数范围小于VGA屏幕的显示范围。如果有需求可为LFSR增加1位长度,并增添额外的代码用于判定取用的伪随机数是否越界。
module random(
input clk,
input rst_n,
// input load,
// input[8:0] seed,
output reg[8:0] rand_num
);
//-----------------------------------------------------------------------------------
always@(posedge clk or negedge rst_n)
if(!rst_n) rand_num <= 9'd132;
// else if(load) rand_num <= seed;
// else if(load) rand_num <= 9'd131;
else
begin
rand_num[0] <= rand_num[8];
rand_num[1] <= rand_num[0];
rand_num[2] <= rand_num[1];
rand_num[3] <= rand_num[2];
rand_num[4] <= rand_num[3]^rand_num[8];
rand_num[5] <= rand_num[4]^rand_num[8];
rand_num[6] <= rand_num[5]^rand_num[8];
rand_num[7] <= rand_num[6];
rand_num[8] <= rand_num[7];
end
endmodule
该模块主要用于创建和更新随机小盒子的中心坐标。更新信号drive由snack_control模块提供,为一个仅保持1个时钟周期拉高信号。在模块中配置flag信号是为了为信号坐标的获取实行一个周期的延时,避免了横纵坐标的相等。
module box_create(
input clk,
input rst_n,
input[8:0] rand_num,
input rand_drive,
output reg[9:0]rand_x,
output reg[9:0]rand_y
);
//-----------------------------------------------------------------------------
reg flag;
always@(posedge clk or negedge rst_n)
if(!rst_n)
begin
rand_x <= 9'd300;
rand_y <= 9'd300;
flag <= 1'b0;
end
else if(rand_drive) begin flag <= 1'b1; rand_x <= rand_num; end
else if(flag == 1'b1) begin rand_y <= rand_num; flag <= 1'b0;end
endmodule
该模块主要实现了以下几个功能:蛇头移动方向控制、蛇体寄存器、死亡检测、长度检测、蛇体渲染等四部分构成。蛇头移动方向控制由按键检测模块和方向寄存器修正规则代码组成;蛇体寄存器由蛇头移动更新状态机、蛇身移位寄存器组组成;长度检测包含长度记录、增长控制、随机小盒子更新信号生成三个功能;蛇体渲染为根据VGA驱动信号计算蛇体在屏幕上的显示,如果采用未注释部分,综合出的代码需要900个LC,若采用注释部分的代码增粗蛇体,就需要1700个LC,这也是本设计中的另一个不足。
module snack_control(
input clk,
input rst_n,
input key_r,
input key_l,
input key_u,
input key_d,
input [9:0]box_x,
input [9:0]box_y,
input [9:0]x_pos,
input [9:0]y_pos,
output reg drive,
output snack_r
);
//------------------------------------------------------------------------------
//蛇头移动方向控制
wire key_r1;
wire key_l1;
wire key_u1;
wire key_d1;
reg[1:0]dir;//蛇头方向寄存器
parameter right = 2'd0;
parameter left = 2'd1;
parameter up = 2'd2;
parameter down = 2'd3;
keycheck r_keycheck(
.clk(clk),
.rst_n(rst_n),
.key(key_r),
.key_v(key_r1)
);
keycheck l_keycheck(
.clk(clk),
.rst_n(rst_n),
.key(key_l),
.key_v(key_l1)
);
keycheck u_keycheck(
.clk(clk),
.rst_n(rst_n),
.key(key_u),
.key_v(key_u1)
);
keycheck d_keycheck(
.clk(clk),
.rst_n(rst_n),
.key(key_d),
.key_v(key_d1)
);
always@(posedge clk or negedge rst_n)
if(!rst_n) dir <= right;
else if(key_r1 && dir != left) dir <= right;
else if(key_l1 && dir != right) dir <= left;
else if(key_u1 && dir != down) dir <= up;
else if(key_d1 && dir != up) dir <= down;
//------------------------------------------------------------------------------
//蛇体寄存器,蛇体坐标和当前移动方向
reg fin;//游戏结束标志位
reg[9:0]snack_x[11:0];
reg[9:0]snack_y[11:0];
reg [18:0]cnt_m; //最大值312500;
parameter update_time = 19'd312500;
//更新计数器
always@(posedge clk or negedge rst_n)
if(!rst_n) cnt_m <= 19'd0;
else if(fin) cnt_m <= 19'd0;
else if(cnt_m == update_time) cnt_m <= 19'd0;
else cnt_m <= cnt_m + 19'd1;
//蛇头
always@(posedge clk or negedge rst_n)
if(!rst_n)
begin
snack_x[0] <= 10'd400;
snack_y[0] <= 10'd300;
end
else if(cnt_m == update_time)
case(dir)
right : begin
if(snack_x[0] == 10'd799) snack_x[0] <= 10'd0;
else snack_x[0] <= snack_x[0] + 10'd1;
end
left : begin
if(snack_x[0] == 10'd0) snack_x[0] <= 10'd799;
else snack_x[0] <= snack_x[0] - 10'd1;
end
up : begin
if(snack_y[0] == 10'd599) snack_y[0] <= 10'd0;
else snack_y[0] <= snack_y[0] + 10'd1;
end
down : begin
if(snack_y[0] == 10'd0) snack_y[0] <= 10'd599;
else snack_y[0] <= snack_y[0] - 10'd1;
end
endcase
//蛇身
always@(posedge clk or negedge rst_n)
if(!rst_n)
begin
snack_x[1] <= 10'd0;
snack_y[1] <= 10'd0;
snack_x[2] <= 10'd0;
snack_y[2] <= 10'd0;
snack_x[3] <= 10'd0;
snack_y[3] <= 10'd0;
snack_x[4] <= 10'd0;
snack_y[4] <= 10'd0;
snack_x[5] <= 10'd0;
snack_y[5] <= 10'd0;
snack_x[6] <= 10'd0;
snack_y[6] <= 10'd0;
snack_x[7] <= 10'd0;
snack_y[7] <= 10'd0;
snack_x[8] <= 10'd0;
snack_y[8] <= 10'd0;
snack_x[9] <= 10'd0;
snack_y[9] <= 10'd0;
snack_x[10] <= 10'd0;
snack_y[10] <= 10'd0;
snack_x[11] <= 10'd0;
snack_y[11] <= 10'd0;
end
else if(cnt_m == update_time)
begin
snack_x[1] <= snack_x[0];
snack_y[1] <= snack_y[0];
snack_x[2] <= snack_x[1];
snack_y[2] <= snack_y[1];
snack_x[3] <= snack_x[2];
snack_y[3] <= snack_y[2];
snack_x[4] <= snack_x[3];
snack_y[4] <= snack_y[3];
snack_x[5] <= snack_x[4];
snack_y[5] <= snack_y[4];
snack_x[6] <= snack_x[5];
snack_y[6] <= snack_y[5];
snack_x[7] <= snack_x[6];
snack_y[7] <= snack_y[6];
snack_x[8] <= snack_x[7];
snack_y[8] <= snack_y[7];
snack_x[9] <= snack_x[8];
snack_y[9] <= snack_y[8];
snack_x[10] <= snack_x[9];
snack_y[10] <= snack_y[9];
snack_x[11] <= snack_x[10];
snack_y[11] <= snack_y[10];
end
//------------------------------------------------------------------------------
//长度检测模块
reg[3:0] length;
always@(posedge clk or negedge rst_n)
if(!rst_n) begin length <= 4'd1;drive <= 1'd0; end
else if(drive) drive <= 1'd0;
else if(snack_x[0] == box_x && snack_y[0] == box_y)
begin
drive <= 1'd1;
if(length < 4'd12) length <= length + 4'd1;
else length <= length;
end
//-------------------------------------------------------------------------------
//死亡检测模块
//reg fin;
always@(posedge clk or negedge rst_n)
if(!rst_n) fin <= 1'b0;
else if(snack_x[0] == snack_x[1] && snack_y[0] == snack_y[1]) fin <= 1'b1;
else if(snack_x[0] == snack_x[2] && snack_y[0] == snack_y[2]) fin <= 1'b1;
else if(snack_x[0] == snack_x[3] && snack_y[0] == snack_y[3]) fin <= 1'b1;
else if(snack_x[0] == snack_x[4] && snack_y[0] == snack_y[4]) fin <= 1'b1;
else if(snack_x[0] == snack_x[5] && snack_y[0] == snack_y[5]) fin <= 1'b1;
else if(snack_x[0] == snack_x[6] && snack_y[0] == snack_y[6]) fin <= 1'b1;
else if(snack_x[0] == snack_x[7] && snack_y[0] == snack_y[7]) fin <= 1'b1;
else if(snack_x[0] == snack_x[8] && snack_y[0] == snack_y[8]) fin <= 1'b1;
else if(snack_x[0] == snack_x[9] && snack_y[0] == snack_y[9]) fin <= 1'b1;
else if(snack_x[0] == snack_x[10] && snack_y[0] == snack_y[10]) fin <= 1'b1;
else if(snack_x[0] == snack_x[11] && snack_y[0] == snack_y[11]) fin <= 1'b1;
//-------------------------------------------------------------------------------
//蛇体渲染模块
assign snack_r = (x_pos == snack_x[0] && y_pos == snack_y[0])||
(x_pos == snack_x[1] && y_pos == snack_y[1]&&length > 4'd1)||
(x_pos == snack_x[2] && y_pos == snack_y[2]&&length > 4'd2)||
(x_pos == snack_x[3] && y_pos == snack_y[3]&&length > 4'd3)||
(x_pos == snack_x[4] && y_pos == snack_y[4]&&length > 4'd4)||
(x_pos == snack_x[5] && y_pos == snack_y[5]&&length > 4'd5)||
(x_pos == snack_x[6] && y_pos == snack_y[6]&&length > 4'd6)||
(x_pos == snack_x[7] && y_pos == snack_y[7]&&length > 4'd7)||
(x_pos == snack_x[8] && y_pos == snack_y[8]&&length > 4'd8)||
(x_pos == snack_x[9] && y_pos == snack_y[9]&&length > 4'd9)||
(x_pos == snack_x[10] && y_pos == snack_y[10]&&length > 4'd10)||
(x_pos == snack_x[11] && y_pos == snack_y[11]&&length > 4'd11);
/*
assign snack_r = ((x_pos >= snack_x[0]-10'd3 && x_pos <= snack_x[0]+10'd3)&&(y_pos >= snack_y[0]-10'd3 && y_pos <= snack_y[0]+10'd3))||
((x_pos >= snack_x[1]-10'd3 && x_pos <= snack_x[1]+10'd3)&&(y_pos >= snack_y[1]-10'd3 && y_pos <= snack_y[1]+10'd3)&&length > 4'd1)||
((x_pos >= snack_x[2]-10'd3 && x_pos <= snack_x[2]+10'd3)&&(y_pos >= snack_y[2]-10'd3 && y_pos <= snack_y[2]+10'd3)&&length > 4'd2)||
((x_pos >= snack_x[3]-10'd3 && x_pos <= snack_x[3]+10'd3)&&(y_pos >= snack_y[3]-10'd3 && y_pos <= snack_y[3]+10'd3)&&length > 4'd3)||
((x_pos >= snack_x[4]-10'd3 && x_pos <= snack_x[4]+10'd3)&&(y_pos >= snack_y[4]-10'd3 && y_pos <= snack_y[4]+10'd3)&&length > 4'd4)||
((x_pos >= snack_x[5]-10'd3 && x_pos <= snack_x[5]+10'd3)&&(y_pos >= snack_y[5]-10'd3 && y_pos <= snack_y[5]+10'd3)&&length > 4'd5)||
((x_pos >= snack_x[6]-10'd3 && x_pos <= snack_x[6]+10'd3)&&(y_pos >= snack_y[6]-10'd3 && y_pos <= snack_y[6]+10'd3)&&length > 4'd6)||
((x_pos >= snack_x[7]-10'd3 && x_pos <= snack_x[7]+10'd3)&&(y_pos >= snack_y[7]-10'd3 && y_pos <= snack_y[7]+10'd3)&&length > 4'd7)||
((x_pos >= snack_x[8]-10'd3 && x_pos <= snack_x[8]+10'd3)&&(y_pos >= snack_y[8]-10'd3 && y_pos <= snack_y[8]+10'd3)&&length > 4'd8)||
((x_pos >= snack_x[9]-10'd3 && x_pos <= snack_x[9]+10'd3)&&(y_pos >= snack_y[9]-10'd3 && y_pos <= snack_y[9]+10'd3)&&length > 4'd9)||
((x_pos >= snack_x[10]-10'd3 && x_pos <= snack_x[10]+10'd3)&&(y_pos >= snack_y[10]-10'd3 && y_pos <= snack_y[10]+10'd3)&&length > 4'd10)||
((x_pos >= snack_x[11]-10'd3 && x_pos <= snack_x[11]+10'd3)&&(y_pos >= snack_y[11]-10'd3 && y_pos <= snack_y[11]+10'd3)&&length > 4'd11);
*/
endmodule