Skip to content

VGA Video in VHDL on FPGA

This project was carried out as part of learning VHDL. It is a VGA video signal generator, which allows displaying an image on a VGA screen. The project was carried out on a Terasic DE1 board (Altera Cyclone II FPGA). Terasic DE1 Board

Objective

The goal of this project was to generate a VGA video signal, to display an image on a VGA screen. We therefore had to generate an HSYNC signal (horizontal synchronization), as well as a VSYNC signal (vertical synchronization), and an RGB signal (or at least one of these colors). The timing of these signals depends on the resolution, we chose a resolution of 640x480, which is the standard VGA resolution.

It is necessary to generate these signals at a precise frequency, to respect VGA standards. The refresh rate must be 60Hz, and the horizontal frequency must be 31.5kHz. It is therefore necessary to use counters to generate these signals.

We used Quartus II software in version 13.0sp1 to program the FPGA. The VHDL code was written using the text editor integrated into Quartus II.

RGB on the DE1

The RGB signal is generated using a DAC (Digital to Analog Converter). The DE1 board has a 4-bit DAC for each channel (Red, Green, Blue) which generates 16 different colors.

VGA Pins with series resistance

Block Diagram

I created a block diagram to represent the project's operation.

Block diagramClick on the image to enlarge it

It consists of three main blocks:

  • the sync block which generates synchronization signals
  • the image_gen block which generates the image to display on the R/G/B pin
  • the image_source block which contains the image to display, a kind of hardcoded ROM

Synchronization Signal

The DE1 board having a clock signal at a frequency of 50MHz, it is necessary to divide this frequency to generate synchronization signals.

Horizontal Synchronization

Timing is very important in VGA. For horizontal synchronization, there must be a pulse lasting 3.77µs at the beginning of each line, and each line lasts 31.77µs (very precisely). With a 50MHz clock signal, this makes a period of 20ns.

31.77us20ns=1588.51589

We must therefore count up to 1589. This corresponds to a slv (for std_logic_vector, the equivalent of a std_logic array, bits) of 11 bits: 2n=1589n10.63211=2048. However, it is impractical to have an odd number of bits, so I chose a vector of size 12 bits.

Pulse duration calculation:

3.77us20ns=188.5189

We must therefore create a counter from 0 to 1589, with a logic output (std_logic) active during the first 189 values.

Vertical Synchronization

Vertical signaling is "based" on the H sync signal: the counter increases by one when the horizontal counter has finished a line (and therefore when it returns to 0).

The refresh rate being 60 Hz, this corresponds to a period of about 16.6 ms.

Compteurvertical=16.6ms31.77us522

The output must be on during the first two lines, and the last two. We must therefore check if the counter is before 2 or after 521.

VHDL Code

VHDL code for synchronization signals
vhdl
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
USE IEEE.STD_LOGIC_UNSIGNED.ALL;
use IEEE.numeric_std.all;

entity sync is
port (
   clk_in_50MHz, RAZ: in std_logic; -- clock input

	Hpos, Vpos: out std_logic_vector(11 downto 0);
	Hsync, Vsync: out std_logic -- output
  );
end sync;

architecture Behavioral of sync is
	-- internal counters
	signal Hcount: std_logic_vector(11 downto 0):=(others =>'0');
	signal Vcount: std_logic_vector(11 downto 0):=(others =>'0');

	-- Hsync constants
	constant HSYNC_DURATION: std_logic_vector(11 downto 0) := X"634";
	constant HSYNC_LOW_PULSE_END: std_logic_vector(11 downto 0) := X"0BC";

	-- Vsync constants
	constant VSYNC_DURATION: std_logic_vector(11 downto 0) := X"20A"; -- 522 -> 16.6/0.03177
	constant VSYNC_END_OF_STAR_PORCH: std_logic_vector(11 downto 0) := X"002";
	constant VSYNC_STAR_OF_END_PORCH: std_logic_vector(11 downto 0) := X"209";


begin
	 process(clk_in_50MHz)
	 begin
		 if(rising_edge(clk_in_50MHz)) then

			if(RAZ = '0') then
				Vcount <= x"000";
				Hcount <= x"000";
			end if;

			-- increment the internal counter by 1
			Hcount <= Hcount + x"01";

			if(Hcount = HSYNC_DURATION) then
				Hcount <= x"000"; -- counter reset

				Vcount <= Vcount + x"01";

				-- if we have reached the end of the 522 lines of the virtual screen
				if(Vcount = VSYNC_DURATION) then
					Vcount <= x"000";
					Vsync <= '1';
				else
					Vsync <= '0';
				end if;


			end if;

			-- between 0 and 188, i.e. 3.76us
			if(Hcount < HSYNC_LOW_PULSE_END) then
				Hsync <= '0';
			else
				Hsync <= '1';
			end if;

			if(Vcount >= VSYNC_END_OF_STAR_PORCH and VSYNC_STAR_OF_END_PORCH <= Vcount) then
				Vsync <= '0';
			else
				Vsync <= '1';
			end if;

		end if;
	 end process;

	 Hpos <= Hcount;
	 Vpos <= Vcount;
end Behavioral;

Image Generation

The specifications for this block required being able to generate:

  • a horizontal line (a few pixels wide)
  • a vertical line
  • a square

There is a small center_pos block that only serves to give the screen center to image_gen. It is then very simple to subtract or add a certain number of pixels to generate a horizontal or vertical line for example.

Four switches on the DE1 board allow you to choose the shape to display. It is possible to change shape by changing the switch values.

Horizontal and vertical lines

Horizontal lineVertical lineSquare
Horizontal lineVertical lineVertical and horizontal line

To generate a horizontal line, simply compare the current vertical position with the vertical position of the screen center. If the current vertical position is equal to the vertical position of the screen center, then display a pixel, otherwise display a black pixel.

vhdl
if(Hp >= Hcenter-10 and Hp <= Hcenter+10) then
    Red <= x"F";
end if;

Similarly, simply do the same with the horizontal position to get a vertical line.

vhdl
if(Vp >= Vcenter-10 and Vp <= Vcenter+10) then
    Red <= x"F";
end if;

And finally, the square is nothing more than the combination of these two combinations:

vhdl
if((Vp >= Vcenter-10 and Vp <= Vcenter+10) and (Hp >= Hcenter-10 and Hp <= Hcenter+10)) then
    Red <= x"F";
end if;

Arbitrary Image

Once this project was completed, I wanted to continue experimenting with VGA. I therefore added an image_source block, which is actually a read-only memory, which contains the pixel values to display, according to their coordinates (X, Y). It is possible to change the values of this ROM by modifying the VHDL code. To facilitate the modification of this ROM, I created a small Python script that generates the VHDL code from an image.

Python code to generate the VHDL code for the ROM
python
import cv2
import numpy as np

filePath = "image.jpg"

# Read the image
img = cv2.imread(filePath, 0)

# resize image
img = cv2.resize(img, (160, 120))

# convert to 4 bit grayscale
bits = 4
img = np.right_shift(img, bits)


image_height, image_width = img.shape

# in form type image is array (0 to (vertical_size-1), 0 to (horizontal_size-1)) of std_logic_vector(3 downto 0);
arrayStr = ""

arrayStr += f"constant vertical_size : integer := {image_height};\n"
arrayStr += f"constant horizontal_size : integer := {image_width};\n\n"

arrayStr += f"type image is array (0 to (vertical_size-1), 0 to (horizontal_size-1)) of std_logic_vector({bits-1} downto 0);\n"

arrayStr += "constant data : image := (\n"

# each line should be in form (x"7", x"8", x"9", x"A")
for i in range(image_height):
    arrayStr += "\t("
    for j in range(image_width):
        arrayStr += f"x\"{img[i][j]:X}\", "
    arrayStr = arrayStr[:-2]
    # if not last line, add comma
    if i != image_height - 1:
        arrayStr += "),\n"
    else:
        arrayStr += ")\n"

arrayStr += ");"

arrayStr += "\n -- image size: " + str(image_height) + "x" + str(image_width) + "\n -- bits: " + str(bits) + "\n -- image: " + filePath

print(arrayStr)

It can be useful to extract a color from the image, I did it in an image editing software. Warning: the size of the image is important, if the image is too large, Quartus II will struggle and the FPGA will not be able to load it into memory.

Some image examples

Very smallA bit biggerAnd now centered

🎉 The first image is a 15x20 pixel image, the second is a 160x120 pixel image.

As you can see, the image is very red: I only connected one color out of the three. It is possible to connect all three colors, but then you must store the values of all three colors in the ROM, which takes up a lot of space.

The rendering is not perfect, but it is quite possible to recognize the image. It's a good start for a first VHDL project. 😄

To display the image, you must therefore read the ROM value at the current position (X, Y), and display it on the R/G/B pin. There is therefore a loop between image_gen and image_source. I patched together a slv_to_integer block for communication between the two blocks, to adapt the data types.

vhdl
OUT1 <= to_integer(unsigned(IN1));
OUT2 <= to_integer(unsigned(IN2));
Loop between image_gen and image_source

In the image_gen block, it is then enough to "let through" the ROM value at the current position (X, Y).

vhdl
Red <= imageIn;

Result

This project allowed me to learn many VHDL functions (although it remains relatively basic), and to understand how VGA works.

I would have liked to have more time to experiment with the SD card reader, to store the image directly on it and read it from the FPGA. Unfortunately, I didn't have time to do it. I also wanted to try to make a video move, but I didn't have time either.