Modelleisenbahn Projekt Mini Diorama Spur N Teil 6 Arduino Sketch Pendelzugsteuerung

Von Sven
modelleisenbahn pendelzugsteuerung arduino oled display moba module

Einfache Pendelzugsteuerung mit Arduino und Komfort

Das Sketch (Programm) für den Arduino Nano wurde im fünften Teil dieses Projektes schon erwähnt und in der ersten Version zum Download angeboten. Wie dort schon erwähnt, soll es in diesem Beitrag um die einzelnen Abschnitte des Arduino Sketch gehen. Das könnte etwas trocken werden, aber durchhalten wird sich für einige unter euch lohnen.

Auf die Plätze …. Fertig …. Los ….

#include "ssd1306.h"
#include "Lok.h"

Zuerst binde ich mittels der include Anweisung einige Bibliotheken ein. In diesem Fall die für das OLED Display mit dem SSD1306 Chipsatz, damit das Display angesprochen werden kann und die Lok.h für die fahrende Lok, welche auf dem Display angezeigt werden soll, nach dem das Programm gestartet wurde. Die Lok.h ist separat vom Hauptprogramm geschrieben. Mehr Infos dazu gibt es hier > Eine Anleitung damit das OLED Display erwacht.

Variablen / Konstanten / Zuordnungen

Im nächsten Abschnitt werden alle benötigten Variablen und Konstanten festgelegt.

unsigned long vergangeneMillis = 0;
unsigned long pendelMillis = 0;
unsigned long laufzeitMillis = 0;
unsigned long lichtschrankenMillis = 0;
const long wechselTagNacht = 604000; //Zeit in Millisekunden
long pendelZeit; // Pendelzeit
const long laufZeit = 15000; // Laufzeitüberwachung

const int lichtschrankeLinks = 2; //D2
const int lichtschrankeRechts = 3; //D3
const int lichtTagNacht = 12; //D12
const int fahrtLinks = 4; //D4
const int fahrtRechts = 5; //D5
const int fahrtFahrt = 6; //D6
const int Eingang3 = 11; //D11
const int Eingang2 = 10; //D10
const int Eingang1 = 9; //D9

byte status_bahnhofLinks = 0;
byte status_bahnhofRechts = 0;
byte status_Fehler = 0;
byte status_Fahrt = 0;
int lichtAusgang = LOW;
int status_Lichtschranken = HIGH;
int status_LichtschrankenAlt = HIGH;
Variable / KonstanteWertBeschreibung
vergangeneMillis0Zeitmanagement für Überwachung und Auslösen von Aktionen, Vergleich zur aktuellen Zeit
pendelMillis0Variable für die zufällig generierte Pendelzeit
laufzeitMillis0Laufzeitüberwachung
lichtschrankenMillis0Fehlerüberwachung (Entprellen und Vorbeugung Fehlfunktion)
wechselTagNacht60400Zeit für Tag / Nachtwechsel (LED 1 oder 2); aller 10 Minuten
pendelZeitVariable für die Generierung der zufälligen Pendelzeit
laufZeit15000Laufzeitüberwachung, max. Fahrzeit = 15 Sekunden
status_bahnhofLinks0Statusbit (1 = Bahnhof links besetzt)
status_bahnhofRechts0Statusbit (1 = Bahnhof rechts besetzt)
status_Fehler0Fehlerauswertung, bei Wert 0 ist alles ok
status_Fahrt0Status für Fahrt, 1 = Es bewegt sich was
lichtAusgangLOWAm Anfang ist es Nacht
statusLichtschrankenHIGHFür Lichtschrankenüberwachung
status_LichtschrankenAltHIGHFür Lichtschrankenüberwachung
lichtschrankeLinks2Zuweisung digitaler I/O 2
lichtschrankeRechts3Zuweisung digitaler I/O 3
lichtTagNacht12Zuweisung digitaler I/O 12
fahrtLinks4Zuweisung digitaler I/O 4
fahrtRechts5Zuweisung digitaler I/O 5
fahrtFahrt6Zuweisung digitaler I/O 6
Eingang311Zuweisung digitaler I/O 11
Eingang210Zuweisung digitaler I/O 10
Eingang19Zuweisung digitaler I/O 9

