大俠好,歡迎來到FPGA技術(shù)江湖,江湖偌大,相見即是緣分。大俠可以關(guān)注FPGA技術(shù)江湖,在“闖蕩江湖”、"行俠仗義"欄里獲取其他感興趣的資源,或者一起煮酒言歡。
今天給各位大俠帶來基于FPGA的“俄羅斯方塊”設(shè)計。
設(shè)計目的
通過此次項目,完成以下目的:
1)?熟悉Xilinx FPGA的架構(gòu)及開發(fā)流程;
2)?設(shè)計一個功能完整的系統(tǒng),掌握FSM + Datapath的設(shè)計方法。
設(shè)計內(nèi)容
1.?項目介紹
本項目主要在FPGA上實現(xiàn)了一個經(jīng)典小游戲“俄羅斯方塊”。本項目基本解決方案是,使用Xilinx Zynq系列開發(fā)板 ZedBoard 作為平臺,實現(xiàn)主控模塊,通過VGA接口來控制屏幕進(jìn)行顯示。
2.?系統(tǒng)框架
整個系統(tǒng)由四部分組成,按鍵輸入處理模塊、控制模塊、數(shù)據(jù)路徑模塊以及VGA顯示接口模塊。整個系統(tǒng)的結(jié)構(gòu)如下圖所示:
下面分別對四個模塊進(jìn)行介紹:
1)?按鍵輸入處理模塊
按鍵處理模塊的主要功能是對輸入系統(tǒng)的up,down,left,right四個控制信號進(jìn)行消抖處理,并對其進(jìn)行上升沿檢測。
消抖模塊采用了一個4位的移位寄存器,先將輸入信號延遲4個時鐘周期,再對其以一個較低的時鐘頻率進(jìn)行采用。消抖模塊的結(jié)構(gòu)如下圖所示:
為了簡化控制系統(tǒng),在本系統(tǒng)的設(shè)計過程中,不考慮長時間按鍵產(chǎn)生連按效果。因而,需要對按鍵進(jìn)行上升沿檢測。上升沿檢測的基本實現(xiàn)方案是加入一組寄存器,對前一個的按鍵信號進(jìn)行暫存,將暫存的值與當(dāng)前值進(jìn)行比較,當(dāng)上一個值為0而當(dāng)前值為1時,即認(rèn)為其檢測到了一個上升沿。
2)?控制模塊
控制模塊采用FSM的方式進(jìn)行控制。在控制模塊中,定義了10個狀態(tài):
S_idle:上電復(fù)位后進(jìn)入空狀態(tài),當(dāng)start信號為1時進(jìn)入S_new狀態(tài)
S_new:用于產(chǎn)生新的俄羅斯方塊。
S_hold:保持狀態(tài)。在這個狀態(tài)中進(jìn)行計時,當(dāng)時間到達(dá)一定間隔時,轉(zhuǎn)到S_down狀態(tài);或者等待輸入信號(up,down,left,right)時,轉(zhuǎn)到S_down(按鍵為down)或者S_move(up,left,right)狀態(tài)。
S_down:判斷當(dāng)前俄羅斯塊能否下移一格。如果可以,則轉(zhuǎn)到S_remove_1狀態(tài),如果不行,則轉(zhuǎn)到S_shift狀態(tài)。
S_move:判斷當(dāng)前俄羅斯塊能夠按照按鍵信號指定的指令進(jìn)行移動,如果可以,則轉(zhuǎn)到S_shift狀態(tài),如果不可以,則轉(zhuǎn)到S_remove_1狀態(tài)。
S_shift:更新俄羅斯方塊的坐標(biāo)信息。返回S_hold。
S_remove_1:更新整個屏幕的矩陣信息。轉(zhuǎn)移到S_remove_2狀態(tài)。
S_remove_2:判斷是否可以消除,將可以消除的行消除,并將上面的行下移一行。重復(fù)此過程,直到?jīng)]有可消除的行為止。跳轉(zhuǎn)到S_isdie狀態(tài)
S_isdie:判斷是否游戲結(jié)束。如果結(jié)束,則跳轉(zhuǎn)到S_stop狀態(tài)。如果沒有,則跳轉(zhuǎn)到S_new狀態(tài),生成新的俄羅斯方塊。
S_stop:清除整個屏幕,并跳轉(zhuǎn)到S_idle狀態(tài)。
整個控制過程的ASMD圖如下圖所示:
3)?數(shù)據(jù)路徑
數(shù)據(jù)路徑模塊主要功能是,根據(jù)控制模塊給出的信號,對俄羅斯方塊當(dāng)前的邏輯狀態(tài)進(jìn)行判斷,更新背景矩陣。具體如下:
方塊:
方塊分為非活動方塊與活動方塊。非活動方塊為:(1)之前下落的方塊;(2)下落后方塊消除之后的結(jié)果。由背景矩陣表示?;顒臃綁K為當(dāng)前下落中的方塊,由活動方塊坐標(biāo)與方塊類型表示(后簡稱方塊)。
背景矩陣:
reg [9:0] R [23:0];
背景矩陣R是24行10列的寄存器組,負(fù)責(zé)保存非活動方塊坐標(biāo),即R中任一位置,如方塊存在,則該位置1,否則為0。
活動方塊坐標(biāo):
output reg [4:0] n,
output reg [3:0] m,
n, m分別為當(dāng)前活動方塊的行、列指針,指向方塊固定點(diǎn)位置。方塊固定點(diǎn)為方塊旋轉(zhuǎn)時不變的格點(diǎn),依據(jù)方塊種類決定,下文方塊模型中詳述。
方塊類型:
output reg [6:0] BLOCK,
BLOCK代表方塊類型,由7位編碼構(gòu)成。
數(shù)據(jù)交換:
Datapath與其余模塊的數(shù)據(jù)交換分為兩部分:
(1)與control_unit間的狀態(tài)指令交互;
(2)控制merge,間接實現(xiàn)對VGA的控制。
方塊模型:
俄羅斯方塊共有7種形狀的方塊(O,L,J,I,T,Z,S),每種方塊有1-4種不同的旋轉(zhuǎn)變形方式。為方便起見,將方塊定位A-G,旋轉(zhuǎn)編號為1-4,將方塊編碼成A_1-G_2的19種,如下圖:(圖中,深色方塊是該種方塊的固定點(diǎn))
方塊運(yùn)動:
產(chǎn)生:
方塊產(chǎn)生由一個簡單的偽隨機(jī)過程決定。系統(tǒng)采用一個3位的計數(shù)器產(chǎn)生隨機(jī)數(shù),進(jìn)入S_new,BLOCK的值被NEW_BLOCK覆蓋,方塊坐標(biāo)n<=1;m<=5;同時,根據(jù)計數(shù)器,NEW_BLOCK的值刷新為A_1,B_1,…,G_1中的一種,作為下一次方塊。
移動:
方塊移動分為四種:旋轉(zhuǎn),下落,向左,向右,由鍵盤KEYBOARD=[UP, DOWN, LEFT, RIGHT]控制。移動分兩步進(jìn)行:(1)判斷;(2)轉(zhuǎn)換。
判斷過程包含S_down,S_move。判斷分兩步:首先,判斷變換后方塊坐標(biāo)是否合法,即變換后是否會造成方塊越界。然后,判斷變換后方塊可能占據(jù)的新位置是否有背景矩陣方塊存在。兩步判斷通過后返回成功信號,否則失敗。因判斷代碼量較多,僅舉一例說明:
判斷D_1向右運(yùn)動(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;
轉(zhuǎn)換過程(S_shift)進(jìn)行方塊的移動或變形。根據(jù)KEYBOARD,移動時,改變方塊坐標(biāo);變形時,方塊按類別變換,如:A_1→A_1;B_1→B_2; B_2→B_3; B_4→B_1;
停止與消除:
方塊停止與消除由兩個狀態(tài)完成:S_remove1,S_remove2。
前一狀態(tài)中,根據(jù)BLOCK, n, m,將活動方塊位置覆蓋至R,變?yōu)榉腔顒臃綁K。
后一狀態(tài)中,根據(jù)行滿狀態(tài),進(jìn)行行的消除與平移,具體如下:
顯然,俄羅斯方塊能影響的最大行數(shù)為4,因此,在REMOVE_2中,僅對R[n-1],R[n],R[n+1],R[n+2]四行依次進(jìn)行處理。處理過程為:如果該行(k)滿,則由k行開始,至1行結(jié)束,逐行向下平移,當(dāng)前平移位置由計數(shù)器REMOVE_2_C控制,當(dāng)前行消除截止由標(biāo)志位SIG確認(rèn)。
每行處理完后,將REMOVE_FINISH[3:0]中相應(yīng)位置1,REMOVE_FINISH全1時,REMOVE_2完成。
死亡判定:
R中的0-3行位于屏幕上方,不進(jìn)行顯示,僅有新生成的方塊坐標(biāo)會進(jìn)入這一區(qū)域。因而,當(dāng)消除完成后,如R[3]不為空,游戲結(jié)束。
4)?顯示部分
輸出結(jié)果通過VGA接口接入顯示屏顯示。VGA(Video Graphics Array)視頻圖形陣列是IBM于1987年提出的一個使用模擬信號的電腦顯示標(biāo)準(zhǔn)。VGA接口即電腦采用VGA標(biāo)準(zhǔn)輸出數(shù)據(jù)的專用接口。VGA接口共有15針,分成3排,每排5個孔,顯卡上應(yīng)用最為廣泛的接口類型,絕大多數(shù)顯卡都帶有此種接口。它傳輸紅、綠、藍(lán)模擬信號以及同步信號(水平和垂直信號)。
使用Verilog HDL語言對VGA進(jìn)行控制一般只需控制行掃描信號、列掃描信號和紅綠藍(lán)三色信號輸出即可。
VGA輸出可分為四個模塊:時鐘分頻模塊、數(shù)據(jù)組織模塊、接口控制模塊和頂層模塊。以下進(jìn)行分塊描述。
時鐘模塊分頻模塊對FPGA系統(tǒng)時鐘進(jìn)行分頻。由于使用的顯示屏參數(shù)為640*480*60Hz,其真實屏幕大小為800*525,因此所需時鐘頻率為800*525*60Hz=25.175MHz,可近似處理為25MHz。FPGA系統(tǒng)時鐘為100M,因此將其四分頻即可基本滿足顯示要求。
數(shù)據(jù)組織模塊是將預(yù)備輸出的數(shù)據(jù)組織為可以通過VGA接口控制的數(shù)據(jù)形式,本次設(shè)計中因接口已經(jīng)協(xié)調(diào),數(shù)據(jù)可不經(jīng)過此模塊進(jìn)行組織,故可忽略該模塊。
接口控制模塊通過VGA接口對顯示屏進(jìn)行控制。VGA的掃描順序是從左到右,從上到下。例如在640X480的顯示模式下,從顯示器的左上角開始往右掃描,直到640個像素掃完,再回到最左邊,開始第二行的掃描,如此往復(fù),到第480行掃完時即完成一幀圖像的顯示。這時又回到左上角,開始下一幀圖像的掃描。如果每秒能完成60幀,則稱屏幕刷新頻率為60Hz。宏觀上,一幀屏幕由480個行和640個列填充而成,而實際上,一幀屏幕除了顯示區(qū),還包含其他未顯示部分,作為邊框或者用來同步。具體而言,一個完整的行同步信號包含了左邊框、顯示區(qū)、右邊框還有返回區(qū)四個部分,總共800個像素,其分配如下:
同樣的,一個完整的垂直同步信號也分為四個區(qū)域,總共525個像素,分配如下:
模塊通過組織輸出行掃描信號、列掃描信號和三原色信號對顯示屏實現(xiàn)控制。
設(shè)計結(jié)果
設(shè)計結(jié)果圖如下:
圖7:設(shè)計結(jié)果圖
設(shè)計代碼
由于代碼量較大,這里只展示了部分代碼,需要的大俠可以按照開篇介紹的方法進(jìn)入“FPGA技術(shù)江湖”知識星球獲取設(shè)計文檔,獲取設(shè)計代碼。
頂層模塊設(shè)計代碼:
`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模塊代碼:
`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
控制模塊代碼:
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
數(shù)據(jù)路徑以及VGA等模塊在這里就不展示,代碼量過大,詳情見開篇介紹。