FPGA.避障游戏

这是一个使用FPGA制作的小游戏,知识主要是VGA的使用和关于游戏的逻辑处理。在这篇中将会讲诉VGA的显示原理和使用,还有一些关于键盘的消抖的小知识。

游戏介绍

游戏规则

利用FPGA,以640*480的分辨率使用VGA显示,玩家利用按键操作位于屏幕左侧的方块移动,来躲避从屏幕右侧向左边移动的留有一定间隙的障碍物。

游戏要求

画面及操作尽量连续,游戏结束时玩家操作的物体变成红色,按下重新开始后复位游戏,随着时间变长加速以提高难度。

基本上整个游戏就像是以前的飞机小游戏,为了增加可玩性,我将游戏设置为方块自动降落,外部只有一个按键,实现方块的向上移动,去躲避向左移动的挡板。

设计分析

模块设定

首先游戏要有显示画面,所以少不了vga的显示模块;其次是要控制方块的移动,需要键盘的输入模块,最后是关于游戏的逻辑控制,这需要一个控制模块。

那这些模块都需要什么哪些信号呢?接着分析下。

键盘模块

键盘模块作为整个避障游戏系统的输入,它主要是为其他模块提供信号,对信号的处理并不多。

输入 功能描述
clk 时钟
reset 复位
up 使方块上升
输出 功能描述
up_key_press 方块上升信号
down_key_press 方块下降信号

前面说了,为了增加可玩性,所以我在系统内部设置了让方块自动下落,所以输入只有up,但是输出时会有down_key_press。

关于键盘的输入输出就是如此,至于模块如何实现这些功能,分析阶段不解释,在下文中会陆陆续续讲解。

VGA模块

vga模块的功能就是将数据显示,原理在下文我会讲解,弄懂它的时序后,问题不会太大,一开始可以尝试先显示个彩条之类的,测试下,找下感觉。

输入 功能描述
clk 时钟
reset 复位
输出 功能描述
dat_act 数据有效标志位
hc 行扫描计数器
vc 列扫描计数器
hsync 行同步信号
vsync 场同步信号

控制模块

控制模块这个部分是整个游戏的规则的设定,可以说,游戏怎么玩完全由这个模块决定,根据游戏的描述和要求,我们是要控制一个方块去躲避不断向左移动的挡板,所以这个挡板和方块怎么“弄出来”就是关键了。

怎么弄出来呢?

首先要有个概念,我们所看到的VGA图像都是一个个像素点组成,使用640*480 的显示模式,这个规定相当于为我们规定了横纵坐标的定义域,在这个二维屏幕上。方块和遮挡板就可以用数学式子“画”出来,例如边长为两个像素点一个方块就是:0<x<2 ,0<y<2。

友情提醒:FPAG中能用正数就尽量不要用负数,数字在FPGA中是用补码表示的,负数的补码往往与我们的思维逻辑有点出入,容易导致出错。

输入 功能描述
clk 时钟
reset 复位
up_key_press 方块上升信号
down_key_press 方块的下降信号
hc 行扫描计数器
vc 列扫描计数器
dat_act 数据有效标志位(用于消隐)
输出 功能描述
disp_RGB 显示所需的数据

总体的设计图

方案设计

键盘模块

键盘模块的要点在于消抖,和控制方块移动。

消抖

无论是什么器件,键盘的消抖都是老套路,分为硬件消抖和软件消抖,硬件消抖如使用RS触发器实现或者是加电容实现,一般是制作板子的时候考虑加上去的,平时我们使用现成的板子,大多数都是使用软件消抖。

键盘产生抖动是机械特性,在我们按下按键是接触点的电压波形大致如下图:

从图可以看出,按下时会有一段上下波动的波形,松开时也有一段。

软件消抖常用的方法是延时,作用就是避开这一段“抖动”的波形,达到消抖的目的。

1
2
3
4
5
6
7
8
9
10
if(counter <= T) //按的时间不够长
begin
counter = counter + 1'b1;
up_key_press <= 0;
end
else //按下足够久了,认为是真的按下
begin
counter <= 0;
up_key_press <= 1;
end

硬件消抖这里也稍微拓展下。

RS触发器实现

图中两个“与非”门构成一个RS触发器。当按键未按下时,输出为0;当键按下时,输出为1。此时即使用按键的机械性能,使按键因弹性抖动而产生瞬时断开(抖动跳开B),只要按键不返回原始状态A,双稳态电路的状态不改变,输出保持为0,不会产生抖动的波形。也就是说,即使B点的电压波形是抖动的,但经双稳态电路之后,其输出为正规的矩形波。这一点通过分析RS触发器的工作过程很容易得到验证。

