Qu'est ce que le langage VHDL ? Comment programmer dasn ce langage ?

Introduction

Le vdhl est un langage informatique qui permet de programmer des cartes FPGA. Les cartes FPGA sont des cartes électroniques qui sont utilisés dans l’industrie pour vérifier que vos idées ou votre programme réponds bien à ce que vous souhaitez faire.

Le vdhl se programme sur différent logiciel : quartus prime (Intel) et vivado (xilinx) Lattice. Pour simuler le programme vdhl vous pouvez utiliser ModelSim.

Le VHDL fait abstraction de la technologie. Ce que l’on fait sur microchip on peut le faire sur n’importe quelle autre carte.

En VDHL il y a un très grand respect du type. On ne pas mettre un float dans une variable int par exemple. Ceci diffère du langage Arduino qui est un « langage un peu plus souple ». De plus le vhdl n’est pas sensitif à la casse, cela veut dire qu’écrire un petit ou un grand A ne change rien.

En VDHL, les indicateurs (signaux, variables, process, fonctions, entiers, architectures) doivent :

  • Commencer obligatoirement par une lettre,
  • Peuvent contenir des lettres, chiffre et -.

Découverte du langage

Le plus simple pour comprendre comment fonctionne le VDHL est de voir un exemple et de l’expliquer :

 

-------------------------------------------------------------
-- Liste des librairies et objets importés
-------------------------------------------------------------
library IEEE;
use IEEE. STD_LOGIC_1166.ALL;
---------------------------------------------------------------
--Déclaration de l'entité
--------------------------------------------------------------
entity porte_OU is 
port ( entree1 : in std_LOGIC;
       entree2 : in std_LOGIC;
	   sortie  : out std_logic);
end porte_OU;
-------------------------------------------------------------
--Architecture interne
-------------------------------------------------------------
architecture arch of porte_OU is
begin
	sortie <= entree1 OR entree2; -- on décrit la porte ou 
end arch; 

Le code que l’on vient d’afficher peut vous faire peur, mais on va le décortiquer ensemble afin de mieux comprendre son fonctionnement :

A) Les librairies

La première chose que l’on déclarer, ce sont les librairies. Elles contiennent des fonctions déjà construite qui vont nous être utile dans la suite du programme.

  • La librairie IEEE : C’est une librairie indispensable pour contrôler des composants en VDHL.
  • STD_LOGIC_1164 : Permet d’utiliser les composants de la librairie IEEE. Use est un mot qui permet d’utiliser la librairie et All permet d’importer toute la librairie.

B) Déclaration de l’entité

Dans la déclaration de l’entité vous devez déclarer les signaux qui vont vous être utile dans la suite de votre programme. L’entity est l’interface entre le monde extérieur et votre carte FGPA.

  • Port (A,B : in std_logic); : signaux caractérisées d’un mode et d’un type. Il y a 4 modes possibles ( in pour les entrées, out pour les sorties, inout pour les signaux bidirectionnel et le buffer pour les signaux de sortie réentrants). Il y a deux types de base pour les ports : scalaire : std_logic et vectoriel : std_logic_vector.

C) Architecture interne

L’architecture interne est le coeur du programme. C’est ici que l’on écrire la correpondance entre les entrées et les sorties.

  • Architecture arch of porte_OU is : c’est l’architecture interne, on l’écrit architecture nom de l’architecture, of nom de l’entité is
  • Signal entrees : std_logic_vector(1 to 2); : C’est les signaux internes entre l’architecture et begin. On écrit les signaux nomDuSignal : typedeSignal := valeurInitiale

Les Signaux

Un signal se déclare de cette façon : signal nomDuSignal : typeDeSignal := valeurInitiale;

Les signaux sont caractérisés par un mode et un type.

Les signaux peuvent déclarés sous forme de différents types : Les bits, booléen, STD_LOGIC et STD_LOGIC_VECTOR.

Le signal peut-être en entrée (in) et en sortie (out).

A) Les bits

Les bits peuvent prendre des valeurs 0 ou 1. Ils sont peu utilisés car insuffisant pour la simulation.

B) Booléen

Peut prendre la valeur vrai ou faux. Type de référence pour les structures conditionnelles.

 

C) Les entiers

Les entiers sont un type prédifinis dans VHDL, vous n’avez donc pas besoin d’appeler la libraire de fonction afin de l’utiliser. On peut déclarer des entiers de -2**31 à 2**31.

Voici un exemple :

