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

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

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


驱动VGA显示器


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

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

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


通过FPGA引脚创建VGA视频信号

以下是驱动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 VGA视频信号,脉冲频率应为:

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

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

我们的第一个视频生成器

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


reg [90] CounterX;
reg [80] CounterY;
电线 CounterXmaxed =(CounterX == 767;
 
总是 @posedge CLK)
如果(CounterXmaxed)
  CounterX <= 0;
否则
  CounterX <= CounterX +1;
 
总是 @posedge CLK)
如果(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;
总是 @posedge CLK)
开始
  vga_HS <=(CounterX [94] == 0; //有效16个时钟
  vga_VS <=(CounterY == 0; //有效的768个时钟
结束


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

分配 vga_h_sync =〜vga_HS;
分配 vga_v_sync =〜vga_VS;


最后,我们可以驱动R,G和B信号。首先,我们可以使用X和Y计数器的一些位来获得漂亮的正方形颜色图案…

分配 R = CounterY [3] | (CounterX == 256;
分配 G =(CounterX [5] ^ CounterX [6]| (CounterX == 256;
分配 B = CounterX [4] | (CounterX == 256;


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


画有用的图片

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

pong 模块(clk,vga_h_sync,vga_v_sync,vga_R,vga_G,vga_B);
输入 clk;
输出 vga_h_sync,vga_v_sync,vga_R,vga_G,vga_B;
 
在显示区域中连线;
线 [90] CounterX;
线 [80] CounterY;
 
hvsync_generator syncgen(.clk(clk)、. vga_h_sync(vga_h_sync)、. vga_v_sync(vga_v_sync)、. inDisplayArea(inDisplayArea)
                            、. CounterX(CounterX)、. CounterY(CounterY));
 
//在屏幕
导线边框=(CounterX [93] == 0|| 周围绘制边框 (CounterX [93] == 79|| (CounterY [83] == 0|| (CounterY [83] == 59;
线 R =边界;
线 G =边界;
线B =边框;
 
reg vga_R,vga_G,vga_B;
总是 @posedge CLK)
开始
  vga_R <= R&inDisplayArea;
  vga_G <= G&inDisplayArea;
  vga_B <= B&inDisplayArea;
最终
 
endmodule



画桨

让我们使用鼠标在屏幕上左右移动操纵杆。
解码器正交页面显示的秘密。代码如下:

reg [80] PaddlePosition;
reg [20] quadAr,quadBr;
总是 @posedge CLK)quadAr <= {quadAr [10],QUADA};
总是 @posedge CLK)quadBr <= {quadBr [10],QUADB};
 
总是 @posedge CLK)
如果(quadAr [2] ^ quadAr [1] ^ quadBr [2] ^ quadBr [1])
开始
  ,如果(quadAr [2] ^ quadBr [1])
  开始
    ,如果(〜&PaddlePosition)//使确保该值不会溢出
      PaddlePosition <= PaddlePosition + 1;
  结束,
  否则
  开始,
    如果(| PaddlePosition)//确保该值不下溢
      PaddlePosition <= PaddlePosition-1;
  年底
结束

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

线边界=(CounterX [93] == 0|| (CounterX [93] == 79|| (CounterY [83] == 0|| (CounterY [83] == 59;
线板=(CounterX> = PaddlePosition + 8&&(CounterX <= PaddlePosition + 120&&(CounterY [84] == 27;
 
线 R =边框| (CounterX [3] ^ CounterY [3]|;
线 G =边界|;
线 B =边界|;

画球

球需要在屏幕上移动,并在碰到物体(边界或球拍)时反弹。
首先,我们展示球。它是16×16像素的正方形。当CounterX和CounterY到达其坐标时,我们将激活球的绘制。

reg [90] ballX;
reg [80] ballY;
reg ball_inX,ball_inY;
 
总是 @posedge CLK)
如果(ball_inX == 0)ball_inX <=(CounterX == ballX)ball_inY; 否则 ball_inX <=!(CounterX == ballX + 16;
 
总是 @posedge CLK)
如果(ball_inY == 0)ball_inY <=(CounterY ==巴利); 否则 ball_inY <=!(CounterY == ballY + 16;
 
钢丝球= ball_inX&ball_inY;

现在进行碰撞。那是这个项目的困难部分。
我们可以检查球相对于屏幕上每个对象的坐标,并确定是否存在碰撞。但是随着对象数量的增加,这将很快成为一场噩梦。
取而代之的是,我们定义4个“热点”像素,在球每侧的中间一个像素。如果物体(边界或球拍)在球绘制其“热点”之一的同时重绘自身,则我们知道球的那一侧存在碰撞。

线边界=(CounterX [93] == 0|| (CounterX [93] == 79|| (CounterY [83] == 0|| (CounterY [83] == 59;
线板=(CounterX> = PaddlePosition + 8&&(CounterX <= PaddlePosition + 120&&(CounterY [84] == 27;
线 BouncingObject =边框|; //积极的,如果边界或桨重绘本身
 
REG CollisionX1,CollisionX2,CollisionY1,CollisionY2; 如果(BouncingObject&(CounterX == ballX)&(CounterY == ballY + 8))CollisionX1 <= 1 则
总是 @(posege clk); 如果(BouncingObject&(CounterX == ballX + 16)&(CounterY == ballY + 8))CollisionX2 <= 1 则总是 @(posege clk); 总是 @(
 
posege clk)如果(BouncingObject&(CounterX == ballX + 8)&(CounterY == ballY))CollisionY1 <= 1; 如果(BouncingObject&(CounterX == ballX + 8)&(CounterY == ballY + 16))CollisionY2 <= 1 则
总是 @(posege clk);

(我通过从未重置碰撞触发器来简化了上面的代码,下面提供了完整的代码)。
现在,我们更新球的位置,但每个视频帧仅更新一次。

reg UpdateBallPosition; //活性只有一次每个视频帧
总是 @posedge CLK)UpdateBallPosition <=(CounterY == 500)&(CounterX == 0;
 
reg ball_dirX,ball_dirY;
总是 @(posege clk)
if(UpdateBallPosition)
开始
  if(〜(CollisionX1&CollisionX2))//如果两个X面都发生碰撞,则不要沿X方向移动
  开始
    ballX <= ballX +(ball_dirX?-11;
    如果(CollisionX2)ball_dirX <= 1; 否则 if(CollisionX1)ball_dirX <= 0; 如果
  结束
 
  (〜(CollisionY1&CollisionY2))//如果两个Y侧都发生碰撞,则不要沿Y方向移动,则
  开始
    ballY <= ballY +(ball_dirY?-11;
    如果(CollisionY2)ball_dirY <= 1; 否则 if(CollisionY1)ball_dirY <= 0;
  年底
结束

最后,我们可以将所有内容整合在一起。

线 R = BouncingObject | 球 (CounterX [3] ^ CounterY [3];
线 G = BouncingObject |;
线 B = BouncingObject |;
 
reg vga_R,vga_G,vga_B;
总是 @posedge CLK)
开始
  vga_R <= R&inDisplayArea;
  vga_G <= G&inDisplayArea;
  vga_B <= B&inDisplayArea;
结束

哇,毕竟并不难。
完整的文件是pong.zip,并与hvsync_generator.zip一起使用
也可以使用HDMI来运行乒乓游戏。

链接