至于其他的硬件消抖电路,如:用电容构成的积分电路实现,采用D触发器实现,这里不再拓展,如有兴趣,可自行查阅资料。

控制移动

这个功能的实现,应该说不难。知道要改变哪个参数能使它移动,改变它就可以实现了,在这个游戏系统中,控制方块上下移动是改变 move_y 这个参数,左右移动是改变move_x,不过在后面测试游戏时,我觉得左右移动没有必要加上去,就把它去了,具体的操作看代码吧。

VGA模块

实现这个游戏,我认为最重要的知识就是VGA的显示原理了。数据怎么显示在屏幕的?640和480指的又是什么?我们先看它的原理。

VGA原理

VGA从扫描方式上分行扫描和场扫描两种,扫描就是一个电子枪(CRT),啾啾啾的扫,水平方向叫行扫描,垂直方向叫场扫描,这个电子枪它又可发出三种颜色光,分别是R(红色),G(绿色),B(蓝色),光的三原色都有,原则上三原色按照比例不同搭配,那你想要什么颜色就可以给你什么颜色,但实际上呢,VGA中红,绿,蓝的输入线分别是3,3,2根;也就是说红色有2^3=8种,同理,绿色八种,蓝色四种,相互搭配,便有8 X 8 X 4=256种搭配,也就是VGA能显示256种颜色。

扫描过程是怎样的?

以行扫描为例:

从图可以看出,电子枪从左往右扫射一行回头再到下一行,直至最后完成一帧画面,又重头开始。那什么时候掉头,什么时候算是完成一帧画面,这就有个区域了,区域怎么定,是由显示模式决定的,看下图。

我们可以看到有多种显示模式,不同的显示模式所需的时钟频率可是不一样的,如果细心查看,就会注意到,行时序的c区和列时序的q区恰好是640和480这两个熟悉的数字,其实这就是显示时序段的范围。

VGA中定义行时序和场时序都需要同步脉冲(Sync a)、显示后沿(Back porch b)、显示时序段(Display interval c)和显示前沿(Front porch d)四部分。只有在显示时序段,也就是C区才可以信号显示出来,其他区域,你就算给VGA信号,你也看不到。

行时序

场时序

那么我们怎么知道,电子枪(CRT)有没有扫描到显示时序段呢?方法是加入行同步计数器和列同步计数器用,反正时序是固定的,行同步计数器是扫一下计数器加一,列计数器是一行扫完计数器加一,两者都扫到C区了,也就是计数器都达到一定数值(a区长度+B区长度),表明屏幕可以显示信号,我就给信号,要黑色,rgb全给0,要白色,全给1,反正就是给信号,这和在坐标轴上画图的感觉是一样一样的。

控制模块

控制模块就像这个游戏系统的控制中心一般,制定了关于这个游戏的一切规则。

主要有那么几个要点:

  1. 将键盘的长脉冲变为一个个冲击信号,不然以FPGA本身的频率,按一下,移动得太快,方块就“上天” 了,障碍物移动也会快到你看不到。
  2. “画”方块和障碍物(挡板),并设定参数让给它们可以移动。
  3. 由于挡板的垂直方向出现的位置要有随机性,所以需要产生随机数。
  4. 设置游戏失败的情况,方块与挡板“撞上”这个时机的设置必须是程序完成。

长脉冲变多个短脉冲

这不难想也不难实现,就是计数器加到一定程度变为标志位变为1,然后计数器清零,标志位也变为0,一定时间内要短脉冲多点,计数器的计数值就小点,反之,大一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
////  板块移动速度控制   ////
reg move;
reg [32:0]counter;
reg [30:0]T_move;
always@(posedge clk,negedge reset)
begin
if(!reset)
begin
T_move = 30'd10_000_00;
counter <= 0;
move <=0;
end
else
begin
if(counter >= T_move)
begin
move = 1;
if(T_move == 100_000)
T_move <=T_move;
else
T_move = T_move-10;
counter = 0;
end
else
begin
move = 0;
if(!stop)
counter= counter + 1;
else
counter = 0;
end
end
end

“画”方块和挡板

关于如何“画”,前面举过画方块的例子,就是把行列计数器当做 x,y。x给个区域,y给个区域,再给个颜色,就画出来了。至于移动呢,移动就代表着位置是个变量,设定一个x,y都是变量的点,然后以这个点为中心画出你要的方块或者挡板,改变这个点的x,y便是将它移动。

以挡板从右向左移动为例:

