Recherche…


Introduction

Dans cette rubrique, nous proposons une méthode simple pour concevoir correctement des circuits numériques simples avec VHDL. La méthode est basée sur des diagrammes graphiques et un principe facile à retenir:

Pensez d'abord au matériel, code VHDL ensuite

Il est destiné aux débutants en conception de matériel numérique utilisant VHDL, avec une compréhension limitée de la sémantique de synthèse du langage.

Remarques

La conception de matériel numérique utilisant VHDL est simple, même pour les débutants, mais il y a quelques points importants à connaître et un petit ensemble de règles à respecter. L'outil utilisé pour transformer une description VHDL en matériel numérique est un synthétiseur logique. La sémantique du langage VHDL utilisé par les synthétiseurs logiques est assez différente de la sémantique de simulation décrite dans le Manuel de référence du langage (LRM). Pire encore: il n'est pas standardisé et varie selon les outils de synthèse.

La méthode proposée introduit plusieurs limitations importantes par souci de simplicité:

  • Aucune bascule déclenchée par le niveau.
  • Les circuits sont synchrones sur le front montant d'une seule horloge.
  • Aucune réinitialisation ou définition asynchrone.
  • Pas de lecteur multiple sur les signaux résolus.

L'exemple de diagramme de blocs , le premier d'une série de trois, présente brièvement les bases du matériel numérique et propose une courte liste de règles pour concevoir un schéma de principe d'un circuit numérique. Les règles aident à garantir une traduction directe du code VHDL qui simule et synthétise comme prévu.

L'exemple de codage explique la traduction d'un diagramme en code VHDL et l'illustre sur un circuit numérique simple.

Enfin, l'exemple du concours de design de John Cooley montre comment appliquer la méthode proposée à un exemple plus complexe de circuit numérique. Il précise également les limitations introduites et en assouplit certains.

Diagramme

Le matériel numérique est construit à partir de deux types de primitives matérielles:

  • Portes combinatoires (inverseurs et / ou xor, adders complets 1 bit, multiplexeurs 1 bit ...) Ces portes logiques effectuent un calcul booléen simple sur leurs entrées et produisent une sortie. Chaque fois qu'une de leurs entrées change, elles commencent à propager des signaux électriques et, après un court délai, la sortie se stabilise à la valeur résultante. Le délai de propagation est important car il est fortement lié à la vitesse à laquelle le circuit numérique peut fonctionner, c'est-à-dire sa fréquence d'horloge maximale.
  • Eléments de mémoire (loquets, D-Flip-Flops, RAMs ...). Contrairement aux portes logiques combinatoires, les éléments de mémoire ne réagissent pas immédiatement au changement de leurs entrées. Ils ont des entrées de données, des entrées de contrôle et des sorties de données. Ils réagissent sur une combinaison particulière d'entrées de contrôle, et non sur un changement de leurs entrées de données. La bascule D déclenchée par un front montant, par exemple, possède une entrée d'horloge et une entrée de données. Sur chaque front montant de l'horloge, les données sont échantillonnées et copiées sur la sortie de données qui reste stable jusqu'au prochain front montant de l'horloge, même si l'entrée de données change entre les deux.

Un circuit matériel numérique est une combinaison de logique combinatoire et d'éléments de mémoire. Les éléments de mémoire ont plusieurs rôles. L'une d'elles est de permettre de réutiliser la même logique combinatoire pour plusieurs opérations consécutives sur des données différentes. Les circuits utilisant ceci sont fréquemment appelés circuits séquentiels . La figure ci-dessous montre un exemple de circuit séquentiel qui accumule des valeurs entières en utilisant le même additionneur combinatoire, grâce à un registre déclenché par front montant. C'est aussi notre premier exemple de diagramme.

Un circuit séquentiel

La tuyauterie est une autre utilisation courante des éléments de mémoire et la base de nombreuses architectures de microprocesseurs. Il vise à augmenter la fréquence d'horloge d'un circuit en divisant un traitement complexe en une succession d'opérations plus simples et en parallélisant l'exécution de plusieurs traitements consécutifs:

Tuyauterie d'un traitement combinatoire complexe

