30 July 2020

FPGA Pong

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, drawing 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. New to the series? Start with Exploring FPGA Graphics.

Updated 2020-08-19. Get in touch with @WillFlux or open an issue on GitHub.

Series Outline

  • Exploring FPGA Graphics - how displays work and simple animated colour graphics
  • FPGA Pong (this post) - race the beam to create the arcade classic
  • FPGA Ad Astra - animated starfields, hardware sprites, and bitmap fonts
  • Life on Screen - bitmaps and Conway’s Game of Life (being written)
  • Hard Lines - 2D drawing (planned)
  • More 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 do. You should be comfortable with programming your FPGA board and reasonably familiar with Verilog.

We’ll be demoing with these boards (FPGA type):

Follow the source README to quickly build a project for either of these boards.

iCEBreaker with VGA Pmod
The first part of this series included VGA designs for iCEBreaker. Maintaining three sets of designs was a significant overhead, so the iCEBreaker VGA version has been dropped from the examples.

Source

All the Verilog designs featured in this series are available in the Exploring FPGAs repo, and source links are included throughout the blog. The designs are open source under the permissive MIT licence, but this blog is subject to normal copyright restrictions.

Invading the Living Room

70s gaming room in Computerspielemuseum, Berlin

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 when it was released in 1972. Originally an arcade cabinet, it was quickly adapted to be played at home attached to 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.

ProTip: If you’re unfamiliar with Pong gameplay, check out this Pong video on YouTube.

Ball & Paddles

In the previous part of this series, we learnt how to draw animated squares and perform collision detection. This gives us all we need to implement a square Pong ball:

    // 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 = 10'd1;  // horizontal speed
    logic [CORDW-1:0] spy = 10'd1;  // vertical speed
    logic b_draw;                   // draw ball?

    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

    always_comb begin
        b_draw = (sx >= bx) && (sx < bx + B_SIZE)
              && (sy >= by) && (sy < by + B_SIZE);
    end

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 compares their vertical position to the ball and moves them 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.

    // paddles
    localparam P_HEIGHT = 40;       // height in pixels
    localparam P_WIDTH  = 10;       // width in pixels
    localparam P_SPEED  = 1;        // speed
    localparam P_OFFSET = 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?

    always_ff @(posedge clk_pix) begin
        if (animate) begin
            // "AI" paddle 1
            if ((p1y + P_HEIGHT/2) < by) begin  // top of ball is below
                if (p1y < V_RES - (P_HEIGHT + P_SPEED)) p1y <= p1y + P_SPEED;  // screen bottom?
            end
            if ((p1y + P_HEIGHT/2) > (by + B_SIZE)) begin  // bottom of ball is above
                if (p1y > P_SPEED) p1y <= p1y - P_SPEED;  // screen top?
            end

            // "AI" paddle 2
            if ((p2y + P_HEIGHT/2) < by) begin
                if (p2y < V_RES - (P_HEIGHT + P_SPEED)) p2y <= p2y + P_SPEED;
            end
            if ((p2y + P_HEIGHT/2) > (by + B_SIZE)) begin
                if (p2y > P_SPEED) p2y <= p2y - P_SPEED;
            end
        end
    end

    always_comb begin
        p1_draw = (sx >= P_OFFSET) && (sx < P_OFFSET + P_WIDTH)
               && (sy >= p1y) && (sy < p1y + P_HEIGHT);
        p2_draw = (sx >= H_RES - P_OFFSET - P_WIDTH) && (sx < H_RES - P_OFFSET)
               && (sy >= p2y) && (sy < p2y + P_HEIGHT);
    end

We compare paddle positions with the top or bottom of the ball to prevent them being too twitchy.

Pong v1

We can use the ball and paddle logic to create a first version of the top module for Pong. Our top module is based on that from Exploring FPGA Graphics, with the same clock generator and display timings.

All the files you need to build this design are available in the FPGA Pong repo. There is a makefile for iCEBreaker and a Vivado project for Arty.

Pong on the iCEBreaker

Pong on the iCEBreaker board. You can easily change the colours of the background and ball/paddles.

Collision

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 and use it when we next animate.

    logic p1_col, p2_col;  // paddle collision?

    // 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 col_p1 and col_p2 is available until the next clock tick, so the ball animation can 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 = 10'd6;  // horizontal speed
    logic [CORDW-1:0] spy = 10'd4;  // vertical speed

    localparam P_SPEED = 4;

Pong v2

Build and test the updated version with paddle collisions:

Controls

So far the FPGA has been playing against itself. If you want to get in on the action, we need to add some controls. 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:

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:

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_btn_ctrl (.clk(clk_pix), .in(btn_ctrl), .out(), .ondn(), .onup(sig_ctrl));
    debounce deb_btn_up (.clk(clk_pix), .in(btn_up), .out(move_up), .ondn(), .onup());
    debounce deb_btn_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
        state_next = IDLE;
        case(state)
            IDLE: state_next = (sig_ctrl) ? PLAY : IDLE;
            PLAY: state_next = (sig_ctrl) ? IDLE : PLAY;
        endcase
    end

    always_ff @(posedge clk_pix) begin
        state <= state_next;
    end

