Verilog编写VGA显示的小游戏

来源:互联网 发布:淘宝有信用卡套现的吗 编辑:程序博客网 时间:2024/05/16 07:35

引言

这是我的数字集成电路课程设计。实现的是一个我们都玩过的小游戏:一块移动挡板,一个飞来飞去的球,挡板需要把球挡住,没挡住就算输。
代码已上传github,地址是
https://github.com/Wujh1995/Verilog-VGA-game

  • 开发平台:Xilinx ISE
  • 开发语言:Verilog HDL
  • 运行平台:Digilent Basys2开发板

模块描述:

  • 顶层模块:SBgame.v
  • VGA显示及控制模块:VGA_Display.v
  • 数码管显示模块:seven_seg.v
  • 数码管译码模块:seg_decoder.v

端口描述:

  • SBgame.v
module SBgame(            input mclk, //全局时钟,50MHz            input rst,  //复位信号,高电平有效            input to_left, //挡板左移,高电平有效            input to_right, //挡板右移,高电平有效            input [3:0] bar_move_speed, //挡板移动速度控制,为零表示静止            output HSync, //VGA行扫描信号            output [2:1] OutBlue, //蓝色            output [2:0] OutGreen, //绿色            output [2:0] OutRed,  //红色             output VSync, //VGA场扫描信号            output [3:0] seg_select, //数码管位选信号            output [6:0] seg_LED //数码管数据信号,没有小数点    );

关于VGA的显示原理,可以参考
http://blog.csdn.net/u013793399/article/details/51319235
Basys2开发板的数码管有4个,但是位选信号只有一个,为了“同时”显示4位,必须以扫描的方式显示,就像数字电路实验那样。

  • VGA_Display.v
module VGA_Dispay(     input clk, //输入时钟,50MHz     input to_left, //同上     input to_right, //同上     input [3:0] bar_move_speed, //同上     output reg hs, //同上     output reg vs, //同上     output reg [2:0] Red,//同上     output reg [2:0] Green,//同上     output reg [1:0] Blue,//同上     output reg lose //游戏失败信号,没能成功挡住球的话就会触发一个高电平脉冲,用于数码管计数    );
  • seven_seg.v
module seven_seg(     input clk, //这些端口上面都已经讲过一次了     input rst, //就没什么好讲的了     input lose,     output reg [3:0] select,      output reg [6:0] seg    );

模块实现

  • 生成VGA扫描信号
    详细原理请看
    http://blog.csdn.net/u013793399/article/details/51319235
    这里只贴出代码
//generate a half frequency clock of 25MHz    always@(posedge(clk))    begin        clk_25M <= ~clk_25M; //600*480 resolution needs 25MHz    end    /*generate the hs && vs timing*/    always@(posedge(clk_25M))     begin        /*conditions of reseting Hcnter && Vcnter*/        if( Hcnt == PLD-1 ) //have reached the edge of one line        begin            Hcnt <= 0; //reset the horizontal counter            if( Vcnt == LFD-1 ) //only when horizontal pointer reach the edge can the vertical counter increase                Vcnt <=0;            else                Vcnt <= Vcnt + 1;        end        else            Hcnt <= Hcnt + 1;        /*generate hs timing*/        if( Hcnt == PAL - 1 + HFP)            hs <= 1'b0;        else if( Hcnt == PAL - 1 + HFP + HPW )            hs <= 1'b1;        /*generate vs timing*/              if( Vcnt == LAF - 1 + VFP )             vs <= 1'b0;        else if( Vcnt == LAF - 1 + VFP + VPW )            vs <= 1'b1;                     end

需要注意的是,这里必须先进行二分频,因为600*480分辨率需要25MHz,多了不行,少了也不行。选用这个分辨率纯粹是因为25MHz比较好得到,如果是800*600分辨率,需要40MHz,我们只有50MHz时钟,比较麻烦。

  • 显示小球与挡板

    我们定出小球的圆心坐标(ball_x_pos,ball_y_pos),以及小球半径ball_r,其中ball_r是个常数,写在了程序里,坐标则是reg型变量,可以更改,这样小球就可以“移动”了。
    同样的,我们定出挡板的四条边的位置,分别为up_pos、down_pos、left_pos、right_pos,都是reg型的变量,所以虽然程序里只让挡板左右移动,但是实际上你也可以让它上下动→_→
    代码如下:

//Display the downside bar and the ball    always @ (posedge clk_25M)       begin          // Display the downside bar        if (Vcnt>=up_pos && Vcnt<=down_pos                  && Hcnt>=left_pos && Hcnt<=right_pos)         begin              Red <= Hcnt[3:1];              Green <= Hcnt[6:4];              Blue <= Hcnt[8:7];         end          // Display the ball        else if ( (Hcnt - ball_x_pos)*(Hcnt - ball_x_pos) + (Vcnt - ball_y_pos)*(Vcnt - ball_y_pos) <= (ball_r * ball_r))          begin              Red <= Hcnt[3:1];              Green <= Hcnt[6:4];              Blue <= Hcnt[8:7];          end          else         begin              Red <= 3'b000;              Green <= 3'b000;              Blue <= 2'b00;          end          end
  • 显示小球的那条长长的不等式,其实就是
    x2+y2<r2
    为的就是显示一个圆。
  • 用Hcnt和Vcnt能表示出屏幕上的每一个像素点,在所需要的像素点处改变VGA的颜色输出,其他像素点输出全零(黑色),就可以显示出一个长方形挡板和一个圆形小球。
  • 让小球和挡板动起来

上面显示的小球和挡板都是静止的,要让他们动起来,就需要让他们每一帧的位置发生变化,具体来说就是让他们每一帧的坐标都不一样。
代码如下:

//flush the image every frame    always @ (posedge vs)     begin                // movement of the bar      if (to_left && left_pos >= LEFT_BOUND)         begin              left_pos <= left_pos - bar_move_speed;              right_pos <= right_pos - bar_move_speed;        end        else if(to_right && right_pos <= RIGHT_BOUND)        begin                   left_pos <= left_pos + bar_move_speed;             right_pos <= right_pos + bar_move_speed;        end          //movement of the ball        if (v_speed == `UP) // go up             ball_y_pos <= ball_y_pos - bar_move_speed;        else //go down            ball_y_pos <= ball_y_pos + bar_move_speed;          if (h_speed == `RIGHT) // go right             ball_x_pos <= ball_x_pos + bar_move_speed;        else //go down            ball_x_pos <= ball_x_pos - bar_move_speed;         end 

要注意的是,这里的触发信号不再是时钟,而是vs场扫描信号。
这是因为我们只需要在每一帧改变一次坐标。

  • 让小球懂得反弹
    这个游戏的小球总不能飞出屏幕吧,下面的代码实现的就是在小球“撞”到屏幕边缘或者挡板时,能够“反弹”回去。
    同时也加上了格挡失败的判定。
//change directions when reach the edge or crush the bar    always @ (negedge vs)     begin        if (ball_y_pos <= UP_BOUND)   // Here, all the jugement should use >= or <= instead of ==        begin               v_speed <= 1;              // Because when the offset is more than 1, the axis may step over the line            lose <= 0;        end        else if (ball_y_pos >= (up_pos - ball_r) && ball_x_pos <= right_pos && ball_x_pos >= left_pos)           v_speed <= 0;          else if (ball_y_pos >= down_pos && ball_y_pos < (DOWN_BOUND - ball_r))        begin            // Ahhh!!! What the fxxk!!! I miss the ball!!!            //Do what you want when lose            lose <= 1;        end        else if (ball_y_pos >= (DOWN_BOUND - ball_r + 1))            v_speed <= 0;       else           v_speed <= v_speed;        if (ball_x_pos <= LEFT_BOUND)           h_speed <= 1;        else if (ball_x_pos >= RIGHT_BOUND)           h_speed <= 0;        else           h_speed <= h_speed;    end 

注意到这里对于“冲撞”的判定用的都是‘≤’或者‘≥’而不是‘==’,这是因为如果速度档位调的高,可能会跳过相等的那个像素点,导致飞出屏幕再也回不来。

  • 4*七段数码管显示
    这个就比较简单了,毕竟原理在数字电路课上都学过。
    这里多了一个模块,就是把4位的二进制数字变成7位的数码管数据。
    代码如下:
`include "Definition.h"module seg_decoder(    input clk,    input [3:0] num,    output reg [6:0] code    );always@(posedge clk)begin    case(num)    4'b0000:        code <= `ZERO;    4'b0001:        code <= `ONE;    4'b0010:        code <= `TWO;    4'b0011:        code <= `THREE;    4'b0100:        code <= `FOUR;    4'b0101:        code <= `FIVE;    4'b0110:        code <= `SIX;    4'b0111:        code <= `SEVEN;    4'b1000:        code <= `EIGHT;    4'b1001:        code <= `NINE;    default:        code <= code;    endcaseendendmodule

七段数码管的数据写作宏,定义在了Definition.h里面

针对4个数码管,实例化了4个decoder

seg_decoder seg0(    .clk(clk),    .num(num0),    .code(out0)    );seg_decoder seg1(    .clk(clk),    .num(num1),    .code(out1)    );seg_decoder seg2(    .clk(clk),    .num(num2),    .code(out2)    );seg_decoder seg3(    .clk(clk),    .num(num3),    .code(out3)    );

扫描显示4个数码管

// Display four segalways@(posedge sclk)begin    if(rst) //high active    begin        cnt <= 0;    end    else    begin        case(cnt)        2'b00:        begin            seg <= out0;            select <= 4'b0111;        end         2'b01:        begin            seg <= out1;            select <= 4'b1011;        end        2'b10:        begin            seg <= out2;            select <= 4'b1101;        end        2'b11:        begin            seg <= out3;            select <= 4'b1110;        end        default:        begin            seg <= seg;            select <= select;        end        endcase        cnt <= cnt + 1;         if(cnt == 2'b11)            cnt<=0;    endend

需要注意,扫描的时钟频率不能过快,因为数码管本身的rising time和falling time就没这么快,如果扫描频率过快,就会全部都显示8

收到lose信号的时候刷新数码管数据,并考虑进位。

// Flush data each time you losealways@(posedge lose or posedge rst)begin    if(rst)    begin        num0 <= 0;        num1 <= 0;        num2 <= 0;        num3 <=0;    end    else if(num0 == 9)    begin        num0 <= 0;        if(num1 == 9)        begin            num1 <= 0;            if(num2 == 9)            begin                num2 <= 0;                if(num3 == 9)                    num3 <= 0;                else                    num3 <= num3 + 1;            end            else                num2 <= num2 + 1;        end        else            num1 <= num1 + 1;    end    else        num0 <= num0 + 1;end

至此,所有的功能已经全部实现。

引脚约束

将程序里的“端口”和板子上的“引脚”联系起来,就是ucf文件要做的事情。
由于这个只适用于Basys2开发板,没什么通用性,只贴代码算了。

# clock pin for Basys2 BoardNET "mclk" LOC = "B8"; # Bank = 0, Signal name = MCLKNET "mclk" CLOCK_DEDICATED_ROUTE = FALSE;# Pin assignment for VGANET "HSYNC"   LOC = "J14"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = HSYNCNET "VSYNC"   LOC = "K13"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = VSYNCNET "to_left"   LOC = "A7"  ; NET "to_right"   LOC = "G12"  ; NET "bar_move_speed<0>"   LOC = "P11"  ; NET "bar_move_speed<1>"   LOC = "L3"  ; NET "bar_move_speed<2>"   LOC = "K3"  ; NET "bar_move_speed<3>"   LOC = "B4"  ; NET "OutRed<2>"  LOC = "F13"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = RED2NET "OutRed<1>"  LOC = "D13"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = RED1NET "OutRed<0>"  LOC = "C14"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = RED0NET "OutGreen<2>"  LOC = "G14"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = GRN2NET "OutGreen<1>"  LOC = "G13"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = GRN1 NET "OutGreen<0>"  LOC = "F14"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = GRN0 NET "OutBlue<2>"  LOC = "J13"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = BLU2NET "OutBlue<1>"  LOC = "H13"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = BLU1 # Pin assignment for 7seg_LEDNET "seg_LED<0>"   LOC = "L14"  ; NET "seg_LED<1>"   LOC = "H12"  ; NET "seg_LED<2>"   LOC = "N14"  ; NET "seg_LED<3>"   LOC = "N11"  ; NET "seg_LED<4>"   LOC = "P12"  ; NET "seg_LED<5>"   LOC = "L13"  ; NET "seg_LED<6>"   LOC = "M12"  ; NET "seg_select<0>"   LOC = "K14"  ; NET "seg_select<1>"   LOC = "M13"  ; NET "seg_select<2>"   LOC = "J12"  ; NET "seg_select<3>"   LOC = "F12"  ; NET "rst"   LOC = "M4"  ; 

要注意的是,
NET “mclk” CLOCK_DEDICATED_ROUTE = FALSE;
这句话千万不能少,少了会报错。
另外Basys2有一个mclk,一个uclk。其中uclk是外部时钟,压根没焊上去,不能用,只能用mclk

至此,所有的事情都已经做好了,烧到板子上,连上VGA,就可以玩了。

0 0