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).
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.
Schéma bloc
J'ai créé un schéma bloc pour représenter le fonctionnement du projet.
Cliquez 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
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:
Calcul de la durée de l'impulsion:
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.
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
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 horizontale | Ligne verticale | Carré |
---|---|---|
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.
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.
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:
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
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
🎉 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.
OUT1 <= to_integer(unsigned(IN1));
OUT2 <= to_integer(unsigned(IN2));
Dans le bloc image_gen
, il suffit alors de "laisser passer" la valeur de la ROM à la position (X, Y) actuelle.
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.