12 March 2022

Racing the Beam

Welcome back to Exploring FPGA Graphics. Last time, we got introduction to FPGA graphics; now, we’re ready to put our graphical skills to work with some simple demos. I hope these examples inspire you to create your own demos and improve your hardware design skills. This post was last updated in June 2022.

In this series, we explore graphics at the hardware level and get a feel for the power of FPGAs. We’ll learn how screens work, play Pong, create starfields and sprites, paint Michelangelo’s David, simulate life, draw lines and triangles, and animate characters and shapes. New to the series? Start with Beginning FPGA Graphics.

Get in touch: GitHub Issues, 1BitSquared Discord, @WillFlux (Mastodon), @WillFlux (Twitter)

Series Outline

Sponsor My Work
If you like what I do, consider sponsoring me on GitHub.
I love FPGAs and want to help more people discover and use them in their projects.
My hardware designs are open source, and my blog is advert free.

Requirements

You should be to run these designs to any recent FPGA board. I include complete designs for the iCEBreaker with 12-Bit DVI Pmod, Digilent Arty A7-35T with Pmod VGA, and Verilator Simulation with SDL. See requirements from Beginning FPGA Graphics for more details.

Five Demos

This post features five simple demos that generate graphics on the fly:

Find the source and build instructions for all these demos in the projf-explore git repo.

Raster Bars

Raster bars are animated bands of colour created by changing the background colour every screen line. Raster bars originated on early 8-bit systems, such as Atari 2600, and continue(d) to be popular on home computers, including the Amiga, and Atari ST.

This demo creates bars but doesn’t animate them. We’ll look at animation later in this post.

Raster Bars

Demo source:

See the README or Sim README for build instructions.

The following Verilog snippet shows the logic that generates the bars:

// screen dimensions (must match display_inst)
localparam V_RES_FULL = 525;  // vertical screen resolution (including blanking)
localparam H_RES      = 640;  // horizontal screen resolution

