FPGA VGA Graphics in Verilog Part 1

Introduction

This tutorial series introduces video graphics programming using FPGAs, starting with creating a VGA driver and moving onto more advanced features including bitmaps, sprites and effects. FPGAs excel at high-speed I/O and custom logic: you'll be surprised how much you can achieve with a few lines of Verilog.

This series is designed around the Digilent Arty and Basys 3 boards. If you're using the Arty you also need the VGA Pmod (the Basys3 has VGA output built-in). Detailed requirements are given below.

I assume you have a basic understanding of Verilog and are comfortable using Xinlinx's Vivado software. If you're new to FPGA development try Getting Started with Verilog & Vivado first.

Once you've completed this tutorial, move onto part 2 where we introduce bitmapped displays.

Find the code and resources for this and other FPGA tutorials at github.com/WillGreen/timetoexplore.

Feedback to @WillFlux is most welcome. Updated June 2018.

Requirements

  1. Digilent Arty or Basys3 board
  2. VGA Pmod if using the Arty (Basys3 has VGA built-in)
  3. VGA capable monitor & cable
  4. Micro USB cable to program and power the board
  5. Xilinx Vivado installed
  6. Arty or Basys3 board file installed, so Vivado knows your board specification

Other FPGA Boards

If you're not using the Arty or Basys you should still be able to follow this tutorial with a few changes to the top.v module:

  1. Hardware I/O: update hardware ports, such as CLK and VGA_R, to match your board.
  2. Clock: if your board clock isn't 100 MHz, you need to update the pixel clock code.
  3. VGA Outputs: if your VGA output isn't 4-bits per colour, adjust VGA assign statements.

How VGA Works

VGA is an analogue video standard using a 15-pin D-sub connector. It doesn't require high clock speeds or complex encoding, so is an excellent place to begin when learning about FPGA graphics. VGA has five main signal pins: one for each of red, green, and blue and two for sync. Horizontal sync demarcates a line. Vertical sync demarcates a screen, also known as a frame.

VGA signals have two phases: drawing pixels and the blanking interval. The sync signals occur within blanking intervals; separated from pixel drawing by the front porch and back porch. When VGA was developed monitors were based on cathode ray tubes (CRTs): the blanking interval gives time for voltage levels to stabilise and for the electron gun to return to the start of a line or screen.

In this article, we'll be creating a classic 640x480 60 Hz VGA display. The required pixel clock is 25 MHz1, which is a nice round fraction of our board's 100 MHz clock. The key to producing a valid VGA signal is getting the timings right. For simplicity, we're going to do our timings in pixels and lines. Each pixel is a tick of the 25 MHz pixel clock (40 ns). A line is a complete set of horizontal pixels.

Horizontal Pixel Timings

  • Front Porch: 16
  • Sync Pulse: 96
  • Back Porch: 48
  • Active Video: 640
  • Total pixels: 800

Vertical Line Timings

  • Active Video: 480
  • Front Porch: 11
  • Sync Pulse: 2
  • Back Porch: 31
  • Total lines: 524

VGA timings from Rick Ballantyne via Martin Hinner. To learn more about display timings, including for HD, see Video Timings: VGA, 720P, 1080P.

VGA Module

Create a new RTL project in Vivado called vga01 with the Arty or Basys 3 board as the target. If you need advice on project creation see part 1 of my introductory tutorial series.

Within your new project create a design source called vga640x480.v [view on github]:

