美文网首页
FPGA-避障游戏

FPGA-避障游戏

作者: 十字街86号 | 来源:发表于2021-11-13 14:35 被阅读0次

    游戏介绍

    游戏规则

    利用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 显示所需的数据

    总体的设计图

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

    写在后面的话

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

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

    相关文章

      网友评论

          本文标题:FPGA-避障游戏

          本文链接:https://www.haomeiwen.com/subject/djqzzltx.html