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, 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-11-25. 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

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. You should be comfortable with programming your FPGA board and reasonably familiar with Verilog. We’ll be demoing the designs with two boards:

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

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 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 look at the iCEBreaker version, but links to versions 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_12m,      // 12 MHz clock
    input  wire logic btn_rst,      // reset button (active high)
    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 de;
    display_timings timings_640x480 (
        .clk_pix,
        .rst(!clk_locked),  // wait for clock lock
        .sx,
        .sy,
        .hsync(dvi_hsync),
        .vsync(dvi_vsync),
        .de
    );

    // size of screen (excluding blanking)
    localparam H_RES = 640;
    localparam V_RES = 480;

    logic animate;  // high for one clock tick at start of 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 = 10'd1;  // horizontal speed
    logic [CORDW-1:0] spy = 10'd1;  // 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

    // DVI clock output
    SB_IO #(
        .PIN_TYPE(6'b010000)
    ) dvi_clk_buf (
        .PACKAGE_PIN(dvi_clk),
        .CLOCK_ENABLE(1'b1),
        .OUTPUT_CLK(clk_pix),
        .D_OUT_0(1'b0),
        .D_OUT_1(1'b1)
    );

    // DVI output
    always_ff @(posedge clk_pix) begin
        dvi_de <= de;
        dvi_r <= !de ? 4'h0 : (b_draw ? 4'hF : 4'h0);
        dvi_g <= !de ? 4'h0 : (b_draw ? 4'hF : 4'h0);
        dvi_b <= !de ? 4'h0 : (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:

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 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.

The paddles are added to the top module in a very similar way to 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?

    // paddle animation
    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

    // draw paddles - are paddles at current screen position?
    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 v2

Build the second version of Pong using our update top module:

Program your board, and you should see something like this:

Pong on the iCEBreaker

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 col_p1 and col_p2 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 = 10'd6;  // horizontal speed
    logic [CORDW-1:0] spy = 10'd4;  // vertical speed

    localparam P_SPEED = 4;  // speed

Pong v3

Build and test the updated version with paddle collisions:

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:

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_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
        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 human-controlled paddle needs 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) 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
            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

Pong v4

Build and test the updated version with buttons (don’t forget to include the debouncing module):

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_SPEED.

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:

    // 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 where the ball hits the paddle. A 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 Final

With this set of changes, our final version is ready to play:

With the default ball and paddles speeds, it is possible to win a point against the AI.

NB. The timing estimate of 40.55 ns (24.66 MHz) for the iCE40 version of top_pong.sv is a little slow for our pixel clock of 25.125 MHz. The final design could probably be tweaked to improve this, but I don’t plan on modifying it at the moment.

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 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
  • 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

Feedback is most welcome; you can get in touch with @WillFlux or open an issue on GitHub.

Next Time

In the next part, we’ll learn about hardware sprites, which allow for fast, colourful, graphics with minimal resources.

©2020 Will Green, Project F