localparam START_COLR = 12'h126;  // bar start colour (blue: 12'h126) (gold: 12'h640)
localparam COLR_NUM   = 10;       // colours steps in each bar (don't overflow)
localparam LINE_NUM   =  2;       // lines of each colour

logic [11:0] bar_colr;  // 12 bit colour (4 bits per channel)
logic bar_inc;  // increase (or decrease) brightness
logic [$clog2(COLR_NUM):0] cnt_colr;  // count colours in each bar
logic [$clog2(LINE_NUM):0] cnt_line;  // count lines of each colour

// update colour for each screen line
always_ff @(posedge clk_pix) begin
    if (sx == H_RES) begin  // on each screen line at the start of blanking
        if (sy == V_RES_FULL-1) begin  // reset colour on last line of screen
            bar_colr <= START_COLR;
            bar_inc <= 1;  // start by increasing brightness
            cnt_colr <= 0;
            cnt_line <= 0;
        end else if (cnt_line == LINE_NUM-1) begin  // colour complete
            cnt_line <= 0;
            if (cnt_colr == COLR_NUM-1) begin  // switch increase/decrease
                bar_inc <= ~bar_inc;
                cnt_colr <= 0;
            end else begin
                bar_colr <= (bar_inc) ? bar_colr + 12'h111 : bar_colr - 12'h111;
                cnt_colr <= cnt_colr + 1;
            end
        end else cnt_line <= cnt_line + 1;
    end
end

// separate colour channels
logic [3:0] paint_r, paint_g, paint_b;
always_comb {paint_r, paint_g, paint_b} = bar_colr;

As discussed in Beginning FPGA Graphics, we represent colours using a 12-bit hex triplet. For example, #FF0 is orange, #137 dark green, and #FFF is white.

Adding 0x111 to a colour increases the brightness of red, green, and blue by one step. To avoid ugly colour jumps, we mustn’t let any colour channel overflow. Our start colour is 12'h126 (dark blue), so we use ten colour steps, finishing with 12'hABF (pale blue). We then reverse direction, subtracting 0x111 from the colour until we return to dark blue.

Try replacing the start colour with 12'h640.

Hitomezashi

Our second demo was inspired by a Numberphile video: Hitomezashi Stitch Patterns. A short series of 1s and 0s creates a beautiful pattern, which appears both ordered and random. If you’re interested in learning about the origin of these stitch designs, check out Sashiko on Wikipedia.

Hitomezashi Stitch Pattern

Demo source:

See the README or Sim README for build instructions.

This pattern requires only equality and boolean operators:

// stitch start values: big-endian vector, so we can write left to right
logic [0:39] v_start;  // 40 vertical lines
logic [0:29] h_start;  // 30 horizontal lines

initial begin  // random start values
    v_start = 40'b01100_00101_00110_10011_10101_10101_01111_01101;
    h_start = 30'b10111_01001_00001_10100_00111_01010;
end

// paint stitch pattern with 16x16 pixel grid
logic stitch;
logic v_line, v_on;
logic h_line, h_on;
always_comb begin
    v_line = (sx[3:0] == 4'b0000);
    h_line = (sy[3:0] == 4'b0000);
    v_on = sy[4] ^ v_start[sx[9:4]];
    h_on = sx[4] ^ h_start[sy[8:4]];
    stitch = (v_line && v_on) || (h_line && h_on);
end

// paint colours: yellow lines, blue background
logic [3:0] paint_r, paint_g, paint_b;
always_comb begin
    paint_r = (stitch) ? 4'hF : 4'h1;
    paint_g = (stitch) ? 4'hC : 4'h3;
    paint_b = (stitch) ? 4'h0 : 4'h7;
end

We draw a line every 16 pixels: 640x480 divided by 16 is 40x30. 40 requires five bits to represent it, while 30 requires only four; otherwise, the horizontal and vertical designs are the same.

The initial block creates a simple ROM to hold our start values. I generated these start values randomly; what happens if you set the start value to all zeros or all ones?

This design makes good use of XOR (exclusive or), denoted by the caret symbol ^.

Missing Pixels

You may have noticed there’s something slightly off: corner pixels are missing. This happens because our lines are 16 pixels long and one pixel wide.

In the magnified version (below), the red bars are 16 pixels long, the white squares are one pixel:

Hitomezashi pattern magnified to show missing corner pixels

I don’t believe there’s a way to fix this issue without significantly complicating the design. If you have a suggestion, get in touch with @WillFlux or open an issue on GitHub.

Hello

A simple ROM can also hold a bitmap pattern.

I’ve chosen the classic greeting, but you can create your own message or picture by altering the initial block. This design uses the compact PICO-8 font, which is in the public domain.

Hello World

Demo source:

See the README or Sim README for build instructions.

This design is little more than an array lookup:

    // bitmap: big-endian vector, so we can write pixels left to right
    logic [0:19] bmap [15];  // 20 pixels by 15 lines

    initial begin
        bmap[0]  = 20'b1010_1110_1000_1000_0110;
        bmap[1]  = 20'b1010_1000_1000_1000_1010;
        bmap[2]  = 20'b1110_1100_1000_1000_1010;
        bmap[3]  = 20'b1010_1000_1000_1000_1010;
        bmap[4]  = 20'b1010_1110_1110_1110_1100;
        bmap[5]  = 20'b0000_0000_0000_0000_0000;
        bmap[6]  = 20'b1010_0110_1110_1000_1100;
        bmap[7]  = 20'b1010_1010_1010_1000_1010;
        bmap[8]  = 20'b1010_1010_1100_1000_1010;
        bmap[9]  = 20'b1110_1010_1010_1000_1010;
        bmap[10] = 20'b1110_1100_1010_1110_1110;
        bmap[11] = 20'b0000_0000_0000_0000_0000;
        bmap[12] = 20'b0000_0000_0000_0000_0000;
        bmap[13] = 20'b0000_0000_0000_0000_0000;
        bmap[14] = 20'b0000_0000_0000_0000_0000;
    end

    // paint at 32x scale in active screen area
    logic picture;
    logic [4:0] x;  // 20 columns need five bits
    logic [3:0] y;  // 15 rows need four bits
    always_comb begin
        x = sx[9:5];  // every 32 horizontal pixels
        y = sy[8:5];  // every 32 vertical pixels
        picture = de ? bmap[y][x] : 0;  // look up pixel (unless we're in blanking)
    end

    // paint colours: yellow lines, blue background
    logic [3:0] paint_r, paint_g, paint_b;
    always_comb begin
        paint_r = (picture) ? 4'hF : 4'h1;
        paint_g = (picture) ? 4'hC : 4'h3;
        paint_b = (picture) ? 4'h0 : 4'h7;
    end

Typically, vectors are defined [19:0] (little-endian) rather than [0:19] (big-endian). We chose big-endian, so we can write the initial block pixels left to right.

We cover bitmap graphics in detail later in this series.

Colour Cycle

So far, we’ve created static designs. This demo takes the colour gradient from Beginning FPGA Graphics and cycles through all 16 blue levels. This demo lets you see all 4,096 colours our graphics output supports.

Colour Cycle

Demo source:

See the README or Sim README for build instructions.

This design is built around a frame counter:

// screen dimensions (must match display_inst)
localparam V_RES = 480;  // vertical screen resolution

logic frame;  // high for one clock tick at the start of vertical blanking
always_comb frame = (sy == V_RES && sx == 0);

// update the colour level every N frames
localparam FRAME_NUM = 30;  // frames between colour level change
logic [$clog2(FRAME_NUM):0] cnt_frame;  // frame counter
logic [3:0] colr_level;  // level of colour being cycled

always_ff @(posedge clk_pix) begin
    if (frame) begin
        if (cnt_frame == FRAME_NUM-1) begin  // every FRAME_NUM frames
            cnt_frame <= 0;
            colr_level <= colr_level + 1;
        end else cnt_frame <= cnt_frame + 1;
    end
end

// determine colour from screen position
logic [3:0] paint_r, paint_g, paint_b;
always_comb begin
    paint_r = sx[7:4];  // 16 horizontal pixels of each red level
    paint_g = sy[7:4];  // 16 vertical pixels of each green level
    paint_b = colr_level;  // blue level changes over time
end

Once per frame, at the start of vertical blanking, we update a counter, cnt_frame. We determine the width of the counter using $clog2, so it’s always the correct size.

I’ve chosen to update the colour every 30 frames (0.5 seconds). Change the value of FRAME_NUM to set the cycle speed.

When the blue intensity reaches 0xF, it automatically overflows to 0x0, creating a disconcerting jump in the blue level. Try updating the design, so the blue level smoothly falls and rises; you can use the raster bars design for inspiration.

Bounce

This demo bounces the ‘blue Peter’ square around the screen:

Bounce

Demo source:

See the README or Sim README for build instructions.

On the surface, bouncing seems simple: reverse direction when we hit an edge. In practice, it’s a little more complex: we need to account for the square’s size and speed.

I’ve chosen to draw the square at the edge of the screen when it collides, irrespective of its speed and position.

// screen dimensions (must match display_inst)
localparam H_RES = 640;  // horizontal screen resolution
localparam V_RES = 480;  // vertical screen resolution

logic frame;  // high for one clock tick at the start of vertical blanking
always_comb frame = (sy == V_RES && sx == 0);

// frame counter lets us to slow down the action
localparam FRAME_NUM = 1;  // slow-mo: animate every N frames
logic [$clog2(FRAME_NUM):0] cnt_frame;  // frame counter
always_ff @(posedge clk_pix) begin
    if (frame) cnt_frame <= (cnt_frame == FRAME_NUM-1) ? 0 : cnt_frame + 1;
end

// square parameters
localparam Q_SIZE = 200;   // size in pixels
logic [CORDW-1:0] qx, qy;  // position (origin at top left)
logic qdx, qdy;            // direction: 0 is right/down
logic [CORDW-1:0] qs = 2;  // speed in pixels/frame

// update square position once per frame
always_ff @(posedge clk_pix) begin
    if (frame && cnt_frame == 0) begin
        // horizontal position
        if (qdx == 0) begin  // moving right
            if (qx + Q_SIZE + qs >= H_RES-1) begin  // hitting right of screen?
                qx <= H_RES - Q_SIZE - 1;  // move right as far as we can
                qdx <= 1;  // move left next frame
            end else qx <= qx + qs;  // continue moving right
        end else begin  // moving left
            if (qx < qs) begin  // hitting left of screen?
                qx <= 0;  // move left as far as we can
                qdx <= 0;  // move right next frame
            end else qx <= qx - qs;  // continue moving left
        end

        // vertical position
        if (qdy == 0) begin  // moving down
            if (qy + Q_SIZE + qs >= V_RES-1) begin  // hitting bottom of screen?
                qy <= V_RES - Q_SIZE - 1;  // move down as far as we can
                qdy <= 1;  // move up next frame
            end else qy <= qy + qs;  // continue moving down
        end else begin  // moving up
            if (qy < qs) begin  // hitting top of screen?
                qy <= 0;  // move up as far as we can
                qdy <= 0;  // move down next frame
            end else qy <= qy - qs;  // continue moving up
        end
    end
end

// define a square with screen coordinates
logic square;
always_comb begin
    square = (sx >= qx) && (sx < qx + Q_SIZE) && (sy >= qy) && (sy < qy + Q_SIZE);
end

// paint colours: white inside square, blue outside
logic [3:0] paint_r, paint_g, paint_b;
always_comb begin
    paint_r = (square) ? 4'hF : 4'h1;
    paint_g = (square) ? 4'hF : 4'h3;
    paint_b = (square) ? 4'hF : 4'h7;
end

ProTip: Our coordinates are unsigned, so (qx < 0) is always true.

Explore

I hope you enjoyed this instalment of Exploring FPGA Graphics, but nothing beats creating your own designs. Here are a few suggestions to get you started:

  • Display your own message in place of ‘Hello World’
  • Animate raster bars up and down the screen
  • Change the colour of the square each time it bounces
  • Combine multiple effects into a single demo

Next Time

Next time, we’ll be playing Pong against our FPGA or jump ahead to learn about hardware sprites. Check out the site map for more FPGA projects and tutorials.

Get in touch: GitHub Issues, 1BitSquared Discord, @WillFlux (Mastodon), @WillFlux (Twitter)

©2022 Will Green, Project F