Le diagramme est une représentation graphique du circuit numérique. Cela aide à prendre les bonnes décisions et à bien comprendre la structure globale avant de procéder au codage. C'est l'équivalent des phases d'analyse préliminaire recommandées dans de nombreuses méthodes de conception de logiciels. Les concepteurs expérimentés sautent fréquemment cette phase de conception, du moins pour les circuits simples. Si vous êtes un débutant dans la conception de matériel numérique, et si vous voulez coder un circuit numérique dans VHDL, adopter les 10 règles simples ci-dessous pour dessiner votre diagramme devrait vous aider à bien faire les choses:

  1. Entourez votre dessin d'un grand rectangle. C'est la limite de votre circuit. Tout ce qui traverse cette limite est un port d'entrée ou de sortie. L'entité VHDL décrira cette limite.
  2. Séparez clairement les registres déclenchés par la tranche (par exemple les blocs carrés) de la logique combinatoire (par exemple, les blocs ronds). En VHDL, ils seront traduits en processus mais de deux types très différents: synchrones et combinatoires.
  3. N'utilisez pas de loquets déclenchés par le niveau, utilisez uniquement des registres déclenchés sur le front montant. Cette contrainte ne provient pas de VHDL, parfaitement utilisable pour modéliser les verrous. C'est juste un conseil raisonnable pour les débutants. Les verrous sont moins souvent nécessaires et leur utilisation pose de nombreux problèmes que nous devrions probablement éviter, du moins pour nos premiers modèles.
  4. Utilisez la même horloge pour tous vos registres déclenchés sur le front montant. Là encore, cette contrainte est là pour simplifier. Il ne provient pas de VHDL, parfaitement utilisable pour modéliser des systèmes multi-horloges. Nommez l' clock . Il vient de l'extérieur et est une entrée de tous les blocs carrés et seulement d'eux. Si vous le souhaitez, ne représentez même pas l'horloge, il en va de même pour tous les blocs carrés et vous pouvez le laisser implicite dans votre diagramme.
  5. Représente les communications entre les blocs avec des flèches nommées et orientées. Pour le bloc dont provient une flèche, la flèche est une sortie. Pour le bloc auquel une flèche va, la flèche est une entrée. Toutes ces flèches deviendront des ports de l'entité VHDL, si elles traversent le grand rectangle ou les signaux de l'architecture VHDL.
  6. Les flèches ont une seule origine mais elles peuvent avoir plusieurs destinations. En effet, si une flèche avait plusieurs origines, nous créerions un signal VHDL avec plusieurs pilotes. Ceci n'est pas complètement impossible mais nécessite un soin particulier pour éviter les courts-circuits. Nous allons donc éviter cela pour l'instant. Si une flèche a plusieurs destinations, fourchez la flèche autant de fois que nécessaire. Utilisez des points pour distinguer les traversées connectées et non connectées.
  7. Certaines flèches proviennent de l'extérieur du grand rectangle. Ce sont les ports d'entrée de l'entité. Une flèche d'entrée ne peut également être la sortie d'aucun de vos blocs. Ceci est imposé par le langage VHDL: les ports d'entrée d'une entité peuvent être lus mais pas écrits. C'est encore pour éviter les courts-circuits.
  8. Des flèches sortent. Ce sont les ports de sortie. Dans les versions VHDL antérieures à 2008, les ports de sortie d'une entité peuvent être écrits mais pas lus. Une flèche de sortie doit donc avoir une seule origine et une seule destination: l'extérieur. Pas de fourche sur les flèches de sortie, une flèche de sortie ne peut pas être aussi l'entrée d'un de vos blocs. Si vous souhaitez utiliser une flèche de sortie comme entrée pour certains de vos blocs, insérez un nouveau bloc rond pour le diviser en deux parties: la partie interne, avec autant de fourchettes que vous le souhaitez, et la flèche de sortie provenant du nouveau bloquer et va dehors. Le nouveau bloc deviendra une simple affectation continue dans VHDL. Une sorte de renommage transparent. Depuis VHDL 2008, les ports peuvent également être lus.
  9. Toutes les flèches qui ne vont pas ou ne vont pas de / vers l'extérieur sont des signaux internes. Vous les déclarerez tous dans l'architecture VHDL.
  10. Chaque cycle du diagramme doit comporter au moins un bloc carré. Ce n'est pas dû à VHDL. Il provient des principes de base de la conception de matériel numérique. Les boucles combinatoires doivent absolument être évitées. Sauf dans de très rares cas, ils ne produisent aucun résultat utile. Et un cycle du diagramme qui ne comprendrait que des blocs ronds serait une boucle combinatoire.