E) STD_LOGIC

STD_LOGIC permet de créer une variable qui peut prendre 3 valeurs : 0,1 ou l’état Z.

Le signal peut prendre deux valeurs : Soit une valeur d’entrée comme ceci en écrivant in  :

Les signaux peuvent être aussi déclaré comme sortant  :

 

 

L’état z dans un signal est une valeur aléatoire qui se trouve entre 0 et 1. C’est très souvent une valeur que l’on veut éviter en électronique car on ne sait pas si c’est 1 ou 0. Néanmoins en VHDL, cet état est reconnu :

 

Le signal STD_LOGIC peut prender plusieurs valeurs différente que l’on vous a détaille dans un tableau :

ValeurDéfinition
‘U’Valeur inconnue, pas initialisé
‘X’Valeur inconnue forcé
‘0’0 forcé
‘1’1 forcé
‘Z’haute impédance  (pas connecté)
‘W’Valeur inconnue faible
‘L’0 faible
‘H’1 faible

F) STD_LOGIC_VECTOR

STD_LOGIC_VECTOR permet de déclarer un tableau de valeur. Vous pouvez l’implémentez de cette façon :

Il y a deux manière de déclarer un tableau, d’utiliser downto ou or :

En utilisant to, vous devez d’abord mettre la première valeur du tableau en premier et après le to la dernière valeur. En écrivant 0 to 1 le tableau aura une valeur.  Cette valeur se déclare après le “=”. Vous pouvez choisir entre la valeur 0 ou 1.

En utilisant downto, c’est l’inverse. On place d’abord le numéro le nombre le plus important au début et le plus petit nombre après le downto. Par exemple écrire 0 downto 3 n’aurait pas été possible.

Ici tableau(3) est le bit de poids fort et tableau(0) est le bit de poids faible.

S vous avez les mêmes valeurs dans un tableau, par exemple faire un tableau de dix zéro ou bien un tableau de trois 1, vous pouvez la fonction other Il y a deux manières de déclarer un tableau :

  • STD_LOGIC_VECTOR (3 downto 0) := (other => ”0”) Veut dire qu’il y aura un tableau de quatre 0.

L’agréga est pratique pour faire un grand tableau du même chiffre mais si vous souhaitez un 1 au milieu des 0 il faudra utiliser la première méthode.

Les nombres signés ou non signés

Il existe deux types de nombre : Les nombres signés et non signés.

  • Les nombres non signés (unsigned) : Ceux qui sont seulement entier positif. Une variable sur 8 bits peut prendre des valeurs entre 0 et 255.
  • Les nombres signés (signed) : Elle peut prendre des valeurs négatives et positives entre -127 et 128.

Avec STD_LOGIC, on ne sait pas si on a un nombre signé ou pas, c’est pour ça que l’on a besoin de la librairie NUMERIC_STD.

Un signal n'est pas une variable !

Dans un process, on peut trouver des affectations de signaux ou de variables. Contrairement aux variables, l’affectation du signal n’a pas un effet immédiat. On ne peut modifier que la valeur future du signal. Par défaut, c’est la valeur que prendra ce signal au prochain pas de simulation qui est affectée, valeur qui ne deviendra effective qu’après la fin du process.

Flots de données

Définition: on écrit explicitement les fonctions booléennes que l’on veut voir implémentées (à réserver aux plus petits circuits pour des raisons de lisibilité) ;

Domaine Concurrent : Le flot de donnée éxecutent les fonctions entre begin et end en domaine concurrent, cela veut que toute les instructions s’éxécutent en parallèle et il y n’y pas d’ordre. Ceci n’est pas vrai pour les modes comportemental et structurelle.

Le traitements en flots de données fonctionnent avec trois types d’affectations :

  • Affectation simple
  • Affectation conditionnel : when … else …
  • Affectation sélectives : with…select…

A) Affectation simple

On va maintenant voir un exemple pour mieux comprendre comment programmer en VDHL. Le premier exercice sera de faire le programme d’un demi-additionneur :

La première chose à faire est d’identifier les portes logiques du schéma : Celle qui se termine avec la sortie1 s’appelle  le XOR ou “ou exclusif” et la porte logique se terminant par Sortie2 est une porte ET (and).

On peut maintenant avoir le tableau logique de ces deux portes :

La porte XOR ou “ou exclusif” :

La porte ET ou “and” :

 

Entree1Entree2Sortie1
000
011
101
110

 

Entree1Entree2Sortie2
000
010
100
111

