Suche…


Einführung

In diesem Thema schlagen wir eine einfache Methode vor, um einfache digitale Schaltungen mit VHDL korrekt zu entwerfen. Die Methode basiert auf grafischen Blockdiagrammen und einem leicht zu merkenden Prinzip:

Denken Sie zuerst an die Hardware, und programmieren Sie als Nächstes VHDL

Es ist für Anfänger in der Entwicklung digitaler Hardware unter Verwendung von VHDL gedacht und besitzt ein begrenztes Verständnis der Synthesesemantik der Sprache.

Bemerkungen

Das Design digitaler Hardware mit VHDL ist selbst für Anfänger einfach. Es gibt jedoch ein paar wichtige Dinge zu beachten und ein paar Regeln zu beachten. Das zur Umwandlung einer VHDL-Beschreibung in digitale Hardware verwendete Werkzeug ist ein Logiksynthesizer. Die Semantik der VHDL-Sprache, die von Logiksynthesizern verwendet wird, unterscheidet sich ziemlich von der Simulationssemantik, die im Language Reference Manual (LRM) beschrieben wird. Schlimmer noch: Es ist nicht standardisiert und variiert je nach Synthesewerkzeug.

Das vorgeschlagene Verfahren führt zur Vereinfachung einige wichtige Einschränkungen ein:

  • Keine pegelgesteuerten Latches.
  • Die Schaltungen sind bei der steigenden Flanke einer einzelnen Uhr synchron.
  • Kein asynchrones Reset oder Setzen.
  • Keine mehrfach gefahrenen Signale.

Das Beispiel eines Blockschaltbilds , zuerst aus einer Reihe von 3, stellt kurz die Grundlagen digitaler Hardware vor und schlägt eine kurze Liste von Regeln vor, um ein Blockschaltbild einer digitalen Schaltung zu entwerfen. Die Regeln helfen dabei, eine unkomplizierte Übersetzung in VHDL-Code zu gewährleisten, die erwartungsgemäß simuliert und synthetisiert.

Das Coding- Beispiel erläutert die Übersetzung von einem Blockdiagramm in VHDL-Code und veranschaulicht diese in einer einfachen digitalen Schaltung.

Das Designwettbewerb- Beispiel von John Cooley zeigt schließlich, wie das vorgeschlagene Verfahren auf ein komplexeres Beispiel für digitale Schaltungen angewendet werden kann. Es führt auch die eingeführten Einschränkungen aus und entspannt einige von ihnen.

Blockschaltbild

Digitale Hardware setzt sich aus zwei Arten von Hardware-Grundelementen zusammen:

  • Kombinatorische Gatter (Inverter und oder oder xor, 1-Bit-Volladdierer, 1-Bit-Multiplexer ...) Diese Logikgatter führen eine einfache Boolesche Berechnung an ihren Eingängen durch und erzeugen einen Ausgang. Jedes Mal, wenn sich einer ihrer Eingänge ändert, fangen sie an, elektrische Signale auszubreiten, und nach einer kurzen Verzögerung stabilisiert sich der Ausgang auf den resultierenden Wert. Die Ausbreitungsverzögerung ist wichtig, da sie stark von der Geschwindigkeit abhängt, mit der die digitale Schaltung laufen kann, dh ihrer maximalen Taktfrequenz.
  • Speicherelemente (Latches, D-Flip-Flops, RAMs ...). Im Gegensatz zu den kombinatorischen Logikgattern reagieren Speicherelemente nicht sofort auf die Änderung ihrer Eingänge. Sie verfügen über Dateneingänge, Steuereingänge und Datenausgänge. Sie reagieren auf eine bestimmte Kombination von Steuereingängen, nicht auf eine Änderung ihrer Dateneingaben. Das durch die ansteigende Flanke getriggerte D-Flip-Flop (DFF) weist beispielsweise einen Takteingang und einen Dateneingang auf. Bei jeder steigenden Flanke des Takts wird der Dateneingang abgetastet und in den Datenausgang kopiert, der bis zur nächsten steigenden Flanke des Takts stabil bleibt, auch wenn sich der Dateneingang dazwischen ändert.