module vga640x480(
    input wire i_clk,           // base clock
    input wire i_pix_stb,       // pixel clock strobe
    input wire i_rst,           // reset: restarts frame
    output wire o_hs,           // horizontal sync
    output wire o_vs,           // vertical sync
    output wire o_blanking,     // high during blanking interval
    output wire o_active,       // high during active pixel drawing
    output wire o_screenend,    // high for one tick at the end of screen
    output wire o_animate,      // high for one tick at end of active drawing
    output wire [9:0] o_x,      // current pixel x position
    output wire [8:0] o_y       // current pixel y position
    );

    // VGA timings https://timetoexplore.net/blog/video-timings-vga-720p-1080p
    localparam HS_STA = 16;              // horizontal sync start
    localparam HS_END = 16 + 96;         // horizontal sync end
    localparam HA_STA = 16 + 96 + 48;    // horizontal active pixel start
    localparam VS_STA = 480 + 11;        // vertical sync start
    localparam VS_END = 480 + 11 + 2;    // vertical sync end
    localparam VA_END = 480;             // vertical active pixel end
    localparam LINE   = 800;             // complete line (pixels)
    localparam SCREEN = 524;             // complete screen (lines)

    reg [9:0] h_count;  // line position
    reg [9:0] v_count;  // screen position

    // generate sync signals (active low for 640x480)
    assign o_hs = ~((h_count >= HS_STA) & (h_count < HS_END));
    assign o_vs = ~((v_count >= VS_STA) & (v_count < VS_END));

    // keep x and y bound within the active pixels
    assign o_x = (h_count < HA_STA) ? 0 : (h_count - HA_STA);
    assign o_y = (v_count >= VA_END) ? (VA_END - 1) : (v_count);

    // blanking: high within the blanking period
    assign o_blanking = ((h_count < HA_STA) | (v_count > VA_END - 1));

    // active: high during active pixel drawing
    assign o_active = ~((h_count < HA_STA) | (v_count > VA_END - 1)); 

    // screenend: high for one tick at the end of the screen
    assign o_screenend = ((v_count == SCREEN - 1) & (h_count == LINE));

    // animate: high for one tick at the end of the final active pixel line
    assign o_animate = ((v_count == VA_END - 1) & (h_count == LINE));

    always @ (posedge i_clk)
    begin
        if (i_rst)  // reset to start of frame
        begin
            h_count <= 0;
            v_count <= 0;
        end
        if (i_pix_stb)  // once per pixel
        begin
            if (h_count == LINE)  // end of line
            begin
                h_count <= 0;
                v_count <= v_count + 1;
            end
            else 
                h_count <= h_count + 1;

            if (v_count == SCREEN)  // end of screen
                v_count <= 0;
        end
    end
endmodule

o_x and o_y represent the horizontal and vertical position within the visible 640x480 display: this is what you use for positioning and drawing graphics. h_count and v_count represent the number of pixels and lines that have occurred since the start of the line or screen (including blanking interval): these are used for sync signals. For 640x480 sync pulses are active low, so we use ~ when assigning them. For now you can ignore the other outputs; they'll be used later.

Before we move onto drawing graphics we need to understand how our module generates a 25 MHz clock and the role of the i_pix_stb input.

Running to Time

Our 640x480 60 Hz VGA signal needs a 25 MHz pixel clock, but the Arty and Basys 3 have a 100 MHz base clock. How do we efficiently divide the base clock by four?

Four is a power of two, so we could use a simple counter, but that isn't useful in the general case. Instead we're going to adopt Dan Gisselquist's fractional clock divider approach. This is a simple and elegant way to divide a clock by almost any amount. This flexibility allows us to deal with different base and pixel clocks.

reg [15:0] cnt;
reg pix_stb; 
always @(posedge CLK)
    {pix_stb, cnt} <= cnt + 16'h4000;  // divide by 4: (2^16)/4 = 0x4000

This works because the value of pix_stb is set whenever the counter cnt rolls over. {x, y} is the Verilog concatenation operator: {4'b1101, 4'b0011} == 8'b11010011.

If your board has a different clock then adjust the value added to the counter as appropriate. For example a 75 MHz board clock requires dividing by 3 to reach 25 MHz, so we add (216)/3 = 0x5555 to the counter.

To make use of the divided clock, we keep the base clock for the sensitivity list but add a test for strobe:

