|
发表于 2009-10-26 10:17:50
|
显示全部楼层
FPGA可以很方便的产生视频信号。
乒乓球游戏由一个在屏幕上反复弹跳的小球和用来挡住小球使之反弹的挡板。在这个设计中,挡板的位置由用户通过鼠标来控制。
[/url]
尽管任何FPGA开发板都可以用来实现此设计,但这里我们仍然使用Pluto板。
[url=file:///E:/ye%20document/eda技术/FPGA设计的乒乓球游戏.files/1_14122626.gif] 驱动VGA监视器(显示器)
VGA监视器需要5个信号来显示一幅图像:
R, G and B (红 绿 蓝信号)。
HS and VS (水平和垂直同步)。
[/url]
这里的R、G、B均为较低电压的模拟信号(范围从0V到0.7V),而HS和VS则是数字信号。
通过FPGA的引脚产生VGA视频信号
FPGA的13和14引脚连接到HS和VS两个数字信号,所以可以直接由FPGA驱动.
1,2,3引脚(R,G,B) 则是75欧姆的模拟信号。对于3.3V的IO,我们使用三个270欧姆的串联电阻来分压。这样就可以在输出端得到3.3*75/(270+75)=0.72V的电压,已经很接近0.7V了。通过分别设置这三个引脚的电平状态,最多可以输出八种颜色。5, 6, 7, 8, 10均接地。
下面是VGA接头(母头)与Pluto开发板在面包板上的连接图。
[url=file:///E:/ye%20document/eda技术/FPGA设计的乒乓球游戏.files/1_14122704.jpg]
下面是VGA接头与12脚插针的连接图(后视图),12脚的插针使得与面包板的连接变得十分容易。图中,3个270欧姆的电阻清晰可见。
[url=file:///E:/ye%20document/eda技术/FPGA设计的乒乓球游戏.files/1_14122717.jpg][/url]
我们也可以使用一块适配板来连接。
同步信号的产生
监视器通常是从上至下,从左至右一行一行显示图像的。这个是规定好的,我们不能改变它。因此设计中我们需要根据规定来产生同步信号(HS和VS),使之以合适的间隔产生一个短脉冲。其中:HS用来指示某一行的开始,而VS则意味着当前显示已经到达屏幕的底端,需要重新回到顶端开始显示。.
对于标准的640x480 VGA 视频信号,两个同步信号的频率为:
垂直同步平率 (VS) 水平同步频率 (HS)
60 Hz (=每秒60个脉冲) 31.5 kHz (=每秒31500个脉冲)
为了产生标准的视频信号,还有很多细节需要处理,比如脉冲的宽度,HS和VS之间的关系等等。详细信息请参看VGA 时序。
我们的第一个视频产生器
大部分现在的VGA监视器都是[多同步的],所以可以支持非标准的同步频率,这样便不一定必须产生精确的60Hz和31.5Hz的频率。但是对于一些老的监视器,仍然需要产生精确的同步信号。
reg [9:0] CounterX;
reg [8:0] CounterY;
wire CounterXmaxed = (CounterX==767);
always @(posedge clk)
if(CounterXmaxed)
CounterX <= 0;
else
CounterX <= CounterX + 1;
always @(posedge clk)
if(CounterXmaxed)
CounterY <= CounterY + 1;
CounterX 从 0 计到 767,CounterY 从 0 计到 511。我们用CounterX 产生 HS, CounterY 产生 VS。当时钟频率为25MHz时, 我们可以得到32.5KHz的水平同步信号HS和63.5Hz的垂直同步信号VS。
我们通过使用D触发器来产生HS和VS信号,从而避免输出毛刺信号。脉冲必须足够宽,这样监视器才能检测到它们。这里我们使 HS 的脉冲宽度为 16 个时钟周期(0.64uS),而使 VS 的脉冲宽度为一整个水平行的时间,共768个时钟周期(30uS)。虽然比要求的宽度稍窄,但仍然能很好的工作。
reg vga_HS, vga_VS;
always @(posedge clk)
begin
vga_HS <= (CounterX[9:4]==0); // 16个时钟周期
vga_VS <= (CounterY==0); // 768个时钟周期
end
为了得到一个负的脉冲,我们将信号翻转得到输出:
assign vga_h_sync = ~vga_HS;
assign vga_v_sync = ~vga_VS;
最后我们再来驱动 R, G , B 信号。首先,为了简单起见,我们使用 X 和 Y 计数器的某几位来得到一个很好的直方彩条图。
assign R = CounterY[3] | (CounterX==256);
assign G = (CounterX[5] ^ CounterX[6]) | (CounterX==256);
assign B = CounterX[4] | (CounterX==256);
画出有用的图形
同步信号的产生最好用一个单独Verilog HDL的模块重写,而把 R, G , B 信号的产生放到模块外部。同样,如果X 和 Y计数器从画图区域开始计数的画,对于R,G,B信号的产生也是十分有用的。
这里是新的设计文件
现在,我们可以用它来画一个围绕监视器屏幕的边框
module pong(clk, vga_h_sync, vga_v_sync, vga_R, vga_G, vga_B);
input clk;
output vga_h_sync, vga_v_sync, vga_R, vga_G, vga_B;
wire inDisplayArea;
wire [9:0] CounterX;
wire [8:0] CounterY;
hvsync_generator syncgen(.clk(clk), .vga_h_sync(vga_h_sync), .vga_v_sync(vga_v_sync),
.inDisplayArea(inDisplayArea), .CounterX(CounterX), .CounterY(CounterY));
// 画出边框
wire border = (CounterX[9:3]==0) || (CounterX[9:3]==79) || (CounterY[8:3]==0) || (CounterY[8:3]==59);
wire R = border;
wire G = border;
wire B = border;
reg vga_R, vga_G, vga_B;
always @(posedge clk)
begin
vga_R <= R & inDisplayArea;
vga_G <= G & inDisplayArea;
vga_B <= B & inDisplayArea;
end
endmodule
画挡板
我们使用鼠标来控制挡板左右移动。
正交解码 为我们揭示了其中的奥秘,下面是其代码:
reg [8:0] PaddlePosition;
reg [2:0] quadAr, quadBr;
always @(posedge clk) quadAr <= {quadAr[1:0], quadA};
always @(posedge clk) quadBr <= {quadBr[1:0], quadB};
always @(posedge clk)
if(quadAr[2] ^ quadAr[1] ^ quadBr[2] ^ quadBr[1])
begin
if(quadAr[2] ^ quadBr[1])
begin
if(~&PaddlePosition) // make sure the value doesn't overflow
PaddlePosition <= PaddlePosition + 1;
end
else
begin
if(|PaddlePosition) // make sure the value doesn't underflow
PaddlePosition <= PaddlePosition - 1;
end
end
已知了挡板的位置,我们就可以在屏幕上画出挡板:
wire border = (CounterX[9:3]==0) || (CounterX[9:3]==79) || (CounterY[8:3]==0) || (CounterY[8:3]==59);
wire paddle = (CounterX>=PaddlePosition+8) && (CounterX<=PaddlePosition+120) && (CounterY[8:4]==27);
wire R = border | (CounterX[3] ^ CounterY[3]) | paddle;
wire G = border | paddle;
wire B = border | paddle;
画小球
小球需要在屏幕上移动,当碰到物体(边框或挡板)的时候又会被弹回。
首先,我们让我们来画出小球。用一个16乘16的小方块来表示它,当CounterX和CounterY的值在小球的坐标内时,我们开始在屏幕上画小球。
reg [9:0] ballX;
reg [8:0] ballY;
reg ball_inX, ball_inY;
always @(posedge clk)
if(ball_inX==0) ball_inX <= (CounterX==ballX) & ball_inY; else ball_inX <= !(CounterX==ballX+16);
always @(posedge clk)
if(ball_inY==0) ball_inY <= (CounterY==ballY); else ball_inY <= !(CounterY==ballY+16);
wire ball = ball_inX & ball_inY;
现在,我们遇到了设计中最难解决的问题--如何判断碰撞的发生。
我们可以通过检测小球的坐标和屏幕上任何一个物体(挡板和边框)的位置来判断是否有碰撞发生,但是当物体的数量增加时,情况将变得十分复杂。因此我们引入一个新的模型,该模型由4个关键点构成,分别分布在小球四个边框的中点。如果在重画物体(边框或挡板)的时候画到了这4个点中的任何一个点,就认为有碰撞发生,并且碰撞发生在所画到的点那一侧。
wire border = (CounterX[9:3]==0) || (CounterX[9:3]==79) || (CounterY[8:3]==0) || (CounterY[8:3]==59);
wire paddle = (CounterX>=PaddlePosition+8) && (CounterX<=PaddlePosition+120) && (CounterY[8:4]==27);
wire BouncingObject = border | paddle; // active if the border or paddle is redrawing itself
reg CollisionX1, CollisionX2, CollisionY1, CollisionY2;
always @(posedge clk) if(BouncingObject & (CounterX==ballX ) & (CounterY==ballY+ 8)) CollisionX1<=1;
always @(posedge clk) if(BouncingObject & (CounterX==ballX+16) & (CounterY==ballY+ 8)) CollisionX2<=1;
always @(posedge clk) if(BouncingObject & (CounterX==ballX+ 8) & (CounterY==ballY )) CollisionY1<=1;
always @(posedge clk) if(BouncingObject & (CounterX==ballX+ 8) & (CounterY==ballY+16)) CollisionY2<=1;
(上面是简化的代码,其中没有复位“碰撞标志”的操作。 完整的代码将在后面给出)。
现在我们来处理小球位置的更新,这里设计更新频率仅为每一帧图像更新一次。
reg UpdateBallPosition;
always @(posedge clk) UpdateBallPosition <= (CounterY==500) & (CounterX==0); // 每帧有效一次
reg ball_dirX, ball_dirY;
always @(posedge clk)
if(UpdateBallPosition)
begin
if(~(CollisionX1 & CollisionX2)) //如果X方向上均发生碰撞,不改变X上的运动方向
begin
ballX <= ballX + (ball_dirX ? -1 : 1);
if(CollisionX2) ball_dirX <= 1; else if(CollisionX1) ball_dirX <= 0;
end
if(~(CollisionY1 & CollisionY2)) // 如果Y方向上均发生碰撞,不改变Y上的运动方向
begin
ballY <= ballY + (ball_dirY ? -1 : 1);
if(CollisionY2) ball_dirY <= 1; else if(CollisionY1) ball_dirY <= 0;
end
end
这样,我们就可以将他们全部在屏幕上画出来。
wire R = BouncingObject | ball | (CounterX[3] ^ CounterY[3]);
wire G = BouncingObject | ball;
wire B = BouncingObject | ball;
reg vga_R, vga_G, vga_B;
always @(posedge clk)
begin
vga_R <= R & inDisplayArea;
vga_G <= G & inDisplayArea;
vga_B <= B & inDisplayArea;
end
通过一步一步的设计发现,其实这个乒乓球的游戏并没有想象中那么难。
这里是完整的代码 pong.zip, hvsync_generator.zip
这里的设计仅仅是功能上的实现,并不是那么激动人心,当加入更多的小球、物体以及记分的功能后,就变得很有意思了。不过,那将是你所要做的了 |
|