游戏介绍
游戏规则
利用FPGA,以640*480的分辨率使用VGA显示,玩家利用按键操作位于屏幕左侧的方块移动,来躲避从屏幕右侧向左边移动的留有一定间隙的障碍物。
游戏要求
画面及操作尽量连续,游戏结束时玩家操作的物体变成红色,按下重新开始后复位游戏,随着时间变长加速以提高难度。
基本上整个游戏就像是以前的飞机小游戏,为了增加可玩性,我将游戏设置为方块自动降落,外部只有一个按键,实现方块的向上移动,去躲避向左移动的挡板。
设计分析
模块设定
首先游戏要有显示画面,所以少不了vga的显示模块;其次是要控制方块的移动,需要键盘的输入模块,最后是关于游戏的逻辑控制,这需要一个控制模块。
那这些模块都需要什么哪些信号呢?接着分析下。
键盘模块
键盘模块作为整个避障游戏系统的输入,它主要是为其他模块提供信号,对信号的处理并不多。
输入 | 功能描述 |
---|---|
clk | 时钟 |
reset | 复位 |
up | 使方块上升 |
输出 | 功能描述 |
up_key_press | 方块上升信号 |
down_key_press | 方块下降信号 |
前面说了,为了增加可玩性,所以我在系统内部设置了让方块自动下落,所以输入只有up,但是输出时会有down_key_press。
关于键盘的输入输出就是如此,至于模块如何实现这些功能,分析阶段不解释,在下文中会陆陆续续讲解。
VGA模块
vga模块的功能就是将数据显示,原理在下文我会讲解,弄懂它的时序后,问题不会太大,一开始可以尝试先显示个彩条之类的,测试下,找下感觉。
输入 | 功能描述 |
---|---|
clk | 时钟 |
reset | 复位 |
输出 | 功能描述 |
dat_act | 数据有效标志位 |
hc | 行扫描计数器 |
vc | 列扫描计数器 |
hsync | 行同步信号 |
vsync | 场同步信号 |
控制模块
控制模块这个部分是整个游戏的规则的设定,可以说,游戏怎么玩完全由这个模块决定,根据游戏的描述和要求,我们是要控制一个方块去躲避不断向左移动的挡板,所以这个挡板和方块怎么“弄出来”就是关键了。
怎么弄出来呢?
首先要有个概念,我们所看到的VGA图像都是一个个像素点组成,使用640*480 的显示模式,这个规定相当于为我们规定了横纵坐标的定义域,在这个二维屏幕上。方块和遮挡板就可以用数学式子“画”出来,例如边长为两个像素点一个方块就是:0<x<2 ,0<y<2。
友情提醒:FPAG中能用正数就尽量不要用负数,数字在FPGA中是用补码表示的,负数的补码往往与我们的思维逻辑有点出入,容易导致出错。
输入 | 功能描述 |
---|---|
clk | 时钟 |
reset | 复位 |
up_key_press | 方块上升信号 |
down_key_press | 方块的下降信号 |
hc | 行扫描计数器 |
vc | 列扫描计数器 |
dat_act | 数据有效标志位(用于消隐) |
输出 | 功能描述 |
disp_RGB | 显示所需的数据 |
总体的设计图
方案设计
键盘模块
键盘模块的要点在于消抖,和控制方块移动。
消抖
无论是什么器件,键盘的消抖都是老套路,分为硬件消抖和软件消抖,硬件消抖如使用RS触发器实现或者是加电容实现,一般是制作板子的时候考虑加上去的,平时我们使用现成的板子,大多数都是使用软件消抖。
键盘产生抖动是机械特性,在我们按下按键是接触点的电压波形大致如下图:
从图可以看出,按下时会有一段上下波动的波形,松开时也有一段。
软件消抖常用的方法是延时,作用就是避开这一段“抖动”的波形,达到消抖的目的。
if(counter <= T) //按的时间不够长
begin
counter = counter + 1'b1;
up_key_press <= 0;
end
else //按下足够久了,认为是真的按下
begin
counter <= 0;
up_key_press <= 1;
end
硬件消抖这里也稍微拓展下。
RS触发器实现
图中两个“与非”门构成一个RS触发器。当按键未按下时,输出为0;当键按下时,输出为1。此时即使用按键的机械性能,使按键因弹性抖动而产生瞬时断开(抖动跳开B),只要按键不返回原始状态A,双稳态电路的状态不改变,输出保持为0,不会产生抖动的波形。也就是说,即使B点的电压波形是抖动的,但经双稳态电路之后,其输出为正规的矩形波。这一点通过分析RS触发器的工作过程很容易得到验证。
至于其他的硬件消抖电路,如:用电容构成的积分电路实现,采用D触发器实现,这里不再拓展,如有兴趣,可自行查阅资料。
控制移动
这个功能的实现,应该说不难。知道要改变哪个参数能使它移动,改变它就可以实现了,在这个游戏系统中,控制方块上下移动是改变 move_y 这个参数,左右移动是改变move_x,不过在后面测试游戏时,我觉得左右移动没有必要加上去,就把它去了,具体的操作看代码吧。
VGA模块
实现这个游戏,我认为最重要的知识就是VGA的显示原理了。数据怎么显示在屏幕的?640和480指的又是什么?我们先看它的原理。
VGA原理
VGA从扫描方式上分行扫描和场扫描两种,扫描就是一个电子枪(CRT),啾啾啾的扫,水平方向叫行扫描,垂直方向叫场扫描,这个电子枪它又可发出三种颜色光,分别是R(红色),G(绿色),B(蓝色),光的三原色都有,原则上三原色按照比例不同搭配,那你想要什么颜色就可以给你什么颜色,但实际上呢,VGA中红,绿,蓝的输入线分别是3,3,2根;也就是说红色有2^3=8种,同理,绿色八种,蓝色四种,相互搭配,便有8 X 8 X 4=256种搭配,也就是VGA能显示256种颜色。
扫描过程是怎样的?
以行扫描为例:
从图可以看出,电子枪从左往右扫射一行回头再到下一行,直至最后完成一帧画面,又重头开始。那什么时候掉头,什么时候算是完成一帧画面,这就有个区域了,区域怎么定,是由显示模式决定的,看下图。
我们可以看到有多种显示模式,不同的显示模式所需的时钟频率可是不一样的,如果细心查看,就会注意到,行时序的c区和列时序的q区恰好是640和480这两个熟悉的数字,其实这就是显示时序段的范围。
VGA中定义行时序和场时序都需要同步脉冲(Sync a)、显示后沿(Back porch b)、显示时序段(Display interval c)和显示前沿(Front porch d)四部分。只有在显示时序段,也就是C区才可以信号显示出来,其他区域,你就算给VGA信号,你也看不到。
行时序
场时序
那么我们怎么知道,电子枪(CRT)有没有扫描到显示时序段呢?方法是加入行同步计数器和列同步计数器用,反正时序是固定的,行同步计数器是扫一下计数器加一,列计数器是一行扫完计数器加一,两者都扫到C区了,也就是计数器都达到一定数值(a区长度+B区长度),表明屏幕可以显示信号,我就给信号,要黑色,rgb全给0,要白色,全给1,反正就是给信号,这和在坐标轴上画图的感觉是一样一样的。
控制模块
控制模块就像这个游戏系统的控制中心一般,制定了关于这个游戏的一切规则。
主要有那么几个要点:
- 将键盘的长脉冲变为一个个冲击信号,不然以FPGA本身的频率,按一下,移动得太快,方块就“上天” 了,障碍物移动也会快到你看不到。
- “画”方块和障碍物(挡板),并设定参数让给它们可以移动。
- 由于挡板的垂直方向出现的位置要有随机性,所以需要产生随机数。
- 设置游戏失败的情况,方块与挡板“撞上”这个时机的设置必须是程序完成。
长脉冲变多个短脉冲
这不难想也不难实现,就是计数器加到一定程度变为标志位变为1,然后计数器清零,标志位也变为0,一定时间内要短脉冲多点,计数器的计数值就小点,反之,大一点。
//// 板块移动速度控制 ////
reg move;
reg [32:0]counter;
reg [30:0]T_move;
always@(posedge clk,negedge reset)
begin
if(!reset)
begin
T_move = 30'd10_000_00;
counter <= 0;
move <=0;
end
else
begin
if(counter >= T_move)
begin
move = 1;
if(T_move == 100_000)
T_move <=T_move;
else
T_move = T_move-10;
counter = 0;
end
else
begin
move = 0;
if(!stop)
counter= counter + 1;
else
counter = 0;
end
end
end
“画”方块和挡板
关于如何“画”,前面举过画方块的例子,就是把行列计数器当做 x,y。x给个区域,y给个区域,再给个颜色,就画出来了。至于移动呢,移动就代表着位置是个变量,设定一个x,y都是变量的点,然后以这个点为中心画出你要的方块或者挡板,改变这个点的x,y便是将它移动。
以挡板从右向左移动为例:
产生随机数
产生伪随机数的方法最常见的是利用一种线性反馈移位寄存器(LFSR),它是由n个D触发器和若干个异或门组成的,如下图:
[图片上传失败...(image-fadcea-1636785474160)]
实际上这个有规律可循的,只不过D触发器一多,显得很乱,很像随机产生的样子,但确实不是真正意义上的随机数,是个伪随机数,但在这里使用足够了的。
但这种方法也有bug,就是高位它不容易变化的时候,挡板垂直方向就不够分散,举个例子,以8个D触发器组成的为例,数字范围从0~1111_1111,如果高位变化不大,如从1110_0000变成1110_0101,高位不怎么变化的话,整个数字大小实际上就是改变一点点,图像表现为前后两个挡板垂直位置上相差几个像素点,这就显得过于集中,而且这种办法无法生成 0 这个数字。
为了将挡板"离散一点",我就将竖直方向的长减去挡板的长度后得到的空隙分段化,分为8段,这样挡板之间的距离要么相等,不然都会有一段距离,显得离散些。怎么实现呢?
每个D触发器都存有一个数字,我从中随机抽取三个数字,做一个case语句的选择,8段8种情况选择。这样随机性增加,挡板也更离散。
/////// 随机数 //////////
reg [7:0] rand_num;
parameter seed = 8'b1111_1111;
always@(posedge clk or negedge reset)
begin
if(!reset)
rand_num <= seed;
else
begin
rand_num[0] <= rand_num[1] ;
rand_num[1] <= rand_num[2] + rand_num[7];
rand_num[2] <= rand_num[3] + rand_num[7];
rand_num[3] <= rand_num[4] ;
rand_num[4] <= rand_num[5] + rand_num[7];
rand_num[5] <= rand_num[6] + rand_num[7];
rand_num[6] <= rand_num[7] ;
rand_num[7] <= rand_num[0] + rand_num[7];
end
end
wire [2:0]choose;
reg [8:0]type;
assign choose = {rand_num[3],rand_num[6],rand_num[2]};
always@(posedge clk )
begin
case(choose)
0:type = 0;
1:type = 40;
2:type = 80;
3:type = 120;
4:type = 160;
5:type = 200;
6:type = 240;
7:type = 280;
endcase
end
////////////////////////////////////////////////////////
游戏失败设置
游戏失败是撞上了,那 撞上 在数学上表示是什么呢?
答案是方块和挡板的坐标有交叉。
方块和挡板之间都有坐标的区域,只要找到它们会交叉的情况,就说明这个时候是撞上了。原理就是如此,具体的可以自己动笔算下。
友情提醒:加减时注意尽量不要出现负数的情况,因为 数字用补码表示的原因,在FPGA中,直接比较 ,-1=ffff_ffff 可是大于0的。
wire die1,die2,die3,die4;
//游戏失败定义,方块与挡板"碰撞"
//失败情况讨论,共设置四块挡板,四种情况
assign die1=((rand<move_y + border)&&(move_y < rand+long)&&(push < move_x+border) && (move_x < push + ban ));
assign die2=((rand1<move_y + border)&&(move_y < rand1+long)&&(push1 < move_x+border) && (move_x < push1 + ban ));
assign die3=((rand2<move_y + border)&&(move_y < rand2+long)&&(push2 < move_x+border) && (move_x < push2 + ban ));
assign die4=((rand3<move_y + border)&&(move_y < rand3+long)&&(push3 < move_x+border) && (move_x < push3 + ban ));
wire false;
assign false = die1||die2||die3||die4;
代码展示
键盘模块
module key(clk,reset,up,up_key_press,down_key_press);
input clk;
input reset;
input up;
output reg up_key_press;
output reg down_key_press;
parameter T = 30'd10_000_00; //控制方块移动速度
////////// up 按键 /////////////
reg [30:0] counter;
reg [30:0] counter2;
always@(posedge clk,negedge reset )
begin
if(!reset)
begin
counter <= 0;
counter2 <= 0;
up_key_press <= 0;
down_key_press <= 0;
end
else
begin
if(up)
begin
if(counter <= T)
begin
counter = counter + 1'b1;
up_key_press <= 0;
end
else
begin
counter <= 0;
up_key_press <= 1;
end
end
else //下降按钮
begin
if(counter2 <= T)
begin
counter2 = counter2 + 1'b1;
down_key_press <= 0;
end
else
begin
counter2 <= 0;
down_key_press <= 1;
end
end
end
end
endmodule
VGA模块
module vga( clk,reset,hsync, vsync,hc,vc,dat_act);
input clk; //系统输入时钟 100MHz
input reset;
output hsync; //VGA 行同步信号
output vsync; //VGA 场同步信号
output dat_act;
output [9:0]hc ,vc; //转成640*480的模式
reg [9:0] hcount; //VGA 行扫描计数器
reg [9:0] vcount; //VGA 场扫描计数器
reg flag;
wire hcount_ov;
wire vcount_ov;
wire hsync;
wire vsync;
reg vga_clk=0;
reg cnt_clk=0; //分频计数
//VGA 行、场扫描时序参数表
parameter hsync_end = 10'd95,
hdat_begin = 10'd143,
hdat_end = 10'd783,
hpixel_end = 10'd799,
vsync_end = 10'd1,
vdat_begin = 10'd34,
vdat_end = 10'd514,
vline_end = 10'd524;
//分频
always @(posedge clk)
begin
if(cnt_clk == 1)
begin
vga_clk <= ~vga_clk;
cnt_clk <= 0;
end
else
cnt_clk <= cnt_clk +1;
end
//************************VGA 驱动部分*******************************//行扫描
always @(posedge vga_clk)
begin
if (hcount_ov)
hcount <= 10'd0;
else
hcount <= hcount + 10'd1;
end
assign hcount_ov = (hcount == hpixel_end);
//场扫描
always @(posedge vga_clk)
begin
if (hcount_ov)
begin
if (vcount_ov)
vcount <= 10'd0;
else
vcount <= vcount + 10'd1;
end
end
assign vcount_ov = (vcount == vline_end);
//数据、同步信号输
assign dat_act = ((hcount >= hdat_begin) && (hcount < hdat_end))&& ((vcount >= vdat_begin) && (vcount < vdat_end));
assign hsync = (hcount > hsync_end);
assign vsync = (vcount > vsync_end);
//计数器转成640 x 480的样式,方便开发
assign hc = hcount - hdat_begin;
assign vc = vcount - vdat_begin;
endmodule
控制模块
module control( clk,reset, disp_RGB,hc,vc,dat_act,up_key_press,down_key_press );
input clk; //系统输入时钟 100MHz
input reset;
input dat_act;
input [9:0]hc,vc;
input up_key_press;
input down_key_press;
output [2:0]disp_RGB; //VGA 数据输出
reg [2:0]data;
reg vga_clk=0;
reg cnt_clk=0; //分频计数
//分频
always @(posedge clk)
begin
if(cnt_clk == 1)
begin
vga_clk <= ~vga_clk;
cnt_clk <= 0;
end
else
cnt_clk <= cnt_clk +1;
end
//定义正方形小块的边长
parameter border = 40;
//定义挡板的宽度
parameter ban = 20;
//定义挡板的长度
parameter long = 200;
//定义挡板的间隔
parameter magin = 160;
//VGA扫描,画出挡板和方块,并设置挡板移动的移动变量push
reg [10:0] push,push1,push2,push3;
reg stop;//用于停止游戏
//小方块移动数据存储器
parameter move_x = 50; //方块的初始位置
reg [9:0]move_y;
/////// 随机数 //////////
reg [7:0] rand_num;
parameter seed = 8'b1111_1111;
always@(posedge clk or negedge reset)
begin
if(!reset)
rand_num <= seed;
else
begin
rand_num[0] <= rand_num[1] ;
rand_num[1] <= rand_num[2] + rand_num[7];
rand_num[2] <= rand_num[3] + rand_num[7];
rand_num[3] <= rand_num[4] ;
rand_num[4] <= rand_num[5] + rand_num[7];
rand_num[5] <= rand_num[6] + rand_num[7];
rand_num[6] <= rand_num[7] ;
rand_num[7] <= rand_num[0] + rand_num[7];
end
end
wire [2:0]choose;
reg [8:0]type;
assign choose = {rand_num[3],rand_num[6],rand_num[2]};
always@(posedge clk )
begin
case(choose)
0:type = 0;
1:type = 40;
2:type = 80;
3:type = 120;
4:type = 160;
5:type = 200;
6:type = 240;
7:type = 280;
default: type = 280;
endcase
end
////////////////////////////////////////////////////////
//// 板块移动速度控制 ////
reg move;
reg [32:0]counter;
reg [30:0]T_move;
always@(posedge clk,negedge reset)
begin
if(!reset)
begin
T_move = 30'd10_000_00;
counter <= 0;
move <=0;
end
else
begin
if(counter >= T_move)
begin
move = 1;
if(T_move == 100_000)
T_move <=T_move;
else
T_move = T_move-10;
counter = 0;
end
else
begin
move = 0;
if(!stop)
counter= counter + 1;
else
counter = 0;
end
end
end
reg [8:0]rand,rand1,rand2,rand3;
always@(posedge clk or negedge reset)
begin
if (!reset)
begin
push<=640; //初始位置设定
push1 <= 640+ magin;
push2 <= 640 + magin + magin;
push3 <= 640 + magin + magin + magin;
end
else if (move)
begin
if(push == 0)
begin
push <= 640;
rand <=type; //第一块板子的位置设定
end
else
begin
push <= push-1'b1;
end
if(push1 == 0)
begin
push1 <= 640;
rand1 <=type; //第二块板子的位置设定
end
else
begin
push1 <= push1-1'b1;
end
if(push2 == 0)
begin
push2 <= 640;
rand2 <=type; //第三块板子的位置设定
end
else
begin
push2<= push2-1'b1;
end
if(push3 == 0)
begin
push3 <= 640;
rand3 <=type;
//第四块板子的位置设定
end
else
begin
push3 <= push3-1'b1;
end
end
else
begin
push <= push;
push1 <= push1;
push2 <= push2;
push3 <= push3;
end
end
wire die1,die2,die3,die4;
//游戏失败定义,方块与挡板"碰撞"
//失败情况讨论,共设置四块挡板,四种情况
assign die1=((rand<move_y + border)&&(move_y < rand+long)&&(push < move_x+border) && (move_x < push + ban ));
assign die2=((rand1<move_y + border)&&(move_y < rand1+long)&&(push1 < move_x+border) && (move_x < push1 + ban ));
assign die3=((rand2<move_y + border)&&(move_y < rand2+long)&&(push2 < move_x+border) && (move_x < push2 + ban ));
assign die4=((rand3<move_y + border)&&(move_y < rand3+long)&&(push3 < move_x+border) && (move_x < push3 + ban ));
wire false;
assign false = die1||die2||die3||die4;
//描述运动,“画图”
always@(posedge vga_clk,negedge reset)
begin
if(!reset)
begin
data <= 0;
stop <= 0;
end
else
begin
if (hc>move_x &&(hc<(move_x+border)&&(vc>move_y)&&(vc<move_y+border))) //小方块
begin
if(!false)
begin
data <= 3'h3; //黄色
stop <= 0;
end
else
begin
data <= 3'h1; //红色
stop <=1;
end
end
else
if ((hc>push) && (hc<=push+ban) && (vc>=rand) && (vc<=rand+long))
begin
data <= 3'h2; //第一根横条
end
else if ((hc>push1) && (hc<=push1+ban) && (vc>=rand1) && (vc<=rand1+long))
begin
data <= 3'h2; //第二根横条
end
else if ((hc>push2) && (hc<=push2+ban) && (vc>=rand2) && (vc<=rand2+long))
begin
data <= 3'h2; //第三根横条
end
else if ((hc>push3) && (hc<=push3+ban) && (vc>=rand3) && (vc<=rand3+long))
begin
data <= 3'h2; //第四根横条
end else
data <= 0;
end
end
/////// 方块移动控制 ////////////
always@(posedge clk or negedge reset)
begin
if (!reset)
begin
move_y <= 240;
end
else if (up_key_press)
begin
if(move_y == 0)
begin
move_y <= move_y;
end
else
begin
move_y <= move_y-1'b1;
end
end
else if (down_key_press)
begin
if(move_y>440)
begin
move_y <= move_y;
end
else
begin
move_y <= move_y+1'b1;
end
end
end
// 信号输出
assign disp_RGB = (dat_act) ? data : 3'h00;
endmodule
TOP模块
module top(clk,reset,up,hsync,vsync,disp_RGB);
input clk;
input reset;
input up;
output hsync; //VGA 行同步信号
output vsync; //VGA 场同步信号
output [2:0]disp_RGB; //VGA 数据输出
wire dat_act;
wire up_key_press;
wire down_key_press;
wire [9:0]hc,vc;
key U1(clk,reset,up,up_key_press,down_key_press);
control U2( clk,reset, disp_RGB,hc,vc,dat_act,up_key_press,down_key_press );
vga U3( clk,reset,hsync, vsync,hc,vc,dat_act);
endmodule
写在后面的话
关于这个小游戏的讲解就到这里,有任何疑问可以在评论处指出或者联系我,我会及时更新,文章若有错误,恳请读者在评论区指出斧正,我会修改。
欢迎大家在评论区与我交流,学习。