always @ (posedge CLK)
begin
    if (pix_stb)   
    begin
    // do stuff once per pixel clock tick
    end
end

It's tempting to simplify this by using always @ (posedge pix_stb). Don't do this! While this can work for simple designs, it quickly leads to unstable designs and debugging headaches. Hardware design is hard enough without messing with clock distribution and crossing clock domains.

Hip to be Square

With a VGA module defined and a suitable pixel clock created, we are now in a position to draw simple graphics. The VGA Pmod supports 4-bits per colour, giving us 16 levels for each output: VGA_R, VGA_G, and VGA_B. For example, VGA_R = 4b'1000 would set red to half brightness, while VGA_B = 4b'0011 would be a dark blue.

We don't have a bitmap to draw on. Instead, we define the extent of each square mathematically based on a range of pixels: where a square exists we set the square's output wire to 1. This wire is then linked to the VGA outputs to control the colour signals.

Create a design source called top.v with the following content [view on github]:

module top(
    input wire CLK,             // board clock: 100 MHz on Arty & Basys 3
    input wire RST_BTN,         // reset button
    output wire VGA_HS_O,       // horizontal sync output
    output wire VGA_VS_O,       // vertical sync output
    output wire [3:0] VGA_R,    // 4-bit VGA red output
    output wire [3:0] VGA_G,    // 4-bit VGA green output
    output wire [3:0] VGA_B     // 4-bit VGA blue output
    );

    wire rst = ~RST_BTN;  // reset is active low on Arty

    // generate a 25 MHz pixel strobe
    reg [15:0] cnt;
    reg pix_stb;
    always @(posedge CLK)
        {pix_stb, cnt} <= cnt + 16'h4000;  // divide by 4: (2^16)/4 = 0x4000

    wire [9:0] x;  // current pixel x position: 10-bit value: 0-1023
    wire [8:0] y;  // current pixel y position:  9-bit value: 0-511

    vga640x480 display (
        .i_clk(CLK),
        .i_pix_stb(pix_stb),
        .i_rst(rst),
        .o_hs(VGA_HS_O), 
        .o_vs(VGA_VS_O), 
        .o_x(x), 
        .o_y(y)
    );

    // Four overlapping squares
    wire sq_a, sq_b, sq_c, sq_d;
    assign sq_a = ((x > 120) & (y >  40) & (x < 280) & (y < 200)) ? 1 : 0;
    assign sq_b = ((x > 200) & (y > 120) & (x < 360) & (y < 280)) ? 1 : 0;
    assign sq_c = ((x > 280) & (y > 200) & (x < 440) & (y < 360)) ? 1 : 0;
    assign sq_d = ((x > 360) & (y > 280) & (x < 520) & (y < 440)) ? 1 : 0;

    assign VGA_R[3] = sq_b;         // square b is red
    assign VGA_G[3] = sq_a | sq_d;  // squares a and d are green
    assign VGA_B[3] = sq_c;         // square c is blue
endmodule

For example, if x is 210 and y is 150 then we're within sq_a and sq_b, so VGA_G[3] and VGA_R[3] are both set to 1, leading to a yellow pixel.

You can only have one assignment for each VGA output because you can only have one input feeding it without using intervening logic. Each colour consists of four separate outputs: you can have separate assignments for VGA_R[0] and VGA_R[3] but not two assignments for VGA_R[3], nor two assignments for VGA_R. For the green output VGA_G[3] we've used 'or' to combine the output of two squares into one VGA output wire.

Constraints

Create a constraints file called arty.xdc (or whatever your board is called) with the following content [view on github].

If you're using the Basys3 board you need to modify these constraints for your board. Choose a button to act as reset and change the pins for the clock and VGA ports. See the Basys3 reference manual and Basys3 master XDC for details.

## Arty Board Constraints
## Adapted from Digilent master file:
##   https://github.com/Digilent/digilent-xdc/blob/master/Arty-Master.xdc

