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

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:

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

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:

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

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_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):

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:

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.

©2021 Will Green, Project F