引言
这是我的数字集成电路课程设计。实现的是一个我们都玩过的小游戏:一块移动挡板,一个飞来飞去的球,挡板需要把球挡住,没挡住就算输。
代码已上传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,就可以玩了。