图解基于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,数码管动态扫描分数~~~~
终于写完~不知道我讲得够不够清楚呢?有什么不清楚的地方欢迎大家发表回复。
最后的最后,把注释过的源代码放上来,给大家做个参考。
大家可以试着自己动手把整个系统写下来,然后调试~个中乐趣,自是没有动过手的人所体会不到的。
在哪个板子上实现的?
我用的板是黑金动力社区的,板上的芯片是EP2C8。
喜欢楼主的代码风格和说话风格,然后....我的来吐槽的XD;;
首先,有几个地方,比如判断身体碰撞的那个if,好长;;这样FPGA会很疼的感觉;;楼主有没有想过这种方法,定义一个30*40的ram,每个单元值为1或0,1表示有方块(WALL或者BODY),0表示没有.这样判断撞墙或者撞身体的时候只需要判断下一个头地址对应的ram数据是否为1,不用分开判断hit_wall和hit_body了.
另外有个小地方,
random_num<=random_num+927; //用加法产生随机数
//随机数高5位为apple格X坐标低5位为apple格Y坐标
这里这里,我要吐槽这里为什么是927?!(莫非是楼主的生日XD),对于这里的随机数,主要就是完备性和平均性吧,不知道能不能满足这2个条件呢;;;;其实简单伪随机数的生成很简单的,楼主可以百度一下M序列,用于贪食蛇啊俄罗斯方块啊这种低要求的程序还是完全能够胜任的;;
以上
look look look
是否可以移植到其他平台?
哇~高见高见~
哈哈哈,做的时候就图简单~嗯...M序列还有RAM。不错不错~~

其他平台?呵呵,算法思想是可以借鉴啦。直接移植源代码就看具体什么平台咯~
嗯,其实还可以加些障碍物,加上调速、关卡,做成俄罗斯方块。。。。。发挥无穷想象力吧~