N'oubliez pas de vérifier soigneusement la dernière règle, elle est aussi essentielle que les autres mais peut être un peu plus difficile à vérifier.

À moins que vous ayez absolument besoin de fonctionnalités que nous avons exclues pour l'instant, comme les verrous, les horloges multiples ou les signaux comportant plusieurs pilotes, vous devriez facilement dessiner un schéma fonctionnel de votre circuit conforme aux 10 règles. Si ce n'est pas le cas, le problème vient probablement du circuit que vous souhaitez, pas du VHDL ou du synthétiseur logique. Et cela signifie probablement que le circuit que vous voulez n'est pas du matériel numérique.

L'application des 10 règles à notre exemple de circuit séquentiel conduirait à un schéma fonctionnel tel que:

Schéma fonctionnel retravaillé du circuit séquentiel

  1. Le grand rectangle autour du diagramme est traversé par 3 flèches, représentant les ports d'entrée et de sortie de l'entité VHDL.
  2. Le diagramme a deux blocs (combinatoires) ronds - l'additionneur et le bloc de renommage de sortie - et un bloc (synchrone) carré - le registre.
  3. Il utilise uniquement des registres à déclenchement par front.
  4. Il n'y a qu'une seule horloge, nommée clock et nous utilisons uniquement son front montant.
  5. Le diagramme a cinq flèches, une avec une fourchette. Ils correspondent à deux signaux internes, deux ports d'entrée et un port de sortie.
  6. Toutes les flèches ont une origine et une destination sauf la flèche nommée Sum qui a deux destinations.
  7. Les flèches Data_in et Clock sont nos deux ports d'entrée. Ils ne sont pas produits par nos propres blocs.
  8. La flèche Data_out est notre port de sortie. Afin d'être compatible avec les versions de VHDL antérieures à 2008, nous avons ajouté un bloc de renommage supplémentaire (Round) entre Sum et Data_out . Ainsi, Data_out a exactement une source et une destination.
  9. Sum et Next_sum sont nos deux signaux internes.
  10. Il y a exactement un cycle dans le graphique et il comprend un bloc carré.

Notre diagramme est conforme aux 10 règles. L'exemple de codage détaillera comment traduire ce type de diagramme en VHDL.

Codage

Cet exemple est le deuxième d'une série de 3. Si vous ne l'avez pas encore fait, lisez d'abord l'exemple du diagramme .

Avec un diagramme conforme aux 10 règles (voir l'exemple du diagramme ), le codage VHDL devient simple:

  • le grand rectangle environnant devient l'entité VHDL,
  • les flèches internes deviennent des signaux VHDL et sont déclarées dans l'architecture,
  • chaque bloc carré devient un processus synchrone dans le corps de l'architecture,
  • chaque bloc rond devient un processus combinatoire dans le corps de l'architecture.

Illustrons ceci sur le schéma synoptique d'un circuit séquentiel:

Un circuit séquentiel

Le modèle VHDL d'un circuit comprend deux unités de compilation:

  • L'entité qui décrit le nom du circuit et son interface (noms des ports, directions et types). C'est une traduction directe du grand rectangle environnant du diagramme. En supposant que les données sont des nombres entiers et que l' clock utilise le bit type VHDL (deux valeurs uniquement: '0' et '1' ), l'entité de notre circuit séquentiel pourrait être:
entity sequential_circuit is
  port(
    Data_in:  in  integer;
    Clock:    in  bit;
    Data_out: out integer
  );
end entity sequential_circuit;
  • L'architecture qui décrit les composants internes du circuit (ce qu'il fait). C'est là que les signaux internes sont déclarés et que tous les processus sont instanciés. Le squelette de l'architecture de notre circuit séquentiel pourrait être:
architecture ten_rules of sequential_circuit is
  signal Sum, Next_sum: integer;
begin
  <...processes...>
end architecture ten_rules;

