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