Voici ce que l’on peut en déduire en traduisant les portes logique en code VHDL :

Sortie1 <= Entree1 XOR Entree2;

Sortie2 <= Entree1 AND Entree2;

Voila ce que cela donne avec le programme VHDL au complet :

 

-- Demi Additionneur 

--Librairie
library ieee;
use iee.std_logic_1164.all;

--Entité
entity Demi_additionneur is 
port( 
		Entree1 : in std_logic;
		Entree2 : in std_logic;
		Sortie1 : out std_logic;
		Sortie2 : out std_logic);
end Demi_additionneur

--Architecture
architecture flots_de_donnee of Demi_additionneur is
begin 
		Sortie1<= Entree1 XOR Entree2;
		Sortie2 <= Entree1 AND Entree2;
end flots_de_donnee

B) Affectation conditionnel : when … else …

On va maintenant voir comment faire un programme avec l’affectation conditionnel, c’est à dire avec un when et else.

Voici un exemple de la structure du programme :

 

Comme on peut voir, nous avons un else ‘0’ à la fin qui fixe la sortie pour la combinaisons non explicité.

On va maintenant reprendre l’exemple du demi-additionneur afin de le programmer avec des when..else.

Pour cela on va reprendre le tableau des valeurs des portes logiques. On va chercher les différents cas pour lequel on obtient 1 en Sortie1 et 1 en Sortie2 :

La porte XOR ou “ou exclusif” :

La porte ET ou “and” :

 

Entree1Entree2Sortie1
000
011
101
110

 

Entree1Entree2Sortie2
000
010
100
111

Comme on peut voir, on obtient 1 en Sortie1 si l’entree1 vaut 0 et l’entree2 vaut 1 ou l’entree1 vaut 1 et l’entree2 vaut 0.

On obtient donc ceci :

Sortie1 <= ‘1’ when ((entree1 =’0′ AND entree2=’1′) OR (entree1 =’1′ AND entree2=’0′)) else ‘0’;

Comme on peut voir, on obtient en Sortie2 si l’entree1 et l’entree2 vallent 1.

On obtient donc ceci :

Sortie2 <= ‘1’ when (entree1 =’1′ AND entree2=’1′) else ‘0’;

Voici ce que cela donne avec le programme en entier :

--Demi Additionneur

--Librairie
library ieee;
use ieee.std_logic_1164.all;

--Entité
entity demi_additionneur is
port ( Entree1 : in std_LOGIC;
	   Entree2 : in std_LOGIC;
	   Sortie1 : out std_LOGIC;
	   Sortie2: out std_LOGIC);
end demi_additionneur;

--Architecture
architecture flots_de_donnee of demi_additionneur is
begin 
		
		Sortie1 <= '1' when ((entree1 ='0' AND entree2='1') OR (entree1 ='1' AND entree2='0')) else '0';
		Sortie2 <= '1' when (entree1 ='1' AND entree2='1') else '0';
end flots_de_donnee;

C) Affectation sélective : structure with…select

On va maintenant comment faire un programme avec l’affectation sélective et la structure with…select.

Voici un exemple de la structure d’un programme :

La structure se termine toujours par une ligne … when … others; fixant la sortie pour les combiniaisons
non explicitées.

On va maintenant reprendre l’exemple du demi-additionneur afin de le programmer avec des with..select. En reprenant le tableau de l’affectation conditionnel, on obtient ceci :

--Demi Additionneur

--Librairie
library ieee;
use ieee.std_logic_1164.all;

--Entité
entity demi_additionneur is
port ( Entree1 : in std_LOGIC;
	   Entree2 : in std_LOGIC;
	   Sortie1 : out std_LOGIC;
	   Sortie2: out std_LOGIC);
end demi_additionneur;

--Architecture
architecture flots_de_donnee of demi_additionneur is
signal entrees : std_LOGIC_vector(1 to 2);
begin 
		entrees <= Entree1 & Entree2;
		Sortie1 <= '1' when "01" or "10",
		           '0' when others;
		Sortie2 <= '1' when "11",
				   '0' when others;
end flots_de_donnee;

Comportemental

Défintion  : de manière très semblable à un langage de programmation informatique, on précise le fonctionnement voulu à l’aide d’une suite d’instructions de contrôles plus ou moins évoluées (conditions, boucles, etc.), dans un process.

Dans cette partie on va voir :

  • Description séquentielle : Process
  • Structure : if…else…end if;
  • Structure : if…then…elsif…then…else…end if;
  • Structure : case…

A) Description séquentielle : Process

Un process est une partie du programme ou les instructions sont exécutées séquentiellement (les unes à la suite des autres).

Voici comment on déclare un process : nom_du_process : process(liste_de_sensibilité)

Liste de sensibilité : liste des signaux dont le changement d’état lance l’exécution du process.

Si la de sensibilité est vide on enlève les parenthèses. Dans ce cas, le process est exécuté sans condition.

Point d'interrogation

Comment réveiller un process ?

Un process s’exécute (se reveille) quand un des signaux de sa liste de sensibilité change de valeur.

Une fois arrivé à la fin du process, celui-ci s’endort jusqu’à l’arrivée d’un événement sur un des signaux de sa liste de sensibilité.

 

nom_du_process : process(liste_de_sensibilite)
--Déclaration des variables interne au process
-- Déclaration des constantes
begin
-- Description séquentielle 
-- de code les uns à la suite des autres
end process nom_de_process;

B) Structure : if…else…end if;

La structure conditionnnelle if esle et endi if peut s’écrire en une ligne ou bien sur plusieurs ligne. Le else n’est pas obligatoire.

Sur une ligne :

if condition then action_du_then; else action_du_else; end if;

Sur plusieurs lignes :

if condition then
-- actions du then
end if;
if condition then
-- actions du then
else
-- actions du else
end if;

C) Structure if … then elsif then else … end if;

Sur une seule ligne  :

if condition then action_du_then elsif action_du_elsif then action_du_then else action_du_else end if;

Sur plusieurs lignes :

if condition then
-- actions du then
elsif condition_e then
-- actions du elsif then
else
-- actions du elsif else
end if;

Cette structure est assez similaire à celle du when then.

D) Structure case

 

Série de contrôles parallèles visant à vérifier une condition.

case signal_variable is
when val =>
-- actions du cas signal_variable = val
when vald to valf =>
-- actions du cas signal_variable compris entre vald et valf
when val1|val2|valn =>
-- actions du cas signal_variable = val1 ou val2 ou valn
when others =>
-- actions pour tous les autres cas
end case;

C’est similaire à la structure with..select.

On va maintenant voir un exemple de description comportemental avec un additionneur 1 bit :

Comme on peut voir cet additionneur possède plusieurs porte logique : deux portes XOR (ou exclusif), deux porte AND (ET exclusif) et une porte OR (ou exclusif).

Voici le tableau récapitulant les entrées et sorties de l’additionneur :

 

Entree1Entree2CinSortieCout
00000
00110
01010
01101
10010
10101
11001
11111
--------Additionneur-------
--Librairie
library ieee;
use ieee.std_logic_1164.all;

--Entité
entity Additionneur is
port ( Entree1 : in std_logic;
	   Entree2 : in std_logic;
	   Cin : in std_logic;
       Sortie : out std_logic; 
       Cout  :  out std_logic; 
end additonneur; 

--architecture 
architecture description_comportemental of additonneur is
			signal Entrees : std_logic_vector ( 2 downto 0);
			signal Sorties : std_logic_vector (1 downto 0);
			
begin
		Entrees <= Entree1 & Entree2 & Cin; -- On concaténate les entrées
		Cout <= Sorties(1);
		Sortie <= Sorties(0);
		
additonneur_process : process(Entrees)
begin 
 case Entrees is
	when "000" => Sorties <= "00";
	when "001" => Sorties <= "01";
	when "010" => Sorties <= "01";
	when "011" => Sorties <= "10";
	when "100" => Sorties <= "01";
	when "101" => Sorties <= "10";
	when "110" => Sorties <= "10";
	when "111" => Sorties <= "11";
	when others => Sorties <=,(others => 'x'); 
	
	end case; 
end process  additonneur_process;
end description_comportemental
	
		

Structurelle

Défintion: on décrit le circuit comme une série de boîtes noires interconnectées au moyen de signaux (utilisé pour des circuits moyens ou grands) ;

A) Instanciation composant

On va voir comment instancier des composants. L’instanciation c’est choisir un composant et l’utiliser comme instance dans la conception de circuits.

Voici comment on déclare une instance :  nom_de_l_instance : nom_du_composant

Port map : indique les signaux internes ou les ports qui sont connectés sur les ports du composant.

Voici comment on déclare un port map :

port map (
[
nom_du_port => ] expression,
[
nom_du_port => ] expression );