Einführung
Erinnerst du dich an das Schlangenspiel aus der Kindheit, bei dem eine Schlange auf dem Bildschirm läuft und versucht, einen Apfel zu essen? Dieser Artikel beschreibt unsere Implementierung des Spiels auf einem FPGA 1 .

Abbildung 1. Gameplay
Stellen wir uns zunächst vor und erläutern die Gründe, warum wir an dem Projekt gearbeitet haben. Wir sind zu dritt : Tymur Lysenko , Daniil Manakovskiy und Sergey Makarov . Als Studienanfänger der Innopolis University hatten wir einen Kurs in "Computerarchitektur", der professionell unterrichtet wird und es dem Lernenden ermöglicht, die Struktur eines Computers auf niedriger Ebene zu verstehen. Irgendwann während des Kurses gaben uns die Instruktoren die Möglichkeit, ein Projekt für ein FPGA für zusätzliche Punkte im Kurs zu entwickeln. Unsere Motivation war nicht nur die Note, sondern auch unser Interesse, mehr Erfahrung im Hardware-Design zu sammeln, die Ergebnisse zu teilen und schließlich ein unterhaltsames Spiel zu haben.
Lassen Sie uns nun auf dunkle, tiefe Details eingehen.
Projektübersicht
Für unser Projekt haben wir ein einfach zu implementierendes und unterhaltsames Spiel ausgewählt, nämlich die "Schlange". Die Struktur der Implementierung sieht wie folgt aus: Zuerst wird eine Eingabe von einem SPI-Joystick genommen, dann verarbeitet, und schließlich wird ein Bild an einen VGA-Monitor ausgegeben und eine Punktzahl wird auf einer 7-Segment-Anzeige (in hexadezimaler Darstellung) angezeigt. Obwohl die Spielelogik intuitiv und unkompliziert ist, waren VGA und der Joystick interessante Herausforderungen und ihre Implementierung hat zu einem guten Spielerlebnis geführt.
Das Spiel hat die folgenden Regeln. Ein Spieler beginnt mit einem einzelnen Schlangenkopf. Das Ziel ist es, Äpfel zu essen, die zufällig auf dem Bildschirm erzeugt werden, nachdem der vorherige gegessen wurde. Außerdem wird die Schlange nach Befriedigung des Hungers um 1 Schwanz verlängert. Die Schwänze bewegen sich nacheinander und folgen dem Kopf. Die Schlange bewegt sich immer. Wenn die Bildschirmränder erreicht sind, wird die Schlange auf eine andere Seite des Bildschirms übertragen. Wenn der Kopf den Schwanz trifft, ist das Spiel vorbei.
- Altera Cyclone IV (EP4CE6E22C8N) mit 6272 logischen Elementen, integriertem 50-MHz-Takt, 3-Bit-Farb-VGA, 8-stelliger 7-Segment-Anzeige. Das FPGA kann keinen analogen Eingang an seine Pins anschließen.
- SPI-Joystick (KY-023)
- Ein VGA-Monitor, der eine Bildwiederholfrequenz von 60 Hz unterstützt
- Quartus Prime Lite Edition 18.0.0 Build 614
- Verilog HDL IEEE 1364-2001
- Steckbrett
- Elektrische Elemente:
- 8 Stecker-Buchsen
- 1 Buchse-Buchse
- 1 Stecker-Stecker
- 4 Widerstände (4,7 KΩ)
Architekturübersicht
Die Architektur des Projekts ist ein wichtiger Faktor. Abbildung 2 zeigt diese Architektur aus Sicht der obersten Ebene:

Abbildung 2. Ansicht des Entwurfs auf oberster Ebene ( pdf )
Wie Sie sehen können, gibt es viele Ein- und Ausgänge sowie einige Module. In diesem Abschnitt wird beschrieben, was jedes Element bedeutet, und es wird angegeben, welche Pins auf der Karte für die Ports verwendet werden.
Haupteingänge
Die für die Implementierung erforderlichen Haupteingaben sind res_x_one , res_x_two , res_y_one , res_y_two , die zum Empfangen einer aktuellen Richtung eines Joysticks verwendet werden. Abbildung 3 zeigt die Zuordnung zwischen ihren Werten und den Richtungen.
Eingabe | Links | Richtig | Auf | Runter | Keine Richtungsänderung |
---|
res_x_one (PIN_30) | 1 | 0 | x | x | 1 |
res_x_two (PIN_52) | 1 | 0 | x | x | 0 |
res_y_one (PIN_39) | x | x | 1 | 0 | 1 |
res_y_two (PIN_44) | x | x | 1 | 0 | 0 |
Abbildung 3. Zuordnung von Joystick-Eingaben und -Richtungen
- clk - die Kartenuhr (PIN_23)
- reset - Signal zum Zurücksetzen des Spiels und zum Beenden des Druckvorgangs (PIN_58)
- Farbe - Wenn 1, werden alle möglichen Farben auf dem Bildschirm ausgegeben und nur zu Demonstrationszwecken verwendet (PIN_68).
Hauptmodule
joystick_input wird verwendet, um einen Richtungscode basierend auf einer Eingabe vom Joystick zu erzeugen.
game_logic
game_logic enthält die gesamte Logik, die zum Spielen eines Spiels erforderlich ist. Das Modul bewegt eine Schlange in eine bestimmte Richtung. Darüber hinaus ist es für das Apfelfressen und die Kollisionserkennung verantwortlich. Darüber hinaus empfängt es die aktuellen x- und y-Koordinaten eines Pixels auf dem Bildschirm und gibt eine an der Position platzierte Entität zurück.
VGA_Draw
Die Schublade setzt die Farbe eines Pixels basierend auf der aktuellen Position ( iVGA_X, iVGA_Y ) und der aktuellen Entität ( ent ) auf einen bestimmten Wert.
VGA_Ctrl
Erzeugt einen Steuerbitstrom zum VGA-Ausgang ( V_Sync, H_Sync, R, G, B ).
SSEG_Display 2
SSEG_Display ist ein Treiber zur Ausgabe der aktuellen Punktzahl auf der 7-Segment-Anzeige.
Vga_clk
VGA_clk empfängt einen 50-MHz-Takt und reduziert ihn auf 25,175 MHz.
game_upd_clk
game_upd_clk ist ein Modul, das eine spezielle Uhr generiert, die eine Aktualisierung eines Spielstatus auslöst.
Ausgänge
- VGA_B - Blauer VGA-Pin (PIN_144)
- VGA_G - Grüner VGA-Pin (PIN_1)
- VGA_R - Roter VGA-Pin (PIN_2)
- VGA_HS - Horizontale VGA-Synchronisation (PIN_142)
- VGA_VS - Vertikale VGA-Synchronisation (PIN_143)
- sseg_a_to_dp - Gibt an, welches der 8 Segmente beleuchtet werden soll (PIN_115, PIN_119, PIN_120, PIN_121, PIN_124, PIN_125, PIN_126, PIN_127).
- sseg_an - Gibt an, welche der 4 7-Segment-Anzeigen verwendet werden sollen (PIN_128, PIN_129, PIN_132, PIN_133).
Implementierung

