- Blocs always en logique combinatoire
- Syntaxe de base
- Affectations procédurales
- Types de données des variables
- Instructions conditionnelles et boucles
- Instructions conditionnelles
- Instruction if-else
- Instruction case
- Boucles
- Boucle for
- Boucle repeat
- Boucle while
- Boucle forever
- Principes généraux de codage des blocs always
- Erreurs courantes dans le code des circuits combinatoires
- Affectation d'une variable dans plusieurs blocs always
- Liste de sensibilité incomplète
- Branches et affectations de sortie incomplètes
- Principes d'utilisation des blocs always dans les circuits combinatoires
- Paramètres et constantes
- Constantes
- Paramètres
- Exemples de conception de circuits logiques combinatoires courants
- Multiplexeur
- Comparateur
- Décodeur
- Décodeur 3-8
- Codeur 8-3
- Codeur à priorité 8-3
- Décodeur pour afficheur à sept segments en hexadécimal
- Convertisseur binaire vers BCD
- Conversion en code Gray
- Codeur à double priorité
- Incémentateur BCD
- Circuit de comparaison
La sortie d'un circuit logique combinatoire à un instant donné ne dépend que de l'état combiné des entrées à cet instant, sans lien avec l'état antérieur du circuit. Le circuit est constitué uniquement de portes logiques, sans élément de mémoire ni boucle de rétroaction. Ce chapitre se concentre sur la description en HDL des circuits combinatoires de moyenne échelle courants, tels que les additionneurs, les comparateurs et les multiplexeurs.
Blocs always en logique combinatoire
- Le langage Verilog HDL utilise des instructions procédurales exécutées séquentiellement pour la description comportementale. Ces instructions sont encapsulées dans un bloc
alwaysouinitial, mais seul le blocalwayspeut être synthétisé pour générer un module matériel capable d'effectuer des opérations logiques ou de contrôle. - Un bloc
alwayspeut être vu comme une boîte noire contenant des descriptions procédurales internes. Les instructions procédurales comprennent diverses structures, mais beaucoup n'ont pas de correspondance matérielle directe. Un code mal écrit dans un blocalwaysconduit souvent à une implémentation inutilement complexe ou non synthétisable.
Syntaxe de base
La liste de sensibilité (@[liste de sensibilité]) est l'unique structure de contrôle temporel dans un bloc always synthétisable. Pour un circuit combinatoire, elle doit inclure toutes les signaux d'entrée. Le corps du module contient un nombre arbitraire d'instructions procédurales. Lorsqu'il n'y a qu'une seule instruction, les délimiteurs begin et end peuvent être omis. Les signaux de sensibilité sont de deux types : sensible au niveau pour les circuits combinatoires, et sensible à l'horloge pour les circuits séquentiels, à l'aide des mots-clés posedge (front montant) et negedge (front descendant).
Affectations procédurales
Les affectations procédurales ne peuvent être utilisées que dans les blocs always ou initial. Il existe deux types d'affectation :
- Affectation bloquante :
[nom_variable] = [expression] ;
L'expression est évaluée et la varible est mise à jour immédiatement. L'exécution de l'instruction suivante est bloquée jusqu'à la fin de l'affectation courante. - Affectation non bloquante :
[nom_variable] <= [expression] ;
L'expression est évaluée à l'instant de l'instruction, mais la mise à jour de la variable est différée à la fin du pas de temps du blocalways. Cette affectation ne bloque pas l'exécution des instructions suivantes.
Les principes d'utilisation de base sont : utiliser les affectations bloquantes pour la logique combinatoire et les affectations non bloquantes pour la logique séquentielle.
Types de données des variables
Dans les affectations procédurales, seuls certains types de variables peuvent être utilisés en partie gauche : reg, integer, real, time et realtime. Le type reg est similaire à wire, mais utilisé pour les sorties procédurales. Le type integer représente généralement un entier signé sur 32 bits et n'est généralement pas utilisé dans la synthèse. Les autres types sont destinés à la modélisation et à la simulation.
Un comparateur d'égalité 1 bit détermine si deux nombres binaires 1 bit a_entree et b_entree sont égaux. La sortie egal vaut 1 s'ils sont égaux, 0 sinon.
module comparateur_1bit (
input a_entree, b_entree,
output reg egal
);
reg temp1, temp2;
always @(a_entree, b_entree) begin
temp1 = ~a_entree & ~b_entree;
temp2 = a_entree & b_entree;
egal = temp1 | temp2;
end
endmodule
Les trois affectations bloquantes sont exécutées séquentiellement. L'ordre est crucial, car temp1 et temp2 doivent être calculées avant d'être utilisées. Pour la logique combinatoire, la liste de sensibilité doit inclure toutes les entrées. L'oubli d'un signal peut conduire à des incohérences entre la synthèse et la simulation. L'utilisation de always @* permet d'inclure automatiquement tous les signaux utilisés dans le bloc.
Ce code utilise des affectations procédurales pour calculer la fonction AND de trois entrées (a & b & c).
Instructions conditionnelles
Il existe deux types d'instructions conditionnelles : if-else et case. Ce sont des instructions séquentielles utilisées à l'intérieur d'un bloc always.
Instruction if-else
L'instruction if-else permet de choisir parmi deux branches alternatives.
Instruction case
Contrairement à l'instruction if qui offre deux branches, l'instruction case est une instruction à branches multiples. Elle est utile pour décrire des circuits à branchements multiples tels que des décodeurs, des sélecteurs de données ou des états. Il existe trois variantes : case, casez et casex. Dans casez, les valeurs z et ? sont traitées comme non pertinentes. Dans casex, les valeurs z, x et ? sont non pertinentes. En simulation, on préfère souvent utiliser ?. On distingue les instructions case complètes (toutes les valeurs possibles sont couvertes) et parallèles (pas de chevauchement).
Boucles
Verilog HDL propose quatre types de boucles pour contrôler l'exécution répétée d'instructions :
forever: exécute indéfiniment une instruction ; souvent utilisée dans un blocinitialpour générer une horloge.repeat: exécute une instruction un nombre déterminé de fois.while: exécute une instruction tant qu'une condition est vraie. Si la condition est fausse dès le départ, l'instruction n'est jamais exécutée.for: boucle conditionnelle.
Principes généraux de codage des blocs always
Lors de l'écriture de code synthétisable, il est essentiel de comprendre comment les structures du langage se mappent sur le matériel, en particulier pour les blocs always. L'objectif est de décrire un circuit matériel, et non un algorithme séquentiel comme en C.
Erreurs courantes dans le code des circuits combinatoires
- Affectation d'une variable dans plusieurs blocs always : Bien que cela puisse simuler correctement pour un circuit asynchrone, le code n'est pas synthétisable.
- Liste de sensibilité incomplète : Pour un circuit combinatoire, toutes les entrées doivent figurer dans la liste.
- Branches et affectations de sortie incomplètes : Une variable non affectée dans toutes les branches conserve sa valeur précédente, ce qui peut être interprété comme une boucle de rétroaction ou un élément de mémoire (latch) lors de la synthèse. Pour éviter cela, toutes les sorties doivent recevoir une affectation explicite dans toutes les conditions. Deux solutions courantes sont d'ajouter une branche
elseexplicite ou de définir une valeur par défaut au début du bloc.
Pour une instruction case, si toutes les valeurs possibles ne sont pas couvertes (non complète), le même problème de latch peut survenir. On y remédie en utilisant un mot-clé default ou en assignant une valeur par défaut avant l'instruction.
Principes d'utilisation des blocs always dans les circuits combinatoires
À respecter :
- Affecter une variable dans un seul bloc
always. - Utiliser des affectations bloquantes pour la logique combinatoire.
- Utiliser
@*pour inclure automatiquement tous les signaux d'entrée. - Assurer que toutes les branches d'une instruction
ifoucasesont présentes. - Assurer que chaque branche affecte toutes les sorties.
- Assigner des valeurs par défaut au début du bloc pour couvrir les cas manquants.
- Décrire explicitement dans le code les instructions
casecomplètes et parallèles. - Comprendre quel circuit est synthétisé à partir des différentes structures de contrôle.
- Réfléchir en termes de matériel, pas en termes de code C.
Paramètres et constantes
Constantes
Dans un code HDL, les valeurs constantes sont souvent utilisées dans les expressions et les limites de tableaux. Il est bon de remplacer les valeurs fixes par des constantes symboliques pour améliorer la lisibilité et la maintenance. Verilog HDL utilise le mot-clé localparam pour déclarer des constantes locales, par exemple pour la largeur d'un bus de données :
localparam LARGEUR_DONNEES = 8,
PLAGE_DONNEES = 2**LARGEUR_DONNEES - 1;
Les expressions telles que 2**LARGEUR_DONNEES-1 sont évaluées à la compilation et ne génèrent pas de circuit physique.
Paramètres
Les modules Verilog peuvent être instanciés en tant que composants dans une conception plus large. Le mécanisme des parameter permet de passer des informations à un module, le rendant ainsi générique et réutilisable. Les paramètres sont constants à l'intérieur du module.
Exemples de conception de circuits logiques combinatoires courants
Multiplexeur
Un multiplexeur (MUX) est un circuit combinatoire à plusieurs entrées et une seule sortie. Il fonctionne comme un interrupteur numérique, sélectionnant l'une des n entrées en fonction d'un signal de commande pour la diriger vers la sortie commune.
Comparateur
Un comparateur est un circuit combinatoire qui détermine la relation de grandeur (égalité, supériorité, infériorité) entre deux valeurs numériques.
Décodeur
Décodeur 3-8
Un décodeur possède n entrées et 2^n sorties. Chaque sortie correspond à une combinaison binaire possible des entrées. Typiquement, une seule sortie est active à la fois.
Codeur 8-3
Un codeur est l'inverse d'un décodeur. Il possède 2^n entrées et n sorties. La sortie code binaire sur n bits l'indice de l'entrée active.
Codeur à priorité 8-3
Ce type de codeur traite les entrées par ordre de priorité.
Décodeur pour afficheur à sept segments en hexadécimal
Ce circuit convertit un code hexadécimal sur 4 bits en signaux de commande pour allumer les segments d'un afficheur à sept segments.
Convertisseur binaire vers BCD
Ce circuit convertit une représentation binaire naturelle en code BCD (Binary-Coded Decimal).
Conversion en code Gray
Le code Gray est un code à distance unitaire où deux valeurs adjacentes ne diffèrent que d'un seul bit.
// Convertisseur 4 bits binaire vers Gray
module bin_vers_gray_4 (
input [3:0] bin,
output reg [3:0] gray
);
always @* begin
gray[3] = bin[3];
gray[2] = bin[3] ^ bin[2];
gray[1] = bin[2] ^ bin[1];
gray[0] = bin[1] ^ bin[0];
end
endmodule
Codeur à double priorité
Un codeur à double priorité renvoie le code de la requête de plus haute priorité et celui de la deuxième priorité. Pour 15 entrées req[14:0] (où le bit le plus élevé a la priorité la plus haute), les sorties premiere[3:0] et seconde[3:0] contiennent les codes binaires correspondants. Si aucune entrée n'est active, les deux sorties valent 4'b0000.
// Codeur à double priorité à 15 entrées
module codeur_double_priorite_15 (
input [14:0] req,
output reg [3:0] premiere,
output reg [3:0] seconde
);
always @* begin
premiere = 4'b0000;
seconde = 4'b0000;
// Étape 1 : Trouver la priorité la plus haute
if (req[14]) premiere = 4'b1110;
else if (req[13]) premiere = 4'b1101;
else if (req[12]) premiere = 4'b1100;
// ... (suite pour les bits 11 à 0)
// Étape 2 : Trouver la seconde priorité (en excluant le bit de première priorité)
case (premiere)
4'b1110: begin // Bit 14 est premier, chercher dans 0-13
if (req[13]) seconde = 4'b1101;
else if (req[12]) seconde = 4'b1100;
// ...
else seconde = 4'b0000;
end
// ... (cas pour les autres valeurs de premiere)
default: seconde = 4'b0000;
endcase
end
endmodule
Incémentateur BCD
Le code BCD (8421) représente chaque chiffre décimal (0-9) par 4 bits. Un incrémenteur BCD à trois chiffres (centaines, dizaines, unités) ajoute 1 au chiffre des unités. Si ce chiffre dépasse 9, il revient à 0 et les dizaines sont incrémentées, et ainsi de suite.
// Incrémenteur BCD à 3 chiffres
module incrementeur_bcd_3 (
input [3:0] centaines,
input [3:0] dizaines,
input [3:0] unites,
output reg [3:0] centaines_sortie,
output reg [3:0] dizaines_sortie,
output reg [3:0] unites_sortie
);
always @* begin
centaines_sortie = centaines;
dizaines_sortie = dizaines;
unites_sortie = unites;
if (unites < 4'b1001) begin
unites_sortie = unites + 1'b1;
end else begin
unites_sortie = 4'b0000;
if (dizaines < 4'b1001) begin
dizaines_sortie = dizaines + 1'b1;
end else begin
dizaines_sortie = 4'b0000;
if (centaines < 4'b1001) begin
centaines_sortie = centaines + 1'b1;
end else begin
centaines_sortie = 4'b0000;
end
end
end
end
endmodule
Circuit de comparaison
Un exemple de circuit de comparaison utilisant l'instruction case : entrées de 2 bits a et b, sortie maximum représentant la valeur la plus grande des deux. Si les valeurs sont égales, la sortie est identique.
// Comparateur 2 bits (maximum) avec case
module comparateur_2bits_max (
input [1:0] a,
input [1:0] b,
output reg [1:0] maximum
);
always @* begin
case ({a, b})
4'b0000: maximum = 2'b00;
4'b0001: maximum = 2'b01;
4'b0010: maximum = 2'b10;
4'b0011: maximum = 2'b11;
4'b0100: maximum = 2'b01;
4'b0101: maximum = 2'b01;
4'b0110: maximum = 2'b10;
4'b0111: maximum = 2'b11;
4'b1000: maximum = 2'b10;
4'b1001: maximum = 2'b10;
4'b1010: maximum = 2'b10;
4'b1011: maximum = 2'b11;
4'b1100: maximum = 2'b11;
4'b1101: maximum = 2'b11;
4'b1110: maximum = 2'b11;
4'b1111: maximum = 2'b11;
default: maximum = 2'b00;
endcase
end
endmodule