Skip to content

cxz66666/FPGA-TankGame

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

一、项目背景介绍

《坦克大战》(Battle City)是1985年日本南梦宫Namco游戏公司在任天堂FC平台上,推出的一款多方位平面射击游戏。游戏以坦克战斗及保卫基地为主题,属于策略型联机类。同时也是FC平台上少有的内建关卡编辑器的几个游戏之一,玩家可自己创建独特的关卡,并通过获取一些道具使坦克和基地得到强化。本游戏从此获取灵感,并基于此进行改编,是一款规则简单,内容趣味性高,可玩性强的游戏。

二、游戏说明

1. 游戏基本规则

  1. 该游戏为双人游戏,两位玩家同属一方,分别控制一辆坦克,根据模式的不同,开展竞争或是合作,摧毁敌方坦克。

  2. 敌方坦克共有四辆,均由AI控制。

  3. 坦克可以在地图范围内自由移动,除非其与其他坦克发生碰撞。

  4. 坦克可以向当前运动方向射击,但是同一时间一辆坦克在地图内只允许存在一发子弹

  5. 当坦克与非本方子弹产生任何接触,坦克即视作被摧毁,并在2s后复活,同时该子弹也会被摧毁。

  6. 坦克重生后,将有1s的无敌时间。

  7. 场上将会随机产生道具:加速,冻结,时间增加,生命增加,无敌,我方坦克吃到之后将会有相应效果。

本游戏共有两种模式:标准模式和无尽模式,下面我们分别对这两个模式的规则和胜利条件进行简要介绍。

(a) 经典模式

场上将会有两辆我方坦克和四辆敌方坦克,两位玩家将会展开对抗,最先摧毁14辆敌方坦克的玩家获得胜利。每辆我方坦克拥有预先设定好的生命值,若被敌方坦克击中,生命值减一,并会在2s后重新投入战斗。若生命值降为0,则游戏结束,另一方获得胜利。

(b) 无尽模式

场上将会有两辆我方坦克和四辆敌方坦克,两位玩家将展开合作,共同在预先设定好的时间内摧毁尽可能多的坦克。每辆我方坦克拥有3点生命值,每摧毁一辆敌方坦克,我方的剩余时间将会增加(不超过预设上限),若剩余时间降为0,则游戏结束。此模式无胜利方。

2. 游戏流程及操作说明

  1. 游戏开始前,在主界面中利用SW[0]选择模式,0为经典模式,1为无尽模式。
  2. 在经典模式下,可以设定血量的多少:SW[1]控制血量加减,0为减,1为加,再通过设定SW[2]完成加减操作。
  3. 在无尽模式下,可以设定时间的多少,每个时间图标代表1s,具体操作与血量设定相同。
  4. 按下BTNC开始游戏。
  5. 对于玩家A,键盘WSAD分别控制上下左右,J键开火。
  6. 对于玩家B,键盘上下左右分别控制上下左右,0键开火。
  7. 游戏结束时,按下BTNU可以回到主界面

3. 道具说明

加速:加快全场运动物(敌我双方坦克和子弹)的运动速度,持续9s。

冻结:使敌方坦克停止运动,持续4s。

时间增加:将时间增加到预设的初始值。

生命增加:增加拾取坦克的生命值1。

无敌:坦克免疫所有敌方子弹攻击,持续8s。

4. 游戏界面说明

(a) 经典模式

经典模式的游戏界面如下图所示。屏幕上方显示了当前模式(经典)以及双方坦克所摧毁的敌方坦克数。每摧毁一个敌方坦克,我们就为ta的计分板上插上一面红旗。屏幕左右两侧分别显示的是玩家1和玩家2所剩生命值,每一颗红心代表一条命。

(b) 无尽模式

无尽模式的游戏界面如下图所示。屏幕上方显示了当前模式(无尽)以及当前所剩余时间,每一个计时器代表1s。屏幕左右两侧显示的也是玩家生命值。

三、整体结构

整个项目的框架如下所示,部分细节点由于空间原因没有全部画出

image-20210114205435987

无尽模式的游戏逻辑:

infinity

经典模式的游戏逻辑

classic

四、模块介绍

1. VGA