Nous avons trois processus à ajouter au corps de l'architecture, un synchrone (bloc carré) et deux combinatoires (blocs ronds).

Un processus synchrone ressemble à ceci:

process(clock)
begin
  if rising_edge(clock) then
    o1 <= i1;
    ...
    ox <= ix;
  end if;
end process;

i1, i2,..., ix sont toutes les flèches qui entrent dans le bloc carré correspondant du diagramme et o1, ..., ox sont toutes les flèches qui génèrent le bloc carré correspondant du diagramme. Absolument rien ne sera changé, sauf les noms des signaux, bien sûr. Rien. Pas même un seul personnage.

Le processus synchrone de notre exemple est donc:

  process(clock)
  begin
    if rising_edge(clock) then
      Sum <= Next_sum;
    end if;
  end process;

Ce qui peut être traduit de manière informelle en: si l' clock change, et alors seulement, si le changement est un front montant ( '0' à '1' ), attribuez la valeur du signal Next_sum au signal Sum .

Un processus combinatoire ressemble à ceci:

process(i1, i2,... , ix)
  variable v1: <type_of_v1>;
  ...
  variable vy: <type_of_vy>;
begin
  v1 := <default_value_for_v1>;
  ...
  vy := <default_value_for_vy>;
  o1 <= <default_value_for_o1>;
  ...
  oz <= <default_value_for_oz>;
  <statements>
end process;

i1, i2,..., in sont toutes les flèches qui entrent dans le bloc rond correspondant du diagramme. tout et pas plus. Nous n'oublierons aucune flèche et nous n’ajouterons rien à la liste.

v1, ..., vy sont des variables dont nous pouvons avoir besoin pour simplifier le code du processus. Ils ont exactement le même rôle que dans tout autre langage de programmation impératif: contenir des valeurs temporaires. Ils doivent absolument tous être assignés avant d'être lus. Si nous ne le garantissons pas, le processus ne sera plus combinatoire puisqu'il modélisera le type d'éléments de mémoire pour conserver la valeur de certaines variables d'une exécution de processus à l'autre. C'est la raison des instructions vi := <default_value_for_vi> au début du processus. Notez que <default_value_for_vi> doit être une constante. Sinon, si ce sont des expressions, nous pourrions utiliser accidentellement des variables dans les expressions et lire une variable avant de l’affecter.

o1, ..., om sont toutes les flèches qui génèrent le bloc rond correspondant de votre diagramme. tout et pas plus. Ils doivent absolument être tous affectés au moins une fois au cours de l'exécution du processus. Comme les structures de contrôle VHDL ( if , case ...) peuvent très facilement empêcher l'attribution d'un signal de sortie, il est fortement conseillé d'attribuer chacune d'entre elles, sans condition, à une valeur constante <default_value_for_oi> au début du processus. De cette façon, même si une instruction if masque une affectation de signal, elle aura de toute façon reçu une valeur.

Absolument rien ne sera changé pour ce squelette VHDL, sauf les noms des variables, le cas échéant, les noms des entrées, les noms des sorties, les valeurs des constantes <default_value_for_..> et des <statements> . N'oubliez pas une seule attribution de valeur par défaut, si vous faites la synthèse, vous en déduirez les éléments de mémoire indésirables (les verrous les plus probables) et le résultat ne sera pas ce que vous vouliez initialement.

Dans notre exemple de circuit séquentiel, le processus additionneur combinatoire est:

  process(Sum, Data_in)
  begin
    Next_sum <= 0;
    Next_sum <= Sum + Data_in;
  end process;

Lesquels peuvent être traduits de manière informelle en: si Sum ou Data_in (ou les deux) changent, affectez la valeur 0 pour signaler Next_sum , puis attribuez-lui à nouveau la valeur Sum + Data_in .

Comme la première affectation (avec la valeur par défaut constante 0 ) est immédiatement suivie d'une autre affectation qui la remplace, nous pouvons simplifier:

  process(Sum, Data_in)
  begin
    Next_sum <= Sum + Data_in;
  end process;

Le deuxième processus combinatoire correspond au bloc que nous avons ajouté sur une flèche de sortie avec plusieurs destinations afin de se conformer aux versions VHDL antérieures à 2008. Son code est simplement:

  process(Sum)
  begin
    Data_out <= 0;
    Data_out <= Sum;
  end process;