## Clock
set_property -dict {PACKAGE_PIN E3  IOSTANDARD LVCMOS33} [get_ports {CLK}];
create_clock -add -name sys_clk_pin -period 10.00 \
    -waveform {0 5} [get_ports {CLK}];

## Reset Button (active low)
set_property -dict {PACKAGE_PIN C2  IOSTANDARD LVCMOS33} [get_ports {RST_BTN}];

## VGA Pmod Header JB
set_property -dict {PACKAGE_PIN E15 IOSTANDARD LVCMOS33} [get_ports {VGA_R[0]}];
set_property -dict {PACKAGE_PIN E16 IOSTANDARD LVCMOS33} [get_ports {VGA_R[1]}];
set_property -dict {PACKAGE_PIN D15 IOSTANDARD LVCMOS33} [get_ports {VGA_R[2]}];
set_property -dict {PACKAGE_PIN C15 IOSTANDARD LVCMOS33} [get_ports {VGA_R[3]}];
set_property -dict {PACKAGE_PIN J17 IOSTANDARD LVCMOS33} [get_ports {VGA_B[0]}];
set_property -dict {PACKAGE_PIN J18 IOSTANDARD LVCMOS33} [get_ports {VGA_B[1]}];
set_property -dict {PACKAGE_PIN K15 IOSTANDARD LVCMOS33} [get_ports {VGA_B[2]}];
set_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports {VGA_B[3]}];

## VGA Pmod Header JC
set_property -dict {PACKAGE_PIN U12 IOSTANDARD LVCMOS33} [get_ports {VGA_G[0]}];
set_property -dict {PACKAGE_PIN V12 IOSTANDARD LVCMOS33} [get_ports {VGA_G[1]}];
set_property -dict {PACKAGE_PIN V10 IOSTANDARD LVCMOS33} [get_ports {VGA_G[2]}];
set_property -dict {PACKAGE_PIN V11 IOSTANDARD LVCMOS33} [get_ports {VGA_G[3]}];
set_property -dict {PACKAGE_PIN U14 IOSTANDARD LVCMOS33} [get_ports {VGA_HS_O}];
set_property -dict {PACKAGE_PIN V14 IOSTANDARD LVCMOS33} [get_ports {VGA_VS_O}];

NB. These constraints are valid for VGA Pmod Rev C. Digilent tells me that earlier Pmod revisions were never publicly released, but if you have an older revision, you may need to swap the colours in the constraints file.

Build & Program

Run synthesis, implementation, bitstream generation. See the FPGA introductory post if you need a reminder on how to do this.

Next, hook up your VGA Pmod to the middle two connectors (JB and JC) on your Arty and use your VGA cable to connect your monitor to the VGA Pmod. Basys3 users can just connect the VGA cable directly to their board. Finally, connect your board to your computer via USB and program it with vga01/vga01.runs/impl_1/top.bit.

You should see four overlapping squares on your screen; from left to right: green, red, blue, green. Check your constraints file if the colours are wrong.

Not bad, but we're a little below spec owing to a pixel clock of 25 MHz, rather than 25.175 MHz: fH should be 31.469 kHz and fV 60.0 Hz.

More Colours

In this example, we only set the value of the most significant colour bit, for red that's VGA_R[3], so our squares will be roughly half maximum brightness. If you want to set them to maximum brightness then set all bits, for example: assign VGA_R = {4{sq_a}};. You can also make a complete range of colours by combining different colour pins, e.g. to make a single orange square, remove all the existing VGA assignments and add the following:

assign VGA_R = {4{sq_a}};  // square a is 100% red
assign VGA_G[3] = sq_a;    // square a is also 50% green

Animation

Static squares are all very well, but we do have 60 frames every second. To animate without tearing, we restrict movement to the time after a frame has finished drawing, but before the next frame starts. The vga640x480 module provide an output called o_animate, which is true for one tick after the end of pixel drawing. We have approximately 1 ms for animation before the next frame begins (33 lines of 800 pixels each 40ns long); for our simple squares this is plenty.

