| fpga4fun.com - where FPGAs are fun. |
FPGAs can become video generators easily.
The pong game consists of a ball bouncing on a screen. A paddle (controlled from a mouse here) enables the user to make the ball bounce back up.

We use a Pluto FPGA board here, although any other FPGA developement board would work.


The R, G and B are analog signals, while HS and VS are digital signals.

Here's a view of the female VGA connector connected to Pluto on a breadboard.

Back view of the female VGA connector to 12-pins header assembly. The 12-pins header makes it easy to connect to a breadboard. The three 270Ω series resistors are clearly visible.
We could also have used an adapter board, like available here on KNJN.com
A monitor always displays a picture line-by-line, from top-to-bottom. Each line is drawn from left-to-right.
That's hard-coded, you cannot change that.
But you specify when the drawing starts by sending short pulses on HS and VS at fixed intervals. HS makes a new line to start drawing; while VS tells that the bottom has been reached (makes the monitor go back up to the top line).
For the standard 640x480 VGA video signal, the frequencies of the pulses should be:
| Vertical Freq (VS) | Horizontal Freq (HS) |
|---|---|
| 60 Hz (=60 pulses per second) | 31.5 kHz (=31500 pulses per second) |
To create a standard video signal, there is more details to take care of, like the duration of the pulses and the relationship between HS and VS. Get an idea on this VGA timing information page.
|
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; |
|
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 clocks end |
|
assign vga_h_sync = ~vga_HS; assign vga_v_sync = ~vga_VS; |
|
assign R = CounterY[3] | (CounterX==256); assign G = (CounterX[5] ^ CounterX[6]) | (CounterX==256); assign B = CounterX[4] | (CounterX==256); |
|
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; 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; |
|
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; |
|
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); // active only once for every video frame 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 |