引言
這是我的數字集成電路課程設計。實現的是一個我們都玩過的小遊戲:一塊移動擋板,一個飛來飛去的球,擋板需要把球擋住,沒擋住就算輸。
代碼已上傳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;
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,就可以玩了。