
Ich interessiere mich für FPGA-Bausteine seit ich vor vielen Jahren zum ersten Mal vom MiSTer gehört habe. Das ist ein elektronisches Gerät, welches verschiedene Computer aus der Vergangenheit ‚nachmachen‘ kann.
In diesem Artikel versuche ich zu erklären was FPGA-Bausteine wirklich sind, da dies meiner Meinung nach in Artikeln wie dem von heise oder dem Buch FPGAs von Rheinwerk, etwas zu kurz kommt. Zusätzlich zeige ich Euch noch den Aufbau eines 6-Bit Addierers für den Tang Nano 9K. Als Zielgruppe stelle ich mir den normalen technisch interessierten Leser bis zum elektronikfremden Informatiker vor.
Für Profis: Um diesen Artikel einigermaßen verständlich zu machen, habe ich an manchen Stellen etwas vereinfacht!
Was geht mit FPGA-Bausteinen?
In erster Linie sind FPGAs konfigurierbare Elektronikbausteine, die zu nahezu beliebigen elektronischen Schaltungen verdrahtet werden können. Das ist sehr, sehr cool, da es dadurch in sehr kurzer Zeit und ohne Löten oder manuellem Verdrahten möglich ist, nahezu jede beliebige elektronische Hardware, bis hin zu einem Mikroprozessor, herzustellen.
Wer will so etwas?
FPGA-Bausteine dienen zum Beispiel dazu, bei der Teilchendetektion am CERN, Teilchen wie das Higgs-Boson zu detektieren, indem sie zuvor auf bestimmte Eigenschaften von Teilchen trainierte neuronale Netze in ihrer Hardware mithilfe von Gatterschaltungen abbilden. Die bei einer Teilchenkollision entstehenden und durch Sensoren gemessenen Daten können in Echtzeit dem FPGA zugeführt werden, das daraufhin die charakteristischen Eigenschaften besonderer Teilchen feststellen und kennzeichnen kann, sodass nur diese für eine spätere Untersuchung gespeichert werden.
Die in diesem Artikel beschriebene Implementierung von Entscheidungsbäumen zeigt Ansätze, die auch bei der FPGA-Umsetzung neuronaler Netze genutzt werden, indem Algorithmen auf FPGA-kompatible Strukturen reduziert werden.
Sie können aber auch einfach verwendet werden um real nicht (mehr) existierende Hardwareschaltungen zu erzeugen. Das beinhaltet aber auch Hardwareschaltungen wie ehemalige Mikroprozessoren, die es heute nicht mehr zu kaufen gibt. Zum Beispiel den MOS Technology 6502, den 6510 im Commodore 64 oder auch den Motorola 68000 des Amiga.
Eine beliebte Anwendung von FPGAs der letzten Jahre, ist es alte Mikroprozessoren oder sogar gleich komplette Boards mit mehreren Chips alter Spielkonsolen zu realisieren. Besonders die Nachbildungen alter Nintendo Gameboys, die auch wirklich alte Game Cartridges aufnehmen können, finden scheinbar so einen großen Absatz, das es gleich mehrere Anbieter dafür gibt: FPGBC, Modretro Chromatik und Analogue Pocket. Letzterer kann aber auch einen alten Commodore 64 oder einen Amiga und sogar uralte Spielautomaten nachmachen.
Wer sich ein bisschen in der Szene auskennt, weiß aber auch dass es keiner FGPAs bedarf, alte Konsolen nachzubauen. Moderne Mikroprozessoren sind so aberwitzig schnell geworden, dass es mit ihnen möglich ist nur durch Software die alte Hardware zu emulieren. Allerdings kann es dabei zu Timing Problemen kommen und echte Kenner der alten Geräte stehen darum auf FPGAs.
Warum interessiert mich so etwas? Wenn ich so einen alten Computer einmal besessen und darauf programmiert, Texte geschrieben oder gespielt hat, der Computer aber nicht mehr in meinem Besitz oder mittlerweile defekt ist, kann ich die alten Programme nicht mehr laufen lassen. Mit Hilfe von FPGA-Bausteinen ist es möglich diese alten Computer wieder neu zu erzeugen und damit meine alten Programme wieder auszuführen. Im Gegensatz zu Mame, welches eine Software ist die ich schon lange kenne, geschieht dies bei FPGAs eben aber auf Hardware Basis.
Wie funktionieren FPGAs?
Um nicht bei Null anzufangen nehme ich als Basis meiner Erklärungen das Logikgatter, da es sowohl bei herkömmlichen Mikroprozessoren als auch bei FPGAs gleichermaßen vorkommt. Für alle, für die Elektronik schwer verständlich ist, vergleiche ich es hier einmal mit einem Lego Baustein. So wie bei Lego Autos, Kräne, Burgen oder Rubik’s Cube Solver aus einzelnen Lego Bausteinen bestehen, so sind sowohl Mikroprozessoren als auch FPGAs aus Logikgattern aufgebaut. Bei der Herstellung von Mikroprozessoren werden diese einmalig auf eine für einen bestimmten Mikroprozessor charakteristischen Weise verbunden und ins Silizium geätzt (die Lego Steine werden miteinander verklebt) und in einem FPGA liegen sie quasi lose vor und können immer wieder auf andere Art und Weise zusammengestellt werden. Dabei ist es faszinierend, dass ein FPGA auch zu verschiedenen Mikroprozessoren konfiguriert werden kann! Diese Flexibilität erfordert natürlich ihren Preis und so werden aus der gleichen Zeit stammende Mikroprozessoren und FPGAs niemals gleich schnell sein, da die freie Konfigurierbarkeit natürlich auch einen gewissen Overhead bedeutet. So wie ein Lego Auto niemals mit einem echten Porsche konkurrieren kann, so ist ein in einem FPGA realisierter MOS 6502 (1975) nicht in der Lage mit einem AMD Ryzen 9950 (2024) zu konkurrieren, obwohl er als FPGA schneller laufen kann als damals im Original. Heutige FPGAs sind aber mühelos in der Lage zu Mikroprozessoren aus vergangenen Zeiten konfiguriert zu werden. Sie werden sogar schneller als die alten Originale sein.
Aber wenn FPGAs nicht schneller sind als aktuelle Mikroprozessoren und diese doch eigentlich alles können, wozu sind sie denn dann, außer alte Mikroprozessoren nachzubilden, gut? Einen Mikroprozessor nachzubilden ist sicher mit einer der Höhepunkte eines FPGA Lebens, aber er kann nahezu jede alte Schaltung nachbilden und damit zum Beispiel als Ersatz für alte digitale Chips dienen, die es schon lange nicht mehr zu kaufen gibt.
Ein zweiter Erklärungsansatz: 74xx
Wie schon in meinem Artikel ‚Ist das die kleinste Zuse der Welt?‘ beschrieben, bestehen Computer aus Logikgattern, die mal aus Blech, Relais, Röhren oder Transistoren aufgebaut sind. Um daraus Computer zu bauen, werden diese so verschaltet, das tausende von UND-, ODER und Negationsfunktionen zu komplexeren Funktionen verbunden wurden.
Um den FPGAs gedanklich ein wenig näher zu kommen, kann man auch auf die in den 70er und 80er Jahren weit verbreiteten und von Texas Instruments hergestellten 74xx Logik ICs (Integrierte Schaltkreise) zurückgreifen. In diesen ICs waren meist verschiedene Funktionen wie Gatter, Flip-Flops, Zähler und Multiplexer enthalten, die über die außen am Gehäuse verteilten Ein- und Ausgänge mit anderen ICs verschaltet werden konnten, um weit komplexere Funktionen bis hin zu ganzen Computern zu bauen. In meiner Lehrzeit habe ich manchmal Schaltungen aus mehreren 74xx ICs auf Lochrasterplatinen mit dünnem Wire Wrap Draht gelötet, die für ganz spezielle Prüfzwecke an Funkgeräten im Prüffeld gedient haben. Dies war eine schwierige Aufgabe, da ich von der Wire Wrap Rolle unzählige Stückchen Draht mit einer recht genau definierten Länge abschneiden, jedes winzigen Stückchen an seinen zwei Enden von Hand abisolieren und an die genau passenden Beinchen der 74xx anlöten musste. Das hat natürlich immer viele Stunden oder Tage gedauert. Hier habe ich also Gatterschaltungen nach Belieben miteinander verbunden um eine komplexere neue Funktion zu erstellen! Das ist im Prinzip genau dass gleiche was man mit FPGAs machen kann, nur dass ich die gesamte Handarbeit nicht mehr leisten muss!
Aber wenn doch beide aus Gattern bestehen, warum können dann FPGAs so viel schneller als Mikroprozessoren sein?
Die Aufgabe eines Mikroprozessors ist es ein Programm auszuführen! Damit das geschehen kann – und sei es nur auf der Ebene der Maschinensprache, also der einzigen Sprache die ein Mikroprozessor tatsächlich versteht – bedarf es einer Ablaufmaschine, die Befehle aus einem Speicher holt, dekodiert, die Eingabeparameter in Registern ablegt, den Befehl ausführt und das Ergebnis wieder im Speicher ablegt. Nehmen wir an, wir wollten eine der grundlegendsten Funktionen überhaupt verwenden, die AND Funktion. Wir wollen zum Beispiel mit dem Mikroprozessor des ESP32 prüfen ob 2 Schalter geschlossen sind und nur wenn beide geschlossen sind ein Licht einschalten. Die beiden Schalter sind so an jeweils einem Eingang des ESP32 angeschlossen dass sie ein HIGH Signal, also einen hohen Spannungspegel anlegen, wenn sie eingeschaltet sind. An einem Ausgang wird ein HIGH ausgeben, wenn beide Eingänge auf HIGH gesetzt sind und schalten damit über einen angeschlossenen Transistor eine LED ein. Dazu benötigen wir als Programmierer ein extrem einfaches Maschinenprogramm:
main: 3FF44000 // Lade Basisadresse der GPIO-Register in a0 00000030 // Lade Maske für GPIO4 und GPIO5 in a1 (Maske: 0b110000) 00040000 // Lade Maske für GPIO18 in a2 (Maske: 0b1000000000000000000) loop: 3C3FF440 // Lade Eingaberegister (GPIO_IN) in a3 0C000031 // AND a3 mit Maske (a1) 0400003A // Prüfe, ob Ergebnis (a4) beide Bits gesetzt hat 08000004 // Falls nicht, springe zu loop 04000000 // Schreibe Maske (GPIO18 HIGH) ins Ausgangsregister 08000002 // Springe zurück zu loop
Damit der Mikroprozessor jetzt mit diesem sehr einfachen Programm arbeiten kann, benötigt es eine komplexe Maschine, die selbst aus hunderten oder tausenden von Gatterschaltungen aufgebaut ist. Diese Maschine muss die einzelnen Befehle nacheinander aus dem ebenfalls aus Gattern aufgebauten Programmspeicher holen und die Funktion jedes einzelnen Befehls nacheinander ausführen. Für jeden Befehl müssen dazu sehr viele Gatter miteinander zusammen arbeiten. Da auch Gatter eine minimale Schaltzeit besitzen und diese sich addiert, umso mehr Gatter auf die Ergebnisse von anderen Gattern angewiesen sind, benötigen diese Vorgänge sehr viele Taktzyklen und damit auch sehr viel Zeit.
Jetzt sehen wir uns einmal an wie viel Zeit ein darauf konfigurierter FPGA Baustein dafür benötigt. Dabei muss nur ein einziges Gatter, meist eine Lookup Table so konfiguriert sein, dass die 2 Eingänge und 1 Ausgang mit ihr verknüpft sind. Das dauert genau einen (1) Taktzyklus lang! Das ist ein extremes Beispiel, aber gut geeignet die Unterschiede aufzuzeigen.
Parallel
Es heißt FPGAs sind so schnell weil sie parallel arbeiten können. Aber es gibt doch heutzutage auch Mehrkernprozessoren mit bis zu 16 Kernen für den Endkonsumenten? Da können die Kerne ja auch parallel arbeiten? Das stimmt auch und Software, die darauf hin programmiert wurde mit vielen Kernen zu arbeiten, kann um ein Vielfaches schneller sein als wenn der Mikroprozessor nur einen Kern hat. Aber jeder Kern mehr bedeutet auch etwas Verwaltungsarbeit für das Betriebssystem und darum werden 2 Kerne niemals doppelt so schnell wie ein Kern sein. Und letztlich ist der gerade beschriebene Sachverhalt mit der Ablaufmaschine für die Ausführung von Code bei jedem einzelnen Kern von Nöten. Die schnellsten heutigen FPGAs wie zum Beispiel der Xilinx Versal ACAP können bis zu 1,5 GHz takten. Damit sind sie aber immer noch etwa nur 1/3 so schnell wie zum Beispiel ein AMD Ryzen 9590 (4700 MHz). Und ein Tang Nano 9K (27 MHz), den ich hier verwende, ist sogar nur 1/174 so schnell.
Die ‚Programmiersprache‘
Anfangs war ich oft über den Begriff ‚Programmiersprache für FPGAs‘ verwirrt. Ich hatte eine rudimentäre Vorstellung davon, wie ein FPGA-Baustein funktioniert, und konnte mir nicht vorstellen, welche Rolle dabei eine Sprache spielen sollte. Ich dachte, die vielen Gatter müssten interaktiv über einen visuellen Editor per Maus verknüpft werden. Doch dem ist nicht so!
Stattdessen verwendet man Hardwarebeschreibungssprachen (HDLs) wie VHDL oder Verilog, um festzulegen, wie die Schaltung innerhalb des FPGA aufgebaut sein soll. Diese Beschreibung wird einmalig in eine Form gebracht, die das FPGA verstehen kann, und anschließend auf den Baustein übertragen.
Es ist also keineswegs so, dass ein Programm entwickelt wird, das ständig läuft, um die Funktionalität des FPGA zu realisieren. Ein FPGA benötigt keinen Interpreter und führt auch keinen kompilierten Code aus, wie es ein Mikroprozessor tun würde. Vielleicht hilft die Vorstellung von Bauarbeitern, die die Anweisungen der Programmiersprache ausführen um damit die Schaltung aufzubauen bzw. die einzelnen Elemente des FPGA zu verdrahten. Anschließend werden die Anweisungen nicht mehr benötigt und die damit hergestellte Schaltung erledigt ihren Job.
6-Bit-Addierer als Beispiel einer Hardwarebeschreibung
Das nachfolgende Programm ist für den Tang Nano 9K, den man zum Beispiel bei AliExpress kaufen kann. Auf der Platine ist der GW1NR-9 FPGA von GOWIN (eine chinesische Firma) und 6 LEDs gelötet. Das Programm ermöglicht die bitweise Eingabe zweier 6-Bit-Zahlen über zwei Tasten, addiert sie und zeigt das Ergebnis auf LEDs an. Durch Entprellung, Phasensteuerung, Schieberegister und Bitumkehrung wird eine präzise Bedienung sichergestellt.
Liegt der Tang Nano 9K so dass der USB Anschluss nach unten zeigt, so ist der rechte Taster eine logische 1 und der linke Taster eine logische 0. Gebe ich nur 0en ein, so ist das nicht wahrnehmbar, da diese ja durch eine nicht leuchtende LED dargestellt wird. Erst ab der Eingabe einer logischen 1 kann man sehen wie der Wert von links nach rechts geschoben wird. Nach der Eingabe der 6 Bits des ersten Wertes wird eine kurze Pause gemacht, anschließend kann ich die 6 Bits des zweiten Wertes eintasten. Nach wiederum einer kurzen Pause wird das addierte Ergebnis angezeigt.
1. Überblick: Was macht das Programm?
Das Programm beschreibt einen 6-Bit-Addierer mit zwei Tasten (btn1_n und btn2_n) zur Eingabe zweier Zahlen und einer LED-Anzeige (led_n) für das Ergebnis. Es arbeitet in mehreren Phasen:
-
Eingabe der ersten Zahl (über Taster, bitweise).
-
Kurze Wartezeit.
-
Eingabe der zweiten Zahl.
-
Kurze Wartezeit.
-
Addition und Anzeige des Ergebnisses auf den LEDs für 10 Sekunden.
-
Zurück zur ersten Zahl.
Die LEDs leuchten aktiv low (d. h. eine 0
schaltet eine LED ein). Die Logik wird getaktet mit 27 MHz.
2. Wichtige Register und Variablen
Das Verhalten des Programms wird durch verschiedene Register gesteuert:
Variable |
Typ / Größe |
Bedeutung |
---|---|---|
|
|
Schieberegister zur Bitweisen Eingabe der Zahlen. |
|
|
Speichern die beiden eingegebenen 6-Bit-Zahlen. |
|
|
Speichert das Additions-Ergebnis. |
|
|
Zählt die eingegebenen Bits (0-6). |
|
|
Zählt die Wartezeit zwischen den Phasen. |
|
|
Steuerung der Phasen des Programms. |
|
|
Zähler für die Entprellung der Tasten. |
|
|
Zeigt an, ob eine Taste stabil gedrückt ist. |
|
|
Speichert Flankenerkennung der Tasten. |
3. Tasten-Entprellung
Das Programm entprellt die Taster mit einem einfachen Zählermechanismus:
if (btn1_n == 1'b1) begin
debounce_counter1 <= 0;
end else if (debounce_counter1 < 20'hFFFFF) begin
debounce_counter1 <= debounce_counter1 + 1;
end
if (debounce_counter1 == 20'hFFFFF) begin
btn1_stable <= btn1_n;
btn1_pressed <= ~btn1_stable & btn1_n; // Erkennung einer Flanke
end else begin
btn1_pressed <= 0;
end
-
Wenn
btn1_n
nicht gedrückt ist, wird der Zähler zurückgesetzt. -
Wenn
btn1_n
gedrückt bleibt, wird der Zähler inkrementiert. -
Sobald der Zähler den Schwellenwert
20'hFFFFF
erreicht, gilt die Taste als stabil gedrückt. -
Das Signal
btn1_pressed
speichert eine Flankenerkennung (Taste wird gerade erst gedrückt).
Das Gleiche passiert für btn2_n
.
4. Bitweise Eingabe mit Schieberegister
Die Eingabe erfolgt bitweise, indem shift_reg
mit 1
oder 0
erweitert wird:
if (btn1_pressed) begin
shift_reg <= {shift_reg[4:0], 1'b1}; // Eins hinzufügen
bit_count <= bit_count + 1;
end else if (btn2_pressed) begin
shift_reg <= {shift_reg[4:0], 1'b0}; // Null hinzufügen
bit_count <= bit_count + 1;
end
-
Jeder Tastendruck von
btn1_n
setzt ein1
-Bit am LSB ein. -
Jeder Tastendruck von
btn2_n
setzt ein0
-Bit am LSB ein. -
Sobald 6 Bits eingegeben wurden, wird die Zahl gespeichert.
5. Steuerung der Phasen (phase
Register)
Das Programm wird über eine endliche Zustandsmaschine (FSM) mit fünf Phasen gesteuert:
case (phase)
0: // Erste Zahl eingeben
1: // Pause
2: // Zweite Zahl eingeben
3: // Pause nach der Eingabe
4: // Ergebnis anzeigen
endcase
Phase 0: Erste Zahl eingeben
if (bit_count < 6) begin
// Schieberegister aktualisieren
end else begin
reg1 <= shift_reg; // Speichern der ersten Zahl
shift_reg <= 6'b000000;
bit_count <= 0;
phase <= 1; // Nächste Phase: Pause
delay_counter <= 0;
end
-
Solange weniger als 6 Bits eingegeben wurden, geht die Eingabe weiter.
-
Sobald 6 Bits gespeichert sind, wird
reg1
gespeichert. -
Phase wechselt zu 1 (Pause vor zweiter Eingabe).
Phase 2: Zweite Zahl eingeben
Funktioniert genauso wie die Eingabe der ersten Zahl, speichert aber das Ergebnis in reg2
.
Phase 3: Pause
Nach der Eingabe der zweiten Zahl wird eine kurze Wartezeit eingefügt, bevor die Addition beginnt. Dies dient dazu, sicherzustellen, dass keine versehentlichen Eingaben mehr erfolgen.
Phase 4: Addition und Anzeige
sum <= reverse_bits(reg1) + reverse_bits(reg2);
led_n <= ~reverse_bits(sum);
-
Beide Zahlen werden bitweise umgekehrt (MSB ↔ LSB), da sie rückwärts eingegeben wurden.
-
Die Zahlen werden addiert.
-
Das Ergebnis wird erneut invertiert (
~reverse_bits(sum)
) für die aktive Low-Anzeige der LEDs.
Nach 10 Sekunden Anzeige (Zähler delay_counter
) wird das System zurückgesetzt:
if (delay_counter < 270_000_000) begin
delay_counter <= delay_counter + 1;
end else begin
phase <= 0; // Neustart
led_n <= 6'b000000; // LEDs aus
end
6. Die Funktion reverse_bits()
Da die Bit-Eingabe von LSB nach MSB erfolgt, wird eine Umkehrung benötigt:
function [5:0] reverse_bits(input [5:0] data);
integer i;
begin
for (i = 0; i < 6; i = i + 1) begin
reverse_bits[i] = data[5 - i]; // Bits umdrehen
end
end
endfunction
-
Zugriff auf jedes einzelne Bit und Umkehr der Reihenfolge.
-
Dadurch stimmen die gespeicherten Werte mit der realen Binärdarstellung überein.
7. Hardware-Zuordnung (Constraints-Datei)
Die Pin-Zuordnung zeigt, dass:
-
Takt
clk
auf Pin 52 liegt (27 MHz). -
Taster
btn1_n
(Pin 3) undbtn2_n
(Pin 4) gegen Pull-Up-Widerstände arbeiten (Low-aktiv). -
LEDs
led_n[5:0]
an den Pins 10, 11, 13, 14, 15, 16 hängen.
Code:
Verilog Programm:
module six_bit_adder(
input clk, input btn1_n, input btn2_n, output reg [5:0] led_n ); // Schieberegister und Register für die Eingabewerte reg [5:0] shift_reg = 6'b000000; // Initialisierung mit 1, LEDs aus reg [5:0] reg1 = 6'b0; reg [5:0] reg2 = 6'b0; reg [5:0] sum = 6'b0; // 6 Bit für die Summe (Overflow wird ignoriert) // Zähler und Steuerungsvariablen reg [3:0] bit_count = 0; // Bit-Zähler für die Eingabe (0-6) reg [31:0] delay_counter = 0; // Verzögerungszähler reg [2:0] phase = 0; // Phase: 0 = erster Wert, 1 = Pause, 2 = zweiter Wert, 3 = Pause nach Eingabe, 4 = Anzeige der Summe // Entprellung reg [19:0] debounce_counter1 = 0; reg [19:0] debounce_counter2 = 0; reg btn1_stable = 1'b1; reg btn2_stable = 1'b1; reg btn1_pressed = 1'b0; reg btn2_pressed = 1'b0; // Funktion zum Vertauschen der Bits function [5:0] reverse_bits(input [5:0] data); integer i; begin for (i = 0; i < 6; i = i + 1) begin reverse_bits[i] = data[5 - i]; end end endfunction always @(posedge clk) begin // Entprellung für btn1 if (btn1_n == 1'b1) begin debounce_counter1 <= 0; end else if (debounce_counter1 < 20'hFFFFF) begin debounce_counter1 <= debounce_counter1 + 1; end if (debounce_counter1 == 20'hFFFFF) begin btn1_stable <= btn1_n; btn1_pressed <= ~btn1_stable & btn1_n; // Erkennung einer Flanke end else begin btn1_pressed <= 0; end // Entprellung für btn2 if (btn2_n == 1'b1) begin debounce_counter2 <= 0; end else if (debounce_counter2 < 20'hFFFFF) begin debounce_counter2 <= debounce_counter2 + 1; end if (debounce_counter2 == 20'hFFFFF) begin btn2_stable <= btn2_n; btn2_pressed <= ~btn2_stable & btn2_n; // Erkennung einer Flanke end else begin btn2_pressed <= 0; end // Hauptlogik case (phase) 0: begin // Eingabe des ersten Wertes if (bit_count < 6) begin if (btn1_pressed) begin shift_reg <= {shift_reg[4:0], 1'b1}; bit_count <= bit_count + 1; end else if (btn2_pressed) begin shift_reg <= {shift_reg[4:0], 1'b0}; bit_count <= bit_count + 1; end end else begin reg1 <= shift_reg; // Ersten Wert speichern shift_reg <= 6'b000000; // Schieberegister zurücksetzen (LEDs aus) bit_count <= 0; phase <= 1; // Zur Pause übergehen delay_counter <= 0; end // LEDs zeigen den aktuellen Schieberegister-Wert (keine Bit-Vertauschung hier) led_n <= ~shift_reg; end 1: begin // Pause nach der Eingabe des ersten Wertes if (delay_counter < 27_000_000) begin // 1 Sekunde warten (bei 27 MHz) delay_counter <= delay_counter + 1; end else begin phase <= 2; // Zur Eingabe des zweiten Wertes übergehen end end 2: begin // Eingabe des zweiten Wertes if (bit_count < 6) begin if (btn1_pressed) begin shift_reg <= {shift_reg[4:0], 1'b1}; bit_count <= bit_count + 1; end else if (btn2_pressed) begin shift_reg <= {shift_reg[4:0], 1'b0}; bit_count <= bit_count + 1; end end else begin reg2 <= shift_reg; // Zweiten Wert speichern shift_reg <= 6'b000000; // Schieberegister zurücksetzen (LEDs aus) bit_count <= 0; phase <= 3; // Pause nach der Eingabe delay_counter <= 0; end // LEDs zeigen den aktuellen Schieberegister-Wert (keine Bit-Vertauschung hier) led_n <= ~shift_reg; end 3: begin // Pause nach der Eingabe des zweiten Wertes if (delay_counter < 27_000_000) begin // 1 Sekunde warten (bei 27 MHz) delay_counter <= delay_counter + 1; end else begin phase <= 4; // Weiter zur Anzeige der Summe end end 4: begin // Berechnung und Anzeige der Summe sum <= reverse_bits(reg1) + reverse_bits(reg2); // Berechnung der Summe led_n <= ~reverse_bits(sum); // Nur in der Ergebnisanzeige: Summe invertiert und Bits vertauscht anzeigen if (delay_counter < 270_000_000) begin // 10 Sekunden warten (bei 27 MHz) delay_counter <= delay_counter + 1; end else begin delay_counter <= 0; phase <= 0; // Zurück zur ersten Eingabe led_n <= 6'b000000; // LEDs ausschalten end end endcase end endmodule
Constraints Datei:
// Takt: 27 MHz IO_LOC "clk" 52; IO_PORT "clk" IO_TYPE=LVCMOS33 PULL_MODE=UP; // Taster: Aktiviert bei Low IO_LOC "btn1_n" 3; IO_PORT "btn1_n" PULL_MODE=UP; IO_LOC "btn2_n" 4; IO_PORT "btn2_n" PULL_MODE=UP; // LEDs: Aktiviert bei Low IO_LOC "led_n[0]" 10; IO_PORT "led_n[0]" PULL_MODE=UP DRIVE=8; IO_LOC "led_n[1]" 11; IO_PORT "led_n[1]" PULL_MODE=UP DRIVE=8; IO_LOC "led_n[2]" 13; IO_PORT "led_n[2]" PULL_MODE=UP DRIVE=8; IO_LOC "led_n[3]" 14; IO_PORT "led_n[3]" PULL_MODE=UP DRIVE=8; IO_LOC "led_n[4]" 15; IO_PORT "led_n[4]" PULL_MODE=UP DRIVE=8; IO_LOC "led_n[5]" 16; IO_PORT "led_n[5]" PULL_MODE=UP DRIVE=8;
Die Entwicklungsumgebung
Holt Euch von GOWIN nach einer Registrierung die Entwicklungsumgebung ‚Gowin V1.9.10.03 Education (Windows x86)‘ und installiert sie auf Eurem Windows Rechner. Ruft die IDE auf und legt mit ‚File-> New…‘ eine neues Projekt namens ‚6-bit-adder‘ an.
Wählt im Fenster ‚Select Device‘ folgende Einstellungen aus:
Klickt ‚Next‘ und ‚Finish‘ an.
Wählt das neu erstellte Projekt aus und wählt aus dem Popupmenü nach einem Rechtsklick ‚Verilog File‘ aus. Kopiert den Programmcode in das Editorfenster der neu entstandenen Datei und speichert.
Wiederholt den Vorgang und legt eine ‚Physical Constraints Datei‘ an.
Kopiert die Zuweisungen in das Editorfenster der neu entstandenen Datei und speichert. Dann solltet Ihr in Eurem Projektfenster in etwa folgendes sehen:
Jetzt könnt Ihr mit dem Button
den Generierungsvorgang starten. Danach könnt Ihr mit dem Button
das Übertragungsprogramm starten.
Klickt auf ‚Save‘. Wenn Ihr wollt, dass die erzeugte Konfigurationsdatei auch nach einem Stromverlust auf dem Tang Nano 9K erhalten bleibt, müsst Ihr vor der Übertragung noch einen Rechtsklick auf die Zeile mit Eurem Projekt machen und im erscheinenden Popupmenü ‚Configure Device‘ wählen.
Im erscheinenden Fenster ‚Device configuration‘ setzt Ihr ‚Access Mode‘ auf ‚Embeded Flash Mode‘. Mit dem ‚Save‘ Button abschließen und mit einem Klick auf diesen Button
an das Tang Nano 9K übertragen.
Das war’s. 🙂