产生随机数

产生伪随机数的方法最常见的是利用一种线性反馈移位寄存器(LFSR),它是由n个D触发器和若干个异或门组成的,如下图:

实际上这个有规律可循的,只不过D触发器一多,显得很乱,很像随机产生的样子,但确实不是真正意义上的随机数,是个伪随机数,但在这里使用足够了的。

但这种方法也有bug,就是高位它不容易变化的时候,挡板垂直方向就不够分散,举个例子,以8个D触发器组成的为例,数字范围从0~1111_1111,如果高位变化不大,如从1110_0000变成1110_0101,高位不怎么变化的话,整个数字大小实际上就是改变一点点,图像表现为前后两个挡板垂直位置上相差几个像素点,这就显得过于集中,而且这种办法无法生成 0 这个数字。

为了将挡板”离散一点”,我就将竖直方向的长减去挡板的长度后得到的空隙分段化,分为8段,这样挡板之间的距离要么相等,不然都会有一段距离,显得离散些。怎么实现呢?

每个D触发器都存有一个数字,我从中随机抽取三个数字,做一个case语句的选择,8段8种情况选择。这样随机性增加,挡板也更离散。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
///////          随机数     //////////
reg [7:0] rand_num;
parameter seed = 8'b1111_1111;
always@(posedge clk or negedge reset)
begin
if(!reset)
rand_num <= seed;
else
begin
rand_num[0] <= rand_num[1] ;
rand_num[1] <= rand_num[2] + rand_num[7];
rand_num[2] <= rand_num[3] + rand_num[7];
rand_num[3] <= rand_num[4] ;
rand_num[4] <= rand_num[5] + rand_num[7];
rand_num[5] <= rand_num[6] + rand_num[7];
rand_num[6] <= rand_num[7] ;
rand_num[7] <= rand_num[0] + rand_num[7];
end
end
wire [2:0]choose;
reg [8:0]type;
assign choose = {rand_num[3],rand_num[6],rand_num[2]};
always@(posedge clk )
begin
case(choose)
0:type = 0;
1:type = 40;
2:type = 80;
3:type = 120;
4:type = 160;
5:type = 200;
6:type = 240;
7:type = 280;
endcase
end
////////////////////////////////////////////////////////

游戏失败设置

游戏失败是撞上了,那 撞上 在数学上表示是什么呢?

答案是方块和挡板的坐标有交叉。

方块和挡板之间都有坐标的区域,只要找到它们会交叉的情况,就说明这个时候是撞上了。原理就是如此,具体的可以自己动笔算下。

友情提醒:加减时注意尽量不要出现负数的情况,因为 数字用补码表示的原因,在FPGA中,直接比较 ,-1=ffff_ffff 可是大于0的。

1
2
3
4
5
6
7
8
9
10
wire die1,die2,die3,die4;
//游戏失败定义,方块与挡板"碰撞"
//失败情况讨论,共设置四块挡板,四种情况
assign die1=((rand<move_y + border)&&(move_y < rand+long)&&(push < move_x+border) && (move_x < push + ban ));
assign die2=((rand1<move_y + border)&&(move_y < rand1+long)&&(push1 < move_x+border) && (move_x < push1 + ban ));
assign die3=((rand2<move_y + border)&&(move_y < rand2+long)&&(push2 < move_x+border) && (move_x < push2 + ban ));
assign die4=((rand3<move_y + border)&&(move_y < rand3+long)&&(push3 < move_x+border) && (move_x < push3 + ban ));

wire false;
assign false = die1||die2||die3||die4;

代码展示

键盘模块



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
module key(clk,reset,up,up_key_press,down_key_press);
input clk;
input reset;
input up;
output reg up_key_press;
output reg down_key_press;

parameter T = 30'd10_000_00; //控制方块移动速度

////////// up 按键 /////////////
reg [30:0] counter;
reg [30:0] counter2;
always@(posedge clk,negedge reset )
begin
if(!reset)
begin
counter <= 0;
counter2 <= 0;
up_key_press <= 0;
down_key_press <= 0;
end
else
begin
if(up)
begin
if(counter <= T)
begin
counter = counter + 1'b1;
up_key_press <= 0;
end
else
begin
counter <= 0;
up_key_press <= 1;
end
end
else //下降按钮
begin
if(counter2 <= T)
begin
counter2 = counter2 + 1'b1;
down_key_press <= 0;
end
else
begin
counter2 <= 0;
down_key_press <= 1;
end
end
end
end

endmodule


VGA模块



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
module vga( clk,reset,hsync, vsync,hc,vc,dat_act);
input clk; //系统输入时钟 100MHz
input reset;