VGA作为一种标准的显示接口得到了广泛的应用,其信号类型为模拟类型,显示卡端的接口为 15 针母插座。 常见的彩色显示器一般都是由CRT阴极射线管阴极射线管构成,其引出线共含 5 个信号:R、G、B三基色信号三基色信号、HS行同步信号行同步信号、VS场同步信号场同步信号。每一个像素的色彩由R红红 .G绿绿. B蓝蓝三基色构成。显示屏一般通过光栅扫描的方式扫描一幅屏幕图像上的各个点,形成整个图片。 另外,VGA 时序控制模块是本设计的重要部分,最终的输出信号行、场同步信号必须严格按照VGA 时序标准产生相应的脉冲信号,如下图所示(以640*480@60HZ为例)。

vga_pic

具体细节这里不再赘述。

2. PS/2

作为一款对战类游戏,良好的键盘输入体验肯定是必不可少的。我们采用的A7提供了基于usb接口、PS/2通信协议。PS/2 通信协议是一种双向同步串行通信协议。通信的两端通过 CLOCK时钟脚时钟脚同步,并通过DATA数据脚数据脚交换数据。

如下是PS/2和主机间的通信时许图。数据在该时钟的下降沿进行读取,对于 PS/2 设备,一般来说从时钟脉冲的上升沿到一个数据转变的时间至少要有5us;数据变化到下降沿的时间至少要有 5us,并且不大于 25us。

ps2

在查阅资料后我们得知,我们得知该协议需要6个引脚。

image-20210113194117915

但实际上PS/2协议只需要4个针脚,也就是GND、VCC、PS2_CLK和PS2_DATA,而该协议的时序图如下:

image-20210114190153221

FPGA或主机接收键盘发回的数据,通过键盘的编码规则判定键盘当前的操作,扫描码有两种不同的类型:通码(make code)和断码(break code)。当一个键被按下或持续按住时,键盘会将该键的通码发送给主机;而当一个键被释放时,键盘会将该键的断码发送给主机。值得注意的是,最后一位校验位有的芯片使用的是奇校验,而有的芯片使用的是偶校验,需要我们根据不同的板子进行调整。

根据这个原理,我们很快写出来了一个简单的键盘接收驱动,在烧到板子内进行验证时却怎么页也读不出来,键盘输入完全无效,USB-HID旁边的busy灯长亮。

在查阅大量资料后我恍然大悟:我使用的键盘为杜伽K320,在我的电脑中被识别为了多个键盘输入,如下图所示

image-20210114190141227

除了PS/2外还存在着其他虚拟出的键盘输入,因此无法传输数据。

为此,我们在淘宝上购买了一块原生支持PS/2协议的键盘,之后使用该键盘可以很流畅的读出我们的键盘输入,调试工作终于有救了。

3. ROM IP核模块

坦克大战中的游戏界面贴图均为静态图片,通过选择将其放置在合适的位置上。为了让玩家获得更好的游戏体验,我们从网上搜集了大量的png格式的图片,通过软件首先转化为bmp,再通过自己写的python脚本转化为coe文件。利用这些图片我们创建了游戏开始界面、敌我坦克图片、子弹图片、道具图片以及游戏结束图片,一共有29张之多。

具体使用的ip核如下所示:

ip_pic

其中只有background图片使用了最近邻插值法,通过采用320*240的图片可以减少1/4的BRAM空间占用,而敌我坦克、子弹等图片使用了PS进行处理,坦克、子弹等图片的边缘均为白色。因此当读到12'b1111_1111_1111时,将其转化为background的图像信息即可,以此实现图片的有效覆盖。

问题分析

(1) 资源问题

我们采用的是Nexys4 DDR-A7开发板,其具体参数如下图所示

image-20210113195559225

在使用vivado查看资源使用率后我们发现,该板子一共有135个fast block RAM,但是一张640*480,12bits的图片居然能使用105个block RAM,也就是说我们差不多连一张背景图都放不下,因此每个图片就只需要26.5个BRAM。

而在实际中,我们则将该图片再转变为640*480的图片。假如图中的5个有坐标的点为320x240网格上的点,图中除了黄色点以为的其他点都是进行线性变换后640x480网格上的像素点,现在我们可以看到P(x,y)附近的四个点都没有正好落在格点上(我们先不管其他点),而他们四个距离黄色点最近,那么我在显示的时候,就把这四个点的颜色都显示成P(x,y)的颜色。