Square Module

To represent an animated square, we're going to create a new module. We're going to base our coordinates on the centre of the square. This doesn't make much difference for squares, but makes sense once we start working with more complex shapes and sprites.

A square moves one pixel in both the horizontal and vertical directions once per frame when i_animate is high. When it reaches the edge of the screen it switches direction.

Add a design source called square.v [view on github]:

module square #(
    H_SIZE=80,      // half square width (for ease of co-ordinate calculations)
    IX=320,         // initial horizontal position of square centre
    IY=240,         // initial vertical position of square centre
    IX_DIR=1,       // initial horizontal direction: 1 is right, 0 is left
    IY_DIR=1,       // initial vertical direction: 1 is down, 0 is up
    D_WIDTH=640,    // width of display
    D_HEIGHT=480    // height of display
    )
    (
    input wire i_clk,         // base clock
    input wire i_ani_stb,     // animation clock: pixel clock is 1 pix/frame
    input wire i_rst,         // reset: returns animation to starting position
    input wire i_animate,     // animate when input is high
    output wire [11:0] o_x1,  // square left edge: 12-bit value: 0-4095
    output wire [11:0] o_x2,  // square right edge
    output wire [11:0] o_y1,  // square top edge
    output wire [11:0] o_y2   // square bottom edge
    );

    reg [11:0] x = IX;   // horizontal position of square centre
    reg [11:0] y = IY;   // vertical position of square centre
    reg x_dir = IX_DIR;  // horizontal animation direction
    reg y_dir = IY_DIR;  // vertical animation direction

    assign o_x1 = x - H_SIZE;  // left: centre minus half horizontal size
    assign o_x2 = x + H_SIZE;  // right
    assign o_y1 = y - H_SIZE;  // top
    assign o_y2 = y + H_SIZE;  // bottom

    always @ (posedge i_clk)
    begin
        if (i_rst)  // on reset return to starting position
        begin
            x <= IX;
            y <= IY;
            x_dir <= IX_DIR;
            y_dir <= IY_DIR;
        end
        if (i_animate && i_ani_stb)
        begin
            x <= (x_dir) ? x + 1 : x - 1;  // move left if positive x_dir
            y <= (y_dir) ? y + 1 : y - 1;  // move down if positive y_dir

            if (x <= H_SIZE + 1)  // edge of square is at left of screen
                x_dir <= 1;  // change direction to right
            if (x >= (D_WIDTH - H_SIZE - 1))  // edge of square at right
                x_dir <= 0;  // change direction to left          
            if (y <= H_SIZE + 1)  // edge of square at top of screen
                y_dir <= 1;  // change direction to down
            if (y >= (D_HEIGHT - H_SIZE - 1))  // edge of square at bottom
                y_dir <= 0;  // change direction to up              
        end
    end
endmodule

This module makes use of Verilog parameters, such as H_SIZE, to allow custom square instances. The values in the module definition, e.g. H_SIZE=80, are defaults used if the user doesn't supply a value when creating a module instance. We use 12-bit values to represent our square position so that the module is usable on a wide range of display resolutions, including 4K.

Top Animation

Now we just need to update the top module to use the square module and create some square instances. In this case, we're animating three coloured squares. Replace your top module with the following then regenerate the bitstream and program your board [view on github]:

module top(
    input wire CLK,             // board clock: 100 MHz on Arty & Basys 3
    input wire RST_BTN,         // reset button
    output wire VGA_HS_O,       // horizontal sync output
    output wire VGA_VS_O,       // vertical sync output
    output wire [3:0] VGA_R,    // 4-bit VGA red output
    output wire [3:0] VGA_G,    // 4-bit VGA green output
    output wire [3:0] VGA_B     // 4-bit VGA blue output
    );

    wire rst = ~RST_BTN;  // reset is active low on Arty

    wire [9:0] x;  // current pixel x position: 10-bit value: 0-1023
    wire [8:0] y;  // current pixel y position:  9-bit value: 0-511
    wire animate;  // high when we're ready to animate at end of drawing

    // generate a 25 MHz pixel strobe
    reg [15:0] cnt = 0;
    reg pix_stb = 0;
    always @(posedge CLK)
        {pix_stb, cnt} <= cnt + 16'h4000;  // divide by 4: (2^16)/4 = 0x4000

    vga640x480 display (
        .i_clk(CLK),
        .i_pix_stb(pix_stb),
        .i_rst(rst),
        .o_hs(VGA_HS_O), 
        .o_vs(VGA_VS_O), 
        .o_x(x), 
        .o_y(y),
        .o_animate(animate)
    );

    wire sq_a, sq_b, sq_c;
    wire [11:0] sq_a_x1, sq_a_x2, sq_a_y1, sq_a_y2;  // 12-bit values: 0-4095 
    wire [11:0] sq_b_x1, sq_b_x2, sq_b_y1, sq_b_y2;
    wire [11:0] sq_c_x1, sq_c_x2, sq_c_y1, sq_c_y2;

    square #(.IX(160), .IY(120), .H_SIZE(60)) sq_a_anim (
        .i_clk(CLK), 
        .i_ani_stb(pix_stb),
        .i_rst(rst),
        .i_animate(animate),
        .o_x1(sq_a_x1),
        .o_x2(sq_a_x2),
        .o_y1(sq_a_y1),
        .o_y2(sq_a_y2)
    );

    square #(.IX(320), .IY(240), .IY_DIR(0)) sq_b_anim (
        .i_clk(CLK), 
        .i_ani_stb(pix_stb),
        .i_rst(rst),
        .i_animate(animate),
        .o_x1(sq_b_x1),
        .o_x2(sq_b_x2),
        .o_y1(sq_b_y1),
        .o_y2(sq_b_y2)
    );    

    square #(.IX(480), .IY(360), .H_SIZE(100)) sq_c_anim (
        .i_clk(CLK), 
        .i_ani_stb(pix_stb),
        .i_rst(rst),
        .i_animate(animate),
        .o_x1(sq_c_x1),
        .o_x2(sq_c_x2),
        .o_y1(sq_c_y1),
        .o_y2(sq_c_y2)
    );

    assign sq_a = ((x > sq_a_x1) & (y > sq_a_y1) &
        (x < sq_a_x2) & (y < sq_a_y2)) ? 1 : 0;
    assign sq_b = ((x > sq_b_x1) & (y > sq_b_y1) &
        (x < sq_b_x2) & (y < sq_b_y2)) ? 1 : 0;
    assign sq_c = ((x > sq_c_x1) & (y > sq_c_y1) &
        (x < sq_c_x2) & (y < sq_c_y2)) ? 1 : 0;

    assign VGA_R[3] = sq_a;  // square a is red
    assign VGA_G[3] = sq_b;  // square b is green
    assign VGA_B[3] = sq_c;  // square c is blue
endmodule

If the squares don't move then your reset button is probably active high. Try updating the reset line in top to wire rst = RST_BTN;.

If you press the reset button on your board, you should see the squares return to their starting positions. NB. On the Arty don't confuse the reset button with the red prog button at the other corner of the board. Pressing prog will wipe the Arty memory. In the event you wipe your program, just reprogram the board to get your squares back.

Try experimenting with additional squares with different parameters, such as H_SIZE, IX_DIR, and IY_DIR.

What's Next?

We've managed to do a great deal with little logic. But while drawing directly from the pixel position is manageable for a few squares, it quickly becomes cumbersome for anything more sophisticated. To allow for more complex graphics we need memory where we can store and combine values. This is the subject of the next part.

1: Strictly speaking, the specification calls for a 25.175MHz pixel clock, but modern multisync and LCD monitors don't have an issue with the slight difference.

©2017-18 Will Green.