Arty VGA Graphics in Verilog Part 1

Introduction

This tutorial series introduces video programming using FPGAs, starting with creating a VGA driver and moving onto more advanced features such as 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 Basys3 boards. If you're using the Arty you also need the VGA Pmod board (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 Vivado. If you're new to FPGA development try Getting Started with Verilog & Vivado first.

Feedback to @WillFlux is most welcome. Last updated February 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 Arty board
  5. Xilinx Vivado installed
  6. Arty board file installed, so Vivado knows your board specification

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 and vertical sync a screen.

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 60Hz VGA display. The required pixel clock is 25MHz1, which is a nice round fraction of the Arty's 100MHz 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 25MHz pixel clock (40ns). 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.

VGA Module

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

Within your new project create a design source called vga640x480.v:

module vga640x480(
    input clk,  // expects 100MHz clock
    output h_sync,
    output v_sync,
    output blanking,
    output animate,
    output [9:0] x,  // 10-bit value: 0-1023
    output [8:0] y   //  9-bit value: 0-511
    );

    localparam H_SYN_STA = 16;              // horizontal sync start
    localparam H_SYN_END = 16 + 96;         // horizontal sync end
    localparam H_PIX_STA = 16 + 96 + 48;    // horizontal active pixel start
    localparam V_SYN_STA = 480 + 11;        // vertical sync start
    localparam V_SYN_END = 480 + 11 + 2;    // vertical sync end
    localparam V_PIX_END = 480;             // vertical active pixel end
    localparam LINE = 800;                  // complete line (pixels)
    localparam SCREEN = 524;                // complete screen (lines)

    reg [9:0] h_count = 0;  // 10-bit value: 0-1023
    reg [9:0] v_count = 0;  // 10-bit value: 0-1023

    // generate sync signals (active low for 640x480)
    assign h_sync = ~((h_count >= H_SYN_STA) & (h_count < H_SYN_END));
    assign v_sync = ~((v_count >= V_SYN_STA) & (v_count < V_SYN_END));

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

    // blanking: true within the blanking period
    assign blanking = ((h_count < H_PIX_STA) | (v_count > V_PIX_END - 1));

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

    reg [1:0] counter = 0;  // 2-bit counter

    always @ (posedge clk)
    begin
        counter <= counter + 1;
        if (counter == 3)  // every fourth clock tick (25MHz)
        begin
            if (h_count == LINE)  // end of line
            begin
                h_count <= 0;
                v_count <= v_count + 1;
            end
            else begin
                h_count <= h_count + 1;
            end

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

x and 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: these are used for sync signals. For 640x480 sync pulses are active low, so we use ~ when assigning them. For now, we ignore the animate and blanking outputs.

Hip to be Square

With a VGA module defined we are now in a position to draw simple graphics. The Digilent VGA Pmod supports 16 levels for each colour 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 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:

module top(
    input CLK,
    input RST_BTN,
    output VGA_HS_O,
    output VGA_VS_O,
    output [3:0] VGA_R,
    output [3:0] VGA_G,
    output [3:0] VGA_B
    );

    wire rst = ~RST_BTN;  // active low

    wire [9:0] x;  // 10-bit value: 0-1023
    wire [8:0] y;  //  9-bit value: 0-511
    wire sq_a, sq_b, sq_c, sq_d;

    vga640x480 display (
        .clk(CLK), 
        .h_sync(VGA_HS_O), 
        .v_sync(VGA_VS_O), 
        .x(x), 
        .y(y)
    );

    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;
    assign VGA_G[3] = sq_a | sq_d;
    assign VGA_B[3] = sq_c;
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 VGA_R[0] and VGA_R[3] but not assignments for VGA_R[3] nor two assignments for VGA_R. For the green output VGA_G[3] we've used 'or' to display two squares in that colour.

Constraints

Create a constraints file called arty.xdc with the following content.

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
## Based on 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. Finally connect your Arty to your computer and program your board using 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.