Pour la même raison que pour l'autre processus combinatoire, nous pouvons le simplifier comme suit:

  process(Sum)
  begin
    Data_out <= Sum;
  end process;

Le code complet du circuit séquentiel est:

-- File sequential_circuit.vhd
entity sequential_circuit is
  port(
    Data_in:  in  integer;
    Clock:    in  bit;
    Data_out: out integer
  );
end entity sequential_circuit;

architecture ten_rules of sequential_circuit is
  signal Sum, Next_sum: integer;
begin
  process(clock)
  begin
    if rising_edge(clock) then
      Sum <= Next_sum;
    end if;
  end process;

  process(Sum, Data_in)
  begin
    Next_sum <= Sum + Data_in;
  end process;

  process(Sum)
  begin
    Data_out <= Sum;
  end process;
end architecture ten_rules;

Note: nous pourrions écrire les trois processus dans n'importe quel ordre, cela ne changerait rien au résultat final en simulation ou en synthèse. En effet, les trois processus sont des instructions concurrentes et VHDL les traite comme s'ils étaient réellement parallèles.

Concours de design de John Cooley

Cet exemple est directement dérivé du concours de design de John Cooley à SNUG'95 (réunion du groupe d'utilisateurs de Synopsys). Le concours était destiné à opposer les concepteurs VHDL et Verilog sur le même problème de conception. Ce que John avait en tête était probablement de déterminer quelle langue était la plus efficace. Les résultats ont été que 8 des 9 concepteurs de Verilog ont réussi à terminer le concours de design, mais aucun des 5 concepteurs de VHDL n’a pu le faire. Espérons que, en utilisant la méthode proposée, nous ferons un meilleur travail.

Caractéristiques

Notre objectif est de concevoir en VHDL synthétisable (entité et architecture) un compteur modulaire synchrone up-3, down-by-5, loadable, avec sortie carry, sortie emprunt et parité. Le compteur est un compteur non signé de 9 bits, il se situe donc entre 0 et 511. La spécification d'interface du compteur est donnée dans le tableau suivant:

prénom Largeur de bit Direction La description
L'HORLOGE 1 Contribution Horloge principale; le compteur est synchronisé sur le front montant de l'HORLOGE
DI 9 Contribution Bus d'entrée de données; le compteur est chargé avec DI lorsque UP et DOWN sont tous deux faibles
UP 1 Contribution Commande de compte par 3; lorsque UP est haut et DOWN est bas, le compteur s'incrémente de 3, entourant sa valeur maximale (511)
VERS LE BAS 1 Contribution Commande décompte par 5; lorsque DOWN est haut et que UP est bas, le compteur décroît de 5, entourant sa valeur minimale (0)
CO 1 Sortie Exécuter le signal; élevé uniquement en comptant au-delà de la valeur maximale (511) et en faisant ainsi le tour
BO 1 Sortie Emprunter le signal; élevé uniquement lorsque le compte à rebours est inférieur à la valeur minimale (0)
FAIRE 9 Sortie Bus de sortie; la valeur actuelle du compteur; lorsque UP et DOWN sont tous deux élevés, le compteur conserve sa valeur
PO 1 Sortie Signal de sortie de parité; high lorsque la valeur actuelle du compteur contient un nombre pair de 1

Lorsque vous comptez au-delà de sa valeur maximale ou lorsque vous comptez en dessous de sa valeur minimale, le compteur se déroule comme suit:

Valeur actuelle du compteur UP DOWN Contrer la valeur suivante CO suivant Suivant BO Prochaine PO
X 00 DI 0 0 parité (DI)
X 11 X 0 0 parité (x)
0 ≤ x ≤ 508 dix x + 3 0 0 parité (x + 3)
509 dix 0 1 0 1
510 dix 1 1 0 0
511 dix 2 1 0 0
5 ≤ x ≤ 511 01 x-5 0 0 parité (x − 5)
4 01 511 0 1 0
3 01 510 0 1 1
2 01 509 0 1 1
1 01 508 0 1 0
0 01 507 0 1 1

Diagramme

Sur la base de ces spécifications, nous pouvons commencer à concevoir un diagramme. Représentons d'abord l'interface:

L'interface externe