Eine digitale Hardwareschaltung ist eine Kombination aus kombinatorischer Logik und Speicherelementen. Speicherelemente haben verschiedene Rollen. Eine davon ist die Wiederverwendung der gleichen kombinatorischen Logik für mehrere aufeinanderfolgende Operationen an verschiedenen Daten. Schaltungen, die dies verwenden, werden häufig als sequentielle Schaltungen bezeichnet . Die folgende Abbildung zeigt ein Beispiel einer sequentiellen Schaltung, die dank eines ansteigenden Flanken-getriggerten Registers ganzzahlige Werte unter Verwendung desselben kombinatorischen Addierers akkumuliert. Es ist auch unser erstes Beispiel eines Blockdiagramms.

Eine sequentielle Schaltung

Pipe-Lining ist eine weitere häufige Verwendung von Speicherelementen und Grundlage vieler Mikroprozessorarchitekturen. Es zielt darauf ab, die Taktfrequenz einer Schaltung zu erhöhen, indem eine komplexe Verarbeitung in einer Abfolge von einfacheren Operationen aufgeteilt wird und die Ausführung mehrerer aufeinander folgender Abläufe parallelisiert wird:

Auskleidung einer komplexen kombinatorischen Verarbeitung

Das Blockschaltbild ist eine grafische Darstellung der digitalen Schaltung. Es hilft, die richtigen Entscheidungen zu treffen und die Gesamtstruktur vor dem Programmieren zu verstehen. Es entspricht den empfohlenen vorläufigen Analysephasen vieler Software-Entwurfsmethoden. Erfahrene Designer überspringen diese Designphase häufig, zumindest für einfache Schaltungen. Wenn Sie jedoch ein Anfänger in der Entwicklung digitaler Hardware sind und eine digitale Schaltung in VHDL programmieren möchten, sollten Sie die folgenden 10 einfachen Regeln anwenden, um Ihr Blockdiagramm zu zeichnen.

  1. Umgeben Sie Ihre Zeichnung mit einem großen Rechteck. Dies ist die Grenze Ihres Stromkreises. Alles, was diese Grenze überschreitet, ist ein Eingabe- oder Ausgabeport. Die VHDL-Entität beschreibt diese Grenze.
  2. Randgetriggerte Register (z. B. quadratische Blöcke) eindeutig von der kombinatorischen Logik (z. B. runde Blöcke) trennen. In VHDL werden sie in zwei sehr unterschiedliche Prozesse übersetzt: synchron und kombinatorisch.
  3. Verwenden Sie keine durch Pegel getriggerten Latches, verwenden Sie nur getriggerte Register mit steigender Flanke. Diese Einschränkung kommt nicht von VHDL, das sich perfekt zum Modellieren von Latches eignet. Es ist nur ein vernünftiger Hinweis für Anfänger. Verriegelungen werden seltener benötigt und ihre Verwendung wirft viele Probleme auf, die wir zumindest bei unseren ersten Entwürfen wahrscheinlich vermeiden sollten.
  4. Verwenden Sie für alle von Ihrer steigenden Flanke ausgelösten Register dieselbe einzige Uhr. Auch hier ist diese Einschränkung der Einfachheit halber hier. Es kommt nicht von VHDL, das sich hervorragend zur Modellierung von Multi-Clock-Systemen eignet. Nennen Sie die clock . Es kommt von außen und ist eine Eingabe von allen quadratischen Blöcken und nur von ihnen. Wenn Sie möchten, stellen Sie nicht einmal die Uhr dar, sie ist für alle quadratischen Blöcke gleich und Sie können sie implizit in Ihrem Diagramm belassen.
  5. Stellen Sie die Kommunikation zwischen Blöcken mit benannten und orientierten Pfeilen dar. Für den Block kommt ein Pfeil, der Pfeil ist eine Ausgabe. Für den Block geht ein Pfeil zu, der Pfeil ist eine Eingabe. Alle diese Pfeile werden zu Ports der VHDL-Entität, wenn sie das große Rechteck oder Signale der VHDL-Architektur kreuzen.
  6. Pfeile haben einen einzigen Ursprung, sie können jedoch mehrere Ziele haben. Wenn ein Pfeil mehrere Ursprünge hat, erzeugen wir ein VHDL-Signal mit mehreren Treibern. Dies ist nicht völlig unmöglich, erfordert jedoch besondere Sorgfalt, um Kurzschlüsse zu vermeiden. Wir werden dies also vorerst vermeiden. Wenn ein Pfeil mehrere Ziele hat, verzweigen Sie den Pfeil so oft wie nötig. Verwenden Sie Punkte, um verbundene und nicht verbundene Kreuzungen zu unterscheiden.
  7. Einige Pfeile kommen von außerhalb des großen Rechtecks. Dies sind die Eingangsports der Entität. Ein Eingabepfeil kann auch nicht die Ausgabe eines Ihrer Blöcke sein. Dies wird von der VHDL-Sprache erzwungen: Die Eingabeports einer Entität können gelesen, aber nicht geschrieben werden. Dies ist wiederum zur Vermeidung von Kurzschlüssen.
  8. Einige Pfeile gehen nach draußen. Dies sind die Ausgangsports. In VHDL-Versionen vor 2008 können die Ausgabeports einer Entität geschrieben, aber nicht gelesen werden. Ein Ausgabepfeil muss also einen einzigen Ursprung und ein einziges Ziel haben: das Äußere. Keine Gabeln auf Ausgabepfeilen, ein Ausgabepfeil kann nicht auch die Eingabe eines Ihrer Blöcke sein. Wenn Sie einen Ausgabepfeil als Eingabe für einige Ihrer Blöcke verwenden möchten, fügen Sie einen neuen runden Block ein, um ihn in zwei Teile aufzuteilen: den internen, mit so vielen Gabeln, wie Sie möchten, und den Ausgabepfeil, der vom neuen stammt Block und geht nach draußen. Der neue Block wird zu einer einfachen fortlaufenden Zuordnung in VHDL. Eine Art transparente Umbenennung. Seit VHDL 2008 können auch Ausgangsanschlüsse gelesen werden.
  9. Alle Pfeile, die nicht von / nach außen kommen oder gehen, sind interne Signale. Sie werden alle in der VHDL-Architektur deklarieren.
  10. Jeder Zyklus im Diagramm muss mindestens einen quadratischen Block umfassen. Dies ist nicht auf VHDL zurückzuführen. Sie beruht auf den Grundprinzipien des digitalen Hardware-Designs. Kombinatorische Schleifen sind unbedingt zu vermeiden. Außer in sehr seltenen Fällen liefern sie kein nützliches Ergebnis. Ein Zyklus des Blockdiagramms, der nur runde Blöcke umfassen würde, wäre eine kombinatorische Schleife.

