I. Les signaux du port VGA▲
Le rôle d’un contrôleur VGA est de générer les signaux nécessaires qui, à l’époque des écrans et moniteurs à tube cathodique (ou CRTCathode Ray Tube), permettaient de gérer les mouvements de balayage du spot en sortie du canon à électrons ainsi que l’intensité des faisceaux rouge, vert et bleu déterminant la couleur du point. Les écrans et moniteurs modernes n’utilisent évidemment plus cette technologie et sont passés à l’ère du numérique, mais on y trouve encore des ports vidéo VGA pour émuler ce balayage.
Il y a au moins quatre versions de ce connecteur 15 broches DE-15 avec des signaux maintenant obsolètes. Les versions les plus récentes (DDC2 et ultérieures) permettent même la communication bidirectionnelle avec un ordinateur.
Dans ce tutoriel, nous détaillerons uniquement les 5 signaux utiles a minima pour générer des images (avec un brochage commun à toutes les versions), mais vous trouverez le brochage avec les explications détaillées dans le document VGA connectors pinouts.
- Les broches (1), (2) et (3) permettent de définir respectivement les composantes rouge (RED), verte (GREEN) et bleue (BLUE) du pixel en cours. Les signaux sur ces broches sont analogiques et doivent évoluer entre 0 V (composante de couleur éteinte) et 0,7 V (composante de couleur illuminée au maximum). Pour éviter le bruit sur les signaux analogiques, ceux-ci sont transmis au moyen de câbles coaxiaux dans les câbles VGA de qualité. Pour la composante rouge par exemple, le signal utile est égal à la différence de tension entre les conducteurs (1) et (6) (RED Ground). Ce sera entre (2) et (7) (GREEN Ground) pour la composante verte, puis entre (3) et (8) (BLUE Ground) pour la composante bleue.
- Les broches (13) (synchronisation horizontale HSYNC) et (14) (synchronisation verticale VSYNC) sont pilotées avec des signaux numériques (à l’état logique haut ou bas). Il s’agit avec ces signaux d’organiser le balayage (scan) des pixels (de gauche à droite, puis du haut vers le bas de l’écran) en le synchronisant avec un signal d’horloge.
I-A. Génération des signaux analogiques▲
Pour générer les signaux analogiques à partir d’un microcontrôleur ou d’un FPGA, il est plus simple de passer par trois convertisseurs numérique-analogique (ou DAC pour Digital-Analog Converter) fonctionnant en parallèle. Pour interfacer le FPGA avec le moniteur VGA, un module comme celui de Digilent fait très bien l’affaire :
Ce module comporte 4 broches numériques par composante de couleur R, G ou B :
- R3, R2, R1 et R0 pour la composante rouge (Red)Â ;
- G3, G2, G1 et G0 pour la composante verte (Green)Â ;
- B3, B2, B1 et B0 pour la composante bleue (Blue).
Chaque composante de couleur est donc définie avec 4 bits et le convertisseur numérique-analogique associé (un simple réseau de résistances R-2R gravé en surface du module PmodVGA) pourra générer 24 = 16 niveaux de tension entre 0 et 0,7 V, soit une profondeur de couleurs 12 bits (RGB444). On est loin des performances à haute résolution des convertisseurs des RAMDAC 24 bits qui équipaient les cartes VGA, mais vous avez là un composant à moins de 20 € et suffisant pour du prototypage rapide (et les adeptes du fer à souder pourront aussi bien faire leur petite carte, et pour moins cher encore).
Sur la copie d’écran de la simulation ci-dessus, la composante de couleur sur 4 bits est définie par la position des interrupteurs SWx. Avec les quatre interrupteurs fermés (soit la valeur binaire 1111), la tension produite est de 0,674 V en théorie, soit un peu moins du 0,7 V requis pour la composante illuminée au maximum.
I-B. Processus de balayage (scan) et signaux de synchronisation▲
Deux broches supplémentaires HS et VS du module PmodVGA permettent de diriger les signaux de synchronisation horizontale et verticale vers le port VGA.
Ces signaux organisent le processus de balayage du spot, pixel par pixel, pour rafraîchir l’image de l’écran. Le balayage commence depuis le coin supérieur gauche de la surface en suivant la ligne de gauche à droite. En fin de ligne, on continue le balayage en reprenant au début de la ligne suivante, et ainsi de suite jusqu’en bas de la surface. Une fois toute la surface parcourue, le processus reprend à nouveau pour rafraîchir l’image. Pour une animation vidéo fluide, le balayage est cadencé par une horloge à une fréquence suffisamment rapide pour rafraîchir une image complète soixante fois par seconde.
Mais à l’époque des écrans à tube cathodique CRTCathod Ray Tube, il fallait aussi laisser le temps nécessaire à l’électronique embarquée pour repositionner le spot du faisceau en début de ligne suivante ou de le retourner à sa position initiale à la fin du balayage (les flèches en pointillés Horizontal retrace et Vertical retrace sur la figure précédente).
Dans le protocole VGA (défini par la norme VESAVideo Electronics Standards Association(1)), on peut prendre en compte ces délais matériels en étendant la zone d’affichage avec des pixels fictifs (qui ne seront pas allumés). Ainsi, pour une surface vidéo active de 640 x 480 pixels, tout se passe au niveau du minutage comme si l’on balayait continuellement une surface équivalente étendue à 800 x 525 pixels. Les zones fictives balayées au taux de rafraîchissement de 60 Hz autour de la zone active donnaient alors du temps au système matériel pour repositionner le spot du faisceau.
Ce protocole est toujours conservé sur les écrans modernes, même si ce temps supplémentaire peut maintenant servir à d’autres choses comme transporter des données.
Pour illustrer le protocole, on donne la représentation ci-dessous :
On y voit :
- au centre, la zone effective d’affichage de l’image (Active Pixels, drawing area) de 640 x 480 pixels. Dans cette zone, il faut présenter les signaux RGB suivant un minutage bien précis pour allumer le pixel en cours durant la phase de balayage ;
- des zones tampons Front porch et Back porch, qui entourent la zone effective d’affichage, où les signaux RGB sont à zéro. Comme vu précédemment, ces zones permettaient de temporiser pour gérer les mouvements de retour du spot. Ces temporisations doivent avoir une durée définie par la norme, qui peut être exprimée de façon équivalente en nombre de pixels balayés ;
- des zones de synchronisation horizontale et verticale dans lesquelles doivent être générés les signaux HSYNC et VSYNC. Une impulsion du signal HSYNC indique la fin d’une ligne et prépare au balayage de la ligne suivante. Une impulsion du signal VSYNC indique la fin du balayage de toute la zone et prépare à un nouveau balayage en reprenant au coin supérieur gauche. Ces impulsions ont aussi une durée bien définie par la norme, qui peut également être exprimée de façon équivalente en nombre de pixels balayés.
Si le taux de rafraîchissement de l’écran est de 60 Hz, avec une surface équivalente étendue à 800 x 525 pixels, chaque pixel doit être traité en 1 / (60 x 800 x 525) ≈ 39,7 ns (nanosecondes). Une horloge à une fréquence de 1 / 39,7.10-9 ≈ 25,2 MHz est donc nécessaire pour cadencer le processus de balayage pixel par pixel.
Dans le tableau ci-dessous(2), on indique les largeurs et hauteurs des différentes zones en nombre équivalent de pixels parcourus (verticalement, en nombre de lignes avec 1 ligne = 800 pixels).
Parameter |
Horizontal |
Vertical |
Active Pixels |
640 |
480 |
Front Porch |
16 |
10 |
Back Porch |
48 |
33 |
Sync Width |
96 |
2 |
Total Pixels |
800 |
525 |
Par exemple :
- la largeur de la bande Horizontal Back Porch définie par la norme est de 48 pixels, soit une temporisation de 48 x 39,7 ns = 1,91 μs avant de commencer à dessiner la ligne ;
- la hauteur de la bande Vertical Back Porch définie par la norme est de 33 lignes, soit une temporisation de 33 x 800 x 39,7 ns = 1,05 ms avant d’arriver au niveau de la première ligne de l’écran ;
- la largeur de la bande Horyzontal Sync définie par la norme est de 96 pixels, ce qui détermine la largeur de l’impulsion du signal de synchronisation : 96 x 39,7 ns = 3,81 μs ;
- etc.
La figure ci-dessous illustre les chronogrammes, en rouge, des signaux de synchronisation horizontale et verticale suivant le timing défini par la norme (en logique négative : signal à l’état haut par défaut, et qui bascule à l’état bas durant l’impulsion). Prenez garde à bien vérifier les unités de temps, le dessin n’est pas à l’échelle :
En pratique, on décale cette représentation de sorte à faire coïncider les coins de la zone d’affichage active avec les coordonnées (0, 0) et (639, 479) :
II. Ressources utilisées▲
On récapitule ici l’environnement matériel et logiciel utilisé dans ce tutoriel :
Les fichiers archivés des démonstrations qui suivent, à importer directement dans Quartus Prime, sont donnés en annexeAnnexe : archives Quartus Prime. |
|
|
|
|
|
|
|
|
III. Programmation FPGA▲
III-A. Module de génération des signaux▲
On commence par programmer le module vga_sync qui va générer les signaux nécessaires au balayage de l’écran :
Les entrées de ce module sont :
- un signal d’horloge clk25 à 25,2 MHz, fréquence de balayage de l’écran pixel par pixel ;
- un signal rst Reset de réinitialisation, actif à l’état bas, et qui pourra être activé sur appui d’un bouton-poussoir intégré en surface de la carte.
En sortie :
- les signaux logiques de synchronisation horizontale et verticale hsync et vsync ;
- les coordonnées du point en cours de la surface balayée x (entre 0 et 799) et y (entre 0 et 524) sur 10 bits évoluant selon le sens de balayage à la fréquence de l’horloge 25,2 MHz ;
- un signal logique inDisplayArea à l’état haut lorsque les coordonnées du pixel en cours sont dans la zone active d’affichage (x entre 0 et 639, et y entre 0 et 479) et à l’état bas sinon.
Le code commenté du module Verilog ci-dessous s’appuie sur le protocole VGA décrit plus haut :
module
vga_sync
#(
parameter
hpixels =
800
, // nombre de pixels par ligne
parameter
vlines =
525
, // nombre de lignes image
parameter
hpulse =
96
, // largeur d'impulsion du signal HSYNC
parameter
vpulse =
2
, // largeur d'impulsion du signal VSYNC
parameter
hbp =
48
, // horizontal back porch
parameter
hfp =
16
, // horizontal front porch
parameter
vbp =
33
, // vertical back porch
parameter
vfp =
10
// vertical front porch
)
(
input
clk25, rst, // signal horloge 25MHz, signal reset
output
wire
[9
:0
] x, y, // coordonnées écran pixel en cours
output
inDisplayArea, // inDisplayArea = 1 si le pixel en cours est dans la zone d'affichage, = 0 sinon
output
hsync, vsync // signaux de synchronisation horizontale et verticale
);
// compteurs 10 bits horizontal et vertical
// counterX : compteur de pixels sur une ligne
// counterY : compteur de lignes
reg
[9
:0
] counterX, counterY;
always
@(
posedge
clk25 or
negedge
rst) // sur front montant de l'horloge 25MHz, ou front descendant du signal Reset
begin
if
(
rst ==
0
) begin
// Remise à zéro des compteurs sur Reset
counterX <=
0
;
counterY <=
0
;
end
else
begin
// compter les pixels jusqu'en bout de ligne
if
(
counterX <
hpixels -
1
)
counterX <=
counterX +
1
;
else
// En fin de ligne, remettre le compteur de pixels à zéro,
// et incrémenter le compteur de lignes.
// Quand toutes les lignes ont été balayées,
// remettre le compteur de lignes à zéro.
begin
counterX <=
0
;
if
(
counterY <
vlines -
1
)
counterY <=
counterY +
1
;
else
counterY <=
0
;
end
end
end
// Génération des signaux de synchronisations (logique négative)
// <expression 1> ? <expression 2> : <expression 3>, opérateur ternaire comme en C
assign
hsync =
((
counterX >=
hpixels -
hbp -
hpulse) &&
(
counterX <
hpixels -
hbp)) ? 1'b0
: 1'b1
;
assign
vsync =
((
counterY >=
vlines -
vbp -
vpulse) &&
(
counterY <
vlines -
vbp)) ? 1'b0
: 1'b1
;
// inDisplayArea = 1 si le pixel en cours est dans la zone d'affichage, = 0 sinon
assign
inDisplayArea =
(
counterX <
hpixels -
hbp -
hfp -
hpulse)
&&
(
counterY <
vlines -
vbp -
vfp -
vpulse);
// Coordonnées écran du pixel en cours
// (x, y) = (0, 0) Ã l'origine de la zone affichable
assign
x =
counterX;
assign
y =
counterY;
endmodule
L’environnement de Quartus Prime propose un raccourci pour lancer le simulateur ModelSim. Ce logiciel permet de visualiser sous forme de chronogrammes les sorties produites en fonction des entrées forcées, le tout par simulation.
Sur les chronogrammes ci-dessus, le signal Reset rst est inactif (forcé à l’état haut), et le signal d’horloge clk25 est un signal carré forcé à 25,2 MHz.
On voit que :
- chaque impulsion du signal hsync prépare au passage à la ligne suivante (ligne 488, 489…) ;
- la largeur de l’impulsion du signal vsync a une durée équivalente au balayage de 2 lignes (lignes 490 et 491), ce qui correspond bien au paramètre Vertical Sync Width ;
- la fin de l’impulsion du signal vsync se produit à la fin de la ligne 491. Il reste donc 33 lignes à balayer pour atteindre la dernière ligne (ligne 524), ce qui correspond bien à la hauteur de la zone Vertical Back Porch.
Sur le diagramme ci-dessous, on mesure la durée de la zone Horizontal Back Porch en plaçant des curseurs (en jaune) calés sur les fronts des signaux :
La durée simulée obtenue est de 1905,6 ns, soit 48 x 39,7 ns. Il y a bien 48 pixels balayés dans cette zone.
La simulation fonctionnelle semble montrer un comportement conforme du module vga_sync. Il reste à passer à la pratique…
III-B. Un premier dessin▲
Pour une première réalisation, on restera sur une image dessinée très modeste : un carré jaune de 200 pixels de côté et centré sur un fond cyan .
La synthèse finale de ce premier projet devrait ressembler au schéma ci-dessous (menu Tools → Netlist Viewers → RTL Viewer) :
- le bloc pll_Clock est un circuit spécialisé disponible dans la bibliothèque de composants de Quartus Prime (IP Catalog → Library → Basic Functions → Clocks, PLLs and Resets → PLL → ALTPLL) : une boucle à verrouillage de phase ou PLL (phase-locked loop) pour asservir la fréquence de sortie sur un multiple de la fréquence d’entrée. L’entrée du bloc est raccordée à l’horloge principale 50 MHz de la carte FPGA (entrée CLOCK_50). Le ratio de fréquence est fixé à 63 / 125 pour réduire la fréquence de l’horloge principale : (63 / 125) x 50 = 25,2 MHz ;
- le bloc du module vga_sync est celui détaillé au chapitre précédentModule de génération des signaux. En entrée, il est raccordé à l’horloge 25,2 MHz et au signal Reset généré par l’appui d’un bouton-poussoir KEY0 en surface de la carte FPGA. Les signaux gérant le mouvement de balayage sont produits sur ses sorties.
Le module drawing se charge de générer les signaux synchronisés des composantes Rouge-Vert-Bleu pendant le balayage des pixels. Pour une mise en œuvre plus rapide (et économiser de la filasse sur le câblage du module PmodVGA avec la carte FPGA), sur les 4 bits par composante, seul le bit de poids fort sera utilisé. Chaque composante de couleur sera soit allumée, soit éteinte. Avec une profondeur de couleur limitée à 3 bits, on peut tout de même restituer de nouvelles couleurs par synthèse additive des couleurs primaires :
- Cyan = Bleu + Vert
- Jaune = Rouge + Vert
- Magenta = Rouge + Bleu
- Blanc = Rouge +Vert + Bleu
Produire un dessin de rectangle ou carré rempli est ce qu’il y a en effet de plus simple. Au cours du balayage de l’écran, les pixels du rectangle sont ceux dont les coordonnées horizontales et verticales sont comprises entre un mini et un maxi qui dépendent chacun de la position et la taille du rectangle.
On en déduit le code du module Verilog drawing :
module
drawing
#(
parameter
square_width =
200
, // taille du carré en pixels
parameter
screen_width =
640
,
parameter
screen_height =
480
)
(
input
inDisplayArea,
input
wire
[9
:0
] x, y,
output
reg
r, g, b
);
assign
inSquare =
(
x >
(
screen_width -
square_width) /
2
) &&
(
x <
(
screen_width +
square_width) /
2
)
&&
(
y >
(
screen_height -
square_width) /
2
) &&
(
y <
(
screen_height +
square_width) /
2
);
always
@(*
)
begin
if
(
inDisplayArea) // si coordonnées dans l'aire d'affichage
begin
// <expression 1> ? <expression 2> : <expression 3>, opérateur ternaire comme en C
r <=
inSquare ? 1'b1
: 1'b0
;
g <=
1'b1
;
b <=
inSquare ? 1'b0
: 1'b1
;
end
else
begin
r <=
1'b0
;
g <=
1'b0
;
b <=
1'b0
;
end
end
endmodule
Le projet étant conçu de façon modulaire, il reste à raccorder les entrées-sorties. L’EDI Quartus Prime permet la description de votre projet sous la forme de schéma-blocs (voir Description graphique par schéma-blocs). Après avoir déposé les instances des différents composants dans la fenêtre de travail, les connexions sont réalisées à la souris :
Les entrées à gauche du schéma doivent être dirigées vers l’horloge principale 50 MHz de la carte FPGA et un des boutons-poussoirs. Les sorties à droite du schéma seront dirigées vers le port GPIO où sera connecté le module PmodVGA.
Les affectations des broches d’entrées-sorties sont accessibles depuis le menu Assignments → Pin planner de Quartus Prime :
Il faudra vous référer à la documentation de la carte FPGA pour trouver l’emplacement physique des broches où connecter les sorties vers le module PmodVGA. Avec la configuration choisie dans ce tutoriel, la localisation des broches est précisée sur le schéma ci-dessous :
Après génération du projet et configuration du FPGA, vous pouvez constater le résultat sur votre écran VGA. Une fraction de seconde après le démarrage, le temps que l’écran se cale sur les signaux, et le résultat est magnifique !
__________________________
« Ce n’est pas une défaillance de votre téléviseur. N’essayez donc pas de régler l’image. Nous maîtrisons, à présent, toute retransmission. Nous contrôlons les horizontales et les verticales… ». D’après la série télévisée Au-delà du réel : l'aventure continue.
__________________________
III-C. Exercice 1Â : dessiner une mire▲
Modifiez le code du module drawing.v pour dessiner une mire à partir de rectangles :
Corrigé en annexeAnnexe : archives Quartus Prime.
III-D. Une première animation▲
Dans ce chapitre, on se propose de réaliser une première animation vidéo. Une balle jaune se déplace en diagonale sur un fond rouge et rebondit indéfiniment sur les bords. Dans la continuité du chapitre précédent et pour rester simple en pratique, la balle sera en fait modélisée par un petit carré.
L’architecture du système va maintenant ressembler à ceci :
- le bloc pll_Clock produit le signal d’horloge à 25,2 MHz comme précédemment ;
- le bloc du module vga_sync est celui détaillé dans un chapitre précédentModule de génération des signaux, à une nuance près. Un signal supplémentaire frame est produit en sortie. Il s’agit d’une impulsion sur un cycle d’horloge au moment de commencer le balayage de la 481e ligne, la ligne qui suit la zone d’affichage active. Cette impulsion qui va donc se répéter 60 fois par seconde déclenche le calcul de la nouvelle position de la balle pour l’image suivante ;
- le bloc du module square_move calcule la nouvelle position de la balle à chaque impulsion du signal d’entrée frame en tenant compte de la vitesse de la balle (supposée constante) et des rebonds sur les bords de l’écran (chocs élastiques). Comme les coordonnées x et y des pixels balayés alimentent le bloc en entrée, le module élabore en sortie un signal inBall à l’état haut si le pixel en cours de balayage est à l’intérieur du contour de la balle (c’est facile à déterminer si la balle est carrée !) ;
- suivant le même principe que précédemmentUn premier dessin, le dernier bloc du module drawing se charge de générer les signaux synchronisés des composantes Rouge-Vert-Bleu pendant le balayage des pixels. Dans la zone d’affichage (inDisplayArea à l’état haut), si l’entrée inBall est à l’état haut, le pixel en cours est en jaune (couleur de la balle), sinon le pixel fait partie de l’arrière-plan en rouge.
La fenêtre de construction du schéma-blocs dans Quartus Prime s’étoffe un peu :
On donne les codes commentés des différents modules :
Et voici une vidéo filmée du résultat sur un écran VGA :
Une fois de plus, ce que vous voyez à l’écran n’est pas une défaillance de votre téléviseur. N’essayez donc pas de régler l’image…
III-E. Animation d’un sprite stocké en ROM▲
Vous aurez remarqué dans ces premières démonstrations que le système n’utilise pas de mémoire pour le stockage de l’image à afficher. Au lieu de cela, les informations de couleur des pixels sont « calculées » et envoyées en continu à la fréquence de 25,2 MHz pour obtenir une image stable rafraîchie 60 fois par seconde. Cette situation où le FPGA sert à la fois de contrôleur VGA, mais aussi de générateur d’images calculées « à la volée » en fonction des coordonnées du pixel en cours, convient donc pour des images mathématiques (tant que les calculs mathématiques peuvent être synthétisés pour la puce FPGA).
Si vous vous lancez dans le jeu vidéo sur FPGA, avec des sprites(3) animés sur des décors qui défilent en arrière-plan, il reste à mettre en œuvre les techniques d’animation 2D avec les sprites ou les décors stockés en mémoire tampon, RAM ou ROM (voir entre autres les feuilles de sprites).
Dans ce chapitre, on se propose de stocker un sprite dans une zone adressable de mémoire ROM de la puce FPGA. Le but est de configurer un circuit qui, au cours du balayage des pixels, récupère en mémoire « à la volée » la composante Rouge-Vert-Bleu du pixel du sprite.
III-E-1. Préparation du fichier d’initialisation de la ROM▲
La ROM (bloc de mémoire intégré M9K configuré en ROM) sera préchargée à partir d’un fichier texte au format ASCII avec l’extension .mif (memory intialization file). Le fichier spécifie les valeurs initiales pour chaque case mémoire ROM adressée. Ce fichier sera lu par Quartus Prime pendant la génération du projet.
Le sprite utilisé pour cette démonstration a une taille de 36 x 54 = 1944 pixels.
On montre ci-dessous un extrait du fichier d’initialisation spaceinvaders.mif qui va précharger la ROM :
DEPTH = 1944; -- The size of memory in words
WIDTH = 3; -- The size of data in bits
ADDRESS_RADIX = HEX; -- The radix for address values
DATA_RADIX = BIN; -- The radix for data values
CONTENT -- start of (address : data pairs)
BEGIN
000 : 000;
001 : 000;
002 : 000;
003 : 000;
004 : 000;
005 : 000;
006 : 000;
007 : 000;
008 : 000;
009 : 000;
00A : 000;
00B : 000;
00C : 100;
00D : 100;
00E : 100;
00F : 100;
010 : 100;
011 : 100;
012 : 100;
013 : 100;
014 : 100;
015 : 100;
…
78D : 000;
78E : 000;
78F : 000;
790 : 000;
791 : 000;
792 : 000;
793 : 000;
794 : 000;
795 : 000;
796 : 000;
797 : 000;
END;
Comme indiqué dans les premières lignes d’entête, les adresses sont au format hexadécimal et les données sont au format binaire sur 3 bits.
Après le BEGIN, chaque ligne du fichier est au format adresse (hexadécimal) : donnée (binaire 3 bits). La première adresse mémoire (000) concerne le pixel du sprite en haut à gauche, et on parcourt les 1944 pixels de gauche à droite en descendant ligne par ligne. Les 3 bits donnent la couleur du pixel du sprite avec un codage Rouge-Vert-Bleu. Par exemple, pour un pixel vert, la donnée sera 010.
Fort heureusement, la génération de ce fichier peut-être automatisée grâce à un script Python avec les modules :
- Pillow pour récupérer les composantes Rouge-Vert-Bleu des pixels du sprite ;
- mif pour travailler en lecture ou écriture avec ces fichiers .mif.
On donne un exemple de script Python ≥ 3.5 (à adapter) pour créer le fichier .mif à partir de l’image du sprite au format .gif :
Quartus Prime accepte aussi les fichiers au format Intel .hex pour précharger la ROM. Le module Python intelhex peut alors vous aider à gérer les fichiers dans ce format.
III-E-2. Configuration de la ROM dans Quartus Prime▲
La bibliothèque de composants réutilisables (IPIntellectual Property Catalog) de Quartus Prime propose des interfaces pour gérer les mémoires ROM ou RAM intégrées. Ici, on sélectionnera le composant ROM:1-PORT :
Quand on insère une instance dans le projet, on suit les indications de l’assistant (MegaWizard) pour paramétrer le composant :
C’est en parcourant les différents onglets de l’assistant que l’on renseigne la taille des données et le lien vers le fichier d’initialisation avec l’extension .mif.
Le schéma-bloc du composant ressemblera finalement à ceci :
- en entrée, on doit fournir l’adresse de la case mémoire (sur 11 bits) que l’on veut accéder. L’adresse est prise en compte sur front montant de l’horloge clock ;
- en sortie, dans le même cycle d’horloge, on récupère la donnée (sur 3 bits) contenue à l’adresse pointée.
III-E-3. Description du projet▲
La nouvelle configuration à synthétiser est la suivante :
- le bloc pll_Clock produit le signal d’horloge à 25,2 MHz ;
- le bloc du module vga_sync reste inchangé et s’occupe du balayage des pixels synchronisé avec l’horloge ;
- le bloc du module rom1 représente la mémoire ROM où est stockée l’image du sprite.
Le bloc du module sprite_generator comprend les calculs de déplacement du sprite selon les mêmes principes que la balle jaune qui rebondit sur les bords de l’écran lors de l’étude précédenteExercice 1 : dessiner une mire. Il englobe également la génération des signaux synchronisés des composantes Rouge-Vert-Bleu des pixels au cours du balayage pixel par pixel. Quand c’est au tour du sprite d’être affiché, le bloc génère en sortie une adresse adr_sprite[10..0] de données en ROM et récupère à l’entrée color_pixel_sprite[2..0] le code couleur du pixel du sprite à l’adresse pointée.
Dans les passages importants du code de ce module, on trouve l’assignation d’un signal inSprite qui passe à l’état haut si le pixel de coordonnées (x, y) en cours de balayage coïncide avec la position du sprite :
// inSprite=1 si le pixel (x, y) en cours de balayage est à l'intérieur du sprite, inSprite=0 sinon
assign
inSprite =
(
x >=
x_sprite) &&
(
x <
x_sprite +
sprite_width)
&&
(
y >=
y_sprite) &&
(
y <
y_sprite +
sprite_height);
Un autre signal (de largeur 11 bits) adr_sprite transporte le calcul de l’adresse en cours du pixel du sprite stocké en ROM :
assign
adr_sprite =
(
y -
y_sprite) *
sprite_width +
(
x -
x_sprite);
À ce moment-là , les composantes Rouge-Vert-Bleu en sortie du bloc rom1 peuvent être récupérées et les signaux envoyés au module PmodVGA :
if
(
inDisplayArea) // si coordonnées dans l'aire d'affichage
begin
if
(
inSprite)
begin
r <=
color_pixel_sprite[2
];
g <=
color_pixel_sprite[1
];
b <=
color_pixel_sprite[0
];
end
else
begin
r <=
1'b0
;
g <=
1'b0
;
b <=
1'b0
;
end
end
On donne une copie du schéma-blocs du projet ainsi que le code complet du module sprite_generator.v :
module
sprite_generator
#(
parameter
sprite_width =
36
, // largeur du sprite en pixels
parameter
sprite_height =
54
, // hauteur du sprite en pixels
parameter
screen_width =
640
, // largeur de l'écran en pixels
parameter
screen_height =
480
// hauteur de l'écran en pixels
)
(
input
clk25, rst, frame, inDisplayArea,
input
wire
[9
:0
] x, y,
input
wire
[2
:0
] color_pixel_sprite,
output
reg
r, g, b,
output
wire
[10
:0
] adr_sprite
);
integer
x_sprite =
(
screen_width -
sprite_width) /
2
;
integer
y_sprite =
(
screen_height -
sprite_height) /
2
;
integer
dir_x_sprite =
1
, dir_y_sprite =
1
; // déplacement en diagonale vers le bas à droite de l'écran
// inSprite=1 si le pixel (x, y) en cours de balayage est à l'intérieur du sprite, inSprite=0 sinon
assign
inSprite =
(
x >=
x_sprite) &&
(
x <
x_sprite +
sprite_width)
&&
(
y >=
y_sprite) &&
(
y <
y_sprite +
sprite_height);
assign
adr_sprite =
(
y -
y_sprite) *
sprite_width +
(
x -
x_sprite);
always
@(
posedge
clk25 or
negedge
rst)
begin
if
(
rst==
0
) begin
// Réinitialisation si appui sur bouton Reset
x_sprite =
(
screen_width -
sprite_width) /
2
;
y_sprite =
(
screen_height -
sprite_height) /
2
;
dir_x_sprite <=
1
;
dir_y_sprite <=
1
;
end
else
begin
if
(
frame) begin
// calcul de la position du sprite en dehors de la zone d'affichage active
x_sprite <=
x_sprite +
dir_x_sprite;
y_sprite <=
y_sprite +
dir_y_sprite;
if
(
x_sprite >
screen_width -
sprite_width) begin
// rebond sur bord droit
dir_x_sprite <=
dir_x_sprite *
(-
1
);
x_sprite <=
screen_width -
sprite_width;
end
else
if
(
x_sprite <
0
) begin
// rebond sur bord gauche
dir_x_sprite <=
dir_x_sprite *
(-
1
);
x_sprite <=
0
;
end
if
(
y_sprite >
screen_height -
sprite_height) begin
// rebond sur bord bas
dir_y_sprite <=
dir_y_sprite *
(-
1
);
y_sprite <=
screen_height -
sprite_height;
end
else
if
(
y_sprite <
0
) begin
// rebond sur bord haut
dir_y_sprite <=
dir_y_sprite *
(-
1
);
y_sprite <=
0
;
end
end
end
end
always
@(*
)
begin
if
(
inDisplayArea) // si coordonnées dans l'aire d'affichage
begin
if
(
inSprite)
begin
r <=
color_pixel_sprite[2
];
g <=
color_pixel_sprite[1
];
b <=
color_pixel_sprite[0
];
end
else
begin
r <=
1'b0
;
g <=
1'b0
;
b <=
1'b0
;
end
end
else
begin
r <=
1'b0
;
g <=
1'b0
;
b <=
1'b0
;
end
end
endmodule
III-E-4. Résultat▲
La vidéo de l’écran filmé ci-dessous (de piètre qualité, certes…) montre un résultat probant d’envahisseur parachuté et errant dans l’espace :
Vous aurez noté que l’accès synchronisé au bloc mémoire à la fréquence de balayage des pixels permet une fois de plus de récupérer « à la volée » la couleur du pixel, juste au moment de son affichage. Dans les cas où le processus d’affichage doit collaborer avec des matériels travaillant à des fréquences différentes pour générer ou stocker les images, sans doute utiliserez-vous les techniques bien connues avec double buffering. L’image est d’abord calculée et stockée dans une mémoire tampon, puis un autre processus parcourt l’image préparée à la fréquence de balayage des pixels pour restitution à l’écran.
III-F. Exercice 2 : sprite formé de deux images▲
Suspendu dans l’espace, même un envahisseur peut se mettre à gigoter…
On reprendra la démonstration précédente avec cette fois un sprite formé de deux images. On alternera les deux images du sprite deux fois par seconde pour donner vie à l’envahisseur et simuler ses mouvements :
Avec un script Python, il faudra générer un nouveau fichier d’initialisation (extension .mif) à partir du sprite suivant avec les deux images de même taille alignées verticalement :
Pour alterner entre les deux images (toutes les demi-secondes ou toutes les 30 frames puisque l’image est rafraîchie 60 fois par seconde), il suffira de lire en ROM soit la partie haute des adresses (les 54 premières lignes du sprite) soit la partie basse (les 54 dernières lignes) en faisant un simple décalage dans l’adresse.
En reprenant le projet précédent, faire les modifications nécessaires pour réaliser cette animation 2D.
Corrigé en annexeAnnexe : archives Quartus Prime.
IV. Conclusion▲
Dans ce tutoriel, vous avez pu découvrir le protocole VGA et sa mise en œuvre dans différents exercices simples de configuration d’une puce FPGA d’Intel à la fois en tant que contrôleur VGA et générateur d’images. L’environnement Quartus Prime d’Intel procure de nombreux outils et assistants de description de votre projet, et les modules personnalisés sont écrits en Verilog standard.
Les principes et les codes exposés sont d’une apparente simplicité. Il faut vous méfier, la simplicité n’est qu’apparente !
La principale difficulté provient des raisonnements auxquels sont habitués les programmeurs C, C++, Java, Python, etc., c’est-à -dire probablement 99 % des lecteurs qui passeront par ici. En programmation traditionnelle, un programme compilé deviendra une suite d’instructions exécutées séquentiellement par un microprocesseur. Même si par abus de langage on parle encore de « programmation » et de « compilation » dans un projet FPGA, les langages HDLHardware Description Language sont conçus pour faire de la « description » d’un circuit hardware, et si vous décrivez plusieurs processus, chacun d’entre eux peut mener à des circuits différents et qui fonctionneront en concurrence. C’est un gros avantage des FPGA, mais aussi une difficulté de représentation pour le développeur qui s’y aventure.
Même quand vous rencontrez des « nœuds » (wire
) ou des « registres » (reg
) qui semblent être manipulés en Verilog comme nos variables traditionnelles, avec des processus rédigés de façon algorithmique (et Verilog a une syntaxe qui ressemble à des langages connus), n’oubliez pas que cette abstraction de haut niveau ne vise qu’à décrire le « comportement » de votre futur circuit et non à produire des instructions exécutées séquentiellement. Vous rencontrerez ainsi des messages d’avertissement ou d’erreurs, des comportements inattendus à cause de soucis de synchronisation, de chemins mal définis dans les circuits et autres problèmes du monde physique. Le débogage passe alors par les test benches et les simulations fonctionnelles ou temporelles pour inspecter vos signaux sans recourir au matériel. Développer sur FPGA, ce n’est pas seulement une question de maîtrise d’un langage comme Verilog, SystemVerilog ou VHDL, c’est un autre métier…
Mon but n’est évidemment pas de vous décourager, au contraire… Il n’y a rien d’insurmontable, mais il faudra se débarrasser de certains réflexes de programmeurs, s’approprier une nouvelle culture et comprendre que des notions de logique séquentielle et combinatoire sont indispensables pour comprendre ce qui se passe dans vos circuits. Mes modestes contributions (voir sur mon site) vous aideront peut-être à y voir plus clair et mettre le pied à l’étrier, elles sont aussi le reflet de ma progression dans ce domaine (et il reste encore beaucoup à faire…)
De nos jours, sans doute devrait-on plutôt s’intéresser aux nouvelles interfaces vidéo numériques comme HDMI ou DisplayPort, et laisser VGA s’éteindre (mais finira-t-il vraiment par disparaître complètement ?). Beaucoup de cartes de développement FPGA intègrent d’ailleurs ces interfaces modernes. Voyez alors dans ce tutoriel un intérêt pédagogique, avec un protocole VGA relativement simple à assimiler, pour découvrir de façon ludique le monde des FPGA et des langages de description de matériel (ou HDLHardware Description Language).
Ou peut-être êtes-vous nostalgique des jeux vidéo sur les bornes d’arcade d’antan que vous pourriez ressusciter avec une carte FPGA. Des développeurs se sont déjà penchés sur le problème, voir par exemple le projet MISTer dans le domaine du retrogaming, où les processeurs des consoles ou ordinateurs se voient leurs fonctionnalités répliquées matériellement grâce à la flexibilité des FPGA (voir réplication de matériel vs émulation logicielle). On en est encore loin dans ce tutoriel, mais vous avez déjà acquis quelques principes qu’il reste à développer. Le travail mérite d’être poursuivi…
Pour terminer, je remercie les membres de Developpez pour leur travail de relecture de cet article et leurs propositions d’amélioration, en particulier : Auteur, LittleWhite, Chrtophe, Delias et escartefigue.
V. Annexe : archives Quartus Prime▲
Archives des différents projets de ce tutoriel au format Quartus Archive (fichiers avec extension .qar), à importer directement depuis Quartus Prime.
Version de Quartus Prime : 20.1.1 Lite Edition
|
|
Le module drawing.v est modifié comme suit: drawing.v Cacher/Afficher le codeSélectionnez |
|
|
|
|
|
Il faut rajouter un compteur qui s’incrémente à chaque nouvelle frame, c’est-à -dire tous les 1/60e de seconde : sprite_generator.v (extrait) Sélectionnez
sprite_generator.v (extrait) Sélectionnez
L’avantage de déclarer un compteur 6 bits, c’est qu’après avoir franchi la valeur 26-1 = 63, il repart à zéro (débordement). sprite_generator.v (extrait) Sélectionnez
Note : l’énoncé exigeait d’« alterner entre les deux images toutes les demi-secondes ou toutes les 30 frames puisque l’image est rafraîchie 60 fois par seconde… ». Hé bien, je triche un peu… ce sera toutes les 32 frames, c’est-à -dire toutes les 0,53 s environ. |