FPGA VGA Graphics in Verilog Part 2


Welcome back to my FPGA graphics tutorial series using the Digilent Arty, Basys 3, or Nexys Video 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.

Once you've completed this tutorial, move onto sprite animation and double buffering in part 3.

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

Feedback to @WillFlux is most welcome. Updated January 2019.


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

  1. Digilent Arty, Basys 3, or Nexys Video board (see below for other boards)
  2. VGA Pmod if using the Arty or Nexys Video (Basys 3 has VGA built-in)
  3. VGA capable monitor & cable
  4. Micro USB cable to program and power the board
  5. Xilinx Vivado installed (including Digilent board files)

Other FPGA Boards

This tutorial requires at least 1,350 Kbits of on-board FPGA memory (block or distributed ram). Provided you can meet the ram requirement it should be relatively easy to adapt this tutorial to other boards by making 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.


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 640 x 360 and limit ourselves to 6-bit colour: 640 x 360 x 6 bits = 1,350 Kbits. 640 x 360 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 (img2fmem.py) 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 use the ready-made graphics and skip to the next section (Memory Mapped).

Using img2fmem.py

Grab a copy of img2fmem.py from the FPGATools repository on Github.

img2fmem.py requires the Pillow package to run. You can install this with Python's pip tool:

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 game.png with your image filename):

python img2fmem.py game.png 6 mem

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

  • game_preview.png - a normal PNG you can view to preview the image conversion
  • game.mem - a text hex version of the image
  • game_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 appropriate 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.

Add a design source called sram.v [view source]:

module sram #(parameter ADDR_WIDTH=8, DATA_WIDTH=8, DEPTH=256, MEMFILE="") (
    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
        if (MEMFILE > 0)
            $display("Loading memory init file '" + MEMFILE + "' into array.");
            $readmemh(MEMFILE, memory_array);

    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 are three changes:

  • We add the parameter for vertical active start VA_STA with a value of 60
  • We change the value of vertical active end VA_END to be 420
  • 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 [view source]:

module vga640x360(
    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 + 10;        // vertical sync start
    localparam VS_END = 480 + 10 + 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 = 525;             // 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 - VA_STA - 1) : (v_count - VA_STA);

    // 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) | 
                        (v_count < VA_STA));

    // 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)
        if (i_rst)  // reset to start of frame
            h_count <= 0;
            v_count <= 0;
        if (i_pix_stb)  // once per pixel
            if (h_count == LINE)  // end of line
                h_count <= 0;
                v_count <= v_count + 1;
                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 with the following design [view source].

The top module references the graphic and palette as game.mem and game_palette.mem respectively. If you created your own graphics under a different name make sure you update the appropriate lines in the top module to match.

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

    wire rst = ~RST_BTN;    // reset is active low on Arty & Nexys Video
    // wire rst = RST_BTN;  // reset is active high on Basys3 (BTNC)

    // 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
    wire active;   // high during active pixel drawing

    vga640x360 display (

    // VRAM frame buffers (read-write)
    localparam SCREEN_WIDTH = 640;
    localparam SCREEN_HEIGHT = 360;
    localparam VRAM_A_WIDTH = 18;  // 2^18 > 640 x 360
    localparam VRAM_D_WIDTH = 6;   // colour bits per pixel

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

    sram #(
        .MEMFILE("game.mem"))  // bitmap to load
        vram (
        .i_write(0),  // we're always reading

    reg [11:0] palette [0:63];  // 64 x 12-bit colour palette entries
    reg [11:0] colour;
    initial begin
        $display("Loading palette.");
        $readmemh("game_palette.mem", palette);  // bitmap palette to load

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

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

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

The sram module takes four parameters: the width of data, the number of values to store (the depth), the address width required to access them, and the name of a memory initialization file to load during build. 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.

To learn more about memory initialization, see the FPGA cookbook entry Initialize Memory in Verilog.

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 "design sources" and "Files of type: Memory Initialization Files" then locate game.mem and game_palette.mem (or whatever you called them). Vivado will automatically identify them as memory files and make them available to your design.


Obtain the appopriate constraints file from the Time To Explore git repo and add it to your project:

For reference the Basys 3 constraints look like this:

## FPGA VGA Graphics Part 2: Basys 3 Board Constraints

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

## Use BTNC as Reset Button (active high)
set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports {RST_BTN}];

## VGA Connector
set_property -dict {PACKAGE_PIN G19 IOSTANDARD LVCMOS33} [get_ports {VGA_R[0]}];
set_property -dict {PACKAGE_PIN H19 IOSTANDARD LVCMOS33} [get_ports {VGA_R[1]}];
set_property -dict {PACKAGE_PIN J19 IOSTANDARD LVCMOS33} [get_ports {VGA_R[2]}];
set_property -dict {PACKAGE_PIN N19 IOSTANDARD LVCMOS33} [get_ports {VGA_R[3]}];
set_property -dict {PACKAGE_PIN N18 IOSTANDARD LVCMOS33} [get_ports {VGA_B[0]}];
set_property -dict {PACKAGE_PIN L18 IOSTANDARD LVCMOS33} [get_ports {VGA_B[1]}];
set_property -dict {PACKAGE_PIN K18 IOSTANDARD LVCMOS33} [get_ports {VGA_B[2]}];
set_property -dict {PACKAGE_PIN J18 IOSTANDARD LVCMOS33} [get_ports {VGA_B[3]}];
set_property -dict {PACKAGE_PIN J17 IOSTANDARD LVCMOS33} [get_ports {VGA_G[0]}];
set_property -dict {PACKAGE_PIN H17 IOSTANDARD LVCMOS33} [get_ports {VGA_G[1]}];
set_property -dict {PACKAGE_PIN G17 IOSTANDARD LVCMOS33} [get_ports {VGA_G[2]}];
set_property -dict {PACKAGE_PIN D17 IOSTANDARD LVCMOS33} [get_ports {VGA_G[3]}];
set_property -dict {PACKAGE_PIN P19 IOSTANDARD LVCMOS33} [get_ports {VGA_HS_O}];
set_property -dict {PACKAGE_PIN R19 IOSTANDARD LVCMOS33} [get_ports {VGA_VS_O}];

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 or Nexys Video and use your VGA cable to connect your monitor to the VGA Pmod. Basys 3 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 bitmap and palette files in top.v.

Bonus: Super VGA Bitmap

If you're lucky enough to have a Nexys Video (or other FPGA with at least 4 Mbit of block ram), then try using an 8-bit 800x600 bitmap. The higher resolution and colour depth make for a better result, especially with photographs. See the end of part 1 if you need help creating an 800x600 VGA driver and updating your top module.

What's Next?

Now we've got a bitmapped display to play with we're ready to experiment with sprites and double buffering. That's the subject of part 3.

Follow @WillFlux for updates.

Find more on FPGAs and Verilog in the Time to Explore FPGA Index.

©2018-2019 Will Green.

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