这是一种很简单很基础的插值,但是对于我们这种小图片还是很管用的,大大减少了Block Memory资源的浪费。

(2) 边缘白框处理

在图片显示出来后,还是存在很多问题,最典型的问题就是图像边框白色和游戏背景图的黑色对比过于强烈,已经极大的影响了游戏体验,我的解决方案也比较简单,就是选择在处理图片的时候,将方块周围的不需要部分全部转变为0xffffff,在转换为coe文件后,只要读取到这种颜色编码oxfff,就选择不显示ROM中的数据,而是显示游戏背景,这样就避免了刚刚提到的问题。而实现起来也比较简单,如下图所示:

.in2( ( player1_tank_data == 12'hfff ) ? 12'h000 : player1_tank_data ),
.in3( ( player1_bullet_data == 12'hfff ) ? 12'h000 : player1_bullet_data ),
.in4( ( player2_tank_data == 12'hfff ) ? 12'h000 : player2_tank_data ),
.in5( ( player2_bullet_data == 12'hfff ) ? 12'h000 : player2_bullet_data ),

不过另一个问题是,因为初始的bmp图片是24位的,在转换为12位coe的时候就会使得很多颜色相近的颜色编码暴力变为同一种颜色编码,这个我最初在图片中埋藏的信息就不知道被转换成什么了。这个问题目前还没有得到很好的解决,总体而言并不是很影响整体游戏体验。

(3) 同步显示问题

我发现我显示的图片整体向右偏移了一个细条,我经过反复查阅资料和参考优秀实验报告,得出这是因为:我VGA模块输出的x,y坐标,在always语句中不会立刻计算这个xy对应的ROM地址值,他会等到下一个时钟周期到来的时候才会更新,ROM地址更新后,ROM的data也不会立刻更新,而是等到再过一个时钟周期,而VGA模块接受到Data后,也是要再过一个时钟周期才能更新,这就是为什么我的模块无法预期进行。

解决方案实际上是这样的:首先把根据x、y计算地址值的always模块改成always@(∗),这样他就会退化成时序逻辑,就不存在非阻塞赋值引发的延时问题了,但ROM模块必须是时序的,不过也有办法解决。我最开始给ROM的时钟是25MHz,与VGA同步,这样就一定会造成延迟,但我可以把ROM的时钟加速到VGA时钟的整数倍如100MHz,就可以在xy改变后、下一个vga时钟前拿到合适的输出。

(4) COE文件与BMP文件互相转换

为了更好、更方便的生成coe文件,我们通过python将bmp文件转化为需要指定宽度、高度的coe格式文件,同时我们也使用c语言将coe文件转化为bmp格式文件,上述代码我们在附录中提供,实验结果证明我们的代码效果非常好,达到我们预期的要求!

4. 随机数生成模块

在该游戏中,有两个部分需要用到随机数产生,一个是道具初始位置的选择,我们需要随机产生[0,619]和[0,459]处的随机数,同时敌方坦克控制上也要用到00-11的随机数控制方向,因此需要一个真题产生随机数的模块。

在Verilog中,实际上是预制了随机数生成的方法,也就是使用:$random ,但是这个方法无法正常通过综合,百度后我发现该随机数实际上只能在testbench中进行使用。这就比较麻烦,我因此搜索了一些通过硬件实现随机数的方法,其中最常用的是LFSR法。

lfsr

通过奇怪的选择实现在[0,2^n-1]之间周期很长的周期序列,但在使用中一定要注意很重要的问题:时钟选择一定要合适!时钟不能过快,否则只能得到相同的随机数。

整体代码请参见附录,我们使用了15位的随机数产生,经过测试已经能很好的满足我们的需要。而针对x,y的位置只需mod max_num即可,而敌方1,2,3,4号坦克控制只需要取随机数的[7:6],[5:4],[3:2],[1:0]位即可。

        rand_num[ 0 ] <= rand_num[ 14 ];
        rand_num[ 1 ] <= rand_num[ 0 ];
        rand_num[ 2 ] <= rand_num[ 1 ];
        rand_num[ 3 ] <= rand_num[ 2 ];
        rand_num[ 4 ] <= rand_num[ 3 ] ^ rand_num[ 14 ];
        rand_num[ 5 ] <= rand_num[ 4 ] ^ rand_num[ 14 ];
        rand_num[ 6 ] <= rand_num[ 5 ] ^ rand_num[ 14 ];
        rand_num[ 7 ] <= rand_num[ 6 ];
        rand_num[ 9 ] <= rand_num[ 7 ];
        rand_num[ 8 ] <= rand_num[ 8 ] ^ rand_num[ 14 ];
        rand_num[ 10 ] <= rand_num[ 9 ];
        rand_num[ 11 ] <= rand_num[ 10 ];
        rand_num[ 12 ] <= rand_num[ 11 ] ^ rand_num[ 14 ];
        rand_num[ 13 ] <= rand_num[ 12 ];
        rand_num[ 14 ] <= rand_num[ 13 ];

5. 游戏整体界面

整体而言我们的游戏界面分为三部分:

  1. 游戏开始界面

    img

  2. 游戏进行中界面

    img

  3. 游戏结束界面

    img

整体状态由顶层的game_mode模块控制,我们提供了经典模式和无尽模式也在此进行选择,因此我们一共需要四个状态:

  1. 初始开始界面状态
  2. 经典模式状态
  3. 无尽模式状态
  4. 游戏介绍状态

而几种模式的选择和切换也在game_mode模块中进行,通过BTN按钮进行切换,而输出模块也根据当前的mode状态来输出,其中的enable和gameover信号可以自行查阅附录中的代码。

6. 坦克控制

一个完整的坦克控制需要三个模块协同:

  1. 坦克使能信号控制(game_mode)
  2. 坦克移动控制 (tank_move)
  3. 坦克显示 (tank_display)

在使能信号控制中,我们根据当前的mode判断是否让坦克移动,通过产生enable信号影响下面两个模块。

在坦克移动控制中,我们采用有限状态机设计思路,如下图所示

always @( * ) begin: signals
    counter_en = 1'b0;
    init = 1'b0;
    tank_dir_feedback[ 2 ] = 1;
    case ( current_state )
        INITIAL: begin
            init = 1;
        end
        UP: begin
            counter_en = 1;
            tank_dir_feedback = 3'b000;
        end
        DOWN: begin
            counter_en = 1;
            tank_dir_feedback = 3'b001;
        end
        LEFT: begin
            counter_en = 1;
            tank_dir_feedback = 3'b010;
        end
        RIGHT: begin
            counter_en = 1;
            tank_dir_feedback = 3'b011;
        end
    endcase
end

通过控制坦克的当前状态计算坦克的坐标,另一方面计算坦克的次态将其付给先态,这样写起来也更加方便具体。

值得注意的是,我们根据当前是否有item_faster即加速道具来控制坦克的移动速度,如下所示

wire [ 31: 0 ] counter_num = item_faster ? 1_500_000 : 2_100_000;

之后我们进行计数,如同所示:

always @( posedge clk ) begin
    if ( !reset_n ) begin
        counter <= 0;
        counter_move_en <= 0;
    end
    else if ( counter == counter_num ) begin
        counter <= 0;
        counter_move_en <= 1;
    end
    else if ( counter_en == 0 ) begin
        counter <= 0;
        counter_move_en <= 0;
    end
    else begin
        counter <= counter + 1;
        counter_move_en <= 0;
    end
end

当计数器达到设定的counter_num时候,我们就将坦克移动一个像素,这样就达到了控制坦克移动速度的目的。

而在坦克展示模块中,似乎没有那么多值得注意的点,一个简单的位置判断和状态判断即可,但要看到我们由于无敌星的展示是在坦克原有的背景上额外添加一层黄色五角星,因此同时需要item_invincible信号进行输出判断。

一方面,坦克四个方向如果使用一张图片配合纯计算,实现难度较高,因此我们采用四张图片展示坦克的四种方向;另一方面,敌方坦克和我方坦克的图片并不相同,因此总的来说我们一共需要2*4=8张图片进行展示,最后使用数据选择器根据坦克的方向、敌我坦克的区分来进行选择,最后配上是否有无敌星的数据进行或运算即可以得到最终的坦克展示数据,写出来也非常的难看。

assign tankData = ( ( tank_en & ~tank_destroyed ) ? ( player_enermy ? outData_enermy : outData ) : 0 ) | ( ( ( tank_revive || item_invincible ) && tank_en ) ? outData_star : 0 );

实际上这样并不算好,因为一个坦克我们使用了8个图片的资源,但是由于图片较小(32*32),我也没有对此进行足够的改进。实际上应该将敌我坦克进行区分,分别使用不同的模块展示,这样可以只使用4个图片,六个图片总的来说可以节省大约5个BRAM。

7. 敌方坦克移动控制

实际上这部分是我们整体设计中最有难度的部分,也是我们调试时间最长的部分,前前后后一共花了一整天的时间在这个模块上,最终实现的效果也只能说一般。

很容易我们首先想到的是让坦克完全随机游走,即每一个时钟周期内通过产生的随机数来得到该时钟周期内坦克行走的方向,而开火键直接全部设置为1,这样虽然方便写,但是最后实现出来的效果非常非常不理想,敌方坦克就像跳舞一样不知道在干嘛,这让人也很苦恼。

我们最初的目的是做一个躲子弹飞快、碰见人就发射的高级AI,但是由于都是第一次使用verilog,对其中的组合逻辑控制还不够了解,也没法使用类似c语言中的dijkstra、A-star等著名的寻路算法,但是我们采用了一种比较简单的方法,基本也能保证游戏性:

  1. 判断自己当前位置是否在玩家1、玩家2的子弹射线上
    • True:根据当前位置选择正确的逃跑路线,并持续至少3个时钟周期。路线选择的方法如下:只考虑和子弹垂直方向的两个方向,如子弹在上下方向那么坦克必定左右游走,并持续至少三个时钟,如果坦克过于靠近一侧边界,那么就向另一个边界走,如果在中间则那边更容易不被子弹射中往那边走。
    • False:根据随机数模块选择是从横/竖方向接近某一玩家的坦克
  2. 判断自己是否和玩家1、玩家2的坦克在同一水平线或同一竖直线中
    • True:调整方向,射击
    • False:根据随机数模块选择是从横/竖方向接近某一玩家的坦克
  3. 根据随机数模块选择是从横/竖方向接近某一玩家的坦克,但每隔三个周期随机向某个方向游走一个周期。
  4. 每个10个时钟周期随机选择目前追逐的玩家1/2,并追逐10个周期。

这样虽然没有达到我们最初的目的,但也很好的满足了游戏性和随机性的需求,因为有至多1/3的周期在随机方向上进行,而其他时间都在做有效移动。

但是这样会导致一个问题:坦克被卡住!

因为整体的碰撞检测和敌方坦克逻辑分属两个模块,在不重构代码的基础上我们很难检测到坦克被卡住了,也有可能出现四个敌方坦克在某个角度刚好被互相卡住的情况,但一般来说出现该情况的概论较小,出现该情况也不是特别影响游戏体验,因此在时间仓促的情况下我没有进行很好的优化,在展望部分我们详细对此进行了分析。

8. 子弹控制

在子弹控制中,我们只采用了一个大模块进行控制,并没有采取子弹展示和移动逻辑分开的情况。整体实现过程也是使用了一个和坦克控制类似的状态机,如图所示:

always @( * ) begin: state_table
    case ( current_state )
        WAIT:
            next_state = start ? READY : WAIT;
        READY: begin
            if ( !tank_fire ) begin
                next_state = READY;
            end
            else begin
                case ( tank_dir )
                    2'b00:
                        next_state = UP;
                    2'b01:
                        next_state = DOWN;
                    2'b10:
                        next_state = LEFT;
                    2'b11:
                        next_state = RIGHT;
                endcase
            end
        end
        UP:
            next_state = ready ? READY : UP;
        DOWN:
            next_state = ready ? READY : DOWN;
        LEFT:
            next_state = ready ? READY : LEFT;
        RIGHT:
            next_state = ready ? READY : RIGHT;
        default:
            next_state = READY;
    endcase
end

炮弹的速度控制这里不再细表,值得注意的是坦克子弹也需要四张图片来选择,最后使用数据选择器进行选择当前方向的图片。

还有一点需要额外说明,针对每个数据的位置,我们记录的都是其左上角点的坐标,无论是任何方向坦克还是炮弹都是如此,因此需要进行的额外计算也比较多,但只要记住每个坐标都是左上角,很多问题都可以慢慢解决。

9. 碰撞检测与信号控制

这个模块我也踩了很多的坑,首先我在多个always里对一个变量进行赋值,但是代码始终过不去综合,最后查阅资料才发现原理只能在一个always里综合,就很自闭,最后信心满满的写完了结果出现了critical warning,一直在说我有timing loop,又是找了很久后才发现是混合使用了组合逻辑和时序逻辑,结果一个的输出影响了另一个的输入,就很让人自闭。

最后我们采用了细分模块object_collide_detection设计,传入两个物体的当前坐标、当前位置和当前状态,从而给出该两个物体是否产生碰撞,但是有一个很重要的问题:该游戏涉及的物体非常的多,6个坦克与6个子弹如果都要进行该检测,结果将会非常的大。最后我们选择使用了约40个object_collide_detection来对需要的物体(坦克对坦克、坦克对子弹)进行检测,最后使用时序电路对结果进行分析处理,即时给出合理的信号,如图是一个object_collide_detection使用样例:

object_collide_detection tank1_enermy1(
                             player1_tank_H, player1_tank_V, player1_tank_en_feedback, player1_tank_dir,
                             TANK_HEIGHT, TANK_WIDTH,
                             enermy1_tank_H, enermy1_tank_V, enermy1_tank_en_feedback, enermy1_tank_dir,
                             TANK_HEIGHT, TANK_WIDTH,
                             player1_tank_collide[ 0 ], player1_tank_tmp[ 0 ]
                         );

player信号控制例子:

always @( posedge clk ) begin: player2_tank_enable_signal

    if ( player2_revive || item_invincible ) begin
        player2_tank_en_feedback <= 1;
    end
    else if ( !reset_n ) begin
        player2_tank_en_feedback <= 1;
    end

    else if ( enermy1_bullet_collide[ 1 ] || enermy2_bullet_collide[ 1 ] || enermy3_bullet_collide[ 1 ] || enermy4_bullet_collide[ 1 ] ) begin
        player2_tank_en_feedback <= 0;
    end


    // player2_tank_en_feedback <= ~player1_bullet_collide[1];

    player2_tank_move_en <= ~( | player2_tank_collide );
end


always @( posedge clk ) begin: player1_tank_enable_signal
    if ( player1_revive || item_invincible ) begin
        player1_tank_en_feedback <= 1;
    end
    else if ( !reset_n ) begin
        player1_tank_en_feedback <= 1;
    end

    else if ( enermy1_bullet_collide[ 0 ] || enermy2_bullet_collide[ 0 ] || enermy3_bullet_collide[ 0 ] || enermy4_bullet_collide[ 0 ] ) begin
        player1_tank_en_feedback <= 0;
    end


    player1_tank_move_en <= ~( | player1_tank_collide );
end

最后我们检测各个模块的高电位即可进行判断赋值。

结果表明,我们设计的object_collide_detection非常的合理,跑起来效果出乎意料的好,可以很好的检测到坦克间的碰撞。

10. 物品生成控制

实际上这部分代码也比较好理解,物品生成主要有三部分组成:

  1. item_logic控制物品生成逻辑
  2. item_display控制物品展示
  3. item_random_generate控制物品随机位置

item_logic通过控制产生的信号作用到其他两个模块,每隔一段时间如果本物品还没有坦克碰到则控制下一个物品的准备生成,而如果在物品展示时间内有坦克碰到,则将该item的输出置为1代表在该物品的生效时间内,一段时间后将其置为0代表该物品失效。

而item_random和item_display比较简单,这里不再赘述。

这里需要提及一下各个物品的生效方法:

  1. 冰冻:如果该信号存在坦克的状态时钟是ready,无法移动
  2. 生命值:收到该信号的上升沿后在控制模块中增加对应玩家的生命值
  3. 时间:收到该信号的上升沿后在控制模块中增加总的游戏剩余
  4. 无敌:收到伤害后如果有该状态HP<=HP
  5. 加速器:将时钟的counter_num调小,变相加快运动速度

11. DispNumber模块

在数逻实验中我们曾经自己通过ISE画图的方式设计过并行数码管显示模块,但是我觉得我当时画图得到的模块不是很好用,并不能满足我们的需求。针对两块板子(A7/K7),我们分别设计了串行和并行的显示模块,因为A7的8个七段数码管只支持并行输入,而K7的七段数码管只支持串行输入,这就比较伤脑筋。

因此我改进了一下实验课中实现过的Disp_num使其可以有32位的输入,整体实现过程并不算复杂,但是还是花费了一点心思,代码在附录中。

seg_a7

A7的并行七段数码管

而串行输入就更具有挑战性了,我们实际上采用了65位的移位寄存器进行左移操作,同时在寄存器的同步输入端补0,当检测到基本上全部充满0时,可以证明我们这组数据已经完全被移入七段数码管中,此时我们终止clk的输入,在一定时间后我们再重复上述的操作,因此就可以得到串行的输入。LED灯同理,采用17位移位寄存器即可,整体代码在附录中。

seg_k7

K7的串行七段数码管

五、资源分配和RTL

  1. 资源分配和引脚图

    此次课程设计使用的板子是Artix-7,我使用的资源也非常的多,包括并行七段数码管、LED灯、VGA、USB-HID等等,综合后资源使用图如下

    image-20210114194154906

引脚分配图如下

image-20210114194259553

逻辑图如下

image-20210114202833948

从上面这几张图我们可以看到,这次课程作业使用的板上资源是非常非常多的,BRAM的使用率达到了95%,一共使用197cells,总计4406nets,连输出输入口都有58个,实际上在期中报告中,我本来规划做出更多的功能,但未能完全实现主要还是受到了板子资源的制约和时间制约。

六、物理验证

本次课程设计基本上全部在Nexys4 Artix-7进行,后期移植到SWORD2.0上,我们通过在Nexys4和SWORD2.0上开展,顺便也验证工程的可移植性。

以下综合、实现、生成比特流均在下述配置下进行,使用开发工具为Vivado 2019.2:

CPU Name: Intel CoreTM i5-9300H CPU @ 2.40GHz

Property Value
Base Frequency 2.4 GHz
Max Turbo Frequency 4.10 GHz
Cache 4 MB Intel® Smart Cache
Cores Number 4
Threads 8
TDP 45W

RAM: 16GB DDR4 2666MHz with Two channel memory

SSD: SAMSUNG MZVLB1T0HBLR-000L2

一、Nexys4物理验证

开始界面

img

七段数码管显示右半部分为初始生命值,左半部分为初始时间

image-20210114201846276

进入游戏(经典模式)

img

img

img

此时我们看下七段数码管和LED灯的输出:

img

LED灯正确表示了剩余生命值,而第三位和第七位的七段数码管的得分也显示正常

重新进入游戏(无尽模式)

img

img

七段数码管最右边一位表示剩余时间,LED灯同时也表示剩余时间

img

可以看到显示正确

游戏结束界面:

img

可以看到显示数据也是完全正确的。

至此,我们简单的物理验证成功,满足了最初的设计需求。

二、SWORD2.0物理验证

开始界面

img

经典模式

img

img

可以看到七段数码管和LED灯均正常显示

游戏结束画面:

img

无尽模式:

img

img

可以看到LED灯表示剩余时间正确,七段数码管显示正确,同时上侧的时间展示正确。

游戏结束画面:

img

七、改进思路

由于时间有限,这个游戏的水准并不是特别理想。我们也想到了很多改进的方案。

1. AI坦克的移动

我们目前的随机移动策略还是很有效的,至少能让AI坦克进行较为有效的移动并开火。但是这还是显得有些笨拙,我们可以考虑换一种更加有效的移动和开火策略以增加游戏难度。比如说,移动决策不仅要根据当前敌方位置和是否开火,还应该考虑到其余三个队友的位置。同时,我们可以让坦克进行一些预判误导,比如说先向下走很长一段路,然后突然掉头,这样子会显著增加难度。

2. 添加障碍物

经典坦克大战的地图中都是有很多障碍物的,比如草,河流,砖块,石头等。它们有着不同的特性和功能,能显著增加游戏的趣味性。我们可以在将来的改进中加入这一特性。

image-20210113220701419

3. 改进图像插值算法

为了节省空间,我们将背景图缩小为320*240,等到使用的时候再使用最近邻插值变换回来。但我们希望在不影响空间占用的情况下进一步提升画质,那么我们就想到了双线性插值。

设目标点$P$和其他四个点$Q_{11},Q_{12},Q_{21},Q_{22}$的颜色为$f(x,y),f(x_1,y_1),f(x_1,y_2),f(x_2,y_1),f(x_2,y_2)$

对目标点在$x$轴的两个投影点进行线性Lagrange插值: $$ \displaystyle { \begin{aligned} f(x,y_{1}) \approx {\frac {x_{2}-x}{x_{2}-x_{1}}}f(Q_{11})+{\frac {x-x_{1}}{x_{2}-x_{1}}}f(Q_{21}),
f(x,y_{2}) \approx {\frac {x_{2}-x}{x_{2}-x_{1}}}f(Q_{12})+{\frac {x-x_{1}}{x_{2}-x_{1}}}f(Q_{22}). \end{aligned}} $$ 然后再对这两个点使用线性Lagrange插值,得出: $$ {\displaystyle {\begin{aligned}f(x,y) \approx {\frac {y_{2}-y}{y_{2}-y_{1}}}\left({\frac {x_{2}-x}{x_{2}-x_{1}}}f(Q_{11})+{\frac {x-x_{1}}{x_{2}-x_{1}}}f(Q_{21})\right)+{\frac {y-y_{1}}{y_{2}-y_{1}}}\left({\frac {x_{2}-x}{x_{2}-x_{1}}}f(Q_{12})+{\frac {x-x_{1}}{x_{2}-x_{1}}}f(Q_{22})\right)\end{aligned}}} $$ 事实上,我们在网上已经看到了相关代码和ip核,比如说,有一个实现方式是这样的。但是由于实现难度较大,也考虑到如此低分辨率下该算法对画质提升有限,我们便暂时放弃了此算法。

img

4. 优化坦克碰撞逻辑

我们当前只是机械的比较坦克图片的外轮廓,这并不是很精确。我们可以考虑取更多特征点进行碰撞检测,让其与视觉效果相匹配。我们可以忽略掉坦克图片四周的白边框,同时加上伸出来的炮管的碰撞检测,比如说,将炮管头也作为一个特征点,将坦克的碰撞体积改为多边形,这样可以让碰撞和实际观测相符合。

5. 添加音效

一个真正的坦克大战游戏怎么能少了音效。实际上,这个功能的实现应当不会很复杂。虽然Nexys4-DDR板子上并没有蜂鸣器,但是我们可以利用其提供的3.5mm接口进行音频输出。Digilent上有着音频输出的相关教程。

但是,这样的输出功率很可能不够,那我们就可以使用板子上的pmod扩展一个音频输出。在Digilent官网上,我们很容易就能搜索到这类组件,并且找到很多相关教程。

img

上述改进思路我们都有进行一定思考,但是苦于板上资源限制和时间限制,并没能得到实现。

八、总结

数逻的课程设计是我们大学这段时间碰到的最困难的课程设计,其困难的主要原因在于我们对Verilog语法和对开发环境的不熟悉。硬件开发与以往使用高级语言进行软件开发的感觉完全不同,两者有着截然不同的逻辑,电路的同步性也让我们需要认真思考代码的实现方式:比如说,在Verilog中,我们难以使用if语句进行条件块的跳转,而是需要在模块中生成相关信号,基于此进行分支;在移动逻辑中,使用状态机往往能够使代码更加清晰易读。除此之外,EDA工具生成过于缓慢,也占用了我们大量的时间。这个作业从头到尾花了我们接近三周的时间,由于临近期末,ddl压力大,我们做的也就比较紧张。如果有更加充裕的时间来做这个作业,我们会做的更好。

九、成员分工

  • raynor:道具逻辑模块,游戏界面设计,游戏逻辑模块,PS2驱动,数码管显示模块
  • sz:坦克逻辑模块,子弹逻辑模块,碰撞检测逻辑,VGA驱动

About

a FPGA tank game for ZJU Digital logic design

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published