Notre circuit comporte 4 entrées (dont l'horloge) et 4 sorties. L'étape suivante consiste à décider combien de registres et de blocs combinatoires nous utiliserons et quels seront leurs rôles. Pour cet exemple simple, nous allons dédier un bloc combinatoire au calcul de la prochaine valeur du compteur, du résultat et de l’emprunt. Un autre bloc combinatoire sera utilisé pour calculer la valeur suivante de la parité. Les valeurs actuelles du compteur, de l'exécution et de l'emprunt seront stockées dans un registre tandis que la valeur actuelle de la parité sera stockée dans un registre distinct. Le résultat est indiqué sur la figure ci-dessous:

Deux blocs combinatoires et deux registres

La vérification de la conformité du diagramme avec nos 10 règles de conception est rapide:

  1. Notre interface externe est correctement représentée par le grand rectangle environnant.
  2. Nos 2 blocs combinatoires (ronds) et nos 2 registres (carrés) sont clairement séparés.
  3. Nous utilisons uniquement des registres déclenchés par un front montant.
  4. Nous utilisons une seule horloge.
  5. Nous avons 4 flèches internes (signaux), 4 flèches d'entrée (ports d'entrée) et 4 flèches de sortie (ports de sortie).
  6. Aucune de nos flèches n'a plusieurs origines. Trois ont plusieurs destinations ( clock , ncnt et do ).
  7. Aucune de nos 4 flèches d'entrée n'est une sortie de nos blocs internes.
  8. Trois de nos flèches de sortie ont exactement une origine et une destination. Mais do a 2 destinations: l'extérieur et l' un de nos blocs combinatoires. Cela viole la règle numéro 8 et doit être corrigé en insérant un nouveau bloc combinatoire si nous voulons nous conformer aux versions VHDL antérieures à 2008:

Un bloc combinatoire supplémentaire

  1. Nous avons maintenant exactement 5 signaux internes ( cnt , nco , nbo , ncnt et npo ).
  2. Il n'y a qu'un cycle dans le diagramme, formé par cnt et ncnt . Il y a un bloc carré dans le cycle.

Codage dans les versions VHDL antérieures à 2008

La traduction de notre diagramme dans VHDL est simple. La valeur actuelle du compteur va de 0 à 511, nous allons donc utiliser un signal bit_vector 9 bits pour le représenter. La seule subtilité vient de la nécessité d'effectuer des opérations binaires (comme le calcul de la parité) et des opérations arithmétiques sur les mêmes données. Le package standard numeric_bit de la bibliothèque ieee résout ce problème: il déclare un type unsigned avec exactement la même déclaration que bit_vector et surcharge les opérateurs arithmétiques de sorte qu'ils prennent tout mélange de unsigned et d'entiers. Afin de calculer le résultat et l'emprunt, nous utiliserons une valeur temporaire unsigned 10 bits.

Les déclarations de la bibliothèque et l'entité:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

Le squelette de l'architecture est:

architecture arc1 of cooley is
  signal cnt:  unsigned(8 downto 0);
  signal ncnt: unsigned(8 downto 0);
  signal nco:  bit;
  signal nbo:  bit;
  signal npo:  bit;
begin
    <...processes...>
end architecture arc1;

Chacun de nos 5 blocs est modélisé comme un processus. Les processus synchrones correspondant à nos deux registres sont très faciles à coder. Nous utilisons simplement le modèle proposé dans l'exemple de codage . Le registre qui stocke l'indicateur de parité, par exemple, est codé:

  poreg: process(clock)
  begin
    if rising_edge(clock) then
      po <= npo;
    end if;
  end process poreg;

et l'autre registre qui stocke co , bo et cnt :

  cobocntreg: process(clock)
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      cnt <= ncnt;
    end if;
  end process cobocntreg;

Le processus combinatoire de renommage est également très simple:

  rename: process(cnt)
  begin
    do <= (others => '0');
    do <= bit_vector(cnt);
  end process rename;

Le calcul de parité peut utiliser une variable et une boucle simple:

  parity: process(ncnt)
    variable tmp: bit;
  begin
    tmp := '0';
    npo <= '0';
    for i in 0 to 8 loop
      tmp := tmp xor ncnt(i);
    end loop;
    npo <= not tmp;
  end process parity;

Le dernier processus combinatoire est le plus complexe de tous, mais l’application stricte de la méthode de traduction proposée le rend également facile:

  u3d5: process(up, down, di, cnt)
    variable tmp: unsigned(9 downto 0);
  begin
    tmp  := (others => '0');
    nco  <= '0';
    nbo  <= '0';
    ncnt <= (others => '0');
    if up = '0' and down = '0' then
      ncnt <= unsigned(di);
    elsif up = '1' and down = '1' then
      ncnt <= cnt;
    elsif up = '1' and down = '0' then
      tmp   := ('0' & cnt) + 3;
      ncnt  <= tmp(8 downto 0);
      nco   <= tmp(9);
    elsif up = '0' and down = '1' then
      tmp   := ('0' & cnt) - 5;
      ncnt  <= tmp(8 downto 0);
      nbo   <= tmp(9);
    end if;
  end process u3d5;

Notez que les deux processus synchrones peuvent également être fusionnés et que l'un de nos processus combinatoires peut être simplifié dans une simple affectation simultanée de signaux. Le code complet, avec les déclarations de bibliothèque et de paquet, et avec les simplifications proposées est le suivant:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

architecture arc2 of cooley is
  signal cnt:  unsigned(8 downto 0);
  signal ncnt: unsigned(8 downto 0);
  signal nco:  bit;
  signal nbo:  bit;
  signal npo:  bit;
begin
  reg: process(clock)
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      po  <= npo;
      cnt <= ncnt;
    end if;
  end process reg;

  do <= bit_vector(cnt);

  parity: process(ncnt)
    variable tmp: bit;
  begin
    tmp := '0';
    npo <= '0';
    for i in 0 to 8 loop
      tmp := tmp xor ncnt(i);
    end loop;
    npo <= not tmp;
  end process parity;

  u3d5: process(up, down, di, cnt)
    variable tmp: unsigned(9 downto 0);
  begin
    tmp  := (others => '0');
    nco  <= '0';
    nbo  <= '0';
    ncnt <= (others => '0');
    if up = '0' and down = '0' then
      ncnt <= unsigned(di);
    elsif up = '1' and down = '1' then
      ncnt <= cnt;
    elsif up = '1' and down = '0' then
      tmp   := ('0' & cnt) + 3;
      ncnt  <= tmp(8 downto 0);
      nco   <= tmp(9);
    elsif up = '0' and down = '1' then
      tmp   := ('0' & cnt) - 5;
      ncnt  <= tmp(8 downto 0);
      nbo   <= tmp(9);
    end if;
  end process u3d5;
end architecture arc2;

Aller un peu plus loin

La méthode proposée est simple et sûre mais repose sur plusieurs contraintes qui peuvent être assouplies.

Ignorer le dessin du diagramme

Les concepteurs expérimentés peuvent ignorer le dessin d'un diagramme pour des conceptions simples. Mais ils pensent toujours au matériel en premier. Ils dessinent dans leur tête plutôt que sur une feuille de papier mais continuent à dessiner.

Utiliser des réinitialisations asynchrones

Dans certaines circonstances, les réinitialisations (ou ensembles) asynchrones peuvent améliorer la qualité d'une conception. La méthode proposée ne prend en charge que les réinitialisations synchrones (c'est-à-dire les réinitialisations prises en compte sur les fronts montants de l'horloge):

  process(clock)
  begin
    if rising_edge(clock) then
      if reset = '1' then
        o <= reset_value_for_o;
      else
        o <= i;
      end if;
    end if;
  end process;

La version avec réinitialisation asynchrone modifie notre modèle en ajoutant le signal de réinitialisation dans la liste de sensibilité et en lui donnant la priorité la plus élevée:

  process(clock, reset)
  begin
    if reset = '1' then
      o <= reset_value_for_o;
    elsif rising_edge(clock) then
      o <= i;
    end if;
  end process;

Fusionner plusieurs processus simples

Nous l'avons déjà utilisé dans la version finale de notre exemple. La fusion de plusieurs processus synchrones, s'ils ont tous la même horloge, est triviale. La fusion de plusieurs processus combinatoires en un seul est également triviale et n'est qu'une simple réorganisation du diagramme.

Nous pouvons également fusionner certains processus combinatoires avec des processus synchrones. Mais pour ce faire, nous devons revenir à notre diagramme et ajouter une onzième règle:

  1. Groupez plusieurs blocs ronds et au moins un bloc carré en dessinant un boîtier autour d'eux. Joignez également les flèches qui peuvent être. Ne laissez pas une flèche traverser les limites de l'enceinte si elle ne vient pas ou ne va pas de / à l'extérieur de l'enceinte. Une fois cela fait, regardez toutes les flèches de sortie du boîtier. Si l'un d'entre eux provient d'un bloc rond du boîtier ou est également une entrée du boîtier, nous ne pouvons pas fusionner ces processus dans un processus synchrone. Sinon nous pouvons.

Dans notre exemple, par exemple, nous ne pouvions pas regrouper les deux processus dans l'enceinte rouge de la figure suivante:

Processus qui ne peuvent pas être fusionnés

parce que ncnt est une sortie du boîtier et son origine est un bloc (combinatoire) rond. Mais on pourrait grouper:

Processus pouvant être fusionnés

Le signal interne npo deviendrait inutile et le processus résultant serait:

  poreg: process(clock)
    variable tmp: bit;
  begin
    if rising_edge(clock) then
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po <= not tmp;
    end if;
  end process poreg;

qui pourrait également être fusionné avec l'autre processus synchrone:

  reg: process(clock)
    variable tmp: bit;
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      cnt <= ncnt;
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po <= not tmp;
    end if;
  end process reg;

Le regroupement pourrait même être:

Plus de regroupement

Menant à l'architecture beaucoup plus simple:

architecture arc5 of cooley is
  signal cnt: unsigned(8 downto 0);
begin
  process(clock)
    variable ncnt: unsigned(9 downto 0);
    variable tmp:  bit;
  begin
    if rising_edge(clock) then
      ncnt := '0' & cnt;
      co   <= '0';
      bo   <= '0';
      if up = '0' and down = '0' then
        ncnt := unsigned('0' & di);
      elsif up = '1' and down = '0' then
        ncnt := ncnt + 3;
        co   <= ncnt(9);
      elsif up = '0' and down = '1' then
        ncnt := ncnt - 5;
        bo   <= ncnt(9);
      end if;
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po  <= not tmp;
      cnt <= ncnt(8 downto 0);
    end if;
  end process;

  do <= bit_vector(cnt);
end architecture arc5;

avec deux processus (l'attribution du signal simultané de do est un raccourci pour le processus équivalent). La solution avec un seul processus est laissée comme exercice. Attention, cela soulève des questions intéressantes et subtiles.

Aller encore plus loin

Les verrous déclenchés par le niveau, les fronts d'horloge qui tombent, les horloges multiples (et les resynchroniseurs entre les domaines d'horloge), les pilotes multiples pour le même signal, etc. ne sont pas mauvais. Ils sont parfois utiles. Mais apprendre à les utiliser et à éviter les pièges associés va bien au-delà de cette courte introduction à la conception de matériel numérique avec VHDL.

Codage en VHDL 2008

VHDL 2008 a introduit plusieurs modifications que nous pouvons utiliser pour simplifier davantage notre code. Dans cet exemple, nous pouvons bénéficier de 2 modifications:

  • les ports de sortie peuvent être lus, nous n'avons plus besoin du signal cnt ,
  • l'opérateur xor unaire peut être utilisé pour calculer la parité.

Le code VHDL 2008 pourrait être:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

architecture arc6 of cooley is
begin
  process(clock)
    variable ncnt: unsigned(9 downto 0);
  begin
    if rising_edge(clock) then
      ncnt := unsigned('0' & do);
      co   <= '0';
      bo   <= '0';
      if up = '0' and down = '0' then
        ncnt := unsigned('0' & di);
      elsif up = '1' and down = '0' then
        ncnt := ncnt + 3;
        co   <= ncnt(9);
      elsif up = '0' and down = '1' then
        ncnt := ncnt - 5;
        bo   <= ncnt(9);
      end if;
      po <= not (xor ncnt(8 downto 0));
      do <= bit_vector(ncnt(8 downto 0));
    end if;
  end process;
end architecture arc6;


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow