Verilog编写VGA显示的小游戏

引言

这是我的数字集成电路课程设计。实现的是一个我们都玩过的小游戏:一块移动挡板,一个飞来飞去的球,挡板需要把球挡住,没挡住就算输。
代码已上传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
    );

模块实现

//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;
    endcase
end

endmodule

七段数码管的数据写作宏,定义在了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 seg
always@(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;
    end
end

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

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

// Flush data each time you lose
always@(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 Board
NET "mclk" LOC = "B8"; # Bank = 0, Signal name = MCLK
NET "mclk" CLOCK_DEDICATED_ROUTE = FALSE;

# Pin assignment for VGA
NET "HSYNC"   LOC = "J14"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = HSYNC
NET "VSYNC"   LOC = "K13"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = VSYNC

NET "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 = RED2
NET "OutRed<1>"  LOC = "D13"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = RED1
NET "OutRed<0>"  LOC = "C14"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = RED0
NET "OutGreen<2>"  LOC = "G14"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = GRN2
NET "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 = BLU2
NET "OutBlue<1>"  LOC = "H13"  ; #| DRIVE = 2  | PULLUP ; # Bank = 1, Signal name = BLU1 

# Pin assignment for 7seg_LED
NET "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,就可以玩了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章