FPGA-避障游戏

游戏介绍

游戏规则

利用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

友情提醒:FPAG中能用正数就尽量不要用负数,数字在FPGA中是用补码表示的,负数的补码往往与我们的思维逻辑有点出入,容易导致出错。

输入 功能描述
clk 时钟
reset 复位
up_key_press 方块上升信号
down_key_press 方块的下降信号
hc 行扫描计数器
vc 列扫描计数器
dat_act 数据有效标志位(用于消隐)
输出 功能描述
disp_RGB 显示所需的数据

总体的设计图

image

方案设计

键盘模块

键盘模块的要点在于消抖,和控制方块移动。

消抖

无论是什么器件,键盘的消抖都是老套路,分为硬件消抖和软件消抖,硬件消抖如使用RS触发器实现或者是加电容实现,一般是制作板子的时候考虑加上去的,平时我们使用现成的板子,大多数都是使用软件消抖。

键盘产生抖动是机械特性,在我们按下按键是接触点的电压波形大致如下图:

image

从图可以看出,按下时会有一段上下波动的波形,松开时也有一段。

软件消抖常用的方法是延时,作用就是避开这一段“抖动”的波形,达到消抖的目的。

if(counter <= T) //按的时间不够长
    begin
        counter = counter + 1'b1;
        up_key_press <= 0;
    end
else  //按下足够久了,认为是真的按下
    begin
         counter <= 0;
         up_key_press <= 1;
    end

硬件消抖这里也稍微拓展下。

RS触发器实现

image

图中两个“与非”门构成一个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种颜色。

扫描过程是怎样的?

以行扫描为例:

image

从图可以看出,电子枪从左往右扫射一行回头再到下一行,直至最后完成一帧画面,又重头开始。那什么时候掉头,什么时候算是完成一帧画面,这就有个区域了,区域怎么定,是由显示模式决定的,看下图。

image

我们可以看到有多种显示模式,不同的显示模式所需的时钟频率可是不一样的,如果细心查看,就会注意到,行时序的c区和列时序的q区恰好是640和480这两个熟悉的数字,其实这就是显示时序段的范围。

VGA中定义行时序和场时序都需要同步脉冲(Sync a)、显示后沿(Back porch b)、显示时序段(Display interval c)和显示前沿(Front porch d)四部分。只有在显示时序段,也就是C区才可以信号显示出来,其他区域,你就算给VGA信号,你也看不到。

行时序

image

场时序

image

那么我们怎么知道,电子枪(CRT)有没有扫描到显示时序段呢?方法是加入行同步计数器和列同步计数器用,反正时序是固定的,行同步计数器是扫一下计数器加一,列计数器是一行扫完计数器加一,两者都扫到C区了,也就是计数器都达到一定数值(a区长度+B区长度),表明屏幕可以显示信号,我就给信号,要黑色,rgb全给0,要白色,全给1,反正就是给信号,这和在坐标轴上画图的感觉是一样一样的。

控制模块

控制模块就像这个游戏系统的控制中心一般,制定了关于这个游戏的一切规则。

主要有那么几个要点:

  1. 将键盘的长脉冲变为一个个冲击信号,不然以FPGA本身的频率,按一下,移动得太快,方块就“上天” 了,障碍物移动也会快到你看不到。
  2. “画”方块和障碍物(挡板),并设定参数让给它们可以移动。
  3. 由于挡板的垂直方向出现的位置要有随机性,所以需要产生随机数。
  4. 设置游戏失败的情况,方块与挡板“撞上”这个时机的设置必须是程序完成。

长脉冲变多个短脉冲

这不难想也不难实现,就是计数器加到一定程度变为标志位变为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便是将它移动。

以挡板从右向左移动为例:

image

产生随机数

产生伪随机数的方法最常见的是利用一种线性反馈移位寄存器(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

代码展示

键盘模块

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=((randmove_x &&(hc<(move_x+border)&&(vc>move_y)&&(vcpush) && (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

写在后面的话

关于这个小游戏的讲解就到这里,有任何疑问可以在评论处指出或者联系我,我会及时更新,文章若有错误,恳请读者在评论区指出斧正,我会修改。

欢迎大家在评论区与我交流,学习。

你可能感兴趣的:(FPGA-避障游戏)