Arty VGA Graphics in Verilog Part 2


Welcome back to my FPGA graphics tutorial series using the Digilent Arty or Basys3 boards. In part 1 we created a VGA module and used it to animate squares on screen. In this second part, we introduce bitmapped displays. By the end of this tutorial, you'll be able to load your own image into FPGA memory and display it on your monitor.

Feedback to @WillFlux is most welcome.


The requirements for this part are the same as part 1:

  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 or Basys3 board file installed, so Vivado knows your board specification


A bitmapped display is backed by a memory array: each pixel corresponds to a location in memory. To create the display we read out the value of the current pixel from its memory location. To set the colour of a pixel, we modify the contents of its associated memory location. Bitmaps provides two significant benefits: we're free to create sophisticated graphics using whatever technique we like, and the setting of pixel colour is separated from the actual screen drawing process. The big downside of a bitmapped display is we need significant memory to store our pixel data.

Memory Requirements: Resolution and Colour Depth

The first consideration is the capability of our VGA output: it supports four bits for each of red, green, and blue for a total of 12 bits per pixel. If we were to use 12 bits per pixel with a standard 640x480 VGA display our memory array would need to be 3,600 Kbits: 640 x 480 x 12 bits = 3,686,400 bits. However, our XC7A35T FPGA only has 1,800 Kbits of block ram. While the Arty board also includes 256 MB of DDR3L memory, the Basys3 does not, and using DDR memory would significantly add to the complexity of our design.

To allow our design to fit within block ram, we lower our screen resolution to 640x360 and limit ourselves to 6-bit colour: 640 x 360 x 6 bits = 1,350 Kbits. 640x360 has the added benefit of having a 16:9 widescreen ratio that matches modern monitors and TVs.

64 Colours

If each pixel is represented by 6 bits, then it can have 26 or 64 possible colours. If we allocate 2 bits to each of red, green and blue, then each colour can have one of four binary values: 00, 01, 10, or 11. However, our VGA output is 12-bit: it expects 4 bits per pixel (zero to fifteen). To use the full brightness of the display we need to extend our 2-bit colours to 4-bits; to do this we duplicate the bits resulting in the four possible values: 0000 (zero), 0101 (five), 1010 (ten) and 1111 (fifteen).

Modern computer displays are usually 24-bit, that is they can display 16.7 million colours. To get a feel for what our fixed 64 colours look like I've created a swatch:

Having gone to the trouble to look at how we might directly drive the display using a 6-bit value we're not going to do it! Instead, we're going to use a little indirection to extend our colour possibilities to the full range of 4,096.

Indirection: Indexed Colour & Palettes

Instead of each 6-bit value representing a colour, it can represent a colour in a palette lookup table. If a particular pixel has the value 001010 (ten), then we consult the tenth entry in the colour table, which contains the 12-bit colour to use. While we're still limited to a total of 64 colours they can now be any 64 from the 4,096 colours the VGA output is capable of producing. For example, a picture of a forest might have many greens, while one of a city would use more greys. This design was common in older computers, for example, the original Amiga chipset supported 32 colours from a possible 4,096: very similar to our design! The GIF and PNG formats still make use of this approach to squeeze the best quality out of 256-colour images.

If you're interested in learning more about different colour palettes, there's an excellent List of color palettes page on Wikipedia.

That's enough colour theory, let's get on with creating an image!

Preparing an Image for FPGA Memory

In order to display our image we need a mechanism to load it and its palette into our design. Verilog supports a method called $readmemh that can load values into memory from a text file of hex values. I've created a handy Python script ( that generates suitable files for images and their palettes. It can convert a source image into 6-bit colour with an optimised 12-bit palette.

If you don't want to prepare your own image then you can get download a ready-made one and skip to the next section.


Grab a copy of from the FPGATools repository on Github. requires the Pillow package to run. You can install this as with any other Python module:

pip install pillow

Use an image editor to crop/resize your image to 640 x 360 pixels and save it in JPEG, PNG, BMP or TIFF format. NB. It's important your image is exactly 640 x 360 pixels otherwise it won't display correctly.

Once you have a suitable source image you can run the following (replacing my-image.png with your image filename):

python my-image.png 6 mem

Once it completes you should find three new files in the same directory as the source image:

  • my-image_preview.png - a normal PNG you can view to preview the image conversion
  • my-image.mem - a text hex version of the image
  • my-image_palette.mem - a text hex version of the image palette

Load up the preview in a web browser or image editor to check the conversion looks OK.

If you open the palette file in a text editor you'll see the sixty-four 12-bit palette values for your image.

Now we have a prepared image it's time to load it into memory.

Memory Mapped

Create a new RTL project in Vivado called vga02 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.

We need to create a memory array to hold our image data. To do this, we'll use a simple sram module suitable for use with FPGA block ram. We load our image file into the ram at design time using Verilog's $readmemh function.

Add a design source called sram.v (replacing my-image.mem with the name of your image memory file):

module sram #(parameter ADDR_WIDTH = 8, DATA_WIDTH = 8, DEPTH = 256) (
    input wire i_clk,
    input wire [ADDR_WIDTH-1:0] i_addr, 
    input wire i_write,
    input wire [DATA_WIDTH-1:0] i_data,
    output reg [DATA_WIDTH-1:0] o_data 

    reg [DATA_WIDTH-1:0] memory_array [0:DEPTH-1]; 

    initial begin
        $display("Loading image into memory array.");
        $readmemh("my-image.mem", memory_array);  // update with your filename

    always @ (posedge i_clk)
        if(i_write) begin
            memory_array[i_addr] <= i_data;
        else begin
            o_data <= memory_array[i_addr];

If you want to learn more about the use of block ram check out my Verilog block ram recipe.

VGA 640x360

We're going to use a modified version of our VGA driver module from part 1. It changes the active vertical resolution to 360 pixels while maintaining standard 640x480 VGA timings. This means our output will still work with a standard VGA display: the 360 vertical pixels will have black bars above and below (letterboxing).

The changes are on four lines:

  • We add the parameter VA_STA with a value of 60
  • We change the value of VA_END to be 420
  • Blanking takes account of VA_STA
  • We subtract VA_STA from the active pixel value on the assign o_y = line.

Create a new module called vga640x360.v with the following content:

module vga640x360(
    input wire i_clk,  // expects 100MHz clock
    output wire o_hs,  // horizontal sync
    output wire o_vs,  // vertical sync
    output wire o_blanking,
    output wire o_animate,
    output wire [9:0] o_x,  // 10-bit value: 0-1023
    output wire [8:0] o_y   //  9-bit value: 0-511

    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_STA = 60;              // vertical active pixel start
    localparam VA_END = 420;             // 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 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 - VA_STA - 1) : (v_count - VA_STA);

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

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

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

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

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

Learn more about video display timings.

Bringing it Together

Create a design source called top.v (replacing my-image_palette.mem with the name of the image palette file you created earlier):

module top(
    input wire CLK,
    output wire VGA_HS_O,
    output wire VGA_VS_O,
    output reg [3:0] VGA_R,
    output reg [3:0] VGA_G,
    output reg [3:0] VGA_B

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

    vga640x360 display (

    localparam VRAM_A_WIDTH = 18; 
    localparam VRAM_D_WIDTH = 6; 
    localparam VRAM_DEPTH = 640*360; 

    reg [VRAM_A_WIDTH-1:0] address;
    wire [VRAM_D_WIDTH-1:0] dataout;

        .i_write(0),  // we're always reading

    reg [11:0] palette [0:63]; 
    reg [11:0] colour;
    initial begin
        $display("Loading palette.");
        $readmemh("my-image_palette.mem", palette);  // update with your filename

    always @ (posedge CLK)
        address <= y * 640 + x;

        if (blanking)
            colour <= 0;
            colour <= palette[dataout];

        VGA_R <= colour[11:8];
        VGA_G <= colour[7:4];
        VGA_B <= colour[3:0];

The sram module takes three parameters: the width of data, the number of values to store (the depth), and the address width required to access them. For our display, we use 6-bits as the data width, 640 x 360 as the depth, and 18-bits for the address as 218 is the smallest value greater than 640 x 360.

Add Image Files to Project

To enable Vivado to find the image file and palette you need to add them to your project. You do this as you would for a design source using "Add Sources" then selecting "Files of type: Memory Initialization Files" then locate my-image.mem and my-image_palette.mem (or whatever you called them). Vivado will automatically identify them as memory files and make them available to your design.


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

## Arty Board Constraints
## Based on

## 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}];

## 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}];

If you're using the Basys3 board, you need to modify the constraints for your board. Change the pins for the clock and VGA ports. See the Basys3 reference manual and Basys3 master XDC for details.

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 vga02/vga02.runs/impl_1/top.bit.

You should see your bitmap image displayed. If you image is distorted or the wrong colours make sure your image file is exactly 640 x 360 and that you've loaded the correct palette file in top.v.

What's Next?

Now we've got a bitmapped display to play with we're ready to experiment with sprites. That'll be the subject of the next post. Follow @WillFlux for updates.

©2018 Will Green.

Graphics Credit: The sample spaceship game image comes from KenneyNL and is public domain.