output hsync; //VGA 行同步信号
output vsync; //VGA 场同步信号
output dat_act;
output [9:0]hc ,vc; //转成640*480的模式

reg [9:0] hcount; //VGA 行扫描计数器
reg [9:0] vcount; //VGA 场扫描计数器

reg flag;
wire hcount_ov;
wire vcount_ov;

wire hsync;
wire vsync;

reg vga_clk=0;
reg cnt_clk=0; //分频计数

//VGA 行、场扫描时序参数表
parameter hsync_end = 10'd95,
hdat_begin = 10'd143,
hdat_end = 10'd783,
hpixel_end = 10'd799,

vsync_end = 10'd1,
vdat_begin = 10'd34,
vdat_end = 10'd514,
vline_end = 10'd524;


//分频
always @(posedge clk)
begin
if(cnt_clk == 1)
begin
vga_clk <= ~vga_clk;
cnt_clk <= 0;
end
else
cnt_clk <= cnt_clk +1;
end

//************************VGA 驱动部分*******************************//行扫描

always @(posedge vga_clk)
begin
if (hcount_ov)
hcount <= 10'd0;
else
hcount <= hcount + 10'd1;
end
assign hcount_ov = (hcount == hpixel_end);

//场扫描
always @(posedge vga_clk)
begin
if (hcount_ov)
begin
if (vcount_ov)
vcount <= 10'd0;
else
vcount <= vcount + 10'd1;
end
end
assign vcount_ov = (vcount == vline_end);

//数据、同步信号输
assign dat_act = ((hcount >= hdat_begin) && (hcount < hdat_end))&& ((vcount >= vdat_begin) && (vcount < vdat_end));
assign hsync = (hcount > hsync_end);
assign vsync = (vcount > vsync_end);

//计数器转成640 x 480的样式,方便开发
assign hc = hcount - hdat_begin;
assign vc = vcount - vdat_begin;

endmodule


控制模块



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
module control( clk,reset, disp_RGB,hc,vc,dat_act,up_key_press,down_key_press );
input clk; //系统输入时钟 100MHz
input reset;
input dat_act;
input [9:0]hc,vc;
input up_key_press;
input down_key_press;

output [2:0]disp_RGB; //VGA 数据输出

reg [2:0]data;
reg vga_clk=0;
reg cnt_clk=0; //分频计数


//分频
always @(posedge clk)
begin
if(cnt_clk == 1)
begin
vga_clk <= ~vga_clk;
cnt_clk <= 0;
end
else
cnt_clk <= cnt_clk +1;
end
//定义正方形小块的边长
parameter border = 40;
//定义挡板的宽度
parameter ban = 20;
//定义挡板的长度
parameter long = 200;
//定义挡板的间隔
parameter magin = 160;

//VGA扫描,画出挡板和方块,并设置挡板移动的移动变量push
reg [10:0] push,push1,push2,push3;
reg stop;//用于停止游戏

//小方块移动数据存储器
parameter move_x = 50; //方块的初始位置
reg [9:0]move_y;

/////// 随机数 //////////
reg [7:0] rand_num;
parameter seed = 8'b1111_1111;
always@(posedge clk or negedge reset)
begin
if(!reset)
rand_num <= seed;
else
begin
rand_num[0] <= rand_num[1] ;
rand_num[1] <= rand_num[2] + rand_num[7];
rand_num[2] <= rand_num[3] + rand_num[7];
rand_num[3] <= rand_num[4] ;
rand_num[4] <= rand_num[5] + rand_num[7];
rand_num[5] <= rand_num[6] + rand_num[7];
rand_num[6] <= rand_num[7] ;
rand_num[7] <= rand_num[0] + rand_num[7];
end
end
wire [2:0]choose;
reg [8:0]type;
assign choose = {rand_num[3],rand_num[6],rand_num[2]};
always@(posedge clk )
begin
case(choose)
0:type = 0;
1:type = 40;
2:type = 80;
3:type = 120;
4:type = 160;
5:type = 200;
6:type = 240;
7:type = 280;
default: type = 280;
endcase
end
////////////////////////////////////////////////////////


//// 板块移动速度控制 ////
reg move;
reg [32:0]counter;
reg [30:0]T_move;
always@(posedge clk,negedge reset)
begin
if(!reset)
begin
T_move = 30'd10_000_00;
counter <= 0;
move <=0;
end
else
begin
if(counter >= T_move)
begin
move = 1;
if(T_move == 100_000)
T_move <=T_move;
else
T_move = T_move-10;
counter = 0;
end
else
begin
move = 0;
if(!stop)
counter= counter + 1;
else
counter = 0;
end
end
end
reg [8:0]rand,rand1,rand2,rand3;
always@(posedge clk or negedge reset)
begin
if (!reset)
begin
push<=640; //初始位置设定
push1 <= 640+ magin;
push2 <= 640 + magin + magin;
push3 <= 640 + magin + magin + magin;
end
else if (move)
begin
if(push == 0)
begin
push <= 640;
rand <=type; //第一块板子的位置设定
end
else
begin
push <= push-1'b1;
end
if(push1 == 0)
begin
push1 <= 640;
rand1 <=type; //第二块板子的位置设定
end
else
begin
push1 <= push1-1'b1;
end
if(push2 == 0)
begin
push2 <= 640;
rand2 <=type; //第三块板子的位置设定
end
else
begin
push2<= push2-1'b1;
end
if(push3 == 0)
begin
push3 <= 640;
rand3 <=type;
//第四块板子的位置设定
end
else
begin
push3 <= push3-1'b1;
end
end
else
begin
push <= push;
push1 <= push1;
push2 <= push2;
push3 <= push3;
end
end


wire die1,die2,die3,die4;
//游戏失败定义,方块与挡板"碰撞"
//失败情况讨论,共设置四块挡板,四种情况
assign die1=((rand<move_y + border)&&(move_y < rand+long)&&(push < move_x+border) && (move_x < push + ban ));
assign die2=((rand1<move_y + border)&&(move_y < rand1+long)&&(push1 < move_x+border) && (move_x < push1 + ban ));
assign die3=((rand2<move_y + border)&&(move_y < rand2+long)&&(push2 < move_x+border) && (move_x < push2 + ban ));
assign die4=((rand3<move_y + border)&&(move_y < rand3+long)&&(push3 < move_x+border) && (move_x < push3 + ban ));

wire false;
assign false = die1||die2||die3||die4;

//描述运动,“画图”
always@(posedge vga_clk,negedge reset)
begin
if(!reset)
begin
data <= 0;
stop <= 0;
end
else
begin
if (hc>move_x &&(hc<(move_x+border)&&(vc>move_y)&&(vc<move_y+border))) //小方块
begin
if(!false)
begin
data <= 3'h3; //黄色
stop <= 0;
end
else
begin
data <= 3'h1; //红色
stop <=1;
end
end
else
if ((hc>push) && (hc<=push+ban) && (vc>=rand) && (vc<=rand+long))
begin
data <= 3'h2; //第一根横条
end
else if ((hc>push1) && (hc<=push1+ban) && (vc>=rand1) && (vc<=rand1+long))
begin
data <= 3'h2; //第二根横条
end
else if ((hc>push2) && (hc<=push2+ban) && (vc>=rand2) && (vc<=rand2+long))
begin
data <= 3'h2; //第三根横条
end
else if ((hc>push3) && (hc<=push3+ban) && (vc>=rand3) && (vc<=rand3+long))
begin
data <= 3'h2; //第四根横条
end else
data <= 0;
end
end


/////// 方块移动控制 ////////////
always@(posedge clk or negedge reset)
begin
if (!reset)
begin
move_y <= 240;
end
else if (up_key_press)
begin
if(move_y == 0)
begin
move_y <= move_y;
end
else
begin
move_y <= move_y-1'b1;
end
end
else if (down_key_press)
begin
if(move_y>440)
begin
move_y <= move_y;
end
else
begin
move_y <= move_y+1'b1;
end
end
end
// 信号输出
assign disp_RGB = (dat_act) ? data : 3'h00;
endmodule


TOP模块



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module top(clk,reset,up,hsync,vsync,disp_RGB);
input clk;
input reset;
input up;

output hsync; //VGA 行同步信号
output vsync; //VGA 场同步信号
output [2:0]disp_RGB; //VGA 数据输出

wire dat_act;
wire up_key_press;
wire down_key_press;
wire [9:0]hc,vc;


key U1(clk,reset,up,up_key_press,down_key_press);
control U2( clk,reset, disp_RGB,hc,vc,dat_act,up_key_press,down_key_press );
vga U3( clk,reset,hsync, vsync,hc,vc,dat_act);
endmodule


写在后面的话

关于这个小游戏的讲解就到这里,有任何疑问可以在评论处指出或者联系我,我会及时更新,文章若有错误,恳请读者在评论区指出斧正,我会修改。

欢迎大家在评论区与我交流,学习。

请我喝杯咖啡~
0%