The first paddle control logic meeds updating to reflect the state of play:

    // 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_SPEED) p1y <= p1y - P_SPEED;  // at top?
                end
                if (move_dn) begin
                    if (p1y < V_RES - (P_HEIGHT + P_SPEED)) p1y <= p1y + P_SPEED;  // at bottom?
                end
            end else begin  // "AI" paddle 1
                if ((p1y + P_HEIGHT/2) < (by + B_SIZE/2)) begin  // ball is below
                    if (p1y < V_RES - (P_HEIGHT + P_SPEED)) p1y <= p1y + P_SPEED;  // at bottom?
                end
                if ((p1y + P_HEIGHT/2) > by + (B_SIZE/2)) begin  // ball is above
                    if (p1y > P_SPEED) p1y <= p1y - P_SPEED;  // at top?
                end
            end

            // "AI" paddle 2
            if ((p2y + P_HEIGHT/2) < (by + B_SIZE/2)) begin
                if (p2y < V_RES - (P_HEIGHT + P_SPEED)) p2y <= p2y + P_SPEED;
            end
            if ((p2y + P_HEIGHT/2) > (by + B_SIZE/2)) begin
                if (p2y > P_SPEED) p2y <= p2y - P_SPEED;
            end
        end
    end

Pong v3

Build and test the updated version with controls:

Press the control (middle) button to take over the left-hand paddle. How do you get on? Is the ball too fast or slow? You can adjust the speed of the ball, changing the values of spx and spy.

Where’s the Skill?

We still don’t have a game: the ball doesn’t go out of play. Let’s fix this.

First off, we’ll add additional states to the game:

    // game state
    enum {INIT, IDLE, START, PLAY, POINT_END} state, state_next;
    always_comb begin
        state_next = INIT;
        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;
        endcase
    end

The five states:

  • 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

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 when the ball collides with the left or right edge of the screen.

It’s a quick change to the ball animation:

    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;

Quick Aside: If you’re not sure what goes where, check out the finished versions.
Lattice iCE40: ice40/top_pong_v4.sv or Xilinx XC7: xc7/top_pong_v4.sv.

We then add support for the INIT and START states to paddle and ball animation:

    // paddle animation
    always_ff @(posedge clk_pix) begin
        if (state == INIT || state == START) begin  // reset paddle positions
            p1y <= (V_RES - P_HEIGHT) >> 1;
            p2y <= (V_RES - P_HEIGHT) >> 1;
        end else if (animate && state != POINT_END) begin

    ...
    // 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;
            spx <= 10'd5;
            spy <= 10'd2;
            lft_col <= 0;
            rgt_col <= 0;
        end else if (animate && state != POINT_END) begin

Bouncing Mad

To make the game more interesting we can change the angle of bounce depending on whether the ball hits the paddle. The simple way to do this is to update the vertical speed depending on where the ball hits the paddle. We create a new parameter called P_SEC, which is the size of a section of paddle:

    // paddles
    localparam P_HEIGHT = 40;           // height in pixels
    localparam P_SEC = P_HEIGHT / 8;    // paddle sections
    localparam P_WIDTH  = 10;           // width in pixels

It’s then just a case of comparing the vertical ball position with that of the paddle when a collision occurs. Here’s the logic for the 1st paddle:

    if (p1_col) begin  // left paddle collision
        dx <= 0;
        bx <= bx + spx;
        if (by < p1y - B_SIZE/2 + P_SEC) begin
            dy <= 1;
            spy <= 10'd5;
        end else if (by < p1y - B_SIZE/2 + 2*P_SEC) begin 
            dy <= 1;
            spy <= 10'd4;
        end else if (by < p1y - B_SIZE/2 + 3*P_SEC) begin 
            dy <= 1;
            spy <= 10'd2;
        end else if (by < p1y - B_SIZE/2 + 5*P_SEC) begin 
            dy <= 1;
            spy <= 0;
        end else if (by < p1y - B_SIZE/2 + 6*P_SEC) begin 
            dy <= 0;
            spy <= 10'd2;
        end else if (by < p1y - B_SIZE/2 + 7*P_SEC) begin 
            dy <= 0;
            spy <= 10'd4;
        end else begin
            dy <= 0;
            spy <= 10'd5;
        end

Repeat this for the second paddle p2y, and you’ve got a pretty decent game of Pong.

Pong v4

Our fourth version is ready to play. With the default speeds it is possible to win a point against the AI:

Scoring

At the moment we don’t show a score for the players. We’ll come back to this once we can draw sprites. Alternatively, you could draw simple blocks on 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
  • Add a net down the middle of the screen
  • Increase the ball speed over time until a point is lost
  • Improve 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 and generate an animated starfield in FPGA Ad Astra.

©2020 Will Green, Project F