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 :
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 :
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.
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.
L’architecture interne est le coeur du programme. C’est ici que l’on écrire la correpondance entre les entrées et les sorties.
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).
Les bits peuvent prendre des valeurs 0 ou 1. Ils sont peu utilisés car insuffisant pour la simulation.
Peut prendre la valeur vrai ou faux. Type de référence pour les structures conditionnelles.
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 :
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 :
Valeur | Dé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 |
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 :
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.
Il existe deux types de nombre : Les nombres signés et non signés.
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.
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.
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 :
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” :
Entree1 | Entree2 | Sortie1 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
Entree1 | Entree2 | Sortie2 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
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
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” :
Entree1 | Entree2 | Sortie1 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
Entree1 | Entree2 | Sortie2 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
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;
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;
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 :
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.
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;
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;
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.
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 :
Entree1 | Entree2 | Cin | Sortie | Cout |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 0 | 1 | 1 | 0 |
0 | 1 | 0 | 1 | 0 |
0 | 1 | 1 | 0 | 1 |
1 | 0 | 0 | 1 | 0 |
1 | 0 | 1 | 0 | 1 |
1 | 1 | 0 | 0 | 1 |
1 | 1 | 1 | 1 | 1 |
--------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
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) ;
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 );