• 图解基于FPGA的贪食蛇驱动程序

  • 2012-05-19 19:28:30 发表
  • 标签:

     今天由我来给大家介绍一下用FPGA来驱动贪食蛇游戏的具体实现方法。

嗯,话不多说,先看实现效果:

http://www.tudou.com/programs/view/HNHUBbfn2uU/?resourceId=3895614_03_05_02

     需要的硬件资源有:带4位按键和VGA接口的FPGA最小系统板,还有显示器一台。

     分辨率640*480

     集成开发环境:quartusII。

     HDL:Verilog

相信大家对“贪食蛇”这个游戏都不陌生,这里我们分六个模块做,以下先简要介绍各个模块实现的功能以便让大家对这个系统有个全局的概念,然后介绍实现的方法,最后附上带注释的源代码。

 

各主要模块

U1:  pll  用于将输入的20M晶振倍频为50M供其他模块使用

U2:Game_Ctrl_Unit  控制游戏的四种状态,之间的转换

U3: Snake_Eatting_Apple  随机产生苹果

U4:Snake  产生各个VGA扫描部件的坐标及控制蛇运动轨迹

U5:VGA_Control VGA扫描控制模块

U6:Key 键盘扫描模块

U7:Seg_Display 数码管显示计分模块

完成后的RTL View 如下

 

U1:Pll

此次我用的输入晶振是20M,经过FPGA内部锁相环5倍频再2分频变成50M,设置如下:

 

 

 

先看键盘扫描模块

U6:Key

以up_key_press为例,介绍消抖的算法。

在每个时钟高电平时并行执行以下两条语句

up_key_press<=0;

up_key_last<=0;

当有按键按下时,每100ms(cnt=5_0000) last=up,last输出比up滞后一个周期,若up_key_last==0&&up==1,则说明按键按下,press输出置1。

 

U2:Game_Ctrl_Unit

游戏状态根据以下条件进行转换。

 

 

一开始有按键按下时游戏开始,撞到墙或身体后画面闪烁几下自动重新开始。

 

 

核心模块——

U4:Snake

      input left_press,

      input right_press,

      input up_press,

      input down_press,//按键输入

      output reg [1:0]snake,//用于表示当前扫描扫描的部件四种状态00:NONE 01:HEAD 10:BODY 11:WALL

      input [9:0]x_pos,

      input [9:0]y_pos,//扫描坐标  单位:“像素点”

      output [5:0]head_x, 

      output [5:0]head_y,//头部格坐标

      input add_cube,//增加体长信号

      input [1:0]game_status,//四种游戏状态

      output reg [6:0]cube_num,

      output reg hit_body,

      output reg hit_wall,

      input die_flash

      ......

     reg [5:0]cube_x[15:0];
     reg [5:0]cube_y[15:0];//体长坐标 单位:“格子”

 

在这里要先介绍一下关于像素与格子。整个屏幕的扫描模式是640*480,如果直接用像素点来表示游戏的各个部件无疑复杂且不好处理。这里采用“降低“分辨率的方法,即用16*16的像素范围为一个”格“,以格作为扫描单位扫描整个屏幕。如下,(X_pos和y_pos是十位的像素点坐标)

 

 

且看下图

 

 

看懂了吗?pos的低4位表示一个格子内像素的坐标,高5位表示格坐标,所以墙的范围是:

if(x_pos[9:4]==0|y_pos[9:4]==0|x_pos[9:4]==39|y_pos[9:4]==29)

                                   snake=WALL;//扫描墙

黄色表示wall,红色表示apple,蓝色表示head,粉红表示body。

那么,剩下黑色的地方就是有效的运动区域了。

接下来,主角上场,看图~~

 

 

 

嗯,cube_x,cube_y表示一整条大蟒蛇身体各节的格坐标

reg [5:0]cube_x[15:0];
 reg [5:0]cube_y[15:0];

这两句大家可以好好揣摩一下哦~

cube[0]表示head这个格,(就是上面蓝色的那个)head是我们主要要控制的地方,它涉及到DIE .EattingApple.还有身体运动轨迹等……所以head单独提取出来作为输出信号head_x和head_y用于后面的模块。

is_exist有16位,即蛇体最长为16*1格,每一位对应一个格,1为该格显示,0则不显示(图中虚框)

每吃下一个苹果蛇长度增加1,相应exist位置1。

如何让它肆虐起来呢?————这里:

always@(posedge CLK_50M or negedge RSTn)

if(cnt==12_500_000) //0.02us*12'500'000=0.25s   每秒移动四次

….

if(game_status==PLAY)

……

begin

             cube_x[1]<=cube_x[0];

             cube_y[1]<=cube_y[0];  

             cube_x[2]<=cube_x[1];                                           

              cube_y[2]<=cube_y[1];                                           

              cube_x[3]<=cube_x[2];                                           

              cube_y[3]<=cube_y[2];                                         

              cube_x[4]<=cube_x[3];                                        

             cube_y[4]<=cube_y[3];                                     

             ….

             …

end

身体运动算法:本长度位移动的下个格坐标为上一个长度位当前格坐标,运动节拍按分频后的节奏,每秒走四格~

碰到身体表示为:

if((cube_y[0]==cube_y[1]&&cube_x[0]==cube_x[1]&&is_exist[1]==1)|

   (cube_y[0]==cube_y[2]&&cube_x[0]==cube_x[2]&&is_exist[2]==1)|

   (cube_y[0]==cube_y[3]&&cube_x[0]==cube_x[3]&&is_exist[3]==1)|

   (cube_y[0]==cube_y[4]&&cube_x[0]==cube_x[4]&&is_exist[4]==1)|

……)

