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,就可以玩了。

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