LED Tetris: Unterschied zwischen den Versionen
(39 dazwischenliegende Versionen von 2 Benutzern werden nicht angezeigt) | |||
Zeile 2: | Zeile 2: | ||
[[Kategorie:ProjekteET MTR BSE WS2020]] | [[Kategorie:ProjekteET MTR BSE WS2020]] | ||
<!-- Kopieren Sie diesen Header in Ihren Artikel, damit er aufgelistet wird. --> | <!-- Kopieren Sie diesen Header in Ihren Artikel, damit er aufgelistet wird. --> | ||
'''Autoren: Yannick Schmidt & Nils Koch''' <br/> | '''Autoren: [[Benutzer: Yannick_Schmidt|Yannick Schmidt]] & [[Benutzer: Nils_Koch|Nils Koch]]''' <br/> | ||
'''Betreuer: Prof. Göbel & Prof. Schneider''' <br/> | '''Betreuer: Prof. Göbel & Prof. Schneider''' <br/> | ||
→ zurück zur Übersicht: [[:Kategorie:ProjekteET_MTR_BSE_WS2020|WS 20/21: Angewandte Elektrotechnik (BSE)]] | → zurück zur Übersicht: [[:Kategorie:ProjekteET_MTR_BSE_WS2020|WS 20/21: Angewandte Elektrotechnik (BSE)]] | ||
[[Datei:Messehintergrund.jpg|mini|rechts|LED-Tetris Messehintergrund]] | |||
== Einleitung == | == Einleitung == | ||
Das Ziel ist es das Spiel "Tetris" auf einer selbstgebauten 10x20 LED Matrix zu realisieren. Der aktuelle Score und der Highscore sollen auf einem kleinen Display angezeigt werden. Die Eingabe erfolgt per selbstgebauten Gamepad. | Das Ziel der Gruppe von [[Benutzer: Yannick_Schmidt|Yannick Schmidt]] & [[Benutzer: Nils_Koch|Nils Koch]] ist es das Spiel "Tetris" auf einer selbstgebauten 10x20 LED Matrix im Rahmen des [[:Kategorie:ProjekteET_MTR_BSE_WS2021|GET-Fachpraktikums]] zu realisieren. Der aktuelle Score und der Highscore sollen auf einem kleinen Display angezeigt werden. Die Eingabe erfolgt per selbstgebauten Gamepad. | ||
== Anforderungen == | == Anforderungen == | ||
-Möglichst dicht gepackte LED Matrix | {| class="mw-datatable" | ||
-Rechteckiger LED Look | |+ Anforderungen | ||
-Start- und Endanimation | ! style="font-weight: bold;" | ID | ||
-Speicherbarer Highscore | ! style="font-weight: bold;" | Inhalt | ||
-Eingabe per kabelgebundenes Gamepad (evtl. auch kabellos per Bluetooth) | ! style="font-weight: bold;" | Ersteller | ||
-Stromversorgung mittels USB Powerbank | ! style="font-weight: bold;" | Datum | ||
-Optional: Staufach für das Gamepad, welches sich per Sensor öffnen lässt | ! style="font-weight: bold;" | Geprüft von | ||
! style="font-weight: bold;" | Datum | |||
|- | |||
| 1 | |||
| Möglichst dicht gepackte LED Matrix | |||
| Yannick Schmidt | |||
| 5.10.2021 | |||
| Yannick Schmidt, Nils Koch | |||
| 26.10.2021 | |||
|- | |||
| 2 | |||
| Rechteckiger LED Look | |||
| Yannick Schmidt | |||
| 5.10.2021 | |||
| Yannick Schmidt, Nils Koch | |||
| 26.10.2021 | |||
|- | |||
| 3 | |||
| Start- und Endanimation | |||
| Yannick Schmidt | |||
| 5.10.2021 | |||
| Yannick Schmidt, Nils Koch | |||
| 26.10.2021 | |||
|- | |||
| 4 | |||
| Speicherbarer Highscore | |||
| Yannick Schmidt | |||
| 5.10.2021 | |||
| Yannick Schmidt, Nils Koch | |||
| 26.10.2021 | |||
|- | |||
| 5 | |||
| Eingabe per kabelgebundenes Gamepad (evtl. auch kabellos per Bluetooth) | |||
| Yannick Schmidt | |||
| 5.10.2021 | |||
| Yannick Schmidt, Nils Koch | |||
| 26.10.2021 | |||
|- | |||
| 6 | |||
| Stromversorgung mittels USB Powerbank | |||
| Yannick Schmidt | |||
| 5.10.2021 | |||
| Yannick Schmidt, Nils Koch | |||
| 26.10.2021 | |||
|- | |||
| 7 | |||
| Optional: Staufach für das Gamepad, welches sich per Sensor öffnen lässt | |||
| Yannick Schmidt | |||
| 5.10.2021 | |||
| Yannick Schmidt, Nils Koch | |||
| 26.10.2021 | |||
|- | |||
|} | |||
== Funktionaler Systementwurf/Technischer Systementwurf == | == Funktionaler Systementwurf/Technischer Systementwurf == | ||
[[Datei:LED-Tetris_Funktionaler_Aufbau.png| | [[Datei:LED-Tetris_Funktionaler_Aufbau.png|800px|mini|left|Systementwurf]] | ||
[[Datei:LED-Tetris_Technischer_Aufbau_Ausgabe.jpg|800px|mini|left|Technischer Aufbau]] | |||
<br clear=all> | |||
<!-- Füllen Sie Ihre Projektskizze bis hierher aus. Fügen Sie einen Projektplan unten ein. --> | <!-- Füllen Sie Ihre Projektskizze bis hierher aus. Fügen Sie einen Projektplan unten ein. --> | ||
== Komponentenspezifikation == | == Komponentenspezifikation == | ||
{| class="mw-datatable" | |||
! style="font-weight: bold;" | Komponente | |||
! style="font-weight: bold;" | Beschreibung | |||
! style="font-weight: bold;" | Abbildung | |||
|- | |||
| Arduino UNO | |||
| Microcontroller<br>14 digitale I/O Pins<br>6 analoge Eingänge | |||
|[[Datei:Arduino UNO R3.jpg|100px|mini|zentriert|Arduino Uno Board]] | |||
|- | |||
| WS2812B ECO | |||
| 3 LEDs<br>Rot, Grün, Blau<br>256 Helligkeitsstufen | |||
|[[Datei:WS2812B.jpg|100px|mini|zentriert|WS2812B]] | |||
|- | |||
| LCD-Display | |||
| I2C-Verbindung<br>16x2 Pixel Auflösung<br>Hintergrundbeleuchtung | |||
|[[Datei:I2c_1602_display.jpg|100px|mini|zentriert|I2C LCD Display]] | |||
|- | |||
| Gitter | |||
| 3 Teile<br>Rechteckiger Ausschnitt für LEDs<br>Bohrlöcher und Stifte zum Verbauen | |||
|[[Datei:Tetris-Gitter.jpg|200px|mini|zentriert|Gitter]] | |||
|- | |||
| Controller | |||
| Design orientiert am SNES Controller<br>Controller-Board mittig befestigt<br>Clip-Verbindung zum einfachen öffnen | |||
|[[Datei:Tetris-Controller.jpg|100px|mini|zentriert|Controller]] | |||
|} | |||
== Umsetzung (HW/SW) == | == Umsetzung (HW/SW) == | ||
=== Hardware === | |||
Die Hardware besteht aus der LED-Einheit und dem Controller | |||
==== LED-Einheit ==== | |||
Die LED-Einheit umfasst eine Bodenplatte, auf der 10 LED-Streifen des Typs WS2812b mit einer Länge von 20 LEDs in Form einer Matrix parallel aufgeklebt sind. | |||
Auf der LED-Matrix wird ein Gitter montiert, welches als Abstandshalter zur Kunststoffscheibe fungiert und einen rechteckigen Leuchteffekt der LEDs erzeugt. | |||
Unterhalb der Matrix ist das LCD-Display montiert. | |||
===== Bodenplatte ===== | |||
[[Datei:LED-Tetris-Zeichnung-Bodenplatte.jpg|200px|right|mini|Abb 1: Bodenplatte]] | |||
Die in der Abbildung 1 zu sehende Zeichnung zeigt die Bodenplatte. <br> | |||
Sie besteht aus einer 3mm Holzplatte, die auf '''196mm Breite''' und '''500mm Höhe''' zugeschnitten.<br> | |||
Es wurden die 6 Bohrlöcher des Gitters übertragen und der Ausschnitt des LCD-Displays ausgeschnitten. | |||
<br clear=all> | |||
===== LED-Matrix ===== | |||
[[Datei:LED-Tetris-Zeichnung-Matrix.jpg|200px|right|mini|Abb 2: LED-Matrix]] | |||
Der in der Abbildung 2 zu sehende Ausschnitt einer Zeichnung zeigt die LED-Matrix.<br> | |||
Sie besteht aus einem WS2812B Eco LED-Stripe, der auf '''10 Abschnitte mit je 20 LEDs''' gekürzt wurde.<br> | |||
Hierbei war beim kleben zu beachten, dass die Streifen genau '''parallel mit 16,67mm Abstand''' liegen. | |||
<br clear=all> | |||
===== Gitter ===== | |||
[[Datei:Tetris-Gitter.jpg|500px|right|mini|Abb 4: LED-Matrix]] | |||
Das auf Abbildung 4 zu sehende Gitter ist als 3D Druck entworfen. <br> | |||
Jedes Loch sitzt über einer LED, wodurch sich ebenfalls ein Abstand von 16,67mm ergibt. <br> | |||
Das Gitter musst auf die Bodenplatte passen, wodurch sich eine '''Breite von 196mm''' ergibt. <br> | |||
Die '''Länge von 343,4mm''' kommt durch die Summe von 20 mal 16,67mm Abstand und 2 mal 5mm Wanddicke . <br> | |||
Zur Streuung des Lichts beträgt die '''Tiefe 16,67mm'''. <br> | |||
Durch das Bauvolumen, des Druckers Dremel 3D20, von 230x150mm musste das Gitter in 3 Teile geteilt werden.<br> | |||
Zur Verbindung der Gitterteile sind '''4mm Stiftlöcher''' in die oberen und unteren Teile eingefügt worden.<br> | |||
Dem mittleren Teil wurden '''3mm Stifte''' hinzugefügt. | |||
Zur Befestigung der Gitterteile auf der Bodenplatte gibt es mittig auf jeder Seite 4mm Bohrungen. | |||
==== Controller ==== | |||
[[Datei:Tetris-Controller.jpg|200px|right|mini|Abb 5: Controller]] | |||
Der in Abbildung 5 zu sehende Controller ist optische dem SNES-Controller nachempfunden. | |||
Anforderungen an den Controller waren, dass er zum einen das Controller-Board umfasst und aus zwei Teilen besteht, wobei diese ohne Schauben befestigt werden sollen.<br> | |||
Das Controller-Board hat die Maße 70x30mm. <br> | |||
Es befinden sich mit dem Abstand von der Mitte aus 32,5mm und 17,5mm Löcher, um das Board zu befestigen. | |||
Anhand dieser Maße gehen bei dem unteren Teil '''1,5mm dicke Stifte''' empor, die das Board in Position halten. Im unteren Teil der Stifte ist der Durchmesser größer, damit das Board angehoben wird. <br> | |||
Links und Rechts befinden sich jeweils '''Halbkreise mit 50mm Durchmesser'''. | |||
Das obere Teil entspricht den Außenmaßen des unteren. Es ist mit einem Ausschnitt für das Controller-Board versehen, welcher einen Offset nach innen hat, damit das Board fest sitzt. | |||
<br clear=all> | |||
---- | |||
==== Elektronik ==== | |||
[[Datei:Tetris-Fritzing.png|500px|right|mini|Abb 6: Elektrische Schaltung]] | |||
Die Abbildung 6 zeigt die Elektrische Schaltung. | |||
Die Stromquelle stellt eine USB 5V Quelle(Netzteil oder Powerbank) dar. | |||
Von ihr aus gehen 5V und Erdung(GND) zum Power-Board. | |||
Das '''Power-Board''' verbindet: | |||
* '''5V''' an das '''LED-Power-Board''', das '''LCD-Display''' und den '''Arduino Uno''' | |||
* '''GND''' an das '''LED-Power-Board''', das '''LCD-Display''', den '''Arduino Uno''' und den '''Connector''' für das Controller-Board. | |||
Der '''Arduino Uno''' verbindet: | |||
* DigitalPins | |||
** DigitalPin '''7''' mit '''LED-Power-Board''' | |||
** DigitalPin '''8,9,10,11''' mit dem '''Connector''' für das Controller-Board | |||
* AnalogPins | |||
** '''A4''' mit '''SDA''' des Displays | |||
** '''A5''' mit '''SCL''' des Displays | |||
Das '''LED-Power-Board''' verbindet: | |||
* '''5V''' und '''GND''' mit '''10 LED-Stripes'''(In der Abbildung nur 4, da übersichtlicher) | |||
* DigitalPin '''7''' mit dem '''Din''' des ersten LED-Stripes | |||
Die '''WS2812B''' Stripes verbinden am Ende eines Stripes den '''Dout''' mit dem '''Din''' des nächsten Stripe. | |||
Der '''Connector''' verbindet 1:1 das '''Controller-Board'''. | |||
Das '''Controller-Board''' verbindet über den Connector: | |||
* Taster '''Links''' mit '''D8''' und '''GND''' | |||
* Taster '''Unten''' mit '''D9''' und '''GND''' | |||
* Taster '''Rechts''' mit '''D10''' und '''GND''' | |||
* Taster '''Drehen''' mit '''D11''' und '''GND''' | |||
=== Software === | |||
Tetris ist wohl eines der am häufigsten programmierten Spiele der Welt und wurde deshalb nicht von Grund auf neu programmiert. Die Struktur des Programms wurde weitestgehend von [http://electronoobs.com/eng_arduino_tut104_code1.php ELECTRONOOBS] übernommen. Der Code wurde optimiert und an unsere Hardware angepasst. | |||
<br> | |||
==== Struktur ==== | |||
Zunächst werden die nötigen Bibliotheken inkludiert. Diese sind "EEPROM.h", um auf den EEPROM-Speicher schreiben und von ihm lesen zu können, "Adafruit_NeoPixel.h", um den LED-Streifen ansteuern zu können, sowie "LiquidCrystal_I2C.h" und "Wire.h", um die Scores an das LCD übertragen zu können. <br> | |||
Anschließend werden über defines die entscheidenden Parameter der Hardware und des Spielverlaufs angelegt, um diese bei Bedarf einfach anpassen zu können. So können hier die Maße des Spielfelds, die Pinbelegung der I/O-Ports und die Geschwindigkeiten der Prozesse im Spiel verändert werden.<br> | |||
Über konstante globale Variablen werden die unterschiedlichen Tetriminos , sowie ihre jeweilige ID und Farbe festgelegt. Weitere globale Variablen werden zum Speichern von Zeiten, der Scores, den letzten Zuständen der Knöpfe, der Daten die den aktuellen Tetrimino definieren, verschiedener Geschwindigkeiten und des Grids angelegt.<br> | |||
Anschließend folgen alle Funktionen, sowie abschließend das Setup und der Loop. | |||
==== Setup ==== | |||
Im Setup werden zunächst die In- & Outputs festgelegt und der aktuelle Highscore wird aus dem EEPROM, welcher auch nach dem Neustart des Arduinio seine Daten noch enthält, ausgelesen. Als nächstes wird das Display initialisiert, die Startnachricht ausgegeben und der LED-Streifen vorbereitet.<br> | |||
Daraufhin werden mögliche Datenreste von vorherigen Spielen überschrieben, die erste Blocksequenz angelegt und die Timings initialisiert. Anschließend wird auf das Drücken des Rotate-Tasters gewartet, bevor das Spiel beginnt.<br> | |||
Nachdem die Startanimation abgespielt wurde, beginnt der loop(). Hier wird in Regelmäßigen Abständen in einer Endlosschleife auf den Spieler reagiert, der Tetrimino nach unten bewegt und das Spielfeld neu ausgegeben. | |||
== Programmcode == | |||
Zur besseren Lesbarkeit des Codes wurden Debug-Funktionen entfernt | |||
<div style="width:1100px; height:800px; overflow:scroll; border: hidden"> | |||
<pre> | |||
//---------------------------------------------// | |||
// Tetris auf dem Arduino // | |||
// GET-Fachpraktikum // | |||
// Nils Koch & Yannick Schmidt // | |||
// Stand: 09.01.2022 // | |||
//---------------------------------------------// | |||
// Codestruktur: http://electronoobs.com/eng_arduino_tut104_code1.php | |||
//---------------------------------------------// | |||
#include <EEPROM.h> | |||
#include <Adafruit_NeoPixel.h> | |||
#include <LiquidCrystal_I2C.h> | |||
#include <Wire.h> | |||
//---------------------------------------------// | |||
//Defines | |||
//Spielfeld | |||
#define GRID_W 10 | |||
#define GRID_H 20 | |||
#define NR_LED GRID_W*GRID_H | |||
//In- & Outputs | |||
#define LED_DATA 7 | |||
#define BTN_LEFT 8 | |||
#define BTN_DOWN 10 | |||
#define BTN_RIGHT 9 | |||
#define BTN_ROTATE 11 | |||
// Tetriminos | |||
#define PIECE_W 4 | |||
#define PIECE_H 4 | |||
#define PIECE_SIZE PIECE_W*PIECE_H | |||
#define DIFF_PIECES 7 | |||
//Spielgeschwindigkeit | |||
#define DROP_MIN 70 | |||
#define DROP_ACC 20 | |||
#define INI_MOVE_DELAY 50 | |||
#define INI_DROP_DELAY 500 | |||
#define INI_DRAW_DELAY 30 | |||
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NR_LED, LED_DATA, NEO_GRB + NEO_KHZ800); | |||
LiquidCrystal_I2C lcd(0x27, 16, 2); | |||
const byte empty[] = {0}; | |||
const byte piece_I[] = { | |||
0, 0, 0, 0, | |||
1, 1, 1, 1, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
0, 1, 0, 0, | |||
0, 1, 0, 0, | |||
0, 1, 0, 0, | |||
0, 1, 0, 0, | |||
0, 0, 0, 0, | |||
1, 1, 1, 1, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
0, 1, 0, 0, | |||
0, 1, 0, 0, | |||
0, 1, 0, 0, | |||
0, 1, 0, 0, | |||
}; | |||
const byte piece_T[] = { | |||
1, 1, 1, 0, | |||
0, 1, 0, 0, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
0, 1, 0, 0, | |||
1, 1, 0, 0, | |||
0, 1, 0, 0, | |||
0, 0, 0, 0, | |||
0, 1, 0, 0, | |||
1, 1, 1, 0, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
0, 1, 0, 0, | |||
0, 1, 1, 0, | |||
0, 1, 0, 0, | |||
0, 0, 0, 0, | |||
}; | |||
const byte piece_L[] = { | |||
0, 0, 0, 0, | |||
1, 1, 1, 0, | |||
1, 0, 0, 0, | |||
0, 0, 0, 0, | |||
1, 1, 0, 0, | |||
0, 1, 0, 0, | |||
0, 1, 0, 0, | |||
0, 0, 0, 0, | |||
0, 0, 1, 0, | |||
1, 1, 1, 0, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
0, 1, 0, 0, | |||
0, 1, 0, 0, | |||
0, 1, 1, 0, | |||
0, 0, 0, 0, | |||
}; | |||
const byte piece_J[] = { | |||
1, 0, 0, 0, | |||
1, 1, 1, 0, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
0, 1, 1, 0, | |||
0, 1, 0, 0, | |||
0, 1, 0, 0, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
1, 1, 1, 0, | |||
0, 0, 1, 0, | |||
0, 0, 0, 0, | |||
0, 1, 0, 0, | |||
0, 1, 0, 0, | |||
1, 1, 0, 0, | |||
0, 0, 0, 0, | |||
}; | |||
const byte piece_S[] = { | |||
0, 1, 1, 0, | |||
1, 1, 0, 0, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
0, 1, 0, 0, | |||
0, 1, 1, 0, | |||
0, 0, 1, 0, | |||
0, 0, 0, 0, | |||
0, 1, 1, 0, | |||
1, 1, 0, 0, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
0, 1, 0, 0, | |||
0, 1, 1, 0, | |||
0, 0, 1, 0, | |||
0, 0, 0, 0, | |||
}; | |||
const byte piece_Z[] = { | |||
1, 1, 0, 0, | |||
0, 1, 1, 0, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
0, 0, 1, 0, | |||
0, 1, 1, 0, | |||
0, 1, 0, 0, | |||
0, 0, 0, 0, | |||
1, 1, 0, 0, | |||
0, 1, 1, 0, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
0, 0, 1, 0, | |||
0, 1, 1, 0, | |||
0, 1, 0, 0, | |||
0, 0, 0, 0, | |||
}; | |||
const byte piece_O[] = { | |||
1, 1, 0, 0, | |||
1, 1, 0, 0, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
1, 1, 0, 0, | |||
1, 1, 0, 0, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
1, 1, 0, 0, | |||
1, 1, 0, 0, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
1, 1, 0, 0, | |||
1, 1, 0, 0, | |||
0, 0, 0, 0, | |||
0, 0, 0, 0, | |||
}; | |||
const byte *pieces[DIFF_PIECES + 1] = { | |||
empty, | |||
piece_S, | |||
piece_Z, | |||
piece_L, | |||
piece_J, | |||
piece_O, | |||
piece_T, | |||
piece_I, | |||
}; | |||
const long piece_colors[DIFF_PIECES] = { | |||
0x005500, // S: grün | |||
0x550000, // Z: rot | |||
0x551500, // L: orange | |||
0x000055, // J: blau | |||
0x555500, // O: gelb | |||
0x200020, // T: lila | |||
0x005555, // I: cyan | |||
}; | |||
//Scores | |||
unsigned int top_score = 0; | |||
unsigned int score = 0; | |||
//Zeiten | |||
unsigned long timeBefore = 0; | |||
unsigned long timeNow = 0; | |||
//Counter | |||
byte i = 0; | |||
//Letzte Werte der knöpfe, um mehrfachausführung zu verhindern | |||
byte old_button = 0; | |||
int old_px = 0; | |||
int old_want_turn = 0; | |||
//Daten für aktuelles Teil | |||
int piece_id; | |||
int piece_rotation; | |||
int piece_x; | |||
int piece_y; | |||
//Move-Geschwindigkeit | |||
long last_move; | |||
int move_delay; | |||
//Drop-Geschwindigkeit | |||
long last_drop; | |||
int drop_delay; | |||
//Draw-Geschwindigkeit | |||
long last_draw; | |||
int draw_delay; | |||
//Spielfeld beinhaltet Farbcodes für jeden Pixel | |||
byte grid[GRID_W * GRID_H]; | |||
//Sequenz von Teilen, um Dopplungen oder Nichtauftreten eines Steins zu vermeiden | |||
byte piece_sequence[DIFF_PIECES]; | |||
byte sequence_count = DIFF_PIECES; | |||
//Pixel an stelle x,y in color einfärben | |||
void pixel(int x, int y, long color) { | |||
int a; | |||
if (x % 2) { | |||
a = x * GRID_H + y; | |||
} | |||
else { | |||
a = (x + 1) * GRID_H - (y + 1); | |||
} | |||
pixels.setPixelColor(a, color); | |||
} | |||
//Spielfeld anzeigen | |||
void draw_grid() { | |||
int x, y; | |||
for (y = 0; y < GRID_H; ++y) { | |||
for (x = 0; x < GRID_W; ++x) { | |||
if (grid[y * GRID_W + x] != 0) { | |||
pixel(x, y, piece_colors[grid[y * GRID_W + x] - 1]); | |||
} | |||
else { | |||
pixel(x, y, 0); | |||
} | |||
} | |||
} | |||
pixels.show(); | |||
} | |||
void choose_new_piece() { | |||
if ( sequence_count >= DIFF_PIECES ) { | |||
// Liste leer | |||
int i, j, k; | |||
for (i = 0; i < DIFF_PIECES; i++) { | |||
do { | |||
// Zufälliges Teil wählen | |||
j = random(DIFF_PIECES) % DIFF_PIECES; | |||
// Prüfen ob schon in der Sequenz | |||
for (k = 0; k < i; k++) { | |||
if (piece_sequence[k] == j) break; | |||
} | |||
//Wenn Teil in Sequenz, neues generieren | |||
} while (k < i); | |||
// Teil hinzufügen | |||
piece_sequence[i] = j; | |||
} | |||
// Counter zurücksetzten | |||
sequence_count = 0; | |||
} | |||
// nächstes Teil laden | |||
piece_id = piece_sequence[sequence_count++] + 1; | |||
// oben in der Mitte starten | |||
piece_y = -4; // oberhalb des Screens beginnen | |||
piece_x = 4; | |||
piece_rotation = 0; | |||
} | |||
void erase_piece_from_grid() { //Tetrimino vom Spielfeld entfernen, um wo anders wieder einzufügen | |||
int x, y; | |||
//Ersten Pixel des Tetriminos finden | |||
const byte *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W); | |||
//Für jeden y-Wert... | |||
for (y = 0; y < PIECE_H; y++) { | |||
int ny = piece_y + y; //y-Koordinate des Pixels berechnen | |||
if (ny < 0 || ny > GRID_H) continue; //Wenn außerhalb des Grids, dann Ignorieren | |||
//...mit jedem x-Wert: | |||
for (x = 0; x < PIECE_W; x++) { | |||
int nx = piece_x + x; //x-Koordinate des Pixels berechnen | |||
if (nx < 0 || nx > GRID_W) continue; //Wenn außerhalb des Grids, dann Ignorieren | |||
if (piece[y * PIECE_W + x] == 1) { //Wenn Pixel im Modell des Tetriminos =1... | |||
grid[ny * GRID_W + nx] = 0; //...Pixel aus dem Grid löschen | |||
} | |||
} | |||
} | |||
} | |||
void add_piece_to_grid() { | |||
int x, y; | |||
//Ersten Pixel des Tetriminos finden | |||
const byte *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W); | |||
//Für jeden y-Wert... | |||
for (y = 0; y < PIECE_H; y++) { | |||
int ny = piece_y + y; //y-Koordinate des Pixels berechnen | |||
if (ny < 0 || ny > GRID_H) continue; //Wenn außerhalb des Grids, dann Ignorieren | |||
//...mit jedem x-Wert: | |||
for (x = 0; x < PIECE_W; x++) { | |||
int nx = piece_x + x; //x-Koordinate des Pixels berechnen | |||
if (nx < 0 || nx > GRID_W) continue; //Wenn außerhalb des Grids, dann Ignorieren | |||
if (piece[y * PIECE_W + x] == 1) { //Wenn Pixel im Modell des Tetriminos =1... | |||
grid[ny * GRID_W + nx] = piece_id; //...Farbe an die Stellen des Tetriminos schreiben | |||
} | |||
} | |||
} | |||
} | |||
void delete_row(int y) { | |||
//Score erhöhen | |||
score = score + 10; | |||
//Überprüfen ob neuer Top-Score erreicht wurde | |||
if (score > top_score) | |||
{ | |||
EEPROM.update(1, score); | |||
} | |||
//Aktuellen Top-Score auslesen | |||
top_score = EEPROM.read(1); | |||
int x; | |||
//Zeile y und alles darüber nach unten verschieben | |||
for (; y > 0; y--) { | |||
for (x = 0; x < GRID_W; x++) { | |||
grid[y * GRID_W + x] = grid[(y - 1) * GRID_W + x]; //Wert aus der Zeile darüber kopieren | |||
} | |||
} | |||
for (x = 0; x < GRID_W; ++x) { | |||
grid[x] = 0; //oberste Zeile komplett Weiß | |||
} | |||
lcd.setCursor(0, 0); | |||
lcd.print("Score: "); | |||
lcd.print(score); | |||
lcd.setCursor(0, 1); | |||
lcd.print("Highscore: "); | |||
lcd.print(top_score); | |||
draw_grid(); | |||
delay(200); | |||
} | |||
void fall_faster() { | |||
if (drop_delay > DROP_MIN) drop_delay -= DROP_ACC; //Fallen beschleunigen | |||
} | |||
void remove_full_rows() { | |||
int x, y, c; | |||
for (y = 0; y < GRID_H; y++) { | |||
c = 0; | |||
for (x = 0; x < GRID_W; ++x) { | |||
if ( grid[y * GRID_W + x] > 0 ) c++; //jedes Feld zählen, das nicht leer ist | |||
} | |||
if (c == GRID_W) { //Wenn jedes Feld belegt | |||
delete_row(y); //Reihe entfernen | |||
fall_faster(); //Fallen beschleunigen | |||
} | |||
} | |||
delay(100); | |||
} | |||
void try_to_move_piece_sideways() { | |||
int new_px = 0; | |||
//Knöpfe einlesen | |||
if (!digitalRead(BTN_LEFT)) | |||
{ | |||
new_px = -1; | |||
} | |||
if (!digitalRead(BTN_RIGHT)) | |||
{ | |||
new_px = 1; | |||
} | |||
//nur bewegen, wenn sich der Bewegungswunsch geändert hat und das Teil an die neue Stelle passt | |||
if (piece_can_fit(piece_x + new_px, piece_y, piece_rotation) == 1) { | |||
piece_x += new_px; | |||
} | |||
old_px = new_px; //Bewegungswunsch als Vergleichswert speichern | |||
} | |||
void try_to_rotate_piece() { | |||
int want_turn = 0; | |||
//Rotationsbutton einlesen | |||
int new_button = !digitalRead(BTN_ROTATE); | |||
//ist der Knopf gedrückt und war nicht gedrückt, Drehung versuchen | |||
if ( new_button > 0 && old_button != new_button ) { | |||
want_turn = 1; | |||
} | |||
old_button = new_button; //Knopfzustand als Vergleichswert speichern | |||
if (want_turn == 1 && want_turn != old_want_turn) { | |||
int new_pr = ( piece_rotation + 1 ) % 4; //Rotation erhöhen | |||
if (piece_can_fit(piece_x, piece_y, new_pr)) { | |||
piece_rotation = new_pr; //Wenn der Tetrimino passt, drehen | |||
} else { | |||
if (piece_can_fit(piece_x - 1, piece_y, new_pr)) { //Wenn Tetrimino weiter links passt, 1 nach links und drehen | |||
piece_x = piece_x - 1; | |||
piece_rotation = new_pr; | |||
} else if (piece_can_fit(piece_x + 1, piece_y, new_pr)) { //Wenn Tetrimino weiter rechts passt, 1 nach rechts und drehen | |||
piece_x = piece_x + 1; | |||
piece_rotation = new_pr; | |||
} | |||
} | |||
} | |||
old_want_turn = want_turn; //Vergelichswert speichern | |||
} | |||
int piece_can_fit(int px, int py, int pr) { | |||
//passt das Teil and die Position px,py mit der Rotation pr | |||
if ( piece_off_edge(px, py, pr) ) return 0; //Nein, wenn außerhalb des Bildschirms | |||
if ( piece_hits_rubble(px, py, pr) ) return 0; //Nein, wenn andere Teile überlagert | |||
return 1; //sonst ja | |||
} | |||
int piece_off_edge(int px, int py, int pr) { | |||
int x, y; | |||
//Modell des Teils finden | |||
const byte *piece = pieces[piece_id] + (pr * PIECE_H * PIECE_W); | |||
for (y = 0; y < PIECE_H; ++y) { | |||
//Koordinaten berechnen | |||
int ny = py + y; | |||
for (x = 0; x < PIECE_W; ++x) { | |||
//Koordinaten berechnen | |||
int nx = px + x; | |||
if (piece[y * PIECE_W + x] > 0) { | |||
if (nx < 0) return 1; // links außerhalb des Felds | |||
if (nx >= GRID_W ) return 1; // rechts außerhalb des Felds | |||
} | |||
} | |||
} | |||
return 0; // auf dem Feld | |||
} | |||
int piece_hits_rubble(int px, int py, int pr) { | |||
int x, y; | |||
//Modell des Teils finden | |||
const byte *piece = pieces[piece_id] + (pr * PIECE_H * PIECE_W); | |||
for (y = 0; y < PIECE_H; ++y) { | |||
int ny = py + y; | |||
if (ny < 0) continue; //Oberhalb des Screens, ignorieren | |||
for (x = 0; x < PIECE_W; ++x) { | |||
int nx = px + x; | |||
if (piece[y * PIECE_W + x] > 0) { | |||
if (ny >= GRID_H ) return 1; // Teil fällt unten aus dem Grid | |||
if (grid[ny * GRID_W + nx] != 0 ) return 1; // Teile überlagern sich | |||
} | |||
} | |||
} | |||
return 0; //keine Kollision | |||
} | |||
void all_white() { //Alle LED nacheinander mit kurzer Verzögerung Weiß machen | |||
for (int led_number = 0; led_number < NR_LED; led_number++) { | |||
pixels.setPixelColor(led_number, pixels.Color(50, 50, 50)); | |||
pixels.show(); | |||
delay(5); | |||
} | |||
} | |||
void game_over() { | |||
//Score zurücksetzen | |||
score = 0; | |||
//Zeit speichern | |||
long over_time = millis(); | |||
timeBefore = over_time; | |||
//Bei erster LED beginnen | |||
int led_number = 0; | |||
while (HIGH) { //Endlosschleife | |||
timeNow = millis(); //Zeit abfragen | |||
if (timeNow - timeBefore >= 250) { //Alle 250ms... | |||
pixels.setPixelColor(led_number, piece_colors[random(100) % DIFF_PIECES]); | |||
pixels.show(); //...Pixel zufüllige Farbe zuweisen und anzeigen | |||
led_number += 1; //Nächste LED | |||
led_number %= NR_LED; //Am Ende des Streifens von vorne beginnen | |||
timeBefore += 250; //Referenzzeit erhöhen | |||
} | |||
//restart? | |||
if (!digitalRead(BTN_ROTATE) && timeNow - over_time >= 1000) { | |||
break; | |||
} | |||
} | |||
setup(); //Setup ausführen | |||
return; //Mit Loop fortfahren | |||
} | |||
void try_to_drop_piece() { | |||
//Tetrimino aus Grid entfernen | |||
erase_piece_from_grid(); | |||
if (piece_can_fit(piece_x, piece_y + 1, piece_rotation)) { //Tetrimino passt an neue Position | |||
piece_y++; //Teil runter bewegen | |||
add_piece_to_grid(); | |||
} | |||
else { //Wenn Teil nicht passt | |||
add_piece_to_grid(); //Tetrimino wieder einfügen | |||
remove_full_rows(); //mögliche volle Reihen entfernen | |||
if (game_is_over() == 1) { //Abfragen, ob Spiel verloren | |||
game_over(); | |||
} | |||
choose_new_piece(); //nächstes Teil wählen | |||
} | |||
} | |||
void try_to_drop_faster() { | |||
if (!digitalRead(BTN_DOWN)) //wird der Button Down gedrückt, versuchen Tetrimino runter zu bewegen | |||
{ | |||
try_to_drop_piece(); | |||
} | |||
} | |||
void react_to_player() { | |||
//Tetrimino aus Grid entfernen | |||
erase_piece_from_grid(); | |||
//Seitwärts abfragen | |||
try_to_move_piece_sideways(); | |||
//Rotation abfragen | |||
try_to_rotate_piece(); | |||
//Tetrimino wieder einfügen | |||
add_piece_to_grid(); | |||
//Nach unten abfragen und durchführen | |||
try_to_drop_faster(); | |||
} | |||
int game_is_over() { | |||
int x, y; | |||
const byte *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W); | |||
for (y = 0; y < PIECE_H; ++y) { | |||
int ny = piece_y + y; | |||
for (x = 0; x < PIECE_W; ++x) { | |||
if (piece[y * PIECE_W + x] > 0) { | |||
if (ny < 0) return 1; // Tetrimino ragt über den Screen, game over | |||
} | |||
} | |||
} | |||
return 0; //noch nicht vorbei | |||
} | |||
void setup() { | |||
//In- und Outputs setzen | |||
pinMode(BTN_LEFT, INPUT_PULLUP); | |||
pinMode(BTN_RIGHT, INPUT_PULLUP); | |||
pinMode(BTN_DOWN, INPUT_PULLUP); | |||
pinMode(BTN_ROTATE, INPUT_PULLUP); | |||
pinMode(LED_DATA, OUTPUT); | |||
//Highscore auslesen | |||
top_score = EEPROM.read(1); | |||
//LCD initialisieren | |||
lcd.init(); | |||
lcd.backlight(); | |||
//Startnachricht ausgeben | |||
lcd.setCursor(0, 0); | |||
lcd.print("Rotate to start"); | |||
//LED-Streifen initialisieren und löschen | |||
pixels.begin(); | |||
pixels.clear(); | |||
pixels.show(); | |||
//Sicherstellen, dass das Grid leer ist | |||
for (i = 0; i < GRID_W * GRID_H; ++i) { | |||
grid[i] = 0; | |||
} | |||
//Timings festlegen | |||
move_delay = INI_MOVE_DELAY; | |||
drop_delay = INI_DROP_DELAY; | |||
draw_delay = INI_DRAW_DELAY; | |||
//Anfangszeit festlegen | |||
last_draw = last_drop = last_move = millis(); | |||
//Auf Tastendruck warten | |||
while (digitalRead(BTN_ROTATE)) {} | |||
delay(500); | |||
//Zufallszahlen initialisieren | |||
randomSeed(millis()); | |||
random(100); | |||
//Tetrimino-Sequenz füllen | |||
choose_new_piece(); | |||
//Scores auf Display schreiben | |||
lcd.clear(); | |||
lcd.setCursor(0, 0); | |||
lcd.print("Score: "); | |||
lcd.print(score); | |||
lcd.setCursor(0, 1); | |||
lcd.print("Highscore: "); | |||
lcd.print(top_score); | |||
//Startanimation | |||
all_white(); | |||
} | |||
void loop() { | |||
//Aktuelle Zeit messen | |||
long t = millis(); | |||
//Wenn genug Zeit seit letztem Move vergangen, auf Eingabe reagieren | |||
if (t - last_move > move_delay ) { | |||
last_move = t; | |||
react_to_player(); | |||
} | |||
//Wenn genug Zeit seit letztem Drop vergangen, Tetrimono nach unten bewegen | |||
if (t - last_drop > drop_delay ) { | |||
last_drop = t; | |||
try_to_drop_piece(); | |||
} | |||
//Wenn genug Zeit seit letzter Ausgabe vergangen, Spielfeld ausgeben | |||
if (t - last_draw > draw_delay ) { | |||
last_draw = t; | |||
draw_grid(); | |||
} | |||
} | |||
</pre> | |||
</div> | |||
== Komponententest == | == Komponententest == | ||
Es wurden erst die einzelnen Komponenten und dann die Gesamtkomponenten auf ihre Funktion & Anforderungen getestet. | |||
=== Einzelkomponententest === | |||
{| class="mw-datatable" | |||
! style="font-weight: bold;" | ID | |||
! style="font-weight: bold;" | Testfallbeschreibung | |||
! style="font-weight: bold;" | Erwartetes Ergebnis | |||
! style="font-weight: bold;" | Testergebnis | |||
! style="font-weight: bold;" | Testperson | |||
! style="font-weight: bold;" | Datum | |||
|+ style = "text-align: left"|Tabelle 1: Testbericht für den Einzelkomponententest | |||
|- | |||
| 1 | |||
| LED-Stripe auf Funktion überprüfen. | |||
| Wenn LEDs weiß leuchten funktionieren die einzelnen RGB LEDs. | |||
| OK | |||
| Yannick Schmidt | |||
| 10.12.2021 | |||
|- | |||
| 2 | |||
| LCD-Display auf Funktion überprüfen. | |||
| Hintergrundbeleuchtung leuchtet, sowie eigener Text wird angezeigt. | |||
| OK | |||
| Yannick Schmidt | |||
| 20.12.2021 | |||
|- | |||
| 3 | |||
| Taster auf Funktion überprüfen. | |||
| Taster geben Schaltsignal an den Arduino weiter. | |||
| OK | |||
| Yannick Schmidt | |||
| 21.12.2021 | |||
|} | |||
=== Gesamtkomponententest === | |||
{| class="mw-datatable" | |||
! style="font-weight: bold;" | ID | |||
! style="font-weight: bold;" | Testfallbeschreibung | |||
! style="font-weight: bold;" | Testergebnis | |||
! style="font-weight: bold;" | Begründung | |||
! style="font-weight: bold;" | Testperson | |||
! style="font-weight: bold;" | Datum | |||
|+ style = "text-align: left"|Tabelle 2: Testbericht für den Einzelkomponententest | |||
|- | |||
| 1 | |||
| Möglichst dicht gepackte LED Matrix. | |||
| OK | |||
| Mit einer LED-Dichte von 60LED/Meter wurde eine dicht gepackte LED Matrix erreicht. | |||
| Yannick Schmidt; Nils Koch | |||
| 6.1.2022 | |||
|- | |||
| 2 | |||
| Rechteckiger LED Look. | |||
| OK | |||
| LEDs leuchten die Felder rechteckig und gleichmäßig aus. | |||
| Yannick Schmidt; Nils Koch | |||
| 6.1.2022 | |||
|- | |||
| 3 | |||
| Start- und Endanimation | |||
| OK | |||
| Animation beim Starten des Spiels durch Weißzeichnen der Matrix.<br>Buntzeichnen der Matrix bei Gameover. | |||
| Yannick Schmidt; Nils Koch | |||
| 9.1.2022 | |||
|- | |||
| 4 | |||
| Speicherbarer Highscore | |||
| OK | |||
| Highscore ist Speicherbar und aktueller Score wird ausgegeben. | |||
| Yannick Schmidt; Nils Koch | |||
| 9.1.2022 | |||
|- | |||
| 5 | |||
| Eingabe per kabelgebundenes Gamepad (evtl. auch kabellos per Bluetooth) | |||
| OK | |||
| Kabelgebundenes Gamepad ist funktionstüchtig. | |||
| Yannick Schmidt; Nils Koch | |||
| 9.1.2022 | |||
|- | |||
| 6 | |||
| Stromversorgung mittels USB Powerbank | |||
| OK | |||
| Stromversorgung ist über ein USB Kabel möglich. | |||
| Yannick Schmidt; Nils Koch | |||
| 9.1.2022 | |||
|- | |||
| 7 | |||
| Optional: Staufach für das Gamepad, welches sich per Sensor öffnen lässt | |||
| FAIL | |||
| Staufach aufgrund von Zeitmangel nicht realisiert worden. | |||
| Yannick Schmidt; Nils Koch | |||
| 9.1.2022 | |||
|} | |||
== Ergebnis == | == Ergebnis == | ||
Alle festen Anforderungen an das Projekt konnten zufriedenstellend umgesetzt werden. Die optionale Anforderung des Staufachs wurde aufgrund von Zeitmangel wieder verworfen. | |||
Verbesserungswürdig sind die elektrische Bauteile, die durch eine Leiterplatte ersetzt werden könnten. | |||
== Zusammenfassung == | == Zusammenfassung == | ||
=== Lessons Learned === | === Lessons Learned === | ||
Das Team hat die folgenden Kompetenzen erlernt: | |||
* Projektplanung und Zeitmanagement | |||
* CAD Konstruktion für 3D Druck | |||
* 3D Druck | |||
* Löten | |||
* Ansteuerung von WS2812 LEDs | |||
=== Besondere Herausforderungen === | |||
Die erste Herausforderung stellte der 3D Druck dar. Ohne Vorkenntnisse mussten sich zunächst die Grundlagen angeeignet werden. Das Design des Gitters musste mehrfach angepasst werden, um typische Druckprobleme, wie Stringing, Fading und verlaufende Überhänge, zu beheben. | |||
Auf gleich mehrere besondere Probleme sind wir im Zusammenhang mit dem LED Streifen gestoßen.<br> | |||
Zu Beginn zeigte sich, dass trotz einwandfreier Funktion des LED-Streifens und des Arduinos keinerlei Ausgabe in der Matrix zu erkennen war. Mehrfache Tests des Streifens und des Microcontrollers konnten keinerlei Aufschluss bringen, denn sobald das vollständige Programm auf den Arduino geladen wurde bleiben die LEDs dunkel, obwohl das Programm lief und über die serielle Schnittstelle eine Ausgabe auf dem PC möglich war. Durch das Auskommentieren scheinbar beliebiger Funktionen lies sich das Problem zwar lösen, aber das Programm war nicht mehr lauffähig.<br> | |||
Die Ursache konnte deshalb im Speicher des Arduinos ausgemacht werden. Durch eine global verfügbares Grid, welches für jeden Pixel des Grids die Farbe in einem long (4 Byte) speichert, werden bereits vor Programmstart alleine 800 der 2048 Byte RAM des Arduinos belegt. Die Gesamtauslastung des RAM, nur durch globale Variablen, lag bei etwa 80%, was die Ausgabe für den Arduino scheinbar unmöglich machte.<br> | |||
Das Problem wurde gelöst, indem im Grid nicht mehr die Farbinformationen zu jedem Pixel gespeichert wurden, sondern nur ein Byte, das die Position der Farbe im Farbarray angab somit konnten hier 600 der 800 Byte für das Grid eingespart werden. Auch für andere Variablen wurde auf kleinere Datentypen zurückgegriffen, um möglichst viel Arbeitsspeicher zu sparen. Die Auslastung durch globale Variablen ließ sich somit auf etwa 60% reduzieren. Außerdem wurden die Debug-Funktionen über #ifdef Befehle so modifiziert, dass sie nur im Testfall kompiliert werden und somit weitere 8% RAM eingespart werden können. Das Problem war hiermit behoben und die ersten Pixel wurden sichtbar.<br> | |||
Das nächste Problem trat bei der Darstellung mehrerer Pixel auf einmal auf. Der Arduino stürzte plötzlich ab und startete neu. Dies war auf eine zu geringe Stromversorgung durch den USB-Port des PC zu erklären. Durch anschließen eines zusätzlichen Stromkabels konnte hier schnell Abhilfe geschaffen werden.<br> | |||
Trotz neuer Stromversorgung war aber noch immer deutlich zu erkennen, dass das LCD mit zunehmender Pixelzahl auf dem LED-Streifen immer dunkler wurde, bis gegen Ende des Spiels kaum noch etwas zu erkennen war. Deshalb wurde ein weiteres Upgrade der Stromversorgung durchgeführt und auf ein Netzteil, welches bei 5 V 3 A liefert, zurückgegriffen. Es wurde direkt mit dem Power-Board verbunden und kann somit den Arduino und alle angeschlossenen Komponenten direkt speisen. Hierbei stellte sich dann noch heraus, dass das benutzte Lightning-Kabel intern falsch verdrahtet war und Masse und Vcc vertauscht wurden. Nachdem auch dieses Problem gelöst war, funktionierte die Ausgabe einwandfrei. | |||
== Projektunterlagen == | == Projektunterlagen == | ||
=== Projektplan === | === Projektplan === | ||
[[Datei:Tetris-Projektplan.jpg]] | |||
=== Projektdurchführung === | === Projektdurchführung === | ||
Nach Festlegung des Projektes wurde begonnen erste Pläne zu machen. Nach Klärung des groben Aufbaus konnte die Teile beschafft werden. | |||
Dabei wurde dann parallel ein eigenes Wiki für die Hardware- und Softwarespezifikationen angelegt und stetig erweitert. | |||
Zunächst wurde die Hardware vollständig in CAD designt und anschließend erste Prototypen im 3D Drucker gebaut. Dabei wurden unterschiedliche Designs ausprobiert. | |||
So sollte beispielsweise in der ursprünglichen Version die Scheibe in eine Nut des Gitters eingefügt werden, was jedoch aufgrund von Fehlern beim Druck wieder verworfen wurde. | |||
Nach Eintreffen der Komponenten wurden diese dem Einzelkomponententest unterzogen. | |||
Die finalen Bauteildesigns wurden nun umgesetzt und konnten zusammengesetzt werden. Anschließend wurden die Prototype-Boards gelötet und angeschlossen. | |||
Die Software konnte nun auf die Hardware angepasst werden und den Gesamtkomponententest durchlaufen | |||
== YouTube Video == | == YouTube Video == | ||
{{#ev:youtube|https://youtu.be/RTTAe8rWgR0|450px|left}} | |||
<br clear=all> | |||
== Weblinks == | == Weblinks == |
Aktuelle Version vom 11. Januar 2022, 13:12 Uhr
Autoren: Yannick Schmidt & Nils Koch
Betreuer: Prof. Göbel & Prof. Schneider
→ zurück zur Übersicht: WS 20/21: Angewandte Elektrotechnik (BSE)
Einleitung
Das Ziel der Gruppe von Yannick Schmidt & Nils Koch ist es das Spiel "Tetris" auf einer selbstgebauten 10x20 LED Matrix im Rahmen des GET-Fachpraktikums zu realisieren. Der aktuelle Score und der Highscore sollen auf einem kleinen Display angezeigt werden. Die Eingabe erfolgt per selbstgebauten Gamepad.
Anforderungen
ID | Inhalt | Ersteller | Datum | Geprüft von | Datum |
---|---|---|---|---|---|
1 | Möglichst dicht gepackte LED Matrix | Yannick Schmidt | 5.10.2021 | Yannick Schmidt, Nils Koch | 26.10.2021 |
2 | Rechteckiger LED Look | Yannick Schmidt | 5.10.2021 | Yannick Schmidt, Nils Koch | 26.10.2021 |
3 | Start- und Endanimation | Yannick Schmidt | 5.10.2021 | Yannick Schmidt, Nils Koch | 26.10.2021 |
4 | Speicherbarer Highscore | Yannick Schmidt | 5.10.2021 | Yannick Schmidt, Nils Koch | 26.10.2021 |
5 | Eingabe per kabelgebundenes Gamepad (evtl. auch kabellos per Bluetooth) | Yannick Schmidt | 5.10.2021 | Yannick Schmidt, Nils Koch | 26.10.2021 |
6 | Stromversorgung mittels USB Powerbank | Yannick Schmidt | 5.10.2021 | Yannick Schmidt, Nils Koch | 26.10.2021 |
7 | Optional: Staufach für das Gamepad, welches sich per Sensor öffnen lässt | Yannick Schmidt | 5.10.2021 | Yannick Schmidt, Nils Koch | 26.10.2021 |
Funktionaler Systementwurf/Technischer Systementwurf
Komponentenspezifikation
Komponente | Beschreibung | Abbildung |
---|---|---|
Arduino UNO | Microcontroller 14 digitale I/O Pins 6 analoge Eingänge |
|
WS2812B ECO | 3 LEDs Rot, Grün, Blau 256 Helligkeitsstufen |
|
LCD-Display | I2C-Verbindung 16x2 Pixel Auflösung Hintergrundbeleuchtung |
|
Gitter | 3 Teile Rechteckiger Ausschnitt für LEDs Bohrlöcher und Stifte zum Verbauen |
|
Controller | Design orientiert am SNES Controller Controller-Board mittig befestigt Clip-Verbindung zum einfachen öffnen |
Umsetzung (HW/SW)
Hardware
Die Hardware besteht aus der LED-Einheit und dem Controller
LED-Einheit
Die LED-Einheit umfasst eine Bodenplatte, auf der 10 LED-Streifen des Typs WS2812b mit einer Länge von 20 LEDs in Form einer Matrix parallel aufgeklebt sind. Auf der LED-Matrix wird ein Gitter montiert, welches als Abstandshalter zur Kunststoffscheibe fungiert und einen rechteckigen Leuchteffekt der LEDs erzeugt. Unterhalb der Matrix ist das LCD-Display montiert.
Bodenplatte
Die in der Abbildung 1 zu sehende Zeichnung zeigt die Bodenplatte.
Sie besteht aus einer 3mm Holzplatte, die auf 196mm Breite und 500mm Höhe zugeschnitten.
Es wurden die 6 Bohrlöcher des Gitters übertragen und der Ausschnitt des LCD-Displays ausgeschnitten.
LED-Matrix
Der in der Abbildung 2 zu sehende Ausschnitt einer Zeichnung zeigt die LED-Matrix.
Sie besteht aus einem WS2812B Eco LED-Stripe, der auf 10 Abschnitte mit je 20 LEDs gekürzt wurde.
Hierbei war beim kleben zu beachten, dass die Streifen genau parallel mit 16,67mm Abstand liegen.
Gitter
Das auf Abbildung 4 zu sehende Gitter ist als 3D Druck entworfen.
Jedes Loch sitzt über einer LED, wodurch sich ebenfalls ein Abstand von 16,67mm ergibt.
Das Gitter musst auf die Bodenplatte passen, wodurch sich eine Breite von 196mm ergibt.
Die Länge von 343,4mm kommt durch die Summe von 20 mal 16,67mm Abstand und 2 mal 5mm Wanddicke .
Zur Streuung des Lichts beträgt die Tiefe 16,67mm.
Durch das Bauvolumen, des Druckers Dremel 3D20, von 230x150mm musste das Gitter in 3 Teile geteilt werden.
Zur Verbindung der Gitterteile sind 4mm Stiftlöcher in die oberen und unteren Teile eingefügt worden.
Dem mittleren Teil wurden 3mm Stifte hinzugefügt.
Zur Befestigung der Gitterteile auf der Bodenplatte gibt es mittig auf jeder Seite 4mm Bohrungen.
Controller
Der in Abbildung 5 zu sehende Controller ist optische dem SNES-Controller nachempfunden.
Anforderungen an den Controller waren, dass er zum einen das Controller-Board umfasst und aus zwei Teilen besteht, wobei diese ohne Schauben befestigt werden sollen.
Das Controller-Board hat die Maße 70x30mm.
Es befinden sich mit dem Abstand von der Mitte aus 32,5mm und 17,5mm Löcher, um das Board zu befestigen.
Anhand dieser Maße gehen bei dem unteren Teil 1,5mm dicke Stifte empor, die das Board in Position halten. Im unteren Teil der Stifte ist der Durchmesser größer, damit das Board angehoben wird.
Links und Rechts befinden sich jeweils Halbkreise mit 50mm Durchmesser.
Das obere Teil entspricht den Außenmaßen des unteren. Es ist mit einem Ausschnitt für das Controller-Board versehen, welcher einen Offset nach innen hat, damit das Board fest sitzt.
Elektronik
Die Abbildung 6 zeigt die Elektrische Schaltung.
Die Stromquelle stellt eine USB 5V Quelle(Netzteil oder Powerbank) dar. Von ihr aus gehen 5V und Erdung(GND) zum Power-Board.
Das Power-Board verbindet:
- 5V an das LED-Power-Board, das LCD-Display und den Arduino Uno
- GND an das LED-Power-Board, das LCD-Display, den Arduino Uno und den Connector für das Controller-Board.
Der Arduino Uno verbindet:
- DigitalPins
- DigitalPin 7 mit LED-Power-Board
- DigitalPin 8,9,10,11 mit dem Connector für das Controller-Board
- AnalogPins
- A4 mit SDA des Displays
- A5 mit SCL des Displays
Das LED-Power-Board verbindet:
- 5V und GND mit 10 LED-Stripes(In der Abbildung nur 4, da übersichtlicher)
- DigitalPin 7 mit dem Din des ersten LED-Stripes
Die WS2812B Stripes verbinden am Ende eines Stripes den Dout mit dem Din des nächsten Stripe.
Der Connector verbindet 1:1 das Controller-Board.
Das Controller-Board verbindet über den Connector:
- Taster Links mit D8 und GND
- Taster Unten mit D9 und GND
- Taster Rechts mit D10 und GND
- Taster Drehen mit D11 und GND
Software
Tetris ist wohl eines der am häufigsten programmierten Spiele der Welt und wurde deshalb nicht von Grund auf neu programmiert. Die Struktur des Programms wurde weitestgehend von ELECTRONOOBS übernommen. Der Code wurde optimiert und an unsere Hardware angepasst.
Struktur
Zunächst werden die nötigen Bibliotheken inkludiert. Diese sind "EEPROM.h", um auf den EEPROM-Speicher schreiben und von ihm lesen zu können, "Adafruit_NeoPixel.h", um den LED-Streifen ansteuern zu können, sowie "LiquidCrystal_I2C.h" und "Wire.h", um die Scores an das LCD übertragen zu können.
Anschließend werden über defines die entscheidenden Parameter der Hardware und des Spielverlaufs angelegt, um diese bei Bedarf einfach anpassen zu können. So können hier die Maße des Spielfelds, die Pinbelegung der I/O-Ports und die Geschwindigkeiten der Prozesse im Spiel verändert werden.
Über konstante globale Variablen werden die unterschiedlichen Tetriminos , sowie ihre jeweilige ID und Farbe festgelegt. Weitere globale Variablen werden zum Speichern von Zeiten, der Scores, den letzten Zuständen der Knöpfe, der Daten die den aktuellen Tetrimino definieren, verschiedener Geschwindigkeiten und des Grids angelegt.
Anschließend folgen alle Funktionen, sowie abschließend das Setup und der Loop.
Setup
Im Setup werden zunächst die In- & Outputs festgelegt und der aktuelle Highscore wird aus dem EEPROM, welcher auch nach dem Neustart des Arduinio seine Daten noch enthält, ausgelesen. Als nächstes wird das Display initialisiert, die Startnachricht ausgegeben und der LED-Streifen vorbereitet.
Daraufhin werden mögliche Datenreste von vorherigen Spielen überschrieben, die erste Blocksequenz angelegt und die Timings initialisiert. Anschließend wird auf das Drücken des Rotate-Tasters gewartet, bevor das Spiel beginnt.
Nachdem die Startanimation abgespielt wurde, beginnt der loop(). Hier wird in Regelmäßigen Abständen in einer Endlosschleife auf den Spieler reagiert, der Tetrimino nach unten bewegt und das Spielfeld neu ausgegeben.
Programmcode
Zur besseren Lesbarkeit des Codes wurden Debug-Funktionen entfernt
Komponententest
Es wurden erst die einzelnen Komponenten und dann die Gesamtkomponenten auf ihre Funktion & Anforderungen getestet.
Einzelkomponententest
ID | Testfallbeschreibung | Erwartetes Ergebnis | Testergebnis | Testperson | Datum |
---|---|---|---|---|---|
1 | LED-Stripe auf Funktion überprüfen. | Wenn LEDs weiß leuchten funktionieren die einzelnen RGB LEDs. | OK | Yannick Schmidt | 10.12.2021 |
2 | LCD-Display auf Funktion überprüfen. | Hintergrundbeleuchtung leuchtet, sowie eigener Text wird angezeigt. | OK | Yannick Schmidt | 20.12.2021 |
3 | Taster auf Funktion überprüfen. | Taster geben Schaltsignal an den Arduino weiter. | OK | Yannick Schmidt | 21.12.2021 |
Gesamtkomponententest
ID | Testfallbeschreibung | Testergebnis | Begründung | Testperson | Datum |
---|---|---|---|---|---|
1 | Möglichst dicht gepackte LED Matrix. | OK | Mit einer LED-Dichte von 60LED/Meter wurde eine dicht gepackte LED Matrix erreicht. | Yannick Schmidt; Nils Koch | 6.1.2022 |
2 | Rechteckiger LED Look. | OK | LEDs leuchten die Felder rechteckig und gleichmäßig aus. | Yannick Schmidt; Nils Koch | 6.1.2022 |
3 | Start- und Endanimation | OK | Animation beim Starten des Spiels durch Weißzeichnen der Matrix. Buntzeichnen der Matrix bei Gameover. |
Yannick Schmidt; Nils Koch | 9.1.2022 |
4 | Speicherbarer Highscore | OK | Highscore ist Speicherbar und aktueller Score wird ausgegeben. | Yannick Schmidt; Nils Koch | 9.1.2022 |
5 | Eingabe per kabelgebundenes Gamepad (evtl. auch kabellos per Bluetooth) | OK | Kabelgebundenes Gamepad ist funktionstüchtig. | Yannick Schmidt; Nils Koch | 9.1.2022 |
6 | Stromversorgung mittels USB Powerbank | OK | Stromversorgung ist über ein USB Kabel möglich. | Yannick Schmidt; Nils Koch | 9.1.2022 |
7 | Optional: Staufach für das Gamepad, welches sich per Sensor öffnen lässt | FAIL | Staufach aufgrund von Zeitmangel nicht realisiert worden. | Yannick Schmidt; Nils Koch | 9.1.2022 |
Ergebnis
Alle festen Anforderungen an das Projekt konnten zufriedenstellend umgesetzt werden. Die optionale Anforderung des Staufachs wurde aufgrund von Zeitmangel wieder verworfen. Verbesserungswürdig sind die elektrische Bauteile, die durch eine Leiterplatte ersetzt werden könnten.
Zusammenfassung
Lessons Learned
Das Team hat die folgenden Kompetenzen erlernt:
- Projektplanung und Zeitmanagement
- CAD Konstruktion für 3D Druck
- 3D Druck
- Löten
- Ansteuerung von WS2812 LEDs
Besondere Herausforderungen
Die erste Herausforderung stellte der 3D Druck dar. Ohne Vorkenntnisse mussten sich zunächst die Grundlagen angeeignet werden. Das Design des Gitters musste mehrfach angepasst werden, um typische Druckprobleme, wie Stringing, Fading und verlaufende Überhänge, zu beheben.
Auf gleich mehrere besondere Probleme sind wir im Zusammenhang mit dem LED Streifen gestoßen.
Zu Beginn zeigte sich, dass trotz einwandfreier Funktion des LED-Streifens und des Arduinos keinerlei Ausgabe in der Matrix zu erkennen war. Mehrfache Tests des Streifens und des Microcontrollers konnten keinerlei Aufschluss bringen, denn sobald das vollständige Programm auf den Arduino geladen wurde bleiben die LEDs dunkel, obwohl das Programm lief und über die serielle Schnittstelle eine Ausgabe auf dem PC möglich war. Durch das Auskommentieren scheinbar beliebiger Funktionen lies sich das Problem zwar lösen, aber das Programm war nicht mehr lauffähig.
Die Ursache konnte deshalb im Speicher des Arduinos ausgemacht werden. Durch eine global verfügbares Grid, welches für jeden Pixel des Grids die Farbe in einem long (4 Byte) speichert, werden bereits vor Programmstart alleine 800 der 2048 Byte RAM des Arduinos belegt. Die Gesamtauslastung des RAM, nur durch globale Variablen, lag bei etwa 80%, was die Ausgabe für den Arduino scheinbar unmöglich machte.
Das Problem wurde gelöst, indem im Grid nicht mehr die Farbinformationen zu jedem Pixel gespeichert wurden, sondern nur ein Byte, das die Position der Farbe im Farbarray angab somit konnten hier 600 der 800 Byte für das Grid eingespart werden. Auch für andere Variablen wurde auf kleinere Datentypen zurückgegriffen, um möglichst viel Arbeitsspeicher zu sparen. Die Auslastung durch globale Variablen ließ sich somit auf etwa 60% reduzieren. Außerdem wurden die Debug-Funktionen über #ifdef Befehle so modifiziert, dass sie nur im Testfall kompiliert werden und somit weitere 8% RAM eingespart werden können. Das Problem war hiermit behoben und die ersten Pixel wurden sichtbar.
Das nächste Problem trat bei der Darstellung mehrerer Pixel auf einmal auf. Der Arduino stürzte plötzlich ab und startete neu. Dies war auf eine zu geringe Stromversorgung durch den USB-Port des PC zu erklären. Durch anschließen eines zusätzlichen Stromkabels konnte hier schnell Abhilfe geschaffen werden.
Trotz neuer Stromversorgung war aber noch immer deutlich zu erkennen, dass das LCD mit zunehmender Pixelzahl auf dem LED-Streifen immer dunkler wurde, bis gegen Ende des Spiels kaum noch etwas zu erkennen war. Deshalb wurde ein weiteres Upgrade der Stromversorgung durchgeführt und auf ein Netzteil, welches bei 5 V 3 A liefert, zurückgegriffen. Es wurde direkt mit dem Power-Board verbunden und kann somit den Arduino und alle angeschlossenen Komponenten direkt speisen. Hierbei stellte sich dann noch heraus, dass das benutzte Lightning-Kabel intern falsch verdrahtet war und Masse und Vcc vertauscht wurden. Nachdem auch dieses Problem gelöst war, funktionierte die Ausgabe einwandfrei.
Projektunterlagen
Projektplan
Projektdurchführung
Nach Festlegung des Projektes wurde begonnen erste Pläne zu machen. Nach Klärung des groben Aufbaus konnte die Teile beschafft werden. Dabei wurde dann parallel ein eigenes Wiki für die Hardware- und Softwarespezifikationen angelegt und stetig erweitert.
Zunächst wurde die Hardware vollständig in CAD designt und anschließend erste Prototypen im 3D Drucker gebaut. Dabei wurden unterschiedliche Designs ausprobiert. So sollte beispielsweise in der ursprünglichen Version die Scheibe in eine Nut des Gitters eingefügt werden, was jedoch aufgrund von Fehlern beim Druck wieder verworfen wurde.
Nach Eintreffen der Komponenten wurden diese dem Einzelkomponententest unterzogen.
Die finalen Bauteildesigns wurden nun umgesetzt und konnten zusammengesetzt werden. Anschließend wurden die Prototype-Boards gelötet und angeschlossen.
Die Software konnte nun auf die Hardware angepasst werden und den Gesamtkomponententest durchlaufen
YouTube Video
Weblinks
Literatur
→ zurück zur Übersicht: WS 21/22: Angewandte Elektrotechnik (BSE)