hit_body<=1;

//头的格Y坐标=任一位身体的格Y坐标且头的格X坐标=任一位身体的格X坐标且身体的该长度位存在  判定为hit_body

根据头部位置信息及按键信息决定head的移动方向

case(direct)       

      UP:

      begin 

             if(cube_y[0]==1)           

              hit_wall<=1;

      else

               cube_y[0]<=cube_y[0]-1;

      end

      DOWN:

      ……

      LEFT:

      ……

      RIGHT

      ……

      endcase

根据按键判断下一步行动head格坐标是否与wall坐标重合,是则hit_wall,否则按哪个就往哪边走咯~

再看下面一段代码:

……

case(addcube_state)

0:begin

      if(add_cube)

      begin                                      

      cube_num<=cube_num+1;

      is_exist[cube_num]<=1;

      addcube_state<=1;//“Eatting”信号

      end                             

        end

      1:begin

      if(!add_cube)

      addcube_state<=0;

      end

当head坐标与apple坐标重合时,收到add_cube信号,“已经吃了多少个“用cube_num表示(复位时默认为3)。

让相应位显示  is_exist[cube_num]<=1; 发出吃下信号  addcube_state<=1;(后面计分就靠这个信号了~)

前面说了那么多都是为接下来这段代码做铺垫,这也是整个系统的核心部分

if(x_pos>=0&&x_pos<640&&y_pos>=0&&y_pos<480)

      begin

      if(x_pos[9:4]==0|y_pos[9:4]==0|x_pos[9:4]==39|y_pos[9:4]==29)

            snake=WALL;//扫描wall

            else if(x_pos[9:4]==cube_x[0]&&y_pos[9:4]==cube_y[0]&&is_exist[0]==1)

                       begin

                             snake=(die_flash==1)?HEAD:NONE;//扫描head

                       end

                  else if

                       ((x_pos[9:4]==cube_x[1]&&y_pos[9:4]==cube_y[1]&&is_exist[1]==1)|

                        (x_pos[9:4]==cube_x[2]&&y_pos[9:4]==cube_y[2]&&is_exist[2]==1)|

                        (x_pos[9:4]==cube_x[3]&&y_pos[9:4]==cube_y[3]&&is_exist[3]==1)|

                        (x_pos[9:4]==cube_x[4]&&y_pos[9:4]==cube_y[4]&&is_exist[4]==1)|

                        (x_pos[9:4]==cube_x[5]&&y_pos[9:4]==cube_y[5]&&is_exist[5]==1)|

                        (x_pos[9:4]==cube_x[6]&&y_pos[9:4]==cube_y[6]&&is_exist[6]==1)|

                        ……(身体有十六位..)

                             snake=(die_flash==1)?BODY:NONE;//扫描body           

                       else snake=NONE;   

                       end

            end

可以看出当程序在扫描像素点的时候,这段代码能帮我们识别出扫描的像素点是游戏中的哪个部件,后面的VGA_Control模块根据这个Snake信号扫描时给予相应点不同的额色就能达到我们看到的效果。这里的die_flash是hit_body或hit_wall后由U2发出的一连串方波信号,扫描的时候若此信号在0和1之间交替变化,则整个屏幕就会出现闪烁的效果。当然,若不是在PLAY状态下,Snake是不会动的~

 

U3:Snake_Eatting_Apple 

好吧,这个模块用了一种比较笨拙的方法产生一个随机数。

      always@(posedge CLK_50M)

            random_num<=random_num+927;  //用加法产生随机数 

            //随机数高5位为apple格X坐标低5位为apple格Y坐标

每个时钟周期random_num都在变,而我们吃下苹果的时刻却因走法、按键的时间等有所不同,所以不同时刻吃下苹果后下一个苹果出现的地方近似随机~

if(clk_cnt==250_000)

……

if(apple_x==head_x&&apple_y==head_y)

begin

      add_cube<=1;//add_cube是和前面U4相关的

      apple_x<=(random_num[10:5]>38)?(random_num[10:5]-25):(random_num[10:5]==0)?1:random_num[10:5];

      apple_y<=(random_num[4:0]>28)?(random_num[4:0]-3):(random_num[4:0]==0)?1:random_num[4:0];

 end   //判断随机数是否超出屏幕坐标范围将随机数转换为下个apple的X Y坐标

 

 

U5:VGA_Control

这里,用另一种比较笨拙的方法产生VGA的扫描频率信号

     

  always@(posedge CLK_50M)

            begin

            clk_25M<=~clk_25M;  //2分频   按25M配置VGA控制模块

            end

这样,我们就把50M的时钟频率降到扫描所需的25M -.-|||

VGA的hsync和vsync控制时序如下,

以800*600*60Hz为例

XD,具体的VGA驱动原理我就不多讲咯~不了解VGA驱动原理的同学做这个游戏之前最好先了解一下。当然,仅凭上面几张图还有源代码就看懂的同学就太牛B了~~

 

U7:Seg_Display

最后一个模块~~~来一个add_cube信号分数就加1,数码管动态扫描分数~~~~

 

终于写完~不知道我讲得够不够清楚呢?有什么不清楚的地方欢迎大家发表回复。

最后的最后,把注释过的源代码放上来,给大家做个参考。

大家可以试着自己动手把整个系统写下来,然后调试~个中乐趣,自是没有动过手的人所体会不到的。

8929736539335.zip