Zwei kleine Funktionen für später

// Text Startsequenz
static void textStart()
{
    ssd1306_drawHLine  (10 , 2, 118);
    ssd1306_drawHLine  (10 , 30, 118);
    ssd1306_printFixed(19, 8, "ALTSTEINAER TAL", STYLE_NORMAL);
    ssd1306_printFixed(22, 16, "moba-module.de", STYLE_NORMAL);
    delay(5000);
    ssd1306_clearScreen();
}

// Fahrende LOK Startsequenz
static void bitmapLok()
{

  for (int i=127; i>0; i--){
    
    ssd1306_drawBitmap(i, 0, 50, 32, Lok);
    ssd1306_clearScreen();
  }
  ssd1306_drawBitmap(0, 0, 50, 32, Lok);
  delay(1000);
  ssd1306_clearScreen();
}

Die Funktion textStart() wird später im Programm beim Start des Arduinos aufgerufen und zeichnet zwei horizontale Linien aufs Display (Zeile 4,5) und gibt den angegebenen Text aus (Zeile 6,7). Im Anschluss daran hat der Arduino 5 Sekunden lang Pause (Zeile 8) bevor nach Ablauf der Zeit das Display gelöscht wird und damit Platz für die nächste Anzeige macht (Zeile 9).

Die Funktion bitmapLok() sorgt dafür, dass die Lok (Bitmap Datei) Pixelweise über das Display von rechts nach links fährt. Das Display hat 128×32 Bildpunkte. In einer Schleife wird die X-Koordinate (i) heruntergezählt, das Bild an der Position ausgegeben und danach das Display geleert. Solange bis die Lok an ihrer Endposition x=0 steht. Da der Arduino Nano ein bißchen “schwach” für diese schnelle Bildabfolge ist (Frequenz), flackert die Bewegung. Danach gibt es 1 Sekunde Pause und das Display wird wieder geleert (Zeilen 22,23).

Setup

void setup()
{
  //Pins definieren
    pinMode(lichtschrankeLinks, INPUT); //D2
    pinMode(lichtschrankeRechts, INPUT); //D3

    pinMode(Eingang1, INPUT); //D9
    pinMode(Eingang2, INPUT); //D10
    pinMode(Eingang3, INPUT); //D11

    pinMode(lichtTagNacht, OUTPUT); //D12

    pinMode(fahrtLinks, OUTPUT); //D4
    pinMode(fahrtRechts, OUTPUT); //D5
    pinMode(fahrtFahrt, OUTPUT); //D6
  
  //Schriftartauswahl
    ssd1306_setFixedFont(ssd1306xled_font6x8);
  //Display Initialisierung
    ssd1306_128x32_i2c_init();

  //Startsequenz
    digitalWrite(fahrtFahrt, HIGH);
    digitalWrite(fahrtLinks, HIGH);
    digitalWrite(fahrtRechts, HIGH);
    ssd1306_clearScreen();
    bitmapLok();
    textStart();

    randomSeed(analogRead(0));
    pendelZeit = random(30000, 300000);   
}

In diesem Abschnitt wird den I/O’s des Arduino eine Variable zugewiesen und definiert, ob diese ein Input (Eingang) oder Output (Ausgang) sein sollen. Die Variablenzuweisungen ermöglichen im Programmablauf ein einfacheres Handling.

In den Zeilen 4 und 5 wird den Lichtschranken, welche die Endposition (Bahnhof links / rechts) überwachen, die entsprechende Variable zugewiesen und diese als Eingang definiert. Die Lichtschranken haben im ungeschaltenen Zustand eine logische 1 (HIGH Signal) also im Fall des Arduino am Ausgang der Lichtschranke und damit am verbundenen Eingangspin des Arduino 5V anliegen. Schaltet die Lichtschranke nimmt sie eine logische 0 (LOW Signal) an. Damit liegen am Ausgang der Lichtschranke und am verbundenen Eingangspin des Arduino 0 Volt an.

In den Zeilen 7 bis 9 werden drei Eingänge definiert. Diese sollen später für schaltbare Sonderfunktionen zur Verfügung stehen.

Die Ansteuerung für das Relais, welches zwischen dem Tag- und Nachtmodus umschaltet, wird in Zeile 11 als Ausgangspin realisiert.

Die Zeilen 13 bis 15 definieren ebenfalls Ausgänge des Arduino. Diese steuern die entsprechenden Relais für das allgemeine Fahrsignal und für die Fahrrichtung (links / rechts) an. Warum 3 Relais wird sich der eine oder andere fragen? Zwei Relais würden es doch auch tun. Ich möchte, dass die Lok auf den Gleisen zu 100 Prozent absolut sicher und nicht nur über die Potenzialdifferenz gleich Null abgeschaltet wird. Demzufolge zwei in Reihe liegende Schaltkontakte.

Für das OLED Display legen wir in Zeile 18 noch einen Font (Schriftart) fest und initialisieren es in Zeile 20.

Im Abschnitt der Startsequenz (ab Zeile 23) setzen wir die digitalen Ausgänge für die Fahrt alle in eine Nullstellung. In diesem Fall werden diese alle auf HIGH gesetzt. Dies ergibt sich aus der Hardware und deren Konfiguration (4 Kanal Relais Board). Danach wird die Anzeige des OLED Display gelöscht um im Anschluss unsere zwei kleinen Funktionen von weiter oben aufzurufen. Das bedeutet, die Linien, Texte und die fahrende Lok werden dargestellt.

Ab Zeile 30 wird die erste Zufallszeit generiert, welche zwischen den einzelnen Bahnhofsfahrten abläuft. Die Zufallsfolge wird generiert und dann die durch Zufall bestimmte Pendelzeit zwischen 30000 und 300000 ms (30 Sekunden bis 5 Minuten) festgelegt. Für die Zufallsgeneratoren gibt es in der Arduino Referenz die Anleitung zum vertiefen. LINK> https://www.arduino.cc/reference/de/language/functions/random-numbers/randomseed/

Arduino Pendelzugsteuerung Programm

In der loop Funktion des Programms werden die folgenden Programmzeilen immer wieder durchlaufen.

void loop()
{

laufzeitMillis = millis();
  
  while (status_Fehler == 0){

    unsigned long jetztMillis = millis();
    
    if(digitalRead(lichtschrankeLinks) == HIGH && digitalRead(lichtschrankeRechts) 
      == HIGH && status_Fahrt == 0)
      {
      ssd1306_clearBlock (16,8,98,24);
      ssd1306_printFixed(16, 16, "Lok in der Mitte", STYLE_NORMAL);
      status_Fahrt = 1;
      digitalWrite(fahrtRechts, HIGH);
      delay(200);
      digitalWrite(fahrtLinks, LOW);
      delay(200);
      digitalWrite(fahrtFahrt, LOW);
      }
    
    if (digitalRead(lichtschrankeLinks) == LOW && status_bahnhofLinks != 1)
     {
     status_bahnhofRechts = 0;
     digitalWrite(fahrtFahrt, HIGH);
     status_Fahrt = 0;
     ssd1306_clearScreen();
     ssd1306_fillRect  (0 , 0, 15, 32);
     status_bahnhofLinks = 1;
     delay(200);
     digitalWrite(fahrtLinks, HIGH);
     delay(200);
     digitalWrite(fahrtRechts, HIGH);
     } 

    if (digitalRead(lichtschrankeRechts) == LOW && status_bahnhofRechts != 1) 
     {
     status_bahnhofLinks = 0;
     digitalWrite(fahrtFahrt, HIGH);
     status_Fahrt = 0;
     ssd1306_clearScreen();
     ssd1306_fillRect  (113 , 0, 128, 32);
     status_bahnhofRechts = 1;
     delay(200);
     digitalWrite(fahrtLinks, HIGH);
     delay(200);
     digitalWrite(fahrtRechts, HIGH);
     } 

In Zeile 4 wird die Laufzeitüberwachung aktiviert und auf die “Jetzt-Zeit” des Mikrocontroller festgelegt. Die Laufzeit wurde in den Variablen auf 15 Sekunden festgelegt. Das heißt, ab jetzt muss sich innerhalb der nächsten 15 Sekunden etwas tun, sonst schalten wir ab. Die Fehlerbehandlung kommt erst weiter hinten im Programm. Die Laufzeitüberwachung soll verhindern, dass bei einem Fehler, z. B. die Lok bewegt sich nicht oder erreicht innerhalb dieser Laufzeit nicht den Bahnhof, diese nicht weiter bestromt wird und dadurch ein größerer Schaden vermieden wird.

Sorgen wir also dafür, dass etwas auf der Strecke passiert.

In Zeile 10 bis 21 ist eine if Bedingung. Die in den dazu gehörigen geschweiften Klammern aufgeführten Befehle werden nur ausgeführt, wenn die Bedingungen in den normalen Klammern wahr sind. In diesem Fall muss die Lichtschranke des linken Bahnhofs nicht betätigt sein (keine Detektion einer Lok) und die Lichtschranke des rechten Bahnhofs nicht betätigt sein und es darf keine Fahrt stattfinden (status_Fahrt == 0). Das ist immer dann der Fall, wenn die Lok in der Mitte der Strecke steht. Also z.B. neu aufgesetzt wurde oder aus welchem Grund auch immer aus einem der beiden Bahnhöfe herausgeschoben wurde.

Sind diese Bedingungen zur gleichen Zeit erfüllt, wird das OLED Display angewiesen eine bestimmten Bereich zu löschen und dort den Text “Lok in der Mitte” darzustellen. Darauf erfolgt der Statuswechsel auf status_Fahrt == 1, weil wir ja die Lok in Bewegung versetzen möchten, was dann in den Zeilen 16 bis 20 passiert. Die Lok soll in den linken Bahnhof fahren. Das Relais für Fahrt rechts bleibt ungeschalten, vorsichtshalber geben wir diesen Befehl mit aus. Danach wird das Relais für die Fahrtrichtung links geschaltet und dann das Relais für Fahrt. Damit wird die vor eingestellte Fahrspannung auf die Schienen geschaltet. Die Lok setzt sich Richtung linken Bahnhof in Bewegung.

Abschaltung in den Bahnhöfen

Da sich die Lok nun bewegt, müssen wir die Bahnhöfe überwachen, ob die installierte Lichtschranke etwas detektiert. Dann müssen wir abschalten. Auch hier kommt wieder eine if Bedingung ins Programm (Zeile 23 bis 35). Ist also die Lichtschranke im linken Bahnhof geschalten (Lok hat diese ausgelöst) und dazu der Status des Bahnhofs unbesetzt (ungleich 1) wird der Status des rechten Bahnhofs auf 0 (unbesetzt) gesetzt und in der nächsten Zeile darauf folgend das Fahrt Relais zurück gesetzt (abgeschaltet). Die Fahrspannung an den Schienen wird abgeschaltet. Der Fahrtstatus wird auf 0 gesetzt, das Display wird komplett gelöscht und in Zeile 29 angewiesen ein gefülltes Kästchen links darzustellen. Das ist die visuelle Darstellung, dass der Bahnhof links jetzt besetzt ist. Demzufolge wird auch der Status des Bahnhofs auf besetzt gestellt (status_bahnhofLinks = 1). Zum Schluss werden noch die beiden Fahrtrichtungsrelais wieder in die Nullstellung gebracht.

Das gleiche Prozedere wird für den rechten Bahnhof in den Zeilen 37 bis 49 programmiert.

Pendelzeit und Fahrtbedingungen

   if(status_Fahrt == 0){
      laufzeitMillis = jetztMillis;
    }

    if (jetztMillis - pendelMillis >= pendelZeit) {
      pendelMillis = jetztMillis;
      pendelZeit = random(30000, 300000);
      
       if (digitalRead(lichtschrankeLinks) == LOW && status_Fahrt == 0){
          status_Fahrt = 1;
          digitalWrite(fahrtLinks, HIGH);
          delay(200);
          digitalWrite(fahrtRechts, LOW);
          delay(200);
          digitalWrite(fahrtFahrt, LOW);
          ssd1306_clearBlock (16,8,98,32);
          ssd1306_printFixed(16, 16, " -> -> -> -> -> ", STYLE_NORMAL);
        }

        if (digitalRead(lichtschrankeRechts) == LOW && status_Fahrt == 0){
          status_Fahrt = 1;
          digitalWrite(fahrtRechts, HIGH);
          delay(200);
          digitalWrite(fahrtLinks, LOW);
          delay(200);
          digitalWrite(fahrtFahrt, LOW);
          ssd1306_clearBlock (16,8,98,32);
          ssd1306_printFixed(16, 16, " <- <- <- <- <- ", STYLE_NORMAL);
        }

    }

Immer wenn der Status der Fahrt = 0 ist, wird die Zeit der Laufzeitüberwachung zurück gesetzt. Damit die Differenz immer fast Null ist, nehmen wir die bis jetzt vergangenen Millisekunden der Programmlaufzeit (jetztMillis).

In Zeile 5 wird überprüft, ob die abgelaufene Zeit, in der sich die Lok in einem Bahnhof befindet (zufällige Pendelzeit), schon abgelaufen ist. Ist das der Fall, so wird die Pendelzeit zurückgesetzt und eine neue zufällige Pendelzeit bestimmt.

Weiter wird abgefragt ob der Fahrtstatus = 0 ist und welche Lichtschranke betätigt ist. Darauf hin wird entschieden, in welche Richtung ein Fahrtbefehl gegeben werden muss (Zeilen 9 und 20).

Je nachdem wird der Fahrtstaus auf 1 gesetzt und das entsprechen Fahrtrichtungsrelais geschaltet (LOW). Gefolgt vom Fahrtrelais. Die Fahrtspannung wird auf die Gleise geschaltet und die Lok setzt sich in Bewegung. Zusätzlich wird auf dem Display die Fahrtrichtung dargestellt.

Pendelzeit auf dem Display anzeigen

if (status_Fahrt == 0) {
      long countdown_minute = (((pendelZeit - jetztMillis + pendelMillis) / 1000) / 60)%60;
      long countdown_sec = ((pendelZeit - jetztMillis + pendelMillis) / 1000) % 60;
            
      char minuten[3];
      char sekunden[2];
      sprintf(minuten, "%d:", countdown_minute);
      sprintf(sekunden, "%02d", countdown_sec);
     
      ssd1306_printFixed2x(42, 16, minuten, STYLE_NORMAL);
      ssd1306_printFixed2x(65, 16, sekunden, STYLE_NORMAL);
            
    }

Wenn der Status der Fahrt = 0 ist (nichts bewegt sich) wird die Pendelzeit bis zur nächsten Fahrt im OLED Display angezeigt. Dazu muss die noch zu vergehende Pendelzeit in Millisekunden in Minuten und Sekunden umgerechnet werden (Zeilen 2 und 3). Da das Display die Zahlen nicht darstellen kann, erfolgt eine Umwandlung und Formatierung der Zahlen bevor sie dann im Klartext ausgegeben werden können.

Fehlerabfrage

//Fehlerabfrage / Fehlercode

    // Beide Lichschranken an
    if (digitalRead(lichtschrankeLinks) == LOW && digitalRead(lichtschrankeRechts) 
    == LOW)
    {
      status_Lichtschranken = LOW;
    }
    else {
      status_Lichtschranken = HIGH;
    }
    if ( status_Lichtschranken != status_LichtschrankenAlt ){
      status_LichtschrankenAlt = status_Lichtschranken;
      lichtschrankenMillis = millis();
    }        
    if ( millis() - lichtschrankenMillis >= 1000 && status_Lichtschranken == LOW ){
      status_Fehler = 2;
    }
    
    // Laufzeitüberwachung
    if (jetztMillis - laufzeitMillis >= laufZeit && status_Fahrt == 1){
      status_Fehler = 1;
    }

Der erste Fehler, welcher abgefragt wird, ist der Zustand, dass beide Lichtschranken geschaltet haben. Dies kann und darf nicht sein. Demzufolge ist das ein unsicherer Zustand und führt zur Abschaltung. Da die Lichtschranken bei Lichtänderungen immer mal wieder prellen (kurz zu und abschalten, siehe auch Beitrag: LINK> https://moba-module.de/tcrt5000-infrarot-ir-sensor-modul-am-arduino-nano-test/) wird dieser unerlaubte Zustand über eine Zeit abgefragt. Liegt der Fehler also länger als eine Sekunde an, dann wird es tatsächlich als Fehler wahrgenommen und im status_Fehler verarbeitet (Zeile 16 bis 18).

Der zweite Fehler ist die Laufzeitüberwachung. Ist also die abgelaufene Zeit während einer Fahrt (status_Fahrt == 1) größer gleich der Laufzeit (Variable laufZeit ist 15 Sekunden), dann wird dies im status_Fehler verarbeitet und führt zur Abschaltung (Zeile 21 bis 23).

Tag – Nacht Wechsel

//Tag Nacht Wechsel -------------------------------
      if (jetztMillis - vergangeneMillis >= wechselTagNacht) {
          vergangeneMillis = jetztMillis;
      
          if (lichtAusgang == LOW) {
            lichtAusgang = HIGH;      
          } else {
            lichtAusgang = LOW;      
          }
          digitalWrite(lichtTagNacht, lichtAusgang);
        
        if (lichtAusgang == LOW) {
          ssd1306_printFixed(34, 0, " Tagmodus ", STYLE_NORMAL);
        } else {
          ssd1306_printFixed(34, 0, "Nachtmodus", STYLE_NORMAL);
        }
      }

Auch hier wird die vergangene Zeit mit der in der Variablen wechselTagNacht verglichen. Ist die abgelaufene Zeit wieder größer oder gleich wird die Zeit zurückgesetzt und in den darauf folgenden Programmzeile entsprechend umgeschaltet. Zusätzlich erfolgt die Ausgabe, welcher Modus gerade geschaltet ist, auf dem Display.

Abschaltung nach Fehler

switch (status_Fehler) {
  case 1:
    digitalWrite(fahrtFahrt, HIGH);
    digitalWrite(fahrtLinks, HIGH);
    digitalWrite(fahrtRechts, HIGH);
    ssd1306_clearScreen();
    ssd1306_drawHLine  (10 , 2, 118);
    ssd1306_drawHLine  (10 , 30, 118);
    ssd1306_printFixed(40, 8, "FEHLER !", STYLE_NORMAL);
    ssd1306_printFixed(7, 16, "Laufzeitüberwachung", STYLE_NORMAL);
    while(1);
    break;
  case 2: //Beide Lichtschranken betätigt - Kritischer Fehler - Anzeigen und alles Abschalten
    digitalWrite(fahrtFahrt, HIGH);
    digitalWrite(fahrtLinks, HIGH);
    digitalWrite(fahrtRechts, HIGH);
    ssd1306_clearScreen();
    ssd1306_drawHLine  (10 , 2, 118);
    ssd1306_drawHLine  (10 , 30, 118);
    ssd1306_printFixed(40, 8, "FEHLER !", STYLE_NORMAL);
    ssd1306_printFixed(4, 16, "Beide Lichtschranken", STYLE_NORMAL);
    while(1);
    break;
 }

Die beiden Fehler werden hier nach ihrem Status abgefangen und entsprechend über die switch Anweisung abgearbeitet. In beiden Fällen werden die Relais unverzögert abgeschaltet, alle in Nullstellung gebracht. Danach wird der entsprechende Fehler im Klartext auf dem Display angezeigt und das Programm mit while(1) in eine Endlosschleife geschickt, welche nur durch ein Hardware-Reset wieder aufgehoben werden kann.

Download der Dateien

Für alle, die diesen Sketch als Inspiration oder Grundlage für eigene Projekte nehmen möchten bieten wir hier den Download der Dateien an.

Zu guter Letzt

Das Sketch funktioniert beim Spur N Mini Diorama ohne Probleme. Inzwischenzeit wurde es noch weiter entwickelt und verschiedene zusätzliche Funktionen eingebaut. Für die ganz Genauen unter euch: Ja – man kann diesen Sketch schöner und eleganter programmieren. Wer möchte kann gern zu uns Kontakt aufnehmen und wir würden nach Prüfung durch uns diesen Beitrag hier veröffentlichen.

Im nächsten Teil werden wir dann den Baufortschritt, Bilder und Fahrvideos zeigen …

LINK> https://moba-module.de/modelleisenbahn-projekt-mini-diorama-spur-n-teil-7-gleis-landschaft-erste-fahrt/