FPGA Quick and Dirty

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:

  1. Eingabe der ersten Zahl (über Taster, bitweise).

  2. Kurze Wartezeit.

  3. Eingabe der zweiten Zahl.

  4. Kurze Wartezeit.

  5. Addition und Anzeige des Ergebnisses auf den LEDs für 10 Sekunden.

  6. 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

shift_reg

reg [5:0]

Schieberegister zur Bitweisen Eingabe der Zahlen.

reg1, reg2

reg [5:0]

Speichern die beiden eingegebenen 6-Bit-Zahlen.

sum

reg [5:0]

Speichert das Additions-Ergebnis.

bit_count

reg [3:0]

Zählt die eingegebenen Bits (0-6).

delay_counter

reg [31:0]

Zählt die Wartezeit zwischen den Phasen.

phase

reg [2:0]

Steuerung der Phasen des Programms.

debounce_counter1, debounce_counter2

reg [19:0]

Zähler für die Entprellung der Tasten.

btn1_stable, btn2_stable

reg

Zeigt an, ob eine Taste stabil gedrückt ist.

btn1_pressed, btn2_pressed

reg

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 ein 1-Bit am LSB ein.

  • Jeder Tastendruck von btn2_n setzt ein 0-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) und btn2_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. 🙂

 

  • Related Posts

    Wie Elon tickt

    Elon Musk gehört zweifellos zu den faszinierendsten Persönlichkeiten unserer Zeit. Bekannt durch ambitionierte Projekte wie Tesla, SpaceX und neuerdings X (vormals Twitter), lohnt es sich, genauer zu betrachten, was diesen…

    Wie ich zusammen mit Grok 3 die Zukunft der KI entwickle!

    Einleitung Mein Protokoll einer Sitzung mit Grok 3 beschreibt meine faszinierende Reise durch die Vision, eine lokal lauffähige KI für einen durchschnittlichen PC zu entwickeln. Er beginnt mit meiner Idee,…

    Schreibe einen Kommentar

    Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

    You Missed

    Der digitale Grenzführer

    • Von Zuseway
    • April 25, 2025
    • 9 views

    Intel unter neuer Führung: Strategien und Herausforderungen

    Motorolas stille Revolution: Eine Reise vom Außenseiter zum Innovator

    • Von Zuseway
    • April 25, 2025
    • 10 views
    Motorolas stille Revolution: Eine Reise vom Außenseiter zum Innovator

    Der smarte Schlosstanz

    • Von Zuseway
    • April 24, 2025
    • 23 views

    Die Zukunft der MacBooks: Ein detaillierter Einblick

    Kabellose Freiheit für Autofahrer: Der Aufstieg der Wireless-CarPlay-Adapter

    • Von Zuseway
    • April 24, 2025
    • 13 views
    Kabellose Freiheit für Autofahrer: Der Aufstieg der Wireless-CarPlay-Adapter