May 20, 2020

Exploring FPGA Graphics

In all beginnings dwells a magic force
Herman Hesse, Stages from The Glass Bead Game

Welcome to Exploring FPGA Graphics with Project F. In this series, we’ll be experimenting with FPGA graphics of all kinds, from a static square, through Pong and the Mandelbrot set, to bitmaps, text scrollers, and even 3D modelling. There’s no microcontroller in any of these designs: they’re simple logic described in SystemVerilog.

This post is a quick introduction to generating graphics with FPGAs: you’ll learn about display signals and simple colour graphics. In the next part, we race the beam to create an animated starfield and greet the world in FPGA Ad Astra.

Revised 2020-06-26. Get in touch with @WillFlux or open an issue on GitHub.

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 also be comfortable with programming your FPGA board and reasonably familiar with Verilog. We’ll demo the projects with these boards:

Follow the README instructions to build a design for either of these boards.

Source

All the Verilog designs featured in this series are available on GitHub: look out for [src] links as your read. The designs are open source under the permissive MIT licence, but this blog post is subject to normal copyright restrictions.

Quick Aside: SystemVerilog?!
We’re using a few choice features from SystemVerilog to make Verilog a little more pleasant (no laughing at the back). If you’re familiar with Verilog, you’ll have no trouble.

Space and Time

The screen you’re looking at is a little universe with its own rules of space and time.

When you look at a screen from afar, you see a smooth two-dimensional image. Looking closely, you see many individual blocks: these are pixels. A typical high-definition image is 1920 pixels across and 1,080 lines down: over 2 million pixels in total. Even a 640x480 image has over 300,000 pixels. The need to handle so much information so quickly is a big part of the challenge of working with graphics.

A VGA cable has five main signals: red, green, blue, horizontal sync, and vertical sync. There are no addressing signals to tell the display where to draw pixels; the secret is time, demarcated by the sync signals. The red, green, and blue wires carry the colour of each pixel in turn. Each pixel lasts a fixed length of time; when the display receives a horizontal sync, it starts a new line; when it receives a vertical sync, it starts a new frame.

The sync signals are part of blanking intervals. Originally designed to allow an electron gun to move to the next line or top of the screen, blanking intervals have been retained and repurposed in contemporary displays: HDMI uses them to transmit audio. The blanking interval has three parts: front porch, sync, and back porch.

Display Timings

Display Timings

In this series, we’re going to use 640x480 as our display resolution. Almost all displays support 640x480, and its low resource requirements make it possible to work with even small FPGAs. All the same principles apply at higher resolutions, such as 1280x720 or 4K.

We’ll use traditional timings, based on the original VGA display:

640x480 Timings      HOR    VER
-------------------------------
Active Pixels        640    480
Front Porch           16     10
Sync Width            96      2
Back Porch            48     33
Blanking Total       160     45
Total Pixels         800    525
Sync Polarity        neg    neg

Learn more from Video Timings: VGA, SVGA, 720p, 1080p.

Taking blanking into account, we have a total of 800x525 pixels. A typical LCD refreshes 60 times a second, so the number of pixels per second is: 800 x 525 x 60 = 25,200,000, which equates to a pixel clock of 25.2 MHz.

CAUTION: CRT Monitors
Any modern display, including multisync CRTs, should be fine with a 25.2 or 25 MHz pixel clock. Fixed-frequency CRTs, such as the original IBM 85xx series, could be damaged by an out-of-spec signal. Use these designs at your own risk.

Running to Time

We’ve decided we need a pixel clock of 25.2 MHz pixel clock, but neither of our demo boards have such a clock. To reach the required frequency, we’re going to use a phase-locked loop (PLL). Almost all FPGAs include one or more PLLs, but there isn’t a standard way to configure them in Verilog, so we have to use vendor-specific designs.

We have provided implementations for Xilinx 7 Series (XC7) and Lattice iCE40; for other FPGAs, you’ll need to consult your vendor documentation. If you can’t reach 25.2 MHz exactly, then 25 MHz or thereabouts should be fine (but see note about CRTs, above). The iCE40 can’t generate 25.2 MHz using the oscillators on iCEBreaker but works fine at 25.125 MHz.

Clock Generator Modules

Display Timings Module

Using our ~25 MHz pixel clock, we can generate timings for our 640x480 display. Creating display timings is straightforward: there’s one counter for horizontal position and one for vertical position. We use these counters to decide on the correct time for sync signals.

640x480 display timing generator [src]:

module display_timings (
    input  wire logic clk_pix,          // pixel clock
    input  wire logic rst,              // reset
    output      logic [9:0] sx,         // horizontal screen position
    output      logic [9:0] sy,         // vertical screen position
    output      logic hsync,            // horizontal sync
    output      logic vsync,            // vertical sync
    output      logic de                // data enable (low in blanking interval)
    );

    // horizontal timings
    parameter HA_END = 639;             // end of active pixels
    parameter HS_STA = HA_END + 16;     // sync starts after front porch
    parameter HS_END = HS_STA + 96;     // sync ends
    parameter LINE   = 799;             // last pixel on line (after back porch)

    // vertical timings
    parameter VA_END = 479;             // end of active pixels
    parameter VS_STA = VA_END + 10;     // sync starts after front porch
    parameter VS_END = HS_STA + 2;      // sync ends
    parameter SCREEN = 524;             // last line on screen (after back porch)

    always_comb begin
        hsync = ~(sx >= HS_STA && sx < HS_END);  // invert: hsync polarity is negative
        vsync = ~(sy >= VS_STA && sy < VS_END);  // invert: vsync polarity is negative
        de = (sx <= HA_END && sy <= VA_END);
    end

    // calculate horizontal and vertical screen position
    always_ff @ (posedge clk_pix) begin
        if (sx == LINE) begin  // last pixel on line?
            sx <= 0;
            sy <= (sy == SCREEN) ? 0 : sy + 1;  // last line on screen?
        end else begin
            sx <= sx + 1;
        end
        if (rst) begin
            sx <= 0;
            sy <= 0;
        end
    end
endmodule

ProTip: The last assignment wins, so the reset overrides the existing sx and sy.

sx and sy store the horizontal and vertical position; their maximum values are 800 and 525 respectively, so we need 10 bits to hold them. de is data enable, which is low during the blanking interval: we use it to decide when to draw pixels. Display modes vary in the polarity of their sync signals; for traditional 640x480 the polarity is negative for both hsync and vsync.

Test Benches

You can exercise the designs with the included test benches (currently Xilinx only):

Some things to check:

  • What is the pixel clock period?
  • How long does the pixel clock take to lock?
  • Does a frame last exactly 1/60th of a second?
  • How much time does a single line last?
  • What is the maximum values of sx and sy when de is low?

Bringing it Together

Now we have our display signals we’re ready to start drawing. To begin we’re going to keep it simple and draw a coloured square. There are three versions of this top module:

Shown below is the Xilinx version:

module top_square (
    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 de;
    display_timings timings_640x480 (
        .clk_pix,
        .rst(!clk_locked),  // wait for clock lock
        .sx,
        .sy,
        .hsync(vga_hsync),
        .vsync(vga_vsync),
        .de
    );

    // 32 x 32 pixel square
    logic q_draw;
    always_comb q_draw = (sx < 32 && sy < 32) ? 1 : 0;

    // VGA output
    always_comb begin
        vga_r = !de ? 4'h0 : (q_draw ? 4'hF : 4'h0);
        vga_g = !de ? 4'h0 : (q_draw ? 4'h8 : 4'h8);
        vga_b = !de ? 4'h0 : (q_draw ? 4'h0 : 4'hF);
    end
endmodule

Let there be Pixels

Combine top_square, display_timings, clock_gen modules with some suitable constraints, and you should be ready to drive a display.

There are instructions on building the designs for Arty and iCEBreaker in the README.

You should see something like (the colours are #0088FF and #FF8800):

A Square

Animation

A static square is a change from test cards, but underwhelming. Let’s make it move before calling it a day. To create a simple animation, we need to update the position of the square every frame. If we updated the position of the square during active drawing, we risk tearing, so we create an animate signal that happens at the start of the blanking period.

We’re going to replicate the behaviour of the video display itself, scanning across then down the screen. The square “beam” disappears off the edge of the screen, like the signal in the blanking interval. Try rebuilding the design with top_beam.

Shown below is the iCE40 DVI version:

module top_beam (
    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 (including blanking)
    localparam H_RES = 800;
    localparam V_RES = 525;

    // square 'Q' - origin at top-left
    localparam Q_SIZE = 32; // square size in pixels
    localparam Q_SPEED = 4; // pixels moved per frame
    logic [CORDW-1:0] qx, qy;     // square position

    logic animate;  // high for one clock tick at start of blanking
    always_comb animate = (sy == 480 && sx == 0);

    // update square position once per frame
    always_ff @(posedge clk_pix) begin
        if (animate) begin
            if (qx >= H_RES - Q_SIZE) begin
                qx <= 0;
                qy <= (qy >= V_RES - Q_SIZE) ? 0 : qy + Q_SIZE;
            end else begin
                qx <= qx + Q_SPEED;
            end
        end
    end

    // is square at current screen position?
    logic q_draw;
    always_comb begin
        q_draw = (sx >= qx) && (sx < qx + Q_SIZE)
              && (sy >= qy) && (sy < qy + Q_SIZE);
    end

    // DVI output
    always_comb begin
        dvi_clk = clk_pix;
        dvi_de  = de;
        dvi_r = !de ? 4'h0 : (q_draw ? 4'hF : 4'h0);
        dvi_g = !de ? 4'h0 : (q_draw ? 4'h8 : 4'h8);
        dvi_b = !de ? 4'h0 : (q_draw ? 4'h0 : 4'hF);
    end
endmodule

Next Time

That’s all for this quick introduction to FPGA graphics. If you’d like experiment some more, try creating different coloured squares that bounce off the edge of the screen.

Ready to create something more exciting? In the next part, we race the beam to create an animated starfield and greet the world in FPGA Ad Astra.

©2020 Will Green, Project F