More Colours

In this example, we only set the value of the most significant colour bit, 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}};  // 100% red
assign VGA_G[3] = sq_a;    // 50% green

Animation

Static squares are all very well, but we do have 60 frames every second. To animate without tearing we need to restrict movement to after a frame has finished drawing, i.e. within the vertical blanking interval. To do this, we use the vga640x480 module output called animate, which is true for one clock tick at the start of the vertical blanking interval. We have approximately 1 ms for animation before the next frame begins (33 lines of 800 pixels each 40ns long).

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 matter much for squares but makes sense once we start working with more complex shapes and sprites.

Add a design source called square.v:

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 increasing
    IY_DIR=1,       // initial vertical direction: 0 is decreasing
    D_WIDTH=640,    // width of display
    D_HEIGHT=480    // height of display
    )
    (
    input clk,
    input rst,
    input animate,
    output [11:0] x1,  // 12-bit value: 0-4095
    output [11:0] x2,
    output [11:0] y1,
    output [11:0] y2
    );

    reg [11:0] x = IX;   // 12-bit value: 0-4095
    reg [11:0] y = IY;
    reg x_dir = IX_DIR;
    reg y_dir = IY_DIR;

    assign x1 = x - H_SIZE;  // left
    assign x2 = x + H_SIZE;  // right
    assign y1 = y - H_SIZE;  // top
    assign y2 = y + H_SIZE;  // bottom

    always @ (posedge clk)
    begin
        if (rst) 
        begin
            x <= IX;
            y <= IY;
            x_dir <= IX_DIR;
            y_dir <= IY_DIR;
        end
        if (animate)
        begin
            x <= (x_dir) ? x + 1 : x - 1;
            y <= (y_dir) ? y + 1 : y - 1; 

            if (x <= H_SIZE)
                x_dir <= 1;
            if (x >= (D_WIDTH - H_SIZE - 1))
                x_dir <= 0;          
            if (y <= H_SIZE)
                y_dir <= 1;
            if (y >= (D_HEIGHT - H_SIZE - 1))
                y_dir <= 0;                
        end
    end
endmodule

This module makes use of Verilog parameters, such as H_SIZE, to allow customisation square instances. The values in the module definition, e.g. H_SIZE=80, are 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:

module top(
    input CLK,
    input RST_BTN,
    output VGA_HS_O,
    output VGA_VS_O,
    output [3:0] VGA_R,
    output [3:0] VGA_G,
    output [3:0] VGA_B
    );

    wire rst = ~RST_BTN;  // active low

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

    vga640x480 display (
        .clk(CLK), 
        .h_sync(VGA_HS_O), 
        .v_sync(VGA_VS_O), 
        .x(x), 
        .y(y),
        .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 (
        .clk(CLK), 
        .rst(rst),
        .animate(animate),
        .x1(sq_a_x1),
        .x2(sq_a_x2),
        .y1(sq_a_y1),
        .y2(sq_a_y2)
    );

    square #(.IX(320), .IY(240), .IY_DIR(0)) sq_b_anim (
        .clk(CLK), 
        .rst(rst),
        .animate(animate),
        .x1(sq_b_x1),
        .x2(sq_b_x2),
        .y1(sq_b_y1),
        .y2(sq_b_y2)
    );    

    square #(.IX(480), .IY(360), .H_SIZE(100)) sq_c_anim (
        .clk(CLK), 
        .rst(rst),
        .animate(animate),
        .x1(sq_c_x1),
        .x2(sq_c_x2),
        .y1(sq_c_y1),
        .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;
    assign VGA_G[3] = sq_b;
    assign VGA_B[3] = sq_c;
endmodule

If you press the red reset button on your Arty board, you should see the squares return to their starting positions. NB. 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 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 will be the subject of the next part.

Look out for the next part of this FPGA graphics series soon. Follow @WillFlux for updates.

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.