Abbildung 4. SPI-Joystick (KY-023)
Bei der Implementierung eines Eingangsmoduls haben wir festgestellt, dass der Stick ein analoges Signal erzeugt. Der Joystick hat 3 Positionen für jede Achse:
- top - ~ 5V Ausgang
- Mitte - ~ 2,5V Ausgang
- niedrig - ~ 0V Ausgang
Die Eingabe ist dem ternären System sehr ähnlich: Für die X-Achse haben wir true
(links), false
(rechts) und einen undetermined
Zustand, in dem sich der Joystick weder links noch rechts befindet. Das Problem ist, dass die FPGA-Karte nur einen digitalen Eingang verarbeiten kann. Daher können wir diese ternäre Logik nicht einfach durch Schreiben von Code in eine Binärlogik konvertieren. Die erste vorgeschlagene Lösung bestand darin, einen Analog-Digital-Wandler zu finden. Dann beschlossen wir, unsere schulischen Kenntnisse der Physik zu nutzen und den Spannungsteiler 3 zu implementieren. Um die drei Zustände zu definieren, benötigen wir zwei Bits: 00 ist false
, 01 ist undefined
und 11 ist true
. Nach einigen Messungen stellten wir fest, dass auf unserer Platine die Grenze zwischen Null und Eins ungefähr 1,7 V beträgt. Daher haben wir das folgende Schema erstellt (Bild erstellt mit Circuitlab 4 ):

Abbildung 5. Schaltung für ADC für Joystick
Die physische Implementierung basiert auf Arduino-Kit-Elementen und sieht wie folgt aus:

Abbildung 6. ADC-Implementierung
Unsere Schaltung nimmt einen Eingang für jede Achse und erzeugt zwei Ausgänge: Der erste kommt direkt vom Stick und wird nur dann Null, wenn der Joystick zero
ausgibt. Die Sekunde ist 0 im undetermined
Zustand, aber immer noch 1 im true
Zustand. Dies ist das genaue Ergebnis, das wir erwartet haben.
Die Logik des Eingangsmoduls lautet:
- Wir übersetzen unsere ternäre Logik in einfache Binärdrähte für jede Richtung;
- Bei jedem Taktzyklus prüfen wir, ob nur eine Richtung
true
(die Schlange kann nicht diagonal gehen); - Wir vergleichen unsere neue Richtung mit der vorherigen, um zu verhindern, dass sich die Schlange selbst frisst, indem wir dem Spieler nicht erlauben, die Richtung in die entgegengesetzte Richtung zu ändern.
Ein Teil des Eingabemodulcodes reg left, right, up, down; initial begin direction = `TOP_DIR; end always @(posedge clk) begin //1 left = two_resistors_x; right = ~one_resistor_x; up = two_resistors_y; down = ~one_resistor_y; if (left + right + up + down == 3'b001) //2 begin if (left && (direction != `RIGHT_DIR)) //3 begin direction = `LEFT_DIR; end //same code for other directions end end
Ausgabe an VGA
Wir haben uns für eine Ausgabe mit einer Auflösung von 640 x 480 auf einem 60-Hz-Bildschirm mit 60 FPS entschieden.
Das VGA-Modul besteht aus 2 Hauptteilen: einem Treiber und einer Schublade . Der Treiber erzeugt einen Bitstrom, der aus vertikalen, horizontalen Synchronisationssignalen und einer Farbe besteht, die den VGA-Ausgängen zugewiesen wird. Ein Artikel 5 von @SlavikMIPT beschreibt die Grundprinzipien der Arbeit mit VGA. Wir haben den Treiber aus dem Artikel an unser Board angepasst.
Wir haben beschlossen, den Bildschirm in ein Raster von 40 x 30 Elementen zu unterteilen, das aus Quadraten von 16 x 16 Pixel besteht. Jedes Element steht für 1 Spieleinheit: entweder einen Apfel, einen Schlangenkopf, einen Schwanz oder nichts.
Der nächste Schritt in unserer Implementierung bestand darin, Sprites für die Entitäten zu erstellen.
Cyclone IV hat nur 3 Bits, um eine Farbe auf VGA darzustellen (1 für Rot, 1 für Grün und 1 für Blau). Aufgrund dieser Einschränkung mussten wir einen Konverter implementieren, um die Farben der Bilder an die verfügbaren anzupassen. Zu diesem Zweck haben wir ein Python-Skript erstellt, das einen RGB-Wert jedes Pixels durch 128 teilt.
Das Python-Skript from PIL import Image, ImageDraw filename = "snake_head" index = 1 im = Image.open(filename + ".png") n = Image.new('RGB', (16, 16)) d = ImageDraw.Draw(n) pix = im.load() size = im.size data = [] code = "sp[" + str(index) + "][{i}][{j}] = 3'b{RGB};\\\n" with open("code_" + filename + ".txt", 'w') as f: for i in range(size[0]): tmp = [] for j in range(size[1]): clr = im.getpixel((i, j)) vg = "{0}{1}{2}".format(int(clr[0] / 128),
Original | Nach dem Skript |

| 
|
Abbildung 7. Vergleich zwischen Eingabe und Ausgabe
Der Hauptzweck der Schublade besteht darin, eine Farbe eines Pixels basierend auf der aktuellen Position ( iVGA_X, iVGA_Y ) und der aktuellen Entität ( ent ) an VGA zu senden. Alle Sprites sind fest codiert, können jedoch leicht geändert werden, indem mit dem obigen Skript ein neuer Code generiert wird.
Schubladenlogik always @(posedge iVGA_CLK or posedge reset) begin if(reset) begin oRed <= 0; oGreen <= 0; oBlue <= 0; end else begin // DRAW CURRENT STATE if (ent == `ENT_NOTHING) begin oRed <= 1; oGreen <= 1; oBlue <= 1; end else begin // Drawing a particular pixel from sprite oRed <= sp[ent][iVGA_X % `H_SQUARE][iVGA_Y % `V_SQUARE][0]; oGreen <= sp[ent][iVGA_X % `H_SQUARE][iVGA_Y % `V_SQUARE][1]; oBlue <= sp[ent][iVGA_X % `H_SQUARE][iVGA_Y % `V_SQUARE][2]; end end end
Ausgabe an die 7-Segment-Anzeige
Um dem Spieler die Anzeige seiner Punktzahl zu ermöglichen, haben wir beschlossen, eine Spielpunktzahl auf der 7-Segment-Anzeige auszugeben. Aus Zeitgründen haben wir den Code aus EP4CE6 Starter Board Documentation 2 verwendet . Dieses Modul gibt eine Hexadezimalzahl an die Anzeige aus.
Spielelogik
Während der Entwicklung haben wir verschiedene Ansätze ausprobiert. Am Ende haben wir jedoch einen Ansatz gefunden, der nur wenig Speicher benötigt, einfach in Hardware zu implementieren ist und von parallelen Berechnungen profitieren kann.
Das Modul führt mehrere Funktionen aus. Da VGA bei jedem Taktzyklus ein Pixel zeichnet, beginnend vom oberen linken zum unteren rechten, muss das VGA_Draw-Modul, das für die Erzeugung einer Farbe für ein Pixel verantwortlich ist, identifizieren, welche Farbe für die aktuellen Koordinaten verwendet werden soll. Das sollte das Spielelogikmodul ausgeben - einen Entitätscode für die angegebenen Koordinaten.
Außerdem muss der Spielstatus erst aktualisiert werden, nachdem der Vollbildmodus gezeichnet wurde. Ein vom Modul game_upd_clk erzeugtes Signal wird verwendet, um zu bestimmen, wann aktualisiert werden soll.
Spielstatus
Der Spielstatus besteht aus:
- Koordinaten des Schlangenkopfes
- Eine Reihe von Koordinaten des Schwanzes der Schlange. Das Array ist in unserer Implementierung durch 128 Elemente begrenzt
- Anzahl der Schwänze
- Koordinaten eines Apfels
- Spiel über Flagge
- Spiel gewann Flagge
Die Aktualisierung des Spielstatus umfasst mehrere Phasen:
- Bewegen Sie den Kopf der Schlange basierend auf einer bestimmten Richtung zu neuen Koordinaten. Wenn sich herausstellt, dass sich eine Koordinate an ihrer Kante befindet und weiter geändert werden muss, muss der Kopf zu einer anderen Kante des Bildschirms springen. Beispielsweise wird eine Richtung nach links festgelegt und die aktuelle X-Koordinate ist 0. Daher sollte die neue X-Koordinate gleich der letzten horizontalen Adresse werden.
- Neue Koordinaten des Schlangenkopfes werden gegen Apfelkoordinaten getestet:
2.1. Wenn sie gleich sind und das Array nicht voll ist, fügen Sie dem Array einen neuen Schwanz hinzu und erhöhen Sie den Schwanzzähler. Wenn der Zähler seinen höchsten Wert erreicht (in unserem Fall 128), wird die Flagge für das gewonnene Spiel eingerichtet und das bedeutet, dass die Schlange nicht mehr wachsen kann und das Spiel weiterhin fortgesetzt wird. Der neue Schwanz wird auf die vorherigen Koordinaten des Schlangenkopfes gelegt. Zufällige Koordinaten für X und Y sollten verwendet werden, um dort einen Apfel zu platzieren.
2.2. Falls sie nicht gleich sind, vertauschen Sie nacheinander die Koordinaten der benachbarten Schwänze. (n + 1) -ter Schwanz sollte Koordinaten von n-ten erhalten, falls der n-te Schwanz vor (n + 1) -ten hinzugefügt wurde. Der erste Schwanz erhält alte Koordinaten des Kopfes. - Überprüfen Sie, ob die neuen Koordinaten des Kopfes der Schlange mit den Koordinaten eines Schwanzes übereinstimmen. In diesem Fall wird das Spiel über der Flagge gehisst und das Spiel gestoppt.
Zufällige Koordinatengenerierung
Zufallszahlen, die durch Aufnehmen von Zufallsbits erzeugt werden, die von 6-Bit -Schieberegistern mit linearer Rückkopplung (LFSR) 6 erzeugt werden . Um die Zahlen in einen Bildschirm einzupassen, werden sie durch die Abmessungen des Spielgitters geteilt und der Rest wird genommen.
Fazit
Nach 8 Wochen Arbeit wurde das Projekt erfolgreich umgesetzt. Wir haben einige Erfahrung in der Spieleentwicklung und haben eine unterhaltsame Version eines "Snake" -Spiels für ein FPGA erhalten. Das Spiel ist spielbar und unsere Fähigkeiten beim Programmieren, Entwerfen einer Architektur und Soft Skills haben sich verbessert.
Anerkannte Segmente
Wir möchten unseren Professoren Muhammad Fahim und Alexander Tormasov unseren besonderen Dank und Dank dafür aussprechen, dass sie uns das profunde Wissen und die Möglichkeit gegeben haben, es in die Praxis umzusetzen. Wir danken Vladislav Ostankovich von Herzen für die Bereitstellung der im Projekt verwendeten Hardware und Temur Kholmatov für die Unterstützung beim Debuggen. Wir würden nicht vergessen, uns daran zu erinnern, dass Anastassiya Boiko wunderschöne Sprites für das Spiel gezeichnet hat. Wir möchten auch Rabab Marouf für das Korrekturlesen und Bearbeiten dieses Artikels unsere aufrichtige Wertschätzung aussprechen .
Vielen Dank für all diejenigen, die uns geholfen haben, das Spiel zu testen und einen Rekord aufzustellen. Ich hoffe es gefällt euch!
Referenzen
[1]: Projekt auf dem Github
[2]: [FPGA] EP4CE6 Starter Board-Dokumentation
[3]: Spannungsteiler
[4]: Werkzeug zur Modellierung von Schaltkreisen
[5]: VGA-Adapter für FPGA Altera Cyclone III
[6]: Linear-Feedback-Schieberegister (LFSR) auf Wikipedia
LFSR in einem FPGA - VHDL & Verilog Code
Eine Apfeltextur
Idee, Zufallszahlen zu generieren
Palnitkar, S. (2003). Verilog HDL: Ein Leitfaden für digitales Design und Synthese, zweite Ausgabe.