Vergessen Sie nicht, die letzte Regel sorgfältig zu überprüfen, sie ist genauso wichtig wie die anderen, aber es kann schwieriger sein, sie zu überprüfen.

Wenn Sie nicht unbedingt Funktionen benötigen, die wir im Moment ausgeschlossen haben, wie Latches, mehrere Takte oder Signale mit mehreren Treibern, sollten Sie leicht ein Blockdiagramm Ihrer Schaltung zeichnen, das den 10 Regeln entspricht. Wenn nicht, liegt das Problem wahrscheinlich bei der gewünschten Schaltung, nicht bei VHDL oder dem Logik-Synthesizer. Wahrscheinlich bedeutet dies, dass die von Ihnen gewünschte Schaltung keine digitale Hardware ist.

Die Anwendung der 10 Regeln auf unser Beispiel einer sequentiellen Schaltung würde zu einem Blockdiagramm wie folgt führen:

Überarbeitetes Blockschaltbild der Folgeschaltung

  1. Das große Rechteck um das Diagramm wird von 3 Pfeilen gekreuzt, die die Eingangs- und Ausgangsports der VHDL-Entität darstellen.
  2. Das Blockschaltbild besteht aus zwei runden (kombinatorischen) Blöcken - dem Addierer und dem Ausgangsumbenennungsblock - und einem quadratischen (synchronen) Block - dem Register.
  3. Es werden nur flankengetriggerte Register verwendet.
  4. Es gibt nur eine Uhr, benannte clock und wir verwenden nur die steigende Flanke.
  5. Das Blockdiagramm hat fünf Pfeile, einen mit einer Gabel. Sie entsprechen zwei internen Signalen, zwei Eingangsanschlüssen und einem Ausgangsanschlüssen.
  6. Alle Pfeile haben einen Ursprung und ein Ziel mit Ausnahme des Pfeils Sum mit zwei Zielen.
  7. Die Pfeile Data_in und Clock sind unsere zwei Eingangsanschlüsse. Sie sind keine Ausgabe unserer eigenen Blöcke.
  8. Der Data_out Pfeil ist unser Ausgabeport. Um mit VHDL-Versionen vor 2008 kompatibel zu sein, haben wir zwischen Sum und Data_out einen zusätzlichen Umbenennungsblock Data_out . Data_out hat also genau eine Quelle und ein Ziel.
  9. Sum und Next_sum sind unsere zwei internen Signale.
  10. Es gibt genau einen Zyklus in der Grafik, der aus einem quadratischen Block besteht.

