Préambule

Objectifs

Plutôt que de long discours, voici quelques exemples de modules écrits en VHDL. Chaque exemple introduit un nouveau type de construction / type de donnée.

Pour chaque exemple, passez la souris sur le code pour avoir des explications sur la façon dont il est construit.

Plan du chapitre

Back to Top

AND3

On va voir

Commençons par le plus simple des modules ou presque : une porte combinatoire AND à trois entrées. En VHDL, les modules sont appelés "entités" (mot-clef entity). 
Voici une description possible d'une porte AND à 3 entrées.

Passez la souris sur les différents éléments du code pour avoir des explications sur la signification de chaque instruction.

Déclaration de l'entité
On commence par déclarer ce qu'on va décrire, ici une "entité" (un module, un bloc, un composant) : 
entity AND_3 is 
Le mot-clef entity est suivi du nom (ici AND_3), et de la liste des entrées-sorties du module
.
Ces entrées-sorties sont appelées ports. L'ordre des entrées-sorties n'a pas d'importance.
Contrairement au C et à Verilog, ll n'y a pas de distinction entre les majuscules et minuscules.

Puis viennent les déclarations du sens des ports :

  • in pour les entrées du module
  • out pour les sorties
  • inout pour les ports bidirectionnels

il faut noter que chaque port est typé. Ici les ports sont de type bit , c'est  dire qu'ils peuvent prendre 2 états binaires. Au passage, on doit aussi déclarer la taille des ports. Par défaut, ils sont sur 1 bit. Pour un bus d'entrée A sur 8 bits, on aurait ce style de déclaration :

A : in bit_vector(7 downto 0); 

On aurait aussi pu déclarer
A : in bit_vector(0 to 7); mais il est plus naturel de positionner le MSB à gauche
Description de l'architecture
En VHDL le comportement du composant est décrit dans une Architecture contenant la partie interne du module. L'architecture doit être nommée, ici "arc" , ce qui permet de définir plusieurs architectures pour la même entité. Le code est défini entre les mots-clef begin et end

l'instruction <= est une instruction d'affectation qui se dit aussi "reçoit" permettant d'exprimer la causalité du calcul entre les entrées et la sortie combinatoire. Le mot-clef = existe aussi mais est utilisé comme opérateur de comparaison entre 2 variables.

Les expressions logiques utilisables sont propres à chaque type de variable. Pour le type prédéfini bit, les opérateurs utilisables sont les mêmes qu'en C :

  • le ET est noté and
  • le OU est noté or
  • le XOR est noté xor
  • le NON est noté not
Pour une porte NOR3, on aurait eu :
s <= not(e1 or e2 or e3);
     
Back to Top

Additionneur complet 1 bit

Nous allons voir


Voici une description possible d'un additionneur complet 1 bit.

Rappel : un additionneur complet 1 bit a trois entrées (a, b, et cin la retenue entrante), et deux sorties (le bit de somme s et le bit de retenue sortante cout).

Déclaration des bibliothèques
Dans le langage VHDL de base, les fonctions arithmétiques ne sont disponibles que pour le type entier integer. Pour augmenter la richesse fonctionnelle du langage, comme par exemple effectuer des fonctions sur une signal quantifié, VHDL utilise les objets de bibliothèques préalablement compilées permettant d'introduire de nouveaux types et fonctions.  L'édition de lien, qui s'appelle "élaboration"en VHDL, utilise les bibliothèques déclarées dans le code avec le mot-clef library. La compilation qui s'appelle "analyse"en VHDL, est effectuée par défaut dans la bibiothèque work.

Les bibliothèques contiennent des objets dont la visibilité externe est assurée les par le biais des paquetages ou packages.

Le paquetage std_logic_1164 de la bibliothèque standard IEEE  définit le type std_logic qui correspond à un type énuméré sur 9 états. Ceci étoffe le réalisme du modèle par rapport au type bit qui n'a que 2 états binaires. Par exemple les états haute imédance 'Z' et inconnu 'X' font partie de ces 9 états.

Le paquetage numeric_std déclare et décrit des fonctions sur les variables de type std_logic , unsigned (vecteur de std_logic non signé), signed (vecteur de std_logic signé). Parmi celles ci figurent la plupart des fonctions arithmétiques, de comparaison et de conversion de type.

Déclaration de l'entité
Les directions des ports sont indiquées ici sous forme condensée en une seule ligne par type de port.

Description de l'architecture
Avec la bibliothèque IEEE et le paquetage numeric_std, les opérations arithmétiques se font sur des nombres de type unsigned (nombre non-signé composé de std_logic) ou signed (nombre signé composé de std_logic). Avant de décrire l'architecture, il est nécessaire de déclarer un signal intermédiaire, ici resultat, de type unsigned sur 2 bits pour pouvoir effectuer l'addition. 

L'architecture déclarée après le mot-clef begin consiste à coder le signal resultat en utilisant l'opérateur + et les entrées étendues à 2 bits du fait que l'opérateur + du paquetage numeric_std est homogène. L'extension à 2 bits se fait avec l'opérateur de concaténation & .
Les sorties s et cout sont donc extraites du signal resultat en considérant le bit de poids séparément conformément à l'algorithme d'addition   2*cout + s = a + b + cin.
Il faut noter qu'il n'est pas possible de coder directement s & cout car l'opérateur & est un opérateur binaire qui ne peut servir que dans l'expression d'affectation et non dan l'expression affectée.
 


Back to Top

Additionneur 4 bits

On va voir


VHDL, comme tous les HDL, permet de décrire les système sous forme structurelle. C'est-à-dire, au lieu de décrire un gros système dans un seul gros fichier, de le séparer en sous-systèmes (sous-modules), eux-mêmes subdivisés en sous-modules, jusqu'à obtenir des modules facilement compréhensibles.

Nous allons ici décrire un additionneur 4 bits en assemblant des additionneurs 1 bit. On aurait aussi pu décrire la fonctionnalité de l'additionneur 4 bits (par opposition à sa structure), cela sera vu à la fin de cet exemple-ci.

On suppose ici que le code de l'additionneur 1 bit vu précédement est disponible dans un fichier à part.

code full adder 8bits
Déclaration des bibliothèques nécessaires et de l'entité
Le module adder4 a 4 ports :
  • a, b et s sur 4 bits
  • cout sur 1 bit.

Les vecteurs étant non signés, il est nécessaire de déclarer préalablement la bibliothèque IEEE qui définit le type unsigned
Déclaration des composants et signaux nécessaires

L'additionneur 4 bits peut être compilé (on dit aussi "analysé") indépendamment de l'additionneur 1 bit. Il est donc nécessaire d'indiquer le composant "prototype" de l'additionneur 1 bit grâce au mot clé component de façon à l'utiliser dans l'architecture

Les instances des additionneurs 1 bits sont reliés entre eux : la retenue sortante de l'un est la retenue entrante du suivant.

Pour relier les additionneurs entre eux par la chaîne de retenue, on créée un signal c correspondant à un vecteur unsigned  constitué des 3 retenues internes. On aurait pu tout aussi bien déclarer 3 signaux de type std_logic


Instanciation des composants

On instancie un module de cette façon :

 nom_de_l'instance composant_a_instancier port map(signaux connectés à l'instance);

Chaque instance doit avoir un nom unique.

Pour relier le ports de l'instance aux signaux du module courant, on utilise ici la connexion par position : le premier signal est relié au premier port, le deuxième au deuxième, etc.

Cette façon de faire oblige à respecter l'ordre dans lequel les ports ont été déclarés.  Il existe une autre façon de relier les signaux, plus propre, elle sera étudiée plus tard.

De même il aurait été possible d'avoir une boucle de génération de code automatique qui aurait évité d'écrire 4 fois a peu près la même chose.

Exercice : plutôt que de décrire l'additionneur 4 bits sous forme d'assemblage d'additionneurs 1 bits, on aurait pu directement décrire son comportement (sa fonctionalité). En vous inspirant du code de l'additionneur 1 bit, écrivez une description concise de l'additionneur 4 bits.

[Afficher la réponse]



Back to Top

Bascule D

On va voir
Jusqu'à présent, les modules étudiés étaient purement combinatoires.  Nous allons maintenant voir comment décrire un module séquentiel.

code dff
Déclaration des bibliothèques et de l'entité
La bibiothèque IEEE est déclarée pour utiliser le type std_logic ainsi que  la fonction rising_edge qui permet de détecter un front montant sur un signal . En logique synchrone, seul le signal d'horloge va utilser cette fonction.
Codage de l'architecure 

Le codage comportemental de la bascule fait appel à un processus. Un processus est un enchaînement d'instructions décrivant un comportement. Il possède une liste de sensibilité spécifiant sur quel signal le processus doit s'exécuter. Le processus en soit est une instruction concurrente mais son contenu est une suite d'instructions séquentielles. Il peut être utilisé aussi bien pour la logique séquentielle que pour la logique combinatoire.

Dans le cas de la logique synchrone faisant des calculs au rythme d'une horloge clk, la syntaxe est donc

process(clk).

Enfin, le corps du processus, codé entre begin et end, contient les instructions à exécuter. Pour une bascule D, cela consiste à recopier la valeur de l'entrée sur la sortie.
q <= d;
Il faut préalablement tester le front sur lequel l'exexution a lieu en utilisant l'instruction :
if clk='1' then
certains outils de synthèse ne respectant pas la prise en compte de la liste de sensibilité, il vaut mieux utiliser la syntaxe :
if clk='1' and clk'event then
ou, si on utilise la bibliothèque IEEE :
 if rising_edge(clk)


Back to Top

Compteur 8 bits avec reset asynchrone

On va voir
Voici une description possible d'un compteur synchrone sur 8 bits, avec reset asynchrone actif à l'état bas.

code compteur
Déclaration des bibliothèques et de l'entité
Le compteur dispose d'une entrée reset asynchrone resetn et d'une sortie q sur 8 bits.
Description de l'architecture
La décalration du signal interne q_int est nécessaire car q est un port de sortie , il ne peut donc pas être utilisé en entrée pour réaliser l'instruction de comptage
q <= q +1 
C'est q_int qui va donc servir à cette fin. Une autre solution aurait été de déclarer le port q de type buffer à la place de out, ce qui aurait évité l'utilisation de q_int.

La valeur du compteur peut changer sur deux événements :
  • front montant de l'horloge
  • état bas du reset

La liste de sensibilité doit donc contenir ces 2 signaux :

process(clk, resetn) 

Conformément à l'utilisation du reset asynchrone sur les bascule, le signal resetn est prioritaire par rapport au front d'horloge, il faut donc commencer le codage du processus par l'instruction 

if resetn='0' then

Il aurait été possible de mettre à 0 q_int par l'instruction q_int <= "00000000"; mais l'instruction q_int <= (others=> '0'); est plus générique. Il faut ensuite indiquer la condition sur le front montant de l'horloge pour déclencher le comptage :

elsif rising_edge(clk) then

Il faut noter que l'instruction de comptage s'effectue en additionnant un signal unsigned sur 8 bit avec un entier :

q_int <= q_int +1 

qui est une opération définie dans le paquetage numeric_std de IEEE. L'addition entre un unsigned et un std_logic n'est pas définie (donc l'écriture q_int <= q_int + '1' n'est pas possible)



Back to Top

Registre à décalage

On va voir
On cherche à réaliser un registre à décalage sur 8 bits, possèdant une sortie supplémentaire (match) active à 1 si le contenu du registre à décalage vaut une valeur précise (val).

code shifter
Déclaration des bibliothèques et de l'entité
Le port d'entrée d_in rentre dans un registre à décalage de 8 bits qui sort sur d_out
La sortie match est activée à 1 lorsque l'entrée val est égale au contenu du registre à décalage.
Processus synchrone de décalage

Après avoir décalré le signal reg est nécesaire pour définir les 8 bascules internes nécessaires au décalage, un processus synchrone (donc sensible à clk) est utilisé.
Les instructions d'affectation utilisées dans le processus sont exécutées séquentiellement. L'execution consiste à remplir un échéancier d'affectation et non à affecter immédiatement le signal. Les affectations des signaux sont différées  elles ne prendront effet qu'une fois que tous les processus auront été exécutés, c'est à dire un temps symbolique après le front montant d'horloge dans notre cas.

Il aurait été possible de regrouper ces 2 instructions en une seule :

reg <= reg(6 downto 0) & d_in;

Processus combinatoires

La génération de la sortie match est un processus combinatoire, de même pour d_out, qui est équivalent à reg(7).

Ce module comporte en tout 3 processus :

  • le bloc process, processus explicite
  • les deux affectations concurrentes, processus implicites.

Tous les processus d'un module s'exécutent les uns en même temps que les autres. Mais l'intérieur du processus process est codé avec des instructions séquentielles.


Back to Top

Simulation

Premier exemple

Commençons par un premier exemple. Ouvrez un terminal (xterm), récupérez l'archive vhdl_first_steps.tgz en cliquant sur ce lien, et sauvez-la dans votre répertoire de travail.
Décompressez cette archive (tar zxvf vhdl_first_steps.tgz), et placez-vous dans le répertoire du premier exemple (cd ex1). Vous y trouverez deux fichiers :

Le fichier full_add.vhd ne contient que la description de l'additionneur. Pour vérifier que cette description est correcte, on peut en effectuer une simulation. Pour cela nous allons lui présenter des vecteurs d'entrée soigneusement choisis, et vérifier que les sorties correspondent bien à nos attentes.

Pour cela, nous allons modéliser (en vhdl toujours) un environnement de test. Pour cela, ouvrez le fichier testbench.vhd.


code testbench
Structure d'un testbench

Comme pour l'additionneur, l'environnement de test est un module. Mais ce module n'a ni entrées ni sorties : il est autonome, produit lui-même les signaux à envoyer à l'additionneur, et vérifie que les sorties de l'additionneur sont bien correctes, qui correspond au schéma suivant :

testbench

Environnement de test de l'additionneur.

Déclaration des signaux
Il nous faut déclarer une équipotentielle pour chaque entrée et sortie de l'additionneur.Les signaux connectés aux entrées de l'additionneur sont produits par le testbench de façon séquentielle (les uns après les autres).
Le composant prototype component de full_add doit être déclaré pour pouvoir instancier full_add et compiler le testbench indépendamment.
Instantiation de l'additionneur
L'additionneur full-add est instancié et a pour nom d'instance inst_additionneur_en_test



Génération des vecteurs de test

Nous ne testerons pas toutes les combinaisons d'entrées possibles, mais juste 4 d'entre elles. Entre chaque combinaison, nous ferons une pause (virtuelle) de 5ns, donnée par l'instruction wait for 5ns;

Notez qu'il n'y a pas de liste de sensibilité car le processus utilise l'instruction wait pour spécifier les points d'arrêt. Dans ce ca c'est lla valeur du temps physique time qui est utilisé comme point d'arrêt du processus.


La génération des vecteurs de test est produite de façon séquentielle, au moyen d'un processus ayant des points d'arrêts wait à chaque moment où les entrées doivent changer.

La simulation

Le module additionneur et son environnement de test étant écrit, reste à lancer effectivement la simulation. Pour cela, nous allons utiliser le simulateur vhdl modelsim.

Dans un premier temps il faut analyser (compiler) les fichiers. Avant toute chose il faut créer la bibliothèque work qui est la bibliothèque par défaut qui va contenir les résultats de compilation. Placez-vous dans le répertoire contenant les fichiers testbench.vhd et full_add.vhd et lancez la commande :

> vlib work

La compilation se fait avec les commandes suivantes :

> vcom full_add.vhd

> vcom testbench.vhd
 

Corriger d'éventuelles erreur avant de passer à la simulation.

Il faut maintenant créer un simulateur  de l'entité "test" de plus haut niveau décrite dans testbench.vhd.

Pour cela lancez la commande 

> vsim test

L'élaboration de test est effectuée et la fenêtre ModelSim apparaît pour lancer la simulation en graphique.

A partir de la console ModelSim, Utilisez la commande suivante pour demander le chronogramme des signaux de test :  

VSIM >  add wave  *

Une fenêtre contenant les signaux à tracer apparaît de façon à visualiser les chronogrammes.  Lancez l'execution de la simulation pendant 20 ns par 

VSIM > run 20 ns


Examinez les chronogrammes. Sont ils corrects ? 

Vous pouvez vous reporter au  didacticiel ModelSim simplifié pour utiliser les fonctionnalités de base de la simulation en mode graphique.

Simulation avancée

Dans le répertoire ex2, vous trouverez un autre exemple de système vhdl_second_exo.tgz en cliquant sur ce lien. Nous reprenons le code de l'additionneur 4 bits, mais sous 2 formes :

Dand l'environnement de test, testbench.vhd,  les 2 types d'additionneur sont instanciés et vérifiés. 

Exercice
  1. Analysez le code de testbench.vhd, en particulier :
      • la génération de l'horloge CLK
      • Le test de tous les cas sur les entrées
      • l'affichage des messages d'erreur en cas d'erreur
  2. Compilez et testez les 2 types d'additionneur avec Modelsim
    Les résultats sont-ils corrects ?

  3. Modifiez le testbench et remplacez le processus synchrone par un processus sensible aux entrées de l'additionneur 
    • Examinez les chronogrammes produits
    • Les simulateurs mentionnent des erreurs, mais jamais au même endroit, et les chronogrammes semblent justes.
    Avec vos connaissances sur les principes de simulation, expliquez pourquoi !

Back to Top

En résumé


Vous avez vu ici quelques exemples de modules en vhdl, comment instancier un composant et lui fournir rapidement quelques vecteurs de test.

L'objectif des prochains chapitres est d'étudier plus précisément l'utilisation et l'organisation du langage, les différents types de données disponibles, la façon de décrire structurellement un système (entité, architecture, signaux, ports), puis fonctionnellement (processus, événements).

Back to Top