Welcome back to Exploring FPGA Graphics. In the previous part, we got an introduction to FPGA graphics; now we’re ready to put our graphical skills to work recreating the arcade classic: Pong.
In this series, we explore graphics at the hardware level and get a feel for the power of FPGAs. We start by learning how displays work, before racing the beam with Pong, starfields and sprites, simulating life with bitmaps, drawing lines and triangles, and finally creating simple 3D models. I’ll be writing and revising this series throughout 2020 and 2021. New to the series? Start with Exploring FPGA Graphics.
Updated 2021-02-02. Get in touch with @WillFlux or open an issue on GitHub.
Series Outline
- Exploring FPGA Graphics - learn how displays work and animate simple shapes
- FPGA Pong (this post) - race the beam to create the arcade classic
- Hardware Sprites - fast, colourful, graphics with minimal resources
- FPGA Ad Astra - demo with hardware sprites and animated starfields
- Framebuffers - driving the display from a bitmap in memory
- Life on Screen - the screen comes alive with Conway’s Game of Life
- Lines and Triangles - drawing lines and triangles with a framebuffer
More parts to follow.
Requirements
For this series, you need an FPGA board with video output. We’ll be working at 640x480, so pretty much any video output will work. It helps to be comfortable with programming your FPGA board and reasonably familiar with Verilog.
We’ll be demoing the designs with two boards:
- iCEBreaker (Lattice iCE40) with 12-Bit DVI Pmod
- Digilent Arty A7-35T (Xilinx Artix-7) with Pmod VGA
Source
The SystemVerilog designs featured in this series are available from the projf-explore repo on GitHub. The designs are open source hardware under the permissive MIT licence, but this blog is subject to normal copyright restrictions.
Invading the Living Room
Photograph by Sergey Galyonkin via Wikimedia under Creative Commons Attribution-Share Alike licence.
Pong may not have been the first computer game, but it has an excellent claim to being the first to break into public consciousness on release in 1972. Originally an arcade cabinet, it was quickly adapted for play at home with a television. If you’d like to learn more about the early history of Atari, including Pong, I recommend Racing the Beam by Nick Montfort and Ian Bogost.
If you’re unfamiliar with Pong gameplay, check out this Pong video on YouTube.
A Square Ball
Pong uses a square ball, so we can reuse our design from Exploring FPGA Graphics. We’re going to start off looking at the Arty (Xilinx) version, but links to designs for both boards can be found below the source listing.
We generate our pixel clock and display timings, then set up a ball and update its position once per frame. For now the ball bounces off all four sides of the screen.
module top_pong_v1 (
input wire logic clk_100m, // 100 MHz clock
input wire logic btn_rst, // reset button (active low)
output logic vga_hsync, // horizontal sync
output logic vga_vsync, // vertical sync
output logic [3:0] vga_r, // 4-bit VGA red
output logic [3:0] vga_g, // 4-bit VGA green
output logic [3:0] vga_b // 4-bit VGA blue
);
// generate pixel clock
logic clk_pix;
logic clk_locked;
clock_gen clock_640x480 (
.clk(clk_100m),
.rst(!btn_rst), // reset button is active low
.clk_pix,
.clk_locked
);
// display timings
localparam CORDW = 10; // screen coordinate width in bits
logic [CORDW-1:0] sx, sy;
logic hsync, vsync, de;
display_timings_480p timings_640x480 (
.clk_pix,
.rst(!clk_locked), // wait for clock lock
.sx,
.sy,
.hsync,
.vsync,
.de
);
// size of screen with and without blanking
localparam H_RES_FULL = 800;
localparam V_RES_FULL = 525;
localparam H_RES = 640;
localparam V_RES = 480;
logic animate; // high for one clock tick at start of vertical blanking
always_comb animate = (sy == V_RES && sx == 0);
// ball
localparam B_SIZE = 8; // size in pixels
logic [CORDW-1:0] bx, by; // position
logic dx, dy; // direction: 0 is right/down
logic [CORDW-1:0] spx = 1; // horizontal speed
logic [CORDW-1:0] spy = 1; // vertical speed
logic b_draw; // draw ball?
// ball animation
always_ff @(posedge clk_pix) begin
if (animate) begin
if (bx >= H_RES - (spx + B_SIZE)) begin // right edge
dx <= 1;
bx <= bx - spx;
end else if (bx < spx) begin // left edge
dx <= 0;
bx <= bx + spx;
end else bx <= (dx) ? bx - spx : bx + spx;
if (by >= V_RES - (spy + B_SIZE)) begin // bottom edge
dy <= 1;
by <= by - spy;
end else if (by < spy) begin // top edge
dy <= 0;
by <= by + spy;
end else by <= (dy) ? by - spy : by + spy;
end
end
// draw ball - is ball at current screen position?
always_comb begin
b_draw = (sx >= bx) && (sx < bx + B_SIZE)
&& (sy >= by) && (sy < by + B_SIZE);
end
// VGA output
always_ff @(posedge clk_pix) begin
vga_hsync <= hsync;
vga_vsync <= vsync;
vga_r <= (de && b_draw) ? 4'hF : 4'h0;
vga_g <= (de && b_draw) ? 4'hF : 4'h0;
vga_b <= (de && b_draw) ? 4'hF : 4'h0;
end
endmodule
Pong v1
Build the first version of pong and test it with your board.
We use the same display controller and clock generator as Exploring FPGA Graphics:
- Xilinx XC7: xc7/top_pong_v1.sv
- Lattice iCE40: ice40/top_pong_v1.sv
Building the Designs
In the FPGA Pong section of the git repo, you’ll find the design files, a makefile for iCEBreaker, a Vivado project for Arty, and instructions for building the designs for both boards.
Paddle, Paddle
Next, we need to add some paddles. The paddles are simple rectangles that move vertically on the left and right sides of the screen. These paddles have a crude AI that moves in the direction of the ball. We check if the paddles have reached the screen edge, but not whether they’ve have collided with the ball.
The paddles are added to the top module in a very similar way to the ball:
// paddles
localparam P_H = 40; // height in pixels
localparam P_W = 10; // width in pixels
localparam P_SP = 1; // speed
localparam P_OFFS = 32; // offset from screen edge
logic [CORDW-1:0] p1y, p2y; // vertical position of paddles 1 and 2
logic p1_draw, p2_draw; // draw paddles?
// paddle animation
always_ff @(posedge clk_pix) begin
if (animate) begin
// "AI" paddle 1
if ((p1y + P_H/2) + P_SP/2 < (by + B_SIZE/2)) begin
if (p1y < V_RES - (P_H + P_SP/2)) // screen bottom?
p1y <= p1y + P_SP; // move down
end else if ((p1y + P_H/2) > (by + B_SIZE/2) + P_SP/2) begin
if (p1y > P_SP) // screen top?
p1y <= p1y - P_SP; // move up
end
// "AI" paddle 2
if ((p2y + P_H/2) + P_SP/2 < (by + B_SIZE/2)) begin
if (p2y < V_RES - (P_H + P_SP/2))
p2y <= p2y + P_SP;
end else if ((p2y + P_H/2) > (by + B_SIZE/2) + P_SP/2) begin
if (p2y > P_SP)
p2y <= p2y - P_SP;
end
end
end
// draw paddles - are paddles at current screen position?
always_comb begin
p1_draw = (sx >= P_OFFS) && (sx < P_OFFS + P_W)
&& (sy >= p1y) && (sy < p1y + P_H);
p2_draw = (sx >= H_RES - P_OFFS - P_W) && (sx < H_RES - P_OFFS)
&& (sy >= p2y) && (sy < p2y + P_H);
end
Each paddle compares the vertical position of its centre with that of the ball. If the paddle is above the ball and adding its speed would move it nearer the ball’s vertical position, the paddle moves down. If the paddle is below the ball and subtracting its speed would move it nearer the ball’s vertical position, the paddle moves up. In either case, the paddle checks if its reached the screen edge before moving.
Pong v2
Build the second version of Pong using our update top module:
- Xilinx XC7: xc7/top_pong_v2.sv
- Lattice iCE40: ice40/top_pong_v2.sv
Program your board, and you should see something like this:
Collide
It’s briefly fun to watch the ball crawl around the screen with the paddles, but this isn’t exactly a game, even a demo of one: the ball shows a blatant disregard for the paddles.
To test for paddle collision, we take advantage of the drawing signals: if we’re drawing the ball and a paddle at the same time, then there’s a collision. The collision occurs during the drawing process, so we need to store it (in p1_col
and p2_col
) and use it to adjust the direction of the ball when we next animate:
// paddle collision detection
always_ff @(posedge clk_pix) begin
if (animate) begin
p1_col <= 0;
p2_col <= 0;
end else if (b_draw) begin
if (p1_draw) p1_col <= 1;
if (p2_draw) p2_col <= 1;
end
end
We reset the collision detection when we receive the animate
signal. However, the value of p1_col
and p2_col
is available until the next clock tick, so the ball animation logic can still use it.
Update the horizontal ball animation to check for paddle collisions:
// ball animation
always_ff @(posedge clk_pix) begin
if (animate) begin
if (p1_col) begin // left paddle collision
dx <= 0;
bx <= bx + spx;
end else if (p2_col) begin // right paddle collision
dx <= 1;
bx <= bx - spx;
end else if (bx >= H_RES - (spx + B_SIZE)) begin // right edge
dx <= 1;
bx <= bx - spx;
end else if (bx < spx) begin // left edge
dx <= 0;
bx <= bx + spx;
end else bx <= (dx) ? bx - spx : bx + spx;
We can also speed up the ball and paddles by editing the following values:
logic [CORDW-1:0] spx = 6; // horizontal speed
logic [CORDW-1:0] spy = 4; // vertical speed
localparam P_SP = 4; // speed
Pong v3
Build and test the updated version with paddle collisions:
- Xilinx XC7: xc7/top_pong_v3.sv
- Lattice iCE40: ice40/top_pong_v3.sv
Buttons
So far the FPGA has been playing against itself. To get in on the action, we’re going to use three buttons: up, control, and down.
Add the buttons to the top module ports:
input wire logic btn_up, // up button
input wire logic btn_ctrl, // control button
input wire logic btn_dn, // down button
We need to map these buttons to FPGA pins in the constraints file. You’ll be happy to hear I’ve already done this for you in the constraints:
- Xilinx XC7: arty.xdc
- Lattice iCE40: icebreaker.pcf
On the iCEBreaker the buttons map as follows:
- Button 1 - down
- Button 2 - control
- Button 3 - up
And on the Arty:
- BTN0 - down
- BTN1 - control
- BTN3 - up
To safely use buttons in our design, we need to debounce them. Debouncing ensures we get a single, clean, transition from button presses. The debounce module looks like this [debounce.sv]:
module debounce (
input wire logic clk, // clock
input wire logic in, // signal input
output logic out, // signal output (debounced)
output logic ondn, // on down (one tick)
output logic onup // on up (one tick)
);
// sync with clock and combat metastability
logic sync_0, sync_1;
always_ff @(posedge clk) sync_0 <= in;
always_ff @(posedge clk) sync_1 <= sync_0;
logic [16:0] cnt; // 2^17 = 1.3 ms counter at 100 MHz
logic idle, max;
always_comb begin
idle = (out == sync_1);
max = &cnt;
ondn = ~idle & max & ~out;
onup = ~idle & max & out;
end
always_ff @(posedge clk) begin
if (idle) begin
cnt <= 0;
end else begin
cnt <= cnt + 1;
if (max) out <= ~out;
end
end
endmodule
We want continuous signals for up and down, but a one-off event for the control button:
// debounce buttons
logic sig_ctrl, move_up, move_dn;
debounce deb_ctrl
(.clk(clk_pix), .in(btn_ctrl), .out(), .ondn(), .onup(sig_ctrl));
debounce deb_up
(.clk(clk_pix), .in(btn_up), .out(move_up), .ondn(), .onup());
debounce deb_dn
(.clk(clk_pix), .in(btn_dn), .out(move_dn), .ondn(), .onup());
The onup()
and ondown()
outputs work like traditional UI events. In this case, sig_ctrl
will be high for one tick when the control button is released.
We start with two states:
- IDLE - both paddles are controlled by the AI (demo)
- PLAY - enable player paddle
Pressing the control button switches between these states, while the up and down buttons are used to control the paddle.
We define the state using an enum and a simple finite state machine. This is overkill for two states but makes it easy to expand our game to include other concepts, such as the end of a point.
// game state
enum {IDLE, PLAY} state, state_next;
always_comb begin
case(state)
IDLE: state_next = (sig_ctrl) ? PLAY : IDLE;
PLAY: state_next = (sig_ctrl) ? IDLE : PLAY;
default: state_next = IDLE;
endcase
end
always_ff @(posedge clk_pix) begin
state <= state_next;
end
ProTip: Using SystemVerilog enums makes finite state machines easier to understand and debug.
The logic for paddle 1 needs updating to allow for human control:
// paddle animation
always_ff @(posedge clk_pix) begin
if (animate) begin
if (state == PLAY) begin // human paddle 1
if (move_up) begin
if (p1y > P_SP) p1y <= p1y - P_SP;
end
if (move_dn) begin
if (p1y < V_RES - (P_H + P_SP)) p1y <= p1y + P_SP;
end
end else begin // "AI" paddle 1
if ((p1y + P_H/2) + P_SP/2 < (by + B_SIZE/2)) begin
if (p1y < V_RES - (P_H + P_SP/2))
p1y <= p1y + P_SP;
end else if ((p1y + P_H/2) > (by + B_SIZE/2) + P_SP/2) begin
if (p1y > P_SP)
p1y <= p1y - P_SP;
end
end
// "AI" paddle 2
if ((p2y + P_H/2) < by) begin
if (p2y < V_RES - (P_H + P_SP)) p2y <= p2y + P_SP;
end
if ((p2y + P_H/2) > (by + B_SIZE)) begin
if (p2y > P_SP) p2y <= p2y - P_SP;
end
end
end
Pong v4
Build and test the updated version with buttons (don’t forget to include the debouncing module):
- Xilinx XC7: xc7/top_pong_v4.sv
- Lattice iCE40: ice40/top_pong_v4.sv
Press the control (middle) button to take over the left-hand paddle from the AI.
How do you get on? Is the ball too fast or slow? You can adjust the speed of the ball by changing spx
and spy
, and the speed of the paddles with P_SP
.
Where’s the Skill?
We still don’t have a game: the ball never goes out of play. Let’s fix that.
First off, we’ll add additional states to the game:
- INIT - initialize ball position, speed, etc. when powered on
- IDLE - both paddles are controlled by the AI (demo)
- START - reset ball position, speed, etc. in preparation to play a point
- PLAY - enable player paddle and play a point
- POINT_END - ball has collided with left or right edge of the screen, so stop
Add these to your top module:
// game state
enum {INIT, IDLE, START, PLAY, POINT_END} state, state_next;
always_comb begin
case(state)
INIT: state_next = IDLE;
IDLE: state_next = (sig_ctrl) ? START : IDLE;
START: state_next = (sig_ctrl) ? PLAY : START;
PLAY: state_next = (lft_col || rgt_col) ? POINT_END : PLAY;
POINT_END: state_next = (sig_ctrl) ? START : POINT_END;
default: state_next = IDLE;
endcase
end
Once a point has ended, the player presses the control button to set up the game for the next point. Pressing the control button no longer switches to demo mode.
You’ll notice that the PLAY state transitions to POINT_END if lft_col
or rgt_col
are high. We need to set these signals when the ball collides with the left or right edge of the screen.
Within the top module, update the ball animation for the right and left edges to record a collision:
end else if (bx >= H_RES - (spx + B_SIZE)) begin // right edge
rgt_col <= 1;
end else if (bx < spx) begin // left edge
lft_col <= 1;
end else bx <= (dx) ? bx - spx : bx + spx;
Then add support for the INIT and START states to the paddle and ball animations:
always_ff @(posedge clk_pix) begin
if (state == INIT || state == START) begin // reset paddle positions
p1y <= (V_RES - P_H) >> 1;
p2y <= (V_RES - P_H) >> 1;
end else if (animate && state != POINT_END) begin
if (state == PLAY) begin // human paddle 1
// ...
// ball animation
always_ff @(posedge clk_pix) begin
if (state == INIT || state == START) begin // reset ball position
bx <= (H_RES - B_SIZE) >> 1;
by <= (V_RES - B_SIZE) >> 1;
dx <= 0; // serve towards player 2 (AI)
dy <= ~dy;
lft_col <= 0;
rgt_col <= 0;
end else if (animate && state != POINT_END) begin
if (p1_col) begin // left paddle collision
// ...
Bouncing Mad
To make the game more interesting, we can change the bounce angle depending on where the ball hits the paddle. A simple way to do this is to update the vertical direction depending on where the ball hits the paddle. If the ball strikes the top half of the paddle, it goes up, if it strikes the bottom half it goes down. We need to add this for collisions with either paddle:
if (p1_col) begin // left paddle collision
dx <= 0;
bx <= bx + spx;
dy <= (by + B_SIZE/2 < p1y + P_H/2) ? 1 : 0;
end else if (p2_col) begin // right paddle collision
dx <= 1;
bx <= bx - spx;
dy <= (by + B_SIZE/2 < p1y + P_H/2) ? 1 : 0;
end else if (bx >= H_RES - (spx + B_SIZE)) begin // right edge
// ...
We can also increase the ball speed over time by counting the number of collisions:
// ball speed control
localparam SPEED_STEP = 5; // speed up after this many collisions
logic [$clog2(SPEED_STEP)-1:0] cnt_sp; // speed counter
always_ff @(posedge clk_pix) begin
if (state == INIT) begin // demo speed
spx <= 6;
spy <= 4;
end else if (state == START) begin // initial game speed
spx <= 4;
spy <= 2;
end else if (state == PLAY && animate && (p1_col || p2_col)) begin
if (cnt_sp == SPEED_STEP-1) begin
spx <= spx + 1;
spy <= spy + 1;
cnt_sp <= 0;
end else begin
cnt_sp <= cnt_sp + 1;
end
end
end
Pong Final
With this set of changes, our final version is ready to play:
- Xilinx XC7: xc7/top_pong.sv
- Lattice iCE40: ice40/top_pong.sv
The complete iCE40 version is shown below:
module top_pong (
input wire logic clk_12m, // 12 MHz clock
input wire logic btn_rst, // reset button (active high)
input wire logic btn_up, // up button
input wire logic btn_ctrl, // control button
input wire logic btn_dn, // down button
output logic dvi_clk, // DVI pixel clock
output logic dvi_hsync, // DVI horizontal sync
output logic dvi_vsync, // DVI vertical sync
output logic dvi_de, // DVI data enable
output logic [3:0] dvi_r, // 4-bit DVI red
output logic [3:0] dvi_g, // 4-bit DVI green
output logic [3:0] dvi_b // 4-bit DVI blue
);
// generate pixel clock
logic clk_pix;
logic clk_locked;
clock_gen clock_640x480 (
.clk(clk_12m),
.rst(btn_rst),
.clk_pix,
.clk_locked
);
// display timings
localparam CORDW = 10; // screen coordinate width in bits
logic [CORDW-1:0] sx, sy;
logic hsync, vsync, de;
display_timings_480p timings_640x480 (
.clk_pix,
.rst(!clk_locked), // wait for clock lock
.sx,
.sy,
.hsync,
.vsync,
.de
);
// size of screen with and without blanking
localparam H_RES_FULL = 800;
localparam V_RES_FULL = 525;
localparam H_RES = 640;
localparam V_RES = 480;
logic animate; // high for one clock tick at start of vertical blanking
always_comb animate = (sy == V_RES && sx == 0);
// debounce buttons
logic sig_ctrl, move_up, move_dn;
debounce deb_ctrl
(.clk(clk_pix), .in(btn_ctrl), .out(), .ondn(), .onup(sig_ctrl));
debounce deb_up
(.clk(clk_pix), .in(btn_up), .out(move_up), .ondn(), .onup());
debounce deb_dn
(.clk(clk_pix), .in(btn_dn), .out(move_dn), .ondn(), .onup());
// ball
localparam B_SIZE = 8; // size in pixels
logic [CORDW-1:0] bx, by; // position
logic dx, dy; // direction: 0 is right/down
logic [CORDW-1:0] spx; // horizontal speed
logic [CORDW-1:0] spy; // vertical speed
logic lft_col, rgt_col; // flag collision with left or right of screen
logic b_draw; // draw ball?
// paddles
localparam P_H = 40; // height in pixels
localparam P_W = 10; // width in pixels
localparam P_SP = 4; // speed
localparam P_OFFS = 32; // offset from screen edge
logic [CORDW-1:0] p1y, p2y; // vertical position of paddles 1 and 2
logic p1_draw, p2_draw; // draw paddles?
logic p1_col, p2_col; // paddle collision?
// game state
enum {INIT, IDLE, START, PLAY, POINT_END} state, state_next;
always_comb begin
case(state)
INIT: state_next = IDLE;
IDLE: state_next = (sig_ctrl) ? START : IDLE;
START: state_next = (sig_ctrl) ? PLAY : START;
PLAY: state_next = (lft_col || rgt_col) ? POINT_END : PLAY;
POINT_END: state_next = (sig_ctrl) ? START : POINT_END;
default: state_next = IDLE;
endcase
end
always_ff @(posedge clk_pix) begin
state <= state_next;
end
// paddle animation
always_ff @(posedge clk_pix) begin
if (state == INIT || state == START) begin // reset paddle positions
p1y <= (V_RES - P_H) >> 1;
p2y <= (V_RES - P_H) >> 1;
end else if (animate && state != POINT_END) begin
if (state == PLAY) begin // human paddle 1
if (move_up) begin
if (p1y > P_SP) p1y <= p1y - P_SP;
end
if (move_dn) begin
if (p1y < V_RES - (P_H + P_SP)) p1y <= p1y + P_SP;
end
end else begin // "AI" paddle 1
if ((p1y + P_H/2) + P_SP/2 < (by + B_SIZE/2)) begin
if (p1y < V_RES - (P_H + P_SP/2))
p1y <= p1y + P_SP;
end else if ((p1y + P_H/2) > (by + B_SIZE/2) + P_SP/2) begin
if (p1y > P_SP)
p1y <= p1y - P_SP;
end
end
// "AI" paddle 2
if ((p2y + P_H/2) + P_SP/2 < (by + B_SIZE/2)) begin
if (p2y < V_RES - (P_H + P_SP/2))
p2y <= p2y + P_SP;
end else if ((p2y + P_H/2) > (by + B_SIZE/2) + P_SP/2) begin
if (p2y > P_SP)
p2y <= p2y - P_SP;
end
end
end
// draw paddles - are paddles at current screen position?
always_comb begin
p1_draw = (sx >= P_OFFS) && (sx < P_OFFS + P_W)
&& (sy >= p1y) && (sy < p1y + P_H);
p2_draw = (sx >= H_RES - P_OFFS - P_W) && (sx < H_RES - P_OFFS)
&& (sy >= p2y) && (sy < p2y + P_H);
end
// paddle collision detection
always_ff @(posedge clk_pix) begin
if (animate) begin
p1_col <= 0;
p2_col <= 0;
end else if (b_draw) begin
if (p1_draw) p1_col <= 1;
if (p2_draw) p2_col <= 1;
end
end
// ball speed control
localparam SPEED_STEP = 5; // speed up after this many collisions
logic [$clog2(SPEED_STEP)-1:0] cnt_sp; // speed counter
always_ff @(posedge clk_pix) begin
if (state == INIT) begin // demo speed
spx <= 6;
spy <= 4;
end else if (state == START) begin // initial game speed
spx <= 4;
spy <= 2;
end else if (state == PLAY && animate && (p1_col || p2_col)) begin
if (cnt_sp == SPEED_STEP-1) begin
spx <= spx + 1;
spy <= spy + 1;
cnt_sp <= 0;
end else begin
cnt_sp <= cnt_sp + 1;
end
end
end
// ball animation
always_ff @(posedge clk_pix) begin
if (state == INIT || state == START) begin // reset ball position
bx <= (H_RES - B_SIZE) >> 1;
by <= (V_RES - B_SIZE) >> 1;
dx <= 0; // serve towards player 2 (AI)
dy <= ~dy;
lft_col <= 0;
rgt_col <= 0;
end else if (animate && state != POINT_END) begin
if (p1_col) begin // left paddle collision
dx <= 0;
bx <= bx + spx;
dy <= (by + B_SIZE/2 < p1y + P_H/2) ? 1 : 0;
end else if (p2_col) begin // right paddle collision
dx <= 1;
bx <= bx - spx;
dy <= (by + B_SIZE/2 < p1y + P_H/2) ? 1 : 0;
end else if (bx >= H_RES - (spx + B_SIZE)) begin // right edge
rgt_col <= 1;
end else if (bx < spx) begin // left edge
lft_col <= 1;
end else bx <= (dx) ? bx - spx : bx + spx;
if (by >= V_RES - (spy + B_SIZE)) begin // bottom edge
dy <= 1;
by <= by - spy;
end else if (by < spy) begin // top edge
dy <= 0;
by <= by + spy;
end else by <= (dy) ? by - spy : by + spy;
end
end
// draw ball - is ball at current screen position?
always_comb begin
b_draw = (sx >= bx) && (sx < bx + B_SIZE)
&& (sy >= by) && (sy < by + B_SIZE);
end
// colours
logic [3:0] red, green, blue;
always_comb begin
red = (de && (b_draw | p1_draw | p2_draw)) ? 4'hF : 4'h0;
green = (de && (b_draw | p1_draw | p2_draw)) ? 4'hF : 4'h0;
blue = (de && (b_draw | p1_draw | p2_draw)) ? 4'hF : 4'h0;
end
// Output DVI clock: 180° out of phase with other DVI signals
SB_IO #(
.PIN_TYPE(6'b010000) // PIN_OUTPUT_DDR
) dvi_clk_io (
.PACKAGE_PIN(dvi_clk),
.OUTPUT_CLK(clk_pix),
.D_OUT_0(1'b0),
.D_OUT_1(1'b1)
);
// Output DVI signals
SB_IO #(
.PIN_TYPE(6'b010100) // PIN_OUTPUT_REGISTERED
) dvi_signal_io [14:0] (
.PACKAGE_PIN({dvi_hsync, dvi_vsync, dvi_de, dvi_r, dvi_g, dvi_b}),
.OUTPUT_CLK(clk_pix),
.D_OUT_0({hsync, vsync, de, red, green, blue}),
.D_OUT_1()
);
endmodule
Scoring
At the moment we don’t show a score for the players. Once you learn about sprites, you can use them to keep score. Alternatively, you could draw simple blocks on the screen to keep score.
Explore
I hope you enjoyed this instalment of Exploring FPGA Graphics, but nothing beats creating your own designs. Here are a few suggestions to improve FPGA Pong:
- Change the colours of the ball and paddles
- Draw a net down the middle of the screen
- Add more sophisticated bouncing, so the ball moves off at different angles from the paddle
- Improve the AI, so it positions the paddle to direct the ball away from the player
Next Time
In the next part, we’ll learn about hardware sprites, which allow for fast, colourful, graphics with minimal resources.
Constructive feedback is always welcome. Get in touch with @WillFlux or open an issue on GitHub.