新闻中心

EEPW首页 > 嵌入式系统 > 设计应用 > 乒乓球比赛

乒乓球比赛

作者:时间:2023-12-22来源:电子森林收藏

FPGA可以轻松成为视频生成器。

本文引用地址:http://www.eepw.com.cn/article/202312/454181.htm

乒乓球游戏包括在屏幕上弹跳的球。桨(此处由鼠标控制)使用户能够使球弹回。

尽管可以使用其他任何FPGA开发板,但我们都使用Pluto FPGA板。


驱动VGA显示器

一个VGA监视器需要5个信号才能显示图片:

  • R,G和B(红色,绿色和蓝色信号)。
  • HS和VS(水平和垂直同步)。

R,G和B是模拟信号,而HS和VS是数字信号。

通过FPGA引脚创建

以下是驱动VGA接口的方法:

  • VGA连接器(HS和VS)的引脚13和14是数字信号,因此可以直接从两个FPGA引脚驱动(或通过低阻值电阻,例如10Ω或20Ω)驱动。
  • 引脚1、2和3(R,G和B)是75 are模拟信号,标称值为0.7V。对于3.3V FPGA输出,请使用三个270Ω串联电阻。电阻与监视器输入中的75Ω电阻形成分压器,因此
  • 3.3V变为3.3 * 75 /(270 + 75)= 0.72V,非常接近0.7V。以0和1的不同组合来驱动这3个引脚时,最多可以得到8种颜色。

接地引脚是引脚5、6、7、8和10。

这是连接到面包板上Pluto的母VGA连接器的视图。

VGA母头连接器至12针接头连接器的后视图。12针插头可轻松连接到面包板。三个270Ω串联电阻清晰可见。我们也可以使用转接板。

频率发生器

监视器始终从上到下逐行显示图片。每条线从左到右绘制。

这是硬编码的,您无法更改。

但是,您可以通过以固定间隔在HS和VS上发送短脉冲来指定何时开始绘制图形。HS画了一条新线开始绘制;而VS告诉您已经到达底部(使监视器回到顶部)。

对于标准640×480 ,脉冲频率应为:

垂直频率(VS)水平频率(HS)
60 Hz(= 60脉冲每秒)31.5 kHz(= 31500脉冲/秒)

要创建标准视频信号,需要处理更多细节,例如脉冲的持续时间以及HS和VS之间的关系。在此页面上获得一个想法。

我们的第一个视频生成器

如今,VGA监视器是多同步的,因此可以适应非标准频率-不再需要精确地生成60Hz和31.5KHz(但是,如果您使用的是旧的(非多同步)VGA监视器,则需要生成精确的频率)。

让我们从X和Y计数器开始。

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计数768个值(从0到767),CounterY计数512个值(0到511)。

现在,使用CounterX生成HS,使用CounterY生成VS。使用25MHz时钟,HS的频率为32.5KHz,VS的频率为63.5Hz。脉冲需要激活足够长的时间,以使监视器能够检测到它们。让我们为HS使用16个时钟脉冲(0.64µs),为VS使用完整的水平线长脉冲(768个时钟或30µs)。这比VGA规范所要求的要短,但仍然可以正常工作。

我们从D触发器生成HS和VS脉冲(以获得无毛刺输出)。

reg vga_HS, vga_VS;always @(posedge clk)begin
  vga_HS <= (CounterX[9:4]==0);   // active for 16 clocks
  vga_VS <= (CounterY==0);   // active for 768 clocksend

VGA输出必须为负,因此我们将信号反相。

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);

…然后我们在VGA监视器上得到一张照片!

画有用的图片

最好将同步生成器重写为HDL模块,以便在外部生成R,G和B。同样,如果X和Y计数器从绘图区域开始计数,它们将更加有用。 可以在这里找到新文件。

现在,我们可以使用它在屏幕周围绘制边框。

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)); 
                            // Draw a border around the screen
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;
  endend

现在知道“ PaddlePosition”的值,我们可以显示桨了。

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个“热点”像素,在球每侧的中间一个像素。如果物体(边界或球拍)在球绘制其“热点”之一的同时重绘自身,则我们知道球的那一侧存在碰撞。

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;       // active only once for every video frame
always @(posedge clk) UpdateBallPosition <= (CounterY==500) & (CounterX==0); 
reg ball_dirX, ball_dirY;
always @(posedge clk)if(UpdateBallPosition)begin
  if(~(CollisionX1 & CollisionX2))        // if collision on both X-sides, don't move in the X direction
  begin
    ballX <= ballX + (ball_dirX ? -1 : 1);
    if(CollisionX2) ball_dirX <= 1; 
    else if(CollisionX1) ball_dirX <= 0;
  end   if(~(CollisionY1 & CollisionY2))        // if collision on both Y-sides, don't move in the Y direction
  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

哇,其实并不难。



评论


相关推荐

技术专区

关闭