Ich weiß nicht, wie das bei euch ist, aber sobald dicke Regenschwaden über das Land ziehen und die Tage kürzer werden, wird es Zeit, die Carrera-Bahn aus dem Keller zu holen. Wie wäre es, wenn wir das gute Stück etwas upgraden würden? Z.B. mit Starting Lights, Stoppen der besten Rundenzeiten und einer Siegerehrung?
Das folgende Projekt besteht aus Starting Lights mit fünf LEDs, drei LCD-Anzeigen für Spieler 1, Spieler 2 und eine Gesamtanzeige.
Als Erstes stellt man die Rundenanzahl ein, drückt auf Start und der Countdown beginnt. Nun läuft das Rennen. Über Lichtschranken werden die Fahrzeuge getrackt. Dabei wird die aktuelle Runde, die Rundenzeit und die Zeit der schnellsten Runde auf den LCD-Displays für Spieler 1 und 2 ausgegeben. Das mittlere Display zeigt die Gesamtzeit an.
In diesem Tutorial führe ich dich Schritt für Schritt durch den Arduino-Code. Let’s go!
Bauteile
- 1x 1x
- 3x LCD-Display (Ja, das D steht schon für Display, liest sich so aber besser ;-) )
- 5x LED Rot (kann man auch parallel schalten, um Reihen zu realisieren.
- 5x Wiederstand 220 Ohm
- 3x Taster
- 1x Potentiometer z.B. 10k – 100k
- 2x IR Lichtschranken, Reflektierend (ich habe ähnliche verwendet, nicht genau die hier)
Schaltung
Arduino-Code
#include <Wire.h> #include <LiquidCrystal_I2C.h> const int lightBarrier1Pin = 2; const int lightBarrier2Pin = 3; const int resetButtonPin = 4; const int potPin = 0; const int startLightPins[] = { 7, 8, 9, 10, 11 }; const int numStartLights = 5; int lightBarrier1State = 0; int lightBarrier2State = 0; int lightBarrier1LastState = 0; int lightBarrier2LastState = 0; int totalLaps = 10; int lapsPlayer1 = 0; int lapsPlayer2 = 0; LiquidCrystal_I2C lcd1(0x25, 16, 2); LiquidCrystal_I2C lcd2(0x27, 16, 2); LiquidCrystal_I2C lcd3(0x26, 16, 2); int theState = 1; unsigned long startTime = 0; unsigned long lapStartTimePlayer1 = 0; unsigned long bestLapPlayer1 = 0; unsigned long lapStartTimePlayer2 = 0; unsigned long bestLapPlayer2 = 0; unsigned long totalTime = 0; boolean interruptP1Triggered = false; boolean interruptP2Triggered = false; long theMinutes, theSeconds, theMillis; void setup() { Serial.begin(115200); pinMode(lightBarrier1Pin, INPUT_PULLUP); pinMode(lightBarrier2Pin, INPUT_PULLUP); pinMode(resetButtonPin, INPUT_PULLUP); lcd1.init(); lcd2.init(); lcd3.init(); lcd1.backlight(); lcd2.backlight(); lcd3.backlight(); for (int i = 0; i < numStartLights; i++) { pinMode(startLightPins[i], OUTPUT); } attachInterrupt(digitalPinToInterrupt(lightBarrier1Pin), lightBarrier1Interrupt, FALLING); attachInterrupt(digitalPinToInterrupt(lightBarrier2Pin), lightBarrier2Interrupt, FALLING); startTime = millis(); bestLapPlayer1 = 0; bestLapPlayer2 = 0; } void loop() { switch (theState) { case 0: // idle break; case 1: // select laps select_laps(); if (digitalRead(resetButtonPin) == LOW) { theState = 2; clear_game(); delay(500); } break; case 2: // get Ready blink 3 times get_ready(); break; case 3: // start lights count down start_lights(); break; case 4: // racing racing(); if (digitalRead(resetButtonPin) == LOW) { theState = 6; // Abbrechen clear_game(); delay(500); } break; case 5: // winner winner(); if (digitalRead(resetButtonPin) == LOW) { theState = 1; delay(500); lcds_clear(); } break; case 6: abort(); break; } delay(10); } void clear_game() { lcds_clear(); startTime = 0; lapStartTimePlayer1 = 0; bestLapPlayer1 = 0; lapStartTimePlayer2 = 0; bestLapPlayer2 = 0; totalTime = 0; lapsPlayer1 = 0 lapsPlayer2 = 0; } void lightBarrier1Interrupt() { interruptP1Triggered = true; } void lightBarrier2Interrupt() { interruptP2Triggered = true; } void select_laps() { lcd1.setCursor(0, 0); lcd1.print("Lap Count: "); int potValue = map(analogRead(potPin), 0, 1023, 0, 99); lcd1.setCursor(14, 0); lcd1.print(potValue < 10 ? " " : ""); lcd1.print(potValue); lcd1.setCursor(0, 1); lcd1.print("< Press Start >"); totalLaps = potValue; } void get_ready() { for (int i = 0; i < numStartLights; i++) { digitalWrite(startLightPins[i], HIGH); } lcd1.setCursor(0, 0); lcd1.print("GET READY!!!"); delay(2000); lcds_clear(); theState++; } void start_lights() { for (int i = 0; i < numStartLights; i++) { delay(1000); digitalWrite(startLightPins[i], LOW); } startTime = millis(); lapStartTimePlayer1 = millis(); lapStartTimePlayer2 = millis(); theState++; } void racing() { if (interruptP1Triggered == true) { lapsPlayer1++; lcd2.clear(); if ((bestLapPlayer1 > (millis() - lapStartTimePlayer1) || (bestLapPlayer1 == 0))) bestLapPlayer1 = millis() - lapStartTimePlayer1; lapStartTimePlayer1 = millis(); interruptP1Triggered = false; } if (interruptP2Triggered == true) { lapsPlayer2++; lcd3.clear(); if ((bestLapPlayer2 > (millis() - lapStartTimePlayer2) || (bestLapPlayer2 == 0))) bestLapPlayer2 = millis() - lapStartTimePlayer2; lapStartTimePlayer2 = millis(); interruptP2Triggered = false; } lcd1.setCursor(0, 0); lcd1.print("Time: "); long timeElapsed = millis() - startTime; convertMillisToTime(timeElapsed, theMinutes, theSeconds, theMillis); lcd1.print(theMinutes < 10 ? "0" : ""); lcd1.print(theMinutes); lcd1.print(":"); lcd1.print(theSeconds < 10 ? "0" : ""); lcd1.print(theSeconds); lcd1.print(":"); lcd1.print(theMillis); convertMillisToTime((millis() - lapStartTimePlayer1), theMinutes, theSeconds, theMillis); lcd2.setCursor(0, 0); lcd2.print(theMinutes < 10 ? "0" : ""); lcd2.print(theMinutes); lcd2.print(":"); lcd2.print(theSeconds < 10 ? "0" : ""); lcd2.print(theSeconds); lcd2.print(":"); lcd2.print(theMillis); lcd2.setCursor(14, 0); lcd2.print(lapsPlayer1 < 10 ? "0" : ""); lcd2.print(lapsPlayer1); convertMillisToTime(bestLapPlayer1, theMinutes, theSeconds, theMillis); lcd2.setCursor(0, 1); if (bestLapPlayer1 != 0) { lcd2.print(theMinutes < 10 ? "0" : ""); lcd2.print(theMinutes); lcd2.print(":"); lcd2.print(theSeconds < 10 ? "0" : ""); lcd2.print(theSeconds); lcd2.print(":"); lcd2.print(theMillis); lcd2.print(" < BEST"); } convertMillisToTime((millis() - lapStartTimePlayer2), theMinutes, theSeconds, theMillis); lcd3.setCursor(0, 0); lcd3.print(theMinutes < 10 ? "0" : ""); lcd3.print(theMinutes); lcd3.print(":"); lcd3.print(theSeconds < 10 ? "0" : ""); lcd3.print(theSeconds); lcd3.print(":"); lcd3.print(theMillis); lcd3.setCursor(14, 0); lcd3.print(lapsPlayer2 < 10 ? "0" : ""); lcd3.print(lapsPlayer2); convertMillisToTime(bestLapPlayer2, theMinutes, theSeconds, theMillis); lcd3.setCursor(0, 1); if (bestLapPlayer2 != 0) { lcd3.print(theMinutes < 10 ? "0" : ""); lcd3.print(theMinutes); lcd3.print(":"); lcd3.print(theSeconds < 10 ? "0" : ""); lcd3.print(theSeconds); lcd3.print(":"); lcd3.print(theMillis); lcd3.print(" < BEST"); } lightBarrier1LastState = lightBarrier1State; lightBarrier2LastState = lightBarrier2State; if ((lapsPlayer1 >= totalLaps) || (lapsPlayer2 >= totalLaps)) { totalTime = millis(); lcds_clear(); theState++; } } void convertMillisToTime(long milliseconds, long& minutes, long& seconds, long& millis) { minutes = milliseconds / 60000; seconds = (milliseconds / 1000) % 60; millis = milliseconds % 1000; } void winner() { lcd1.setCursor(0, 0); if (lapsPlayer1 >= totalLaps) { convertMillisToTime(bestLapPlayer1, theMinutes, theSeconds, theMillis); lcd1.print("BEST: "); lcd1.print(theMinutes < 10 ? "0" : ""); lcd1.print(theMinutes); lcd1.print(":"); lcd1.print(theSeconds < 10 ? "0" : ""); lcd1.print(theSeconds); lcd1.print(":"); lcd1.print(theMillis); if (millis() % 1000 < 500) { lcd2.clear(); } else { lcd2.setCursor(0, 0); lcd2.print("Congratulations"); lcd2.setCursor(0, 1); lcd2.print(" Player 1"); } } else { convertMillisToTime(bestLapPlayer2, theMinutes, theSeconds, theMillis); lcd1.print("BEST: "); lcd1.print(theMinutes < 10 ? "0" : ""); lcd1.print(theMinutes); lcd1.print(":"); lcd1.print(theSeconds < 10 ? "0" : ""); lcd1.print(theSeconds); lcd1.print(":"); lcd1.print(theMillis); if (millis() % 1000 < 500) { lcd3.clear(); } else { lcd3.setCursor(0, 0); lcd3.print("Congratulations"); lcd3.setCursor(0, 1); lcd3.print(" Player 2"); } } lcd1.setCursor(0, 1); lcd1.print("Time: "); long timeElapsed = totalTime - startTime; convertMillisToTime(timeElapsed, theMinutes, theSeconds, theMillis); lcd1.print(theMinutes < 10 ? "0" : ""); lcd1.print(theMinutes); lcd1.print(":"); lcd1.print(theSeconds < 10 ? "0" : ""); lcd1.print(theSeconds); lcd1.print(":"); lcd1.print(theMillis); } void abort() { lcds_clear(); delay(500); lcd1.setCursor(0, 0); lcd1.print("> Race aborded <"); delay(2000); lcds_clear(); theState = 1; } void lcds_clear() { lcd1.clear(); lcd2.clear(); lcd3.clear(); }
Code-Erklärungen
Dieser Code ermöglicht die Auswahl der zu fahrenden Runden, die Anzeige von Starting Lights mit Countdown, die Anzeige der schnellsten Runde für Spieler 1 und 2, die Gesamtzeit und die Anzeige des Gewinners. Lass uns den Code Schritt für Schritt durchgehen:
Schritt 1: Import von Bibliotheken und Deklaration der Pins
#include <Wire.h> #include <LiquidCrystal_I2C.h> const int lightBarrier1Pin = 2; const int lightBarrier2Pin = 3; const int resetButtonPin = 4; const int potPin = 0; const int startLightPins[] = { 7, 8, 9, 10, 11 }; const int numStartLights = 5;
In diesem Abschnitt wird die erforderliche Bibliothek Wire
für die Kommunikation mit dem I2C-LCD-Display und die Pins für die Lichtschranken, den Reset-Knopf, den Potentiometer und die Startlichter deklariert. Sie ist automatisch im Lieferumfang der Arduino-Software
Schritt 2: Deklaration von Variablen und Objekten
int lightBarrier1State = 0; int lightBarrier2State = 0; int lightBarrier1LastState = 0; int lightBarrier2LastState = 0; int totalLaps = 10; int lapsPlayer1 = 0; int lapsPlayer2 = 0; LiquidCrystal_I2C lcd1(0x25, 16, 2); LiquidCrystal_I2C lcd2(0x27, 16, 2); LiquidCrystal_I2C lcd3(0x26, 16, 2); int theState = 1; unsigned long startTime = 0; unsigned long lapStartTimePlayer1 = 0; unsigned long bestLapPlayer1 = 0; unsigned long lapStartTimePlayer2 = 0; unsigned long bestLapPlayer2 = 0; unsigned long totalTime = 0; boolean interruptP1Triggered = false; boolean interruptP2Triggered = false; long theMinutes, theSeconds, theMillis;
Hier werden verschiedene Variablen für den Zustand des Spiels, die Zeiten, die Anzahl der Runden, die Lichtschranken und die LCD-Displays deklariert.
Schritt 3: Setup-Funktion
void setup() { // Initialisierung von Pins und Objekten pinMode(lightBarrier1Pin, INPUT_PULLUP); pinMode(lightBarrier2Pin, INPUT_PULLUP); pinMode(resetButtonPin, INPUT_PULLUP); lcd1.init(); lcd2.init(); lcd3.init(); lcd1.backlight(); lcd2.backlight(); lcd3.backlight(); for (int i = 0; i < numStartLights; i++) { pinMode(startLightPins[i], OUTPUT); } // Interrupts für die Lichtschranken aktivieren attachInterrupt(digitalPinToInterrupt(lightBarrier1Pin), lightBarrier1Interrupt, FALLING); attachInterrupt(digitalPinToInterrupt(lightBarrier2Pin), lightBarrier2Interrupt, FALLING); // Initialisierung von Startzeiten und besten Rundenzeiten startTime = millis(); bestLapPlayer1 = 0; bestLapPlayer2 = 0; }
In der setup
-Methode werden die Pins initialisiert, die Interrupts für die Lichtschranken aktiviert und die LCD-Displays initialisiert. Außerdem werden die Startzeiten und besten Rundenzeiten auf Null gesetzt.
Schritt 4: Loop-Methode
void loop() { switch (theState) { case 0: // idle // Keine Aktion break; case 1: // select laps select_laps(); if (digitalRead(resetButtonPin) == LOW) { theState = 2; clear_game(); delay(500); } break; case 2: // get Ready blink 3 times get_ready(); break; case 3: // start lights count down start_lights(); break; case 4: // racing racing(); if (digitalRead(resetButtonPin) == LOW) { theState = 6; // Abbrechen clear_game(); delay(500); } break; case 5: // winner winner(); if (digitalRead(resetButtonPin) == LOW) { theState = 1; delay(500); lcds_clear(); } break; case 6: abort(); break; } delay(10); }
Die loop
-Methode steuert den Hauptablauf des Spiels, abhängig von seinem Zustand theState
. Hier werden Funktionen aufgerufen, die die Auswahl der Runden, die »Get Ready«-Anzeige, den Countdown der Startlichter, das Rennen selbst und die Anzeige des Gewinners handhaben.
Schritt 5: Implementierung weiterer Funktionen
Der Code enthält auch eine Reihe von Hilfsfunktionen, die in den oben genannten Schritten aufgerufen werden:
clear_game()
: Setzt das Spiel zurück und löscht alle Zeiten und Runden.lightBarrier1Interrupt()
undlightBarrier2Interrupt()
: Funktionen, die aufgerufen werden, wenn die Lichtschranken ausgelöst werden.select_laps()
: Ermöglicht die Auswahl der zu fahrenden Runden über ein Potentiometer.get_ready()
: Zeigt die “Get Ready”-Anzeige und blinkt die Startlichter.start_lights()
: Startet den Countdown der Startlichter.racing()
: Verwaltet den Rennablauf, zeichnet Zeiten, Runden und die beste Rundenzeit auf.convertMillisToTime()
: Konvertiert Millisekunden in Minuten, Sekunden und Millisekunden.winner()
: Zeigt den Gewinner und die Zeiten an.abort()
: Beendet das Spiel vorzeitig.lcds_clear()
: Löscht die Anzeigen auf den LCD-Displays.
Jetzt weißt du, wie dieser Arduino-Code für die Carrera-Bahn-Steuerung funktioniert und wie er in verschiedene Abschnitte unterteilt ist, um die verschiedenen Aspekte des Spiels zu handhaben. Du kannst diesen Code verwenden und anpassen, um dein eigenes Carrera-Bahn-Projekt zu erstellen.
Info: Was ist eine State Machine?
Eine State Machine, oder zu Deutsch “Zustandsmaschine”, ist ein fundamentales Konzept in der Softwareentwicklung. In diesem Tutorial spielt sie eine zentrale Rolle bei der Realisierung der Funktionen für die Carrera-Bahn-Steuerung mithilfe des Arduino-Mikrocontrollers.
switch (theState) { case 1: // select laps ... break; case 2: // get Ready blink 3 times ... break; case 3: // start lights count down ... break; case 4: // racing ... break; case 5: // winner ... break; case 6: // abort ... break; }
Sie dient dazu, den Programmablauf in klar definierte Zustände zu gliedern. In diesem Projekt repräsentieren die Zustände verschiedene Phasen des Rennbahn-Erlebnisses, wie das Zählen der Runden, den Countdown und die Anzeige der Rundenzeiten.
Die State Machine ermöglicht es dem Arduino, zwischen diesen Zuständen zu wechseln, abhängig von bestimmten Ereignissen, wie dem Drücken eines Tasters oder dem Passieren einer Lichtschranke. So behält das System stets den Überblick über den aktuellen Zustand des Rennens und kann entsprechend reagieren.
Diese klare Strukturierung des Programms erleichtert nicht nur die Entwicklung und Wartung, sondern ermöglicht auch eine benutzerfreundliche Steuerung.
Info: Was sind Interrupts und ihre Bedeutung in der Elektronik und Programmierung?
Interrupts (Unterbrechungen), sind ein wichtiger Mechanismus in der Elektronik und Programmierung. Sie dienen dazu, den normalen Ablauf eines Mikrocontrollers oder Computers vorübergehend zu unterbrechen, um auf externe Ereignisse oder Signale zu reagieren. Hier sind einige Schlüsselaspekte von Interrupts und ihre Bedeutung:
- Ereignisbasierte Reaktion: Interrupts ermöglichen es einem System, unmittelbar auf bestimmte Ereignisse oder Signale zu reagieren, ohne den Hauptprogrammfluss zu unterbrechen. Dies ist besonders nützlich in Echtzeit-Anwendungen, bei denen es auf schnelle Reaktionen ankommt.
- Externe Signale: Interrupts werden oft genutzt, um auf externe Hardware-Signale wie Tastendrücke, Sensoreingaben oder Kommunikationsanfragen zu reagieren. Wenn ein solches Signal erkannt wird, kann der Mikrocontroller die aktuelle Aufgabe unterbrechen und eine vordefinierte Funktion ausführen.
- Ressourceneffizienz: Durch die Verwendung von Interrupts kann der Mikrocontroller Ressourcen sparen, da er nicht ständig auf bestimmte Ereignisse prüfen muss. Dies trägt zur Effizienz und Energieeinsparung bei.
- In diesem Tutorial wird die Carrera-Bahn-Steuerung mithilfe von Interrupts auf Lichtschranken reagieren. Sobald ein Auto eine Lichtschranke passiert, wird ein Interrupt ausgelöst, der die Rundenzeit zählt und den Rundenstand aktualisiert. Da die Autos sehr schnell sind, ist es wichtig, dass das System schnell reagiert (keine Garantie von meiner Seite aus).
Wenn dir das Projekt gefallen hat und du von weiteren interessanten Projekten inspiriert werden willst, sieh dir doch mal mein neues E-Book »Arduino Projekte Volume 1« an!
- Die beliebtesten Arduino-Projekte von StartHardware
- Inklusive Schaltplan, Beschreibung und Code
- Arduino-Schnellstart-Kapitel
- Kompakter Programmierkurs
Super Projekt ! Aber doch etwas schwer verständlich aufgebaut ! z.B. Taster !? Wohin und Wofür ? Wenn schon eigenes Projekt dann auch eine Startampel mit aleem PiPaPo also mit SafetyCar Licht , Grüne Startampeln , gelbes Fehlstartlicht.
Ich finde es toll das sich hingesetzt wird und sich was überlegt wird , ziehe ich meinen Hut vor, aber dann halbfertige Projekte zu veröffentlichen finde ich dann nicht so schön !
Hallo Herr Hermann,
ich finde Ihr Projekt sehr interessant.
Ich suche so etwas für meine Bahn. Allerdings plane ich
vierspurig. Sollte aber kein Problem sein.
Nur wie ist das mit der Lichtschranke gedacht, da es ja eine Reflexlichtschranke
ist und nicht eine Gabellichtschranke.
Vielen Dank für eine Antwort
Mit freundlichen Grüßen
M.Krüll