Unser Blockdiagramm entspricht den 10 Regeln. Im Coding- Beispiel wird detailliert beschrieben, wie diese Art von Blockdiagrammen in VHDL übersetzt werden.

Codierung

Dieses Beispiel ist das zweite einer Reihe von 3. Wenn Sie dies noch nicht getan haben, lesen Sie zuerst das Beispiel für das Blockdiagramm .

Mit einem Blockdiagramm , das mit den 10 Regeln entspricht (siehe Blockschaltbild Beispiel), die VHDL - Codierung wird einfach:

  • das große umgebende Rechteck wird zur VHDL-Entität,
  • interne Pfeile werden zu VHDL-Signalen und in der Architektur deklariert,
  • Jeder quadratische Block wird zu einem synchronen Prozess im Architekturkörper.
  • Jeder Rundblock wird zu einem kombinatorischen Prozess im Architekturkörper.

Lassen Sie uns das im Blockschaltbild einer sequentiellen Schaltung veranschaulichen:

Eine sequentielle Schaltung

Das VHDL-Modell einer Schaltung umfasst zwei Kompilierungseinheiten:

  • Die Entität, die den Namen der Schaltung und ihre Schnittstelle beschreibt (Portnamen, Richtungen und Typen). Es ist eine direkte Übersetzung des großen umgebenden Rechtecks ​​des Blockdiagramms. Unter der Annahme, dass die Daten Ganzzahlen sind und der clock das VHDL- bit (nur zwei Werte: '0' und '1' ) verwendet, könnte die Entität unserer sequentiellen Schaltung folgendermaßen lauten:
entity sequential_circuit is
  port(
    Data_in:  in  integer;
    Clock:    in  bit;
    Data_out: out integer
  );
end entity sequential_circuit;
  • Die Architektur, die das Innere der Schaltung beschreibt (was sie tut). Hier werden die internen Signale deklariert und alle Prozesse instanziiert. Das Grundgerüst der Architektur unserer sequentiellen Schaltung könnte sein:
architecture ten_rules of sequential_circuit is
  signal Sum, Next_sum: integer;
begin
  <...processes...>
end architecture ten_rules;

Dem Architekturkörper müssen drei Prozesse hinzugefügt werden, ein synchroner (quadratischer Block) und zwei kombinatorische (runde Blöcke).

Ein synchroner Prozess sieht folgendermaßen aus:

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

Dabei sind i1, i2,..., ix alle Pfeile, die in den entsprechenden quadratischen Block des Diagramms o1, ..., ox und o1, ..., ox sind alle Pfeile, die den entsprechenden quadratischen Block des Diagramms ausgeben. Es darf absolut nichts geändert werden, außer den Namen der Signale. Nichts. Nicht einmal ein einzelner Charakter.

Der synchrone Prozess unseres Beispiels lautet also:

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

Das kann informell übersetzt werden in: Wenn sich die clock ändert, und nur dann, wenn die Änderung eine steigende Flanke ist ( '0' nach '1' ), weisen Next_sum dem Signal Sum den Wert des Signals Next_sum zu.

Ein kombinatorischer Prozess sieht folgendermaßen aus:

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;

Dabei sind i1, i2,..., in alle Pfeile, die in den entsprechenden runden Block des Diagramms gehen. alles und nicht mehr. Wir werden keinen Pfeil vergessen und der Liste nichts anderes hinzufügen.

v1, ..., vy sind Variablen, die wir möglicherweise benötigen, um den Code des Prozesses zu vereinfachen. Sie haben genau dieselbe Rolle wie in jeder anderen imperativen Programmiersprache: Halten Sie temporäre Werte. Sie müssen unbedingt alle zugewiesen sein, bevor sie gelesen werden. Wenn wir dies nicht garantieren, ist der Prozess nicht mehr kombinatorisch, da er Speicherelemente modelliert, um den Wert einiger Variablen von einer Prozessausführung zur nächsten beizubehalten. Dies ist der Grund für die vi := <default_value_for_vi> am Anfang des Prozesses. Beachten Sie, dass der <default_value_for_vi> Konstanten sein muss. Wenn nicht, wenn es sich um Ausdrücke handelt, könnten wir versehentlich Variablen in den Ausdrücken verwenden und eine Variable lesen, bevor Sie sie zuweisen.

o1, ..., om sind alle Pfeile, die den entsprechenden runden Block Ihres Diagramms ausgeben. alles und nicht mehr. Sie müssen unbedingt mindestens einmal während der Prozessausführung zugewiesen werden. Da die VHDL-Kontrollstrukturen ( if , case ...) sehr leicht verhindern können, dass ein Ausgangssignal zugewiesen wird, <default_value_for_oi> wir dringend, jeder von ihnen bedingungslos einen konstanten Wert <default_value_for_oi> zu Beginn des Prozesses <default_value_for_oi> . Auch wenn eine if Anweisung eine Signalzuordnung maskiert, hat sie trotzdem einen Wert erhalten.

An diesem VHDL-Skelett darf absolut nichts geändert werden, außer den Namen der Variablen (falls vorhanden), der Namen der Eingänge, der Namen der Ausgänge, der Werte der Konstanten <default_value_for_..> und der <statements> . Vergessen Sie nicht eine einzelne Standardwertzuweisung. Wenn Sie dies tun, werden unerwünschte Speicherelemente (höchstwahrscheinlich Latches) abgeleitet, und das Ergebnis ist nicht das, was Sie ursprünglich wollten.

In unserer beispielhaften sequentiellen Schaltung lautet der kombinatorische Addiererprozess:

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

Das kann informell übersetzt werden in: Wenn Sum oder Data_in (oder beide) geändert werden, weisen Sie dem Signal Next_sum den Wert 0 zu, und weisen Sie ihm erneut den Wert Sum + Data_in .

Da auf die erste Zuweisung (mit dem konstanten Standardwert 0 ) unmittelbar eine andere Zuweisung folgt, die sie überschreibt, können wir Folgendes vereinfachen:

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

Der zweite kombinatorische Prozess entspricht dem runden Block, den wir auf einem Ausgabepfeil mit mehr als einem Ziel hinzugefügt haben, um die VHDL-Versionen vor 2008 zu erfüllen. Sein Code lautet einfach:

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

Aus dem gleichen Grund wie bei den anderen kombinatorischen Prozessen können wir das wie folgt vereinfachen:

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

Der vollständige Code für die sequentielle Schaltung lautet:

-- 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;

Hinweis: Wir könnten die drei Prozesse in beliebiger Reihenfolge schreiben, es würde sich nichts am Endergebnis in der Simulation oder in der Synthese ändern. Dies ist darauf zurückzuführen, dass es sich bei den drei Prozessen um gleichzeitige Anweisungen handelt, und VHDL behandelt sie so, als ob sie wirklich parallel wären.

John Cooleys Designwettbewerb

Dieses Beispiel ist direkt von John Cooleys Designwettbewerb auf der SNUG'95 (Sitzung der Synopsys Users Group) abgeleitet. Der Wettbewerb sollte VHDL- und Verilog-Designern das gleiche Designproblem entgegenstellen. Was John im Sinn hatte, war wahrscheinlich zu bestimmen, welche Sprache am effizientesten war. Das Ergebnis war, dass 8 der 9 Verilog-Designer den Designwettbewerb abschließen konnten, jedoch keiner der 5 VHDL-Designer. Hoffentlich werden wir mit der vorgeschlagenen Methode viel bessere Arbeit leisten.

Spezifikationen

Unser Ziel ist es, in einfach synthetisierbarer VHDL (Entity und Architektur) einen synchronen Up-by-3-Down-by-5-Loadable-Modul 512-Zähler mit Carry-Ausgang, Borrow-Ausgang und Paritätsausgang zu entwerfen. Der Zähler ist ein 9-Bit-Zähler ohne Vorzeichen und liegt daher zwischen 0 und 511. Die Schnittstellenspezifikation des Zählers ist in der folgenden Tabelle angegeben:

Name Bitbreite Richtung Beschreibung
UHR 1 Eingang Hauptuhr; Der Zähler wird mit der steigenden Flanke von CLOCK synchronisiert
DI 9 Eingang Dateneingangsbus; Der Zähler wird mit DI geladen, wenn sowohl UP als auch DOWN niedrig sind
OBEN 1 Eingang Up-by-3-Zählbefehl; Wenn UP hoch ist und DOWN niedrig ist, erhöht sich der Zähler um 3, wobei der Maximalwert umbrochen wird (511).
NIEDER 1 Eingang Down-by-5-Zählbefehl; Wenn DOWN hoch und UP niedrig ist, verringert sich der Zähler um 5, wobei der minimale Wert (0) umbrochen wird.
CO 1 Ausgabe Signal durchführen; hoch nur, wenn über den Maximalwert hinaus gezählt wird (511) und somit umgeschlagen wird
BO 1 Ausgabe Signal ausleihen; hoch nur beim Abwärtszählen unter den Minimalwert (0) und damit Umwickeln
TUN 9 Ausgabe Ausgangsbus; der aktuelle Wert des Zählers; Wenn UP und DOWN beide hoch sind, behält der Zähler seinen Wert
PO 1 Ausgabe Paritäts-Out-Signal; hoch, wenn der aktuelle Wert des Zählers eine gerade Zahl von 1en enthält

Beim Aufwärtszählen über den Maximalwert oder beim Abwärtszählen unter den Minimalwert springt der Zähler um:

Gegenstromwert OBEN UNTEN Nächsten Wert zählen Nächstes CO Nächstes BO Nächste Bestellung
x 00 DI 0 0 Parität (DI)
x 11 x 0 0 Parität (x)
0 ≤ x ≤ 508 10 x + 3 0 0 Parität (x + 3)
509 10 0 1 0 1
510 10 1 1 0 0
511 10 2 1 0 0
5 ≤ x ≤ 511 01 x-5 0 0 Parität (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

Blockschaltbild

Basierend auf diesen Spezifikationen können wir mit dem Entwurf eines Blockdiagramms beginnen. Lassen Sie uns zuerst die Schnittstelle darstellen:

Die externe Schnittstelle

Unsere Schaltung hat 4 Eingänge (einschließlich der Uhr) und 4 Ausgänge. Der nächste Schritt besteht darin, zu entscheiden, wie viele Register und kombinatorische Blöcke wir verwenden werden und welche Rolle sie spielen werden. Für dieses einfache Beispiel widmen wir einen kombinatorischen Block der Berechnung des nächsten Wertes des Zählers, der Durchführung und der Entlehnung. Ein weiterer kombinatorischer Block wird verwendet, um den nächsten Wert der Parität zu berechnen. Die aktuellen Werte des Zählers, der Durchführung und des Entleihens werden in einem Register gespeichert, während der aktuelle Wert des Paritäts-Outs in einem separaten Register gespeichert wird. Das Ergebnis ist in der folgenden Abbildung dargestellt:

Zwei kombinatorische Blöcke und zwei Register

Die Überprüfung, ob das Blockdiagramm unseren 10 Entwurfsregeln entspricht, ist schnell erledigt:

  1. Unsere externe Schnittstelle wird durch das große umgebende Rechteck richtig dargestellt.
  2. Unsere 2 Kombinationsblöcke (rund) und unsere 2 Register (Quadrat) sind klar voneinander getrennt.
  3. Wir verwenden nur die von der steigenden Flanke ausgelösten Register.
  4. Wir verwenden nur eine Uhr.
  5. Wir haben 4 interne Pfeile (Signale), 4 Eingabepfeile (Eingangsanschlüsse) und 4 Ausgangspfeile (Ausgangsanschlüsse).
  6. Keiner unserer Pfeile hat mehrere Ursprünge. Drei haben mehrere Ziele ( clock , ncnt und do ).
  7. Keiner unserer 4 Eingabepfeile ist ein Ausgang unserer internen Blöcke.
  8. Drei unserer Ausgabepfeile haben genau einen Ursprung und ein Ziel. Aber do hat 2 Ziele: das Äußere und eines unserer kombinatorischen Blöcke. Dies verstößt gegen Regel Nr. 8 und muss durch Einfügen eines neuen kombinatorischen Blocks behoben werden, wenn die VHDL-Versionen vor 2008 erfüllt werden sollen:

Ein zusätzlicher kombinatorischer Block

  1. Wir haben jetzt genau 5 interne Signale ( cnt , nco , nbo , ncnt und npo ).
  2. Es gibt nur einen Zyklus in dem Diagramm durch gebildet cnt und ncnt . Es gibt einen quadratischen Block im Zyklus.

Codierung in VHDL-Versionen vor 2008

Das Übersetzen unseres Blockdiagramms in VHDL ist unkompliziert. Der aktuelle Wert des Zählers reicht von 0 bis 511, daher verwenden wir ein 9-Bit- bit_vector Signal, um ihn darzustellen. Die einzige Subtilität ergibt sich aus der Notwendigkeit, bitweise (wie das Berechnen der Parität) und Rechenoperationen mit denselben Daten durchzuführen. Das standardmäßige numeric_bit Paket von library ieee löst dieses ieee : Es deklariert einen unsigned Typ mit genau derselben Deklaration wie bit_vector und bit_vector die arithmetischen Operatoren so, dass sie eine beliebige Mischung aus unsigned und Ganzzahlen annehmen. Um die Durchführung und das Ausleihen zu berechnen, verwenden wir einen unsigned 10-Bit-Wert.

Die Bibliotheksdeklarationen und die Entität:

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;

Das Skelett der Architektur ist:

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;

Jeder unserer 5 Blöcke wird als Prozess modelliert. Die synchronen Prozesse, die unseren zwei Registern entsprechen, sind sehr einfach zu codieren. Wir verwenden einfach das im Coding- Beispiel vorgeschlagene Muster. Das Register, in dem beispielsweise das Parity-Out-Flag gespeichert ist, ist codiert:

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

und das andere Register, das co , bo und cnt speichert:

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

Das Umbenennen des kombinatorischen Prozesses ist ebenfalls sehr einfach:

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

Die Paritätsberechnung kann eine Variable und eine einfache Schleife verwenden:

  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;

Der letzte kombinatorische Prozess ist der komplexeste von allen, aber die strikte Anwendung der vorgeschlagenen Übersetzungsmethode macht es auch einfach:

  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;

Beachten Sie, dass die beiden synchronen Prozesse auch zusammengeführt werden können und dass einer unserer kombinatorischen Prozesse in einer einfachen gleichzeitigen Signalzuweisung vereinfacht werden kann. Der vollständige Code mit Bibliotheks- und Paketdeklarationen sowie den vorgeschlagenen Vereinfachungen lautet wie folgt:

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;

Ein bisschen weiter gehen

Das vorgeschlagene Verfahren ist einfach und sicher, beruht jedoch auf mehreren Einschränkungen, die gelockert werden können.

Überspringen Sie die Blockdiagrammzeichnung

Erfahrene Designer können das Zeichnen eines Blockdiagramms für einfache Konstruktionen überspringen. Aber zuerst denken sie noch an Hardware. Sie zeichnen statt auf einem Blatt Papier in den Kopf, aber sie zeichnen irgendwie weiter.

Verwenden Sie asynchrone Rücksetzungen

Es gibt Umstände, unter denen asynchrone Zurücksetzungen (oder Sets) die Qualität eines Entwurfs verbessern können. Die vorgeschlagene Methode unterstützt nur synchrone Rücksetzungen (dh Rücksetzungen, die bei steigenden Taktflanken berücksichtigt werden):

  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;

Die Version mit asynchronem Reset modifiziert unsere Vorlage, indem sie das Reset-Signal in die Sensitivitätsliste hinzufügt und ihm die höchste Priorität einräumt:

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

Mehrere einfache Prozesse zusammenführen

Wir haben dies bereits in der endgültigen Version unseres Beispiels verwendet. Das Zusammenführen mehrerer synchroner Prozesse, wenn alle dieselbe Uhr haben, ist trivial. Das Zusammenführen mehrerer kombinatorischer Prozesse in einem Prozess ist ebenfalls trivial und stellt lediglich eine einfache Reorganisation des Blockdiagramms dar.

Wir können auch einige kombinatorische Prozesse mit synchronen Prozessen zusammenführen. Dafür müssen wir jedoch zu unserem Blockdiagramm zurückkehren und eine elfte Regel hinzufügen:

  1. Gruppieren Sie mehrere runde Blöcke und mindestens einen quadratischen Block, indem Sie eine Umrandung zeichnen. Umschließen Sie auch die Pfeile, die sein können. Lassen Sie nicht zu, dass ein Pfeil die Begrenzung des Gehäuses überschreitet, wenn er nicht vom oder außerhalb des Gehäuses kommt. Sehen Sie sich danach alle Ausgabepfeile des Gehäuses an. Wenn einer von ihnen aus einem runden Block des Gehäuses stammt oder auch ein Eingang des Gehäuses ist, können diese Prozesse nicht in einem synchronen Prozess zusammengeführt werden. Sonst können wir.

In unserem Gegenbeispiel konnten wir beispielsweise die beiden Prozesse nicht in der roten Umhüllung der folgenden Abbildung zusammenfassen:

Prozesse, die nicht zusammengeführt werden können

weil ncnt eine Ausgabe des Gehäuses ist und sein Ursprung ein runder (kombinatorischer) Block ist. Wir könnten aber gruppieren:

Prozesse, die zusammengeführt werden können

Das interne Signal npo würde unbrauchbar und der resultierende Prozess wäre:

  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;

die auch mit dem anderen synchronen Prozess zusammengeführt werden könnte:

  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;

Die Gruppierung könnte sogar sein:

Mehr Gruppierung

Dies führt zu einer viel einfacheren Architektur:

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;

mit zwei Prozessen (die gleichzeitige Signalzuweisung von do ist eine Abkürzung für den äquivalenten Prozess). Die Lösung mit nur einem Prozess bleibt als Übung übrig. Achtung, es wirft interessante und subtile Fragen auf.

Noch weiter gehen

Pegelgetriggerte Latches, fallende Taktflanken, mehrere Takte (und Resynchronisatoren zwischen Taktdomänen), mehrere Treiber für dasselbe Signal usw. sind nicht böse. Sie sind manchmal nützlich. Zu lernen, wie man sie verwendet und wie man die damit verbundenen Fallstricke vermeidet, geht jedoch weit über diese kurze Einführung in das Design digitaler Hardware mit VHDL hinaus.

Codierung in VHDL 2008

Mit VHDL 2008 wurden mehrere Modifikationen eingeführt, mit denen wir unseren Code weiter vereinfachen können. In diesem Beispiel können wir von zwei Modifikationen profitieren:

  • Ausgabeports können gelesen werden, das cnt Signal wird nicht mehr benötigt,
  • Mit dem unären xor Operator kann die Parität berechnet werden.

Der VHDL 2008-Code könnte sein:

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
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow