Zum Inhalt springen
Arduino Carrera Bahn Track Schaltplan LCD I2C UNO Titel

Arduino Carrera-Bahn Projekt – Rundenzeit Counter mit LCD

    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.


    Sieh dir jetzt meinen neuen Arduino-Videokurs an: Jetzt ansehen!


    In diesem Tutorial führe ich dich Schritt für Schritt durch den Arduino-Code. Let’s go!

    Arduino Carrera Bahn Track Schaltplan LCD I2C UNO Draufsicht

    Bauteile

    Schaltung

    Arduino Carrera Bahn Track Schaltplan LCD I2C UNO 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() und lightBarrier2Interrupt(): 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:

    1. 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.
    2. 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.
    3. 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.
    4. 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


    Schreibe einen Kommentar

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

     

    Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.