vhdl
Digitales Hardware-Design mit VHDL auf den Punkt gebracht
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.
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:
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.
- 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.
- 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.
- 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.
- 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. - 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.
- 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.
- 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.
- 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.
- Alle Pfeile, die nicht von / nach außen kommen oder gehen, sind interne Signale. Sie werden alle in der VHDL-Architektur deklarieren.
- 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:
- Das große Rechteck um das Diagramm wird von 3 Pfeilen gekreuzt, die die Eingangs- und Ausgangsports der VHDL-Entität darstellen.
- Das Blockschaltbild besteht aus zwei runden (kombinatorischen) Blöcken - dem Addierer und dem Ausgangsumbenennungsblock - und einem quadratischen (synchronen) Block - dem Register.
- Es werden nur flankengetriggerte Register verwendet.
- Es gibt nur eine Uhr, benannte
clock
und wir verwenden nur die steigende Flanke. - Das Blockdiagramm hat fünf Pfeile, einen mit einer Gabel. Sie entsprechen zwei internen Signalen, zwei Eingangsanschlüssen und einem Ausgangsanschlüssen.
- Alle Pfeile haben einen Ursprung und ein Ziel mit Ausnahme des Pfeils
Sum
mit zwei Zielen. - Die Pfeile
Data_in
undClock
sind unsere zwei Eingangsanschlüsse. Sie sind keine Ausgabe unserer eigenen Blöcke. - Der
Data_out
Pfeil ist unser Ausgabeport. Um mit VHDL-Versionen vor 2008 kompatibel zu sein, haben wir zwischenSum
undData_out
einen zusätzlichen UmbenennungsblockData_out
.Data_out
hat also genau eine Quelle und ein Ziel. -
Sum
undNext_sum
sind unsere zwei internen Signale. - 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:
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:
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:
Die Überprüfung, ob das Blockdiagramm unseren 10 Entwurfsregeln entspricht, ist schnell erledigt:
- Unsere externe Schnittstelle wird durch das große umgebende Rechteck richtig dargestellt.
- Unsere 2 Kombinationsblöcke (rund) und unsere 2 Register (Quadrat) sind klar voneinander getrennt.
- Wir verwenden nur die von der steigenden Flanke ausgelösten Register.
- Wir verwenden nur eine Uhr.
- Wir haben 4 interne Pfeile (Signale), 4 Eingabepfeile (Eingangsanschlüsse) und 4 Ausgangspfeile (Ausgangsanschlüsse).
- Keiner unserer Pfeile hat mehrere Ursprünge. Drei haben mehrere Ziele (
clock
,ncnt
unddo
). - Keiner unserer 4 Eingabepfeile ist ein Ausgang unserer internen Blöcke.
- 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:
- Wir haben jetzt genau 5 interne Signale (
cnt
,nco
,nbo
,ncnt
undnpo
). - Es gibt nur einen Zyklus in dem Diagramm durch gebildet
cnt
undncnt
. 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:
- 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:
weil ncnt
eine Ausgabe des Gehäuses ist und sein Ursprung ein runder (kombinatorischer) Block ist. Wir könnten aber gruppieren:
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:
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;