基于FPGA的“俄罗斯方块”设计(附设计文档)

2020-12-30 10:35:13 浏览数 (1)

大侠好,欢迎来到FPGA技术江湖,江湖偌大,相见即是缘分。大侠可以关注FPGA技术江湖,在“闯荡江湖”、"行侠仗义"栏里获取其他感兴趣的资源,或者一起煮酒言欢。

今天给各位大侠带来基于FPGA的“俄罗斯方块”设计,设计思路以及代码参考文档,各位大侠可以在“FPGA技术江湖”知识星球内获取,如何加入知识星球可以查看如下文章欢迎加入FPGA专业技术交流群、知识星球!(交流群QQ、微信双向选择)。

以后机会多多,慢慢分享一些项目开发以及深入学习方面的内容,欢迎各位大侠一起交流学习,取其精华,去其糟粕,共同进步。话不多说,上货。

设计目的

通过此次项目,完成以下目的:

1) 熟悉Xilinx FPGA的架构及开发流程;

2) 设计一个功能完整的系统,掌握FSM Datapath的设计方法。

设计内容

1. 项目介绍

本项目主要在FPGA上实现了一个经典小游戏“俄罗斯方块”。本项目基本解决方案是,使用Xilinx Zynq系列开发板 ZedBoard 作为平台,实现主控模块,通过VGA接口来控制屏幕进行显示。

2. 系统框架

整个系统由四部分组成,按键输入处理模块、控制模块、数据路径模块以及VGA显示接口模块。整个系统的结构如下图所示:

下面分别对四个模块进行介绍:

1) 按键输入处理模块

按键处理模块的主要功能是对输入系统的up,down,left,right四个控制信号进行消抖处理,并对其进行上升沿检测。

消抖模块采用了一个4位的移位寄存器,先将输入信号延迟4个时钟周期,再对其以一个较低的时钟频率进行采用。消抖模块的结构如下图所示:

为了简化控制系统,在本系统的设计过程中,不考虑长时间按键产生连按效果。因而,需要对按键进行上升沿检测。上升沿检测的基本实现方案是加入一组寄存器,对前一个的按键信号进行暂存,将暂存的值与当前值进行比较,当上一个值为0而当前值为1时,即认为其检测到了一个上升沿。

2) 控制模块

控制模块采用FSM的方式进行控制。在控制模块中,定义了10个状态:

S_idle:上电复位后进入空状态,当start信号为1时进入S_new状态

S_new:用于产生新的俄罗斯方块。

S_hold:保持状态。在这个状态中进行计时,当时间到达一定间隔时,转到S_down状态;或者等待输入信号(up,down,left,right)时,转到S_down(按键为down)或者S_move(up,left,right)状态。

S_down:判断当前俄罗斯块能否下移一格。如果可以,则转到S_remove_1状态,如果不行,则转到S_shift状态。

S_move:判断当前俄罗斯块能够按照按键信号指定的指令进行移动,如果可以,则转到S_shift状态,如果不可以,则转到S_remove_1状态。

S_shift:更新俄罗斯方块的坐标信息。返回S_hold。

S_remove_1:更新整个屏幕的矩阵信息。转移到S_remove_2状态。

S_remove_2:判断是否可以消除,将可以消除的行消除,并将上面的行下移一行。重复此过程,直到没有可消除的行为止。跳转到S_isdie状态

S_isdie:判断是否游戏结束。如果结束,则跳转到S_stop状态。如果没有,则跳转到S_new状态,生成新的俄罗斯方块。

S_stop:清除整个屏幕,并跳转到S_idle状态。

整个控制过程的ASMD图如下图所示:

3) 数据路径

数据路径模块主要功能是,根据控制模块给出的信号,对俄罗斯方块当前的逻辑状态进行判断,更新背景矩阵。具体如下:

方块:

方块分为非活动方块与活动方块。非活动方块为:(1)之前下落的方块;(2)下落后方块消除之后的结果。由背景矩阵表示。活动方块为当前下落中的方块,由活动方块坐标与方块类型表示(后简称方块)。

背景矩阵:

reg [9:0] R [23:0];

背景矩阵R是24行10列的寄存器组,负责保存非活动方块坐标,即R中任一位置,如方块存在,则该位置1,否则为0。

活动方块坐标:

output reg [4:0] n,

output reg [3:0] m,

n, m分别为当前活动方块的行、列指针,指向方块固定点位置。方块固定点为方块旋转时不变的格点,依据方块种类决定,下文方块模型中详述。

方块类型:

output reg [6:0] BLOCK,

BLOCK代表方块类型,由7位编码构成。

数据交换:

Datapath与其余模块的数据交换分为两部分:

(1)与control_unit间的状态指令交互;

(2)控制merge,间接实现对VGA的控制。

方块模型:

俄罗斯方块共有7种形状的方块(O,L,J,I,T,Z,S),每种方块有1-4种不同的旋转变形方式。为方便起见,将方块定位A-G,旋转编号为1-4,将方块编码成A_1-G_2的19种,如下图:(图中,深色方块是该种方块的固定点)

方块运动:

产生:

方块产生由一个简单的伪随机过程决定。系统采用一个3位的计数器产生随机数,进入S_new,BLOCK的值被NEW_BLOCK覆盖,方块坐标n<=1;m<=5;同时,根据计数器,NEW_BLOCK的值刷新为A_1,B_1,…,G_1中的一种,作为下一次方块。

移动:

方块移动分为四种:旋转,下落,向左,向右,由键盘KEYBOARD=[UP, DOWN, LEFT, RIGHT]控制。移动分两步进行:(1)判断;(2)转换。

判断过程包含S_down,S_move。判断分两步:首先,判断变换后方块坐标是否合法,即变换后是否会造成方块越界。然后,判断变换后方块可能占据的新位置是否有背景矩阵方块存在。两步判断通过后返回成功信号,否则失败。因判断代码量较多,仅举一例说明:

判断D_1向右运动(MOVE_ABLE初值为0):

if (m<=8)

if (!((R[n-1][m 1])|(R[n][m 1])|(R[n 1][m 1])|(R[n 2][m 1])))

MOVE_ABLE=1;

else MOVE_ABLE=0;

转换过程(S_shift)进行方块的移动或变形。根据KEYBOARD,移动时,改变方块坐标;变形时,方块按类别变换,如:A_1→A_1;B_1→B_2; B_2→B_3; B_4→B_1;

停止与消除:

方块停止与消除由两个状态完成:S_remove1,S_remove2。

前一状态中,根据BLOCK, n, m,将活动方块位置覆盖至R,变为非活动方块。

后一状态中,根据行满状态,进行行的消除与平移,具体如下:

显然,俄罗斯方块能影响的最大行数为4,因此,在REMOVE_2中,仅对R[n-1],R[n],R[n 1],R[n 2]四行依次进行处理。处理过程为:如果该行(k)满,则由k行开始,至1行结束,逐行向下平移,当前平移位置由计数器REMOVE_2_C控制,当前行消除截止由标志位SIG确认。

每行处理完后,将REMOVE_FINISH[3:0]中相应位置1,REMOVE_FINISH全1时,REMOVE_2完成。

死亡判定:

R中的0-3行位于屏幕上方,不进行显示,仅有新生成的方块坐标会进入这一区域。因而,当消除完成后,如R[3]不为空,游戏结束。

4) 显示部分

输出结果通过VGA接口接入显示屏显示。VGA(Video Graphics Array)视频图形阵列是IBM于1987年提出的一个使用模拟信号的电脑显示标准。VGA接口即电脑采用VGA标准输出数据的专用接口。VGA接口共有15针,分成3排,每排5个孔,显卡上应用最为广泛的接口类型,绝大多数显卡都带有此种接口。它传输红、绿、蓝模拟信号以及同步信号(水平和垂直信号)。

使用Verilog HDL语言对VGA进行控制一般只需控制行扫描信号、列扫描信号和红绿蓝三色信号输出即可。

VGA输出可分为四个模块:时钟分频模块、数据组织模块、接口控制模块和顶层模块。以下进行分块描述。

时钟模块分频模块对FPGA系统时钟进行分频。由于使用的显示屏参数为640*480*60Hz,其真实屏幕大小为800*525,因此所需时钟频率为800*525*60Hz=25.175MHz,可近似处理为25MHz。FPGA系统时钟为100M,因此将其四分频即可基本满足显示要求。

数据组织模块是将预备输出的数据组织为可以通过VGA接口控制的数据形式,本次设计中因接口已经协调,数据可不经过此模块进行组织,故可忽略该模块。

接口控制模块通过VGA接口对显示屏进行控制。VGA的扫描顺序是从左到右,从上到下。例如在640X480的显示模式下,从显示器的左上角开始往右扫描,直到640个像素扫完,再回到最左边,开始第二行的扫描,如此往复,到第480行扫完时即完成一帧图像的显示。这时又回到左上角,开始下一帧图像的扫描。如果每秒能完成60帧,则称屏幕刷新频率为60Hz。宏观上,一帧屏幕由480个行和640个列填充而成,而实际上,一帧屏幕除了显示区,还包含其他未显示部分,作为边框或者用来同步。具体而言,一个完整的行同步信号包含了左边框、显示区、右边框还有返回区四个部分,总共800个像素,其分配如下:

同样的,一个完整的垂直同步信号也分为四个区域,总共525个像素,分配如下:

模块通过组织输出行扫描信号、列扫描信号和三原色信号对显示屏实现控制。

设计结果

设计结果图如下:

图7:设计结果图

设计代码

由于代码量较大,这里只展示了部分代码,需要的大侠可以按照开篇介绍的方法进入“FPGA技术江湖”知识星球获取设计文档,获取设计代码。

顶层模块设计代码:

代码语言:javascript复制
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company: 
// Engineer: 
// 
// Create Date: 
// Design Name: 
// Module Name: tetris
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//////////////////////////////////////////////////////////////////////////////////


module tetris #(
    parameter ROW = 20,
    parameter COL = 10
    )(
    input clk,
    input rst,
    input UP_KEY,
    input LEFT_KEY,
    input RIGHT_KEY,
    input DOWN_KEY,
    input start,
    output vsync_r,
    output hsync_r,
    output [3:0]OutRed, OutGreen,
    output [3:0]OutBlue
    );
    
    wire [3:0] opcode;
    wire gen_random;
    wire hold;
    wire shift;
    wire move_down;
    wire remove_1;
    wire remove_2;
    wire stop;
    wire move;
    wire isdie;
    wire shift_finish;
    wire down_comp;
    wire move_comp;
    wire die;
    wire [ROW*COL-1:0] data_out;
    
    wire [6:0] BLOCK;
    wire [3:0] m;
    wire [4:0] n;
    wire [(ROW 4)*COL-1:0] M_OUT;
    
    wire rotate;
    wire left;
    wire right;
    wire down;
    wire auto_down;
    wire rst_n;
    assign rst_n = ~rst;
    
    key u_key (
        .clk(clk),
        .rst_n(rst_n),
        .UP_KEY(UP_KEY),
        .LEFT_KEY(LEFT_KEY),
        .RIGHT_KEY(RIGHT_KEY),
        .DOWN_KEY(DOWN_KEY),
        .rotate(rotate),
        .left(left),
        .right(right),
        .down(down)
    );
    
    game_control_unit u_Controller (
        .clk(clk),
        .rst_n(rst_n),
        .rotate(rotate),
        .left(left),
        .right(right),
        .down(down),
        .start(start),
        .opcode(opcode),
        .gen_random(gen_random),
        .hold(hold),
         .shift(shift),
        .move_down(move_down),
        .remove_1(remove_1),
        .remove_2(remove_2),
        .stop(stop),
        .move(move),
        .isdie(isdie),
        .shift_finish(shift_finish),
        .down_comp(down_comp),
        .move_comp(move_comp),
        .die(die),
        .auto_down(auto_down),
        .remove_2_finish(remove_2_finish)
        );
        
    Datapath_Unit u_Datapath (
        .clk(clk),
        .rst_n(rst_n),
        .NEW(gen_random),
        .MOVE(move),
        .DOWN(move_down),
        .DIE(isdie),
        .SHIFT(shift),
        .REMOVE_1(remove_1),
        .REMOVE_2(remove_2),
        .KEYBOARD(opcode),
        .MOVE_ABLE(move_comp),
        .SHIFT_FINISH(shift_finish),
        .DOWN_ABLE(down_comp),
        .DIE_TRUE(die),
        .M_OUT(M_OUT),
        .n(n),
        .m(m),
        .BLOCK(BLOCK),
        .REMOVE_2_FINISH(remove_2_finish),
        .STOP(stop),
        .AUTODOWN(auto_down)
        );
        
    merge u_merge (
    .clk(clk),
        .rst_n(rst_n),
        .data_in(M_OUT),
        .shape(BLOCK),
        .x_pos(m),
        .y_pos(n),
        .data_out(data_out)
        
        );
     
    top u_VGA (
        .clk(clk),
        .rst(rst),
        .number(data_out),
        .hsync_r(hsync_r),
        .vsync_r(vsync_r),
        .OutRed(OutRed),
        .OutGreen(OutGreen),
        .OutBlue(OutBlue)        
    );
    
    
endmodule

KeyBoard模块代码:

代码语言:javascript复制
`timescale 1ns / 1ps
module key(
    input clk,
    input rst_n,
    input UP_KEY,
    input LEFT_KEY,
    input RIGHT_KEY,
    input DOWN_KEY,
    output reg rotate,
    output reg left,
    output reg right,
    output reg down
    );
    
    reg [3:0] shift_up;
    reg [3:0] shift_left;
    reg [3:0] shift_right;
    reg [3:0] shift_down;
    
    always @(posedge clk or negedge rst_n)
    begin
        if (!rst_n)
            shift_up <= 0;
        else
            shift_up <= {shift_up[2:0], UP_KEY};
    end
    
    always @(posedge clk or negedge rst_n)
    begin
        if (!rst_n)
            shift_right <= 0;
        else
            shift_right <= {shift_right[2:0], RIGHT_KEY};
    end 
    
    always @(posedge clk or negedge rst_n)
    begin
        if (!rst_n)
            shift_left <= 0;
        else
            shift_left <= {shift_left[2:0], LEFT_KEY};
    end
    
    always @(posedge clk or negedge rst_n)
    begin
        if (!rst_n)
            shift_down <= 0;
        else
            shift_down <= {shift_down[2:0], DOWN_KEY};
    end


    reg clk_div;
    reg [7:0] clk_cnt;
    always @ (posedge clk or negedge rst_n)
    begin
        if (!rst_n)
        begin
            clk_cnt <= 0;
            clk_div <= 0;
        end
        else if (clk_cnt <= 8'd49)
        begin
            clk_cnt <= clk_cnt   1;
            clk_div <= clk_div;
        end
        else
        begin
            clk_cnt <= 0;
            clk_div <= ~clk_div;
             end
      end

      always @(posedge clk_div or negedge rst_n)
      begin
          if (!rst_n)
          begin
              rotate <= 0;
              left <= 0;
              right <= 0;
              down <= 0;
          end
          else
          begin
              rotate <= shift_up[3];
              left <= shift_left[3];
              right <= shift_right[3];
              down <= shift_down[3];
          end
      end
      
 endmodule

控制模块代码:

代码语言:javascript复制
module game_control_unit (
    input clk,
    input rst_n,
    input rotate,
    input left,
    input right,
    input down,
    input start,
    output reg [3:0] opcode,
    output reg gen_random,
    output reg hold,
    output reg shift,
    output reg move_down,
    output reg remove_1,
    output reg remove_2,
    output reg stop,
    output reg move,
    output reg isdie,
    output reg auto_down,
    input shift_finish,
    input remove_2_finish,
    input down_comp,
    input move_comp,
    input die
    );
    
    reg left_reg;
    reg right_reg;
    reg up_reg;
    reg down_reg;
    
    always @(posedge clk or negedge rst_n)
    begin
        if (!rst_n)
        begin
            left_reg <= 0;
            right_reg <= 0;
            up_reg <= 0;
            down_reg <= 0;
        end
        else
        begin
            left_reg <= left;
            right_reg <= right;
            up_reg <= rotate;
            down_reg <=  down;
        end
    end
    
    reg auto_down_reg;
    
    always @ (posedge clk or negedge rst_n)
    begin
        if (!rst_n)
            auto_down_reg <= 0;
        else if (time_cnt == time_val)
            auto_down_reg <= 1;
        else 
            auto_down_reg <= 0;
    end
    always @ (posedge clk or negedge rst_n)
    begin
        if (!rst_n)
            auto_down <= 0;
        else
            auto_down <= auto_down_reg;
    end
    
    parameter time_val = 26'd25000001;
    reg [25:0] time_cnt;

    localparam  S_idle      = 4'd0,
                S_new       = 4'd1,
                S_hold      = 4'd2,
                S_move      = 4'd3,
                S_shift     = 4'd4,
                S_down      = 4'd5,
                S_remove_1  = 4'd6,
                S_remove_2  = 4'd7,
                S_isdie     = 4'd8,
                S_stop      = 4'd9;

    reg [3:0] state, next_state;

    always @(posedge clk or negedge rst_n)
    begin
        if (!rst_n)
            state <= S_idle;
        else
            state <= next_state;
    end
    


    always @ (posedge clk or negedge rst_n)
    begin
        if (!rst_n)
            time_cnt <= 0;
            else if (hold == 0 && time_cnt < time_val)
            time_cnt <= time_cnt   1;
        else if (move_down == 1)
            time_cnt <= 0;
        else begin
            time_cnt <= time_cnt;
        end
    end
    always @ (posedge clk or negedge rst_n)
    begin
        if (!rst_n) opcode<=0;
        else opcode<={right, left, down, rotate};
    end 
        

    always @ (*)
    begin
        next_state = S_idle;
        hold = 1;
        gen_random = 0;
        //opcode = 4'b0000;
        shift = 0;
        move_down = 0;
        remove_1 = 0;
        remove_2 = 0;
        stop = 0;
        move = 0;
        isdie = 0;
        case (state)
        S_idle:
        begin
            if (start)
                next_state = S_new;
            else
                next_state = S_idle;
        end
        S_new:
        begin
            gen_random = 1;
            next_state = S_hold;
        end
                S_hold:
        begin
            hold = 0;
            if (time_cnt == time_val)
            begin
                next_state = S_down;
            end
            else if ((down_reg == 0) && (down == 1))
            begin
               next_state = S_down;
            end
            else if ((left_reg == 0 && left == 1)|| ( right_reg == 0 && right == 1)||(up_reg == 0 && rotate == 1))
            begin
                next_state = S_move;
            end
            else
                next_state = S_hold;
        end
        S_move:
        begin
            move = 1;
            if (move_comp)
                next_state = S_shift;
            else
                next_state = S_hold;
        end
        S_shift:
        begin
            shift = 1;
            next_state = S_hold;
        end
        S_down:
        begin
            move_down = 1;
            if (down_comp)
                next_state = S_shift;
            else
                next_state = S_remove_1;                
        end
        S_remove_1:
        begin
            remove_1 = 1;
            next_state = S_remove_2;
        end
        S_remove_2:
        begin
            remove_2 = 1;
            if (remove_2_finish)
                next_state = S_isdie;
            else
                next_state = S_remove_2;
        end
        S_isdie:
        begin
            isdie = 1;
            if (die == 1)
                next_state = S_stop;
            else
                next_state = S_new;
        end
        S_stop:
        begin
            stop = 1;
            next_state = S_idle;
        end
        default next_state = S_idle;
        endcase
    end
    
endmodule

数据路径以及VGA等模块在这里就不展示,代码量过大,详情见开篇介绍。

END

后续会持续更新,带来Vivado、 ISE、Quartus II 、candence等安装相关设计教程,学习资源、项目资源、好文推荐等,希望大侠持续关注。

大侠们,江湖偌大,继续闯荡,愿一切安好,有缘再见!

0 人点赞