Skip to content

Vidéo VGA en VHDL sur FPGA

Ce projet a été réalisé dans le cadre de l'apprentissage de VHDL. Il s'agit d'un générateur de signal vidéo VGA, qui permet d'afficher une image sur un écran VGA. Le projet a été réalisé sur une carte Terasic DE1 (FPGA Altera Cyclone II). Carte Terasic DE1

Objectif

Le but de ce projet était de générer un signal vidéo VGA, afin d'afficher une image sur un écran VGA. Il fallait donc générer un signal HSYNC (de synchronisation horizontale), ainsi qu'un un signal VSYNC (de synchronisation verticale), et un signal RGB (du moins, une de ces couleurs). Le timing de ces signaux dépendant de la résolution, nous avons choisi une résolution de 640x480, qui est la résolution VGA standard.

Il est nécessaire de générer ces signaux à une fréquence précise, afin de respecter les normes VGA. La fréquence de rafraîchissement doit être de 60Hz, et la fréquence horizontale doit être de 31.5kHz. Il est donc nécéssaire d'utiliser des compteurs pour générer ces signaux.

Nous avons utilisé le logiciel Quartus II en version 13.0sp1 pour programmer le FPGA. Le code VHDL a été écrit à l'aide de l'éditeur de texte intégré à Quartus II.

RGB sur la DE1

Le signal RGB est généré à l'aide d'un DAC (Digital to Analog Converter). La carte DE1 possède un DAC 4 bits, pour chaque canal (Rouge, Vert, Bleu, pour RVB, RGB en anglais) qui permet de générer 16 couleurs différentes.

Pins VGA avec résistance en série

Schéma bloc

J'ai créé un schéma bloc pour représenter le fonctionnement du projet.

Schéma blocCliquez sur l'image pour l'agrandir

Il est composé de trois grands blocs :

  • le bloc sync qui génère les signaux de synchronisation
  • le bloc image_gen qui génère l'image à afficher sur le pin R/G/B
  • le bloc image_source qui contient l'image à afficher, une sorte de ROM hardcodée

Signal de synchronisation

La carte DE1 disposant d'un signal d'horloge à une fréquence de 50MHz, il est nécessaire de diviser cette fréquence pour générer les signaux de synchronisation.

Synchronisation horizontale

Les timings est très important en VGA. Pour la synchronisation horizontale, il doit y avoir une impulsion d'une durée de 3.77µs au début de chaque ligne, et chaque ligne dure 31,77µs (très précisément). Avec un signal d'horloge à 50MHz, cela fait donc une période de 20ns.

31,77us20ns=1588.51589

Nous devons donc compter jusqu'à 1589. Cela correspond à un slv (pour std_logic_vector, l'équivalent d'un tableau de std_logic, des bits) de 11 bits: 2n=1589n10.63211=2048. Cependant, il est peu pratique d'avoir un nombre de bits impaire, j'ai donc choisi un vecteur d'une taille de 12 bits.

Calcul de la durée de l'impulsion:

3,77us20ns=188.5189

Il faut donc créer un compteur de 0 à 1589, avec une sortie logique (std_logic) active pendant les 189 premières valeurs.

Synchronisation verticale

La signalisation verticale est "basée" sur le signal de synchro. H: le compteur augmente de un quand le compteur horizontal à terminé une ligne (et donc quand il revient à 0).

La fréquence de rafraîchissement étant de 60 Hz, cela correspond à une période d'environ 16.6 ms.

Compteurvertical=16,6ms31,77us522

La sortie doit être allumée pendant les deux premières lignes, et les deux dernières. On doit donc regarder si le compteur est avant 2 ou après 521.

Code VHDL

Code VHDL pour les signaux de synchronisation
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; -- entrée horloge

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

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

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

	-- constantes Vsync
	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;

			-- on augmente le compteur interne de 1
			Hcount <= Hcount + x"01";

			if(Hcount = HSYNC_DURATION) then
				Hcount <= x"000"; -- remise à zéro du compteur

				Vcount <= Vcount + x"01";

				-- si on a atteint la fin des 522 lignes de l'écran virtuel
				if(Vcount = VSYNC_DURATION) then
					Vcount <= x"000";
					Vsync <= '1';
				else
					Vsync <= '0';
				end if;


			end if;

			-- entre 0 et 188, soit 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;

Génération de l'image

Le cahier des charges pour ce bloc demandait d'être en capacité de pouvoir générer:

  • une ligne horizontale (de quelques pixels de large)
  • une ligne verticale
  • un carré

Il existe un petit bloc center_pos qui ne sert qu'à donner le centre de l'écran à image_gen. Il est alors très simple de soustraire ou d'ajouter un certain nombre de pixels pour générer une ligne horizontale ou verticale par exemple.

Quatre interrupteurs sur la carte DE1 permettent de choisir la forme à afficher. Il est possible de changer de forme en changeant la valeur des interrupteurs.

Lignes horizontales et verticales

Ligne horizontaleLigne verticaleCarré
Ligne horizontaleLigne verticaleLigne verticale et horizontale

Pour générer une ligne horizontale, il suffit de comparer la position verticale actuelle avec la position verticale du centre de l'écran. Si la position verticale actuelle est égale à la position verticale du centre de l'écran, alors on affiche un pixel, sinon on affiche un pixel noir.

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

De même, il suffit de faire la même chose avec la position horizontale pour obtenir une ligne verticale.

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

Et finalement, le carré n'est rien de plus que la combinaison de ces deux combinaisons:

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

Image arbitraire

Une fois ce projet terminé, j'ai voulu continuer d'expérimenter avec le VGA. J'ai donc ajouté un bloc image_source, qui est en réalisé une mémoire morte, qui contient les valeurs des pixels à afficher, selon leurs coordonnées (X, Y). Il est possible de changer les valeurs de cette ROM en modifiant le code VHDL. Pour faciliter la modification de cette ROM, j'ai créé un petit script Python qui permet de générer le code VHDL à partir d'une image.

Code Python pour générer le code VHDL de la 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)

Il peut être utile d'extraire une couleur de l'image, je l'ai fait dans un logiciel d'édition d'image. Attention: la taille de l'image est importante, si l'image est trop grande, Quartus II aura du mal et le FPGA ne pourra pas la charger dans la mémoire.

Quelques exemples d'images

Très petitUn peu plus grandEt maintenant centré

🎉 La première image est une image de 15x20 pixels, la deuxième est une image de 160x120 pixels.

Comme vous pouvez le constater, l'image est très rouge: je n'ai connecté qu'une seule couleur sur les trois. Il est possible de connecter les trois couleurs, mais il faut alors stocker les valeurs des trois couleurs dans la ROM, ce qui prend beaucoup de place.

Le rendu n'est pas parfait, mais il est tout à fait possible de reconnaître l'image. C'est un bon début pour un premier projet en VHDL. 😄

Pour afficher l'image, il faut donc lire la valeur de la ROM à la position (X, Y) actuelle, et l'afficher sur le pin R/G/B. Il y a donc une boucle entre image_gen et image_source. J'ai bricolé un bloc slv_to_integer pour la communication entre les deux blocs, pour adapter les types de données.

vhdl
OUT1 <= to_integer(unsigned(IN1));
OUT2 <= to_integer(unsigned(IN2));
Boucle entre image_gen et image_source

Dans le bloc image_gen, il suffit alors de "laisser passer" la valeur de la ROM à la position (X, Y) actuelle.

vhdl
Red <= imageIn;

Résultat

Ce projet m'a permis d'apprendre beaucoup de fonctions en VHDL (bien que cela reste relativement basique), et de comprendre le fonctionnement du VGA.

J'aurais voulu avoir plus de temps pour expérimenter avec le lecteur de carte SD, pour stocker l'image directement dessus et la lire depuis le FPGA. Malheureusement, je n'ai pas eu le temps de le faire. Je voulais aussi essayer de faire bouger une vidéo, mais je n'ai pas eu le temps non plus.