Anleitung Ein Endlicher Automat entsteht

Idee: Im Forum taucht immer wieder der Rat auf: „Das kann man mit einem Endlichen Automaten lösen.“ Hier ein Beispiel, wie einer entsteht.

Zielgruppe: Anfänger, die noch nie einen Endlichen Automaten programmiert haben, oder jene, die es getan haben, aber nicht wussten.

Motivation: Mit einem Endlichen Automaten kann ich Sketche realisieren, an die ich mich vorher nicht getraut hätte.

Aufgabenstellung: Ein Kohlekran auf einer Eisenbahnanlage soll Kohle von einem Kohlehaufen in einen Eisenbahnwagen verladen.

Material: Neben einem Arduino, ich verwende einen UNO, drei LEDs mit je einem Vorwiderstand und ein Schrittmotor, wobei die Ansteuerung des Schrittmotors nebensächlich ist.

Quellen: Viele nette Menschen, die ihr Wissen im Internet zur Verfügung stellen so wie in diesem Forum.
Tabelle für die Schrittmotoransteuerung (Link leider nicht mehr aktuell)

1. Schritt: Konzept der Ablaufsteuerung
Vor meinem geistigen Auge befindet sich ein Kohlehaufen und ein zu beladener Wagen. Die Anfangsposition sei beim Kohlehaufen, die Schaufel geöffnet.

  • Kran dreht zum Taster für den Nullpunkt (links von Kohlehaufen)
  • Kran dreht auf Startposition beim Kohlehaufen
  • am Kohlehaufen, Schaufel runter
  • am Kohlehaufen, Schaufel schließen
  • am Kohlehaufen, Schaufel rauf
  • Kran dreht zum Wagen
  • am Wagen, Schaufel runter
  • am Wagen, Schaufel öffnen
  • am Wagen, Schaufel rauf
  • Kran dreht zum Kohlehaufen

Die Punkte 1 und 2 werden nur am Anfang durchlaufen, die Punkte 3 bis 10 wiederholt.

2. Schritt: Konzept testen
Um zu sehen, ob die Logik stimmt, nutzen wir den seriellen Monitor der IDE.

enum ZUSTAENDE {SucheNull, Anfangsposition, KSrunter, KSschliessen, KSrauf, WKdrehen, WSrunter, WSoeffnen, WSrauf, KKdrehen};
byte zustand = SucheNull;

void setup() {
  Serial.begin(9600);
  Serial.println("Programmstart");
}

void loop() {
  switch (zustand) {
    case SucheNull:
      if (MilliVerzoegerung(1000)) {
        Serial.println("1. Kran dreht zum Taster fuer den Nullpunkt (links von Kohlehaufen)");
        zustand = Anfangsposition;
      }
      break;
    case Anfangsposition:
      if (MilliVerzoegerung(1000)) {
        Serial.println("2. Kran dreht auf Startposition beim Kohlehaufen");
        zustand = KSrunter;
      }
      break;
    case KSrunter:
      if (MilliVerzoegerung(2000)) {
        Serial.println("3. am Kohlehaufen, Schaufel runter");
        zustand = KSschliessen;
      }
      break;
    case KSschliessen:
      if (MilliVerzoegerung(2000)) {
        Serial.println("4. am Kohlehaufen, Schaufel schliessen");
        zustand = KSrauf;
      }
      break;
    case KSrauf:
      if (MilliVerzoegerung(2000)) {
        Serial.println("5. am Kohlehaufen, Schaufel rauf");
        zustand = WKdrehen;
      }
      break;
    case WKdrehen:
      if (MilliVerzoegerung(2000)) {
        Serial.println("6. Kran dreht zum Wagen");
        zustand = WSrunter;
      }
      break;
    case WSrunter:
      if (MilliVerzoegerung(2000)) {
        Serial.println("7. am Wagen, Schaufel runter");
        zustand = WSoeffnen;
      }
      break;
    case WSoeffnen:
      if (MilliVerzoegerung(2000)) {
        Serial.println("8. am Wagen, Schaufel oeffnen");
        zustand = WSrauf;
      }
      break;
    case WSrauf:
      if (MilliVerzoegerung(2000)) {
        Serial.println("9. am Wagen, Schaufel rauf");
        zustand = KKdrehen;
      }
      break;
    case KKdrehen:
      if (MilliVerzoegerung(2000)) {
        Serial.println("10. Kran dreht zum Kohlehaufen");
        zustand = KSrunter;
      }
      break;
  }
}

bool MilliVerzoegerung(unsigned long millisIntervall) {  // Während die Zeit läuft, darf diese Funktion wegen der static-Variablen nicht ein zweites Mal aufgerufen werden!
  static unsigned long intervall = 0;
  static unsigned long prevMillis;
  unsigned long aktMillis = millis();
  if (intervall == 0) {
    intervall = millisIntervall;
    prevMillis = aktMillis;
  }
  if (aktMillis - prevMillis >=  intervall) {
    intervall = 0;
    return true;
  }
  return false;
}

Die Funktion MilliVerzoegerung() wartet die angegebenen Millisekunden, bis sie den Zustand true zurückgibt, wodurch die Befehle innerhalb der Bedingung ausgeführt werden. Anders als delay() ist diese Funktion nicht blockierend, was den wesentlichen Unterschied ausmacht. Allerdings darf diese Funktion wegen der static-Variablen nicht ein zweites Mal aufgerufen werden, während eine Zeit läuft!

Damit ist der Endliche Automat fertig.

1 Like

3. Schritt: Funktionalität ergänzen
Was folgt ist die Definition der Ein- und Ausgänge, die Ansteuerung des Kranmotors und der Schaufel. Die Nutzung des seriellen Monitors muss leider entfallen, da sie zu langsam ist!

Für die Kranbewegung nutze ich das „Schrittmotor-Set S-SPSM-5V“ mit Getriebe. Um die Massenträgheit des Krans zu simulieren, wird die Bewegung beschleunigt und verzögert. Ein Servo bewegt die Schaufel auf (170 Grad) und ab (10 Grad).

Derzeitiger Zwischenstand des Sketches:

#include <Servo.h>
Servo servoblau;       // Erzeugt ein Objekt
const byte pinFreigabe = 2;  // LOW=dreht; HIGH=stopp
const byte pinNullTaster = 3;  // HIGH-LOW-Flanke=Start; LOW-HIGH-Flanke =Stopp
const byte pinMotor[] = {4, 5, 6, 7};  // IN1, IN2, IN3, IN4 des Boards S-SPSM-5V von Pollin
const byte pinSchaufelaufzu = 8;
const byte pinServoSchaufel = 9;
const unsigned int SchrittZeitNullTaster = 7777; // Verzoegerungszeit im Millisekunden für Geschwindigkeit
const unsigned int SchrittZeitKranDrehen = 5555; // Verzoegerungszeit im Millisekunden für Geschwindigkeit
const unsigned int AnfangspositionKran = 480; // Schritte (4096 entspricht 360 Grad)
const unsigned int WagenpositionKran = 1024; // Schritte (4096 entspricht 360 Grad)
const unsigned int BeschlSchritte = 100;
const unsigned int BeschlIntervall = 50;
const unsigned int VerzSchritte = 100;
const unsigned int VerzIntervall = 50;
enum ZUSTAENDE {InitSchaufelRauf, SucheNull, Anfangsposition, KSrunter, KSschliessen, KSrauf, KdrehenW, WSrunter, WSoeffnen, WSrauf, KdrehenK};
byte zustand = InitSchaufelRauf;
const int verzZeitServoSchaufel = 40;
const byte posServoSchaufelOben = 165;
const byte posServoSchaufelUntenKohle = 10;
const byte posServoSchaufelUntenWagen = 30;
enum ZUSTAENDESERVO {Initialisierung, Bewegung};
byte zustandServo = Initialisierung;

void setup() {
  servoblau.attach(pinServoSchaufel);              // gibt den Pin an wo das Servo 1 angeschlossen ist
  for (byte i = 0; i < sizeof(pinMotor); i++) {
    pinMode(pinMotor[i], OUTPUT);
  }
  pinMode(pinSchaufelaufzu, OUTPUT);
  pinMode(pinServoSchaufel, OUTPUT);
  pinMode(pinFreigabe, INPUT_PULLUP);
  pinMode(pinNullTaster, INPUT_PULLUP);
}

void loop() {
  if (digitalRead(pinFreigabe) == LOW) {
    switch (zustand) {         // hier beginnt der endliche Automat
      case InitSchaufelRauf:
        if (servoSchaufel(posServoSchaufelOben)) { // Schaufel rauf fahren
          zustand = SucheNull;
        }
        break;
      case SucheNull:
        if (digitalRead(pinNullTaster) == HIGH) {
          schrittMotor(-4096, SchrittZeitNullTaster, 0, 0, 0, 100);  // 4096 entspricht 360 Grad Drehung
        } else {
          schrittMotor(0, SchrittZeitNullTaster, 0, 0, 0, 100);
          zustand = Anfangsposition;
        }
        break;
      case Anfangsposition:
        if (schrittMotor(AnfangspositionKran, SchrittZeitNullTaster, 0, 0, 0, 100)) {
          zustand = KSrunter;
        }
        break;
      case KSrunter:
        if (servoSchaufel(posServoSchaufelUntenKohle)) { // Schaufel runter fahren
          zustand = KSschliessen;
        }
        break;
      case KSschliessen:
        digitalWrite(pinSchaufelaufzu, HIGH);   // Schaufel schließen
        if (MilliVerzoegerung(2000)) {
          zustand = KSrauf;
        }
        break;
      case KSrauf:
        if (servoSchaufel(posServoSchaufelOben)) { // Schaufel rauf fahren
          zustand = KdrehenW;
        }
        break;
      case KdrehenW:
        if (schrittMotor(WagenpositionKran, SchrittZeitKranDrehen, BeschlSchritte, BeschlIntervall, VerzSchritte, VerzIntervall)) {
          zustand = WSrunter;
        }
        break;
      case WSrunter:
        if (servoSchaufel(posServoSchaufelUntenWagen)) { // Schaufel runter fahren
          zustand = WSoeffnen;
        }
        break;
      case WSoeffnen:
        digitalWrite(pinSchaufelaufzu, LOW);   // Schaufel oeffnen
        if (MilliVerzoegerung(2000)) {
          zustand = WSrauf;
        }
        break;
      case WSrauf:
        if (servoSchaufel(posServoSchaufelOben)) { // Schaufel rauf fahren
          zustand = KdrehenK;
        }
        break;
      case KdrehenK:
        if (schrittMotor(-WagenpositionKran, SchrittZeitKranDrehen, BeschlSchritte, BeschlIntervall, VerzSchritte, VerzIntervall)) {
          zustand = KSrunter;
        }
        break;
    }
  } else {
    off();
  }
}

void off() {
  for (byte i = 0; i < sizeof(pinMotor); i++) {
    digitalWrite(pinMotor[i], LOW);
  }
  digitalWrite(pinSchaufelaufzu, LOW);
  zustand = InitSchaufelRauf;
}

bool MicroVerzoegerung(unsigned long microsIntervall) {  // Während die Zeit läuft, darf diese Funktion wegen der static-Variablen nicht ein zweites Mal aufgerufen werden!
  static unsigned long intervall = 0;
  static unsigned long prevMicros;
  unsigned long aktMicros = micros();
  if (intervall == 0) {
    intervall = microsIntervall;
    prevMicros = aktMicros;
  }
  if (aktMicros - prevMicros >=  intervall) {
    intervall = 0;
    return true;
  }
  return false;
}

bool MilliVerzoegerung(unsigned long millisIntervall) {  // Während die Zeit läuft, darf diese Funktion wegen der static-Variablen nicht ein zweites Mal aufgerufen werden!
  static unsigned long intervall = 0;
  static unsigned long prevMillis;
  unsigned long aktMillis = millis();
  if (intervall == 0) {
    intervall = millisIntervall;
    prevMillis = aktMillis;
  }
  if (aktMillis - prevMillis >=  intervall) {
    intervall = 0;
    return true;
  }
  return false;
}

bool servoSchaufel(byte sollPosition) {
  static byte istPosition;
  int zeit = verzZeitServoSchaufel;
  switch (zustandServo) {
    case Initialisierung:
      istPosition = servoblau.read();
      zustandServo = Bewegung;
      break;
    case Bewegung:
      if (sollPosition == istPosition) {
        zustandServo = Initialisierung;
        return true;
      } else {
        if (MilliVerzoegerung(zeit)) {
          if (sollPosition > istPosition) {
            istPosition++;
          } else {
            istPosition--;
          }
          servoblau.write(istPosition);
        }
      }
      break;
  }
  return false;
}

bool schrittMotor(int sollSchritte, unsigned int sollIntervall, unsigned int beschleunigungSchritte, unsigned int beschleunigungIntervall, unsigned int verzoegerungSchritte, unsigned int verzoegerungIntervall) {
  // const byte schrittfolge[] = {B1000, B0100, B0010, B0001};  // Vollschritt eine Phase
  // const byte schrittfolge[] = {B1001, B1010, B0110, B0101};  // Vollschritt zwei Phasen
  const byte schrittfolge[] = {B1000, B1100, B0100, B0110, B0010, B0011, B0001, B1001};  // Halbschritt
  static unsigned int schrittZaehler;
  static unsigned long zeit;
  static byte schritt = 0;
  byte bitfolge = 0;
  if (schrittZaehler == 0 || sollSchritte == 0) {
    schrittZaehler = abs(sollSchritte);  // 360° = 64 * 64 = 4096
    zeit = sollIntervall + beschleunigungIntervall * beschleunigungSchritte;
  }
  if (MicroVerzoegerung(zeit)) {
    bitfolge = schrittfolge[schritt];
    for (byte j = 0; j < sizeof(pinMotor); j++) {
      digitalWrite(pinMotor[j], (bitfolge >> j) & 1);
    }
    if (abs(sollSchritte) - schrittZaehler < beschleunigungSchritte && beschleunigungSchritte > 0) {
      zeit -= beschleunigungIntervall;
    }
    if (schrittZaehler < verzoegerungSchritte) {
      zeit += verzoegerungIntervall;
    }
    if (schrittZaehler > 0) {
      schrittZaehler--;
    }
    if (sollSchritte > 0) {
      schritt--;
    }
    if (sollSchritte < 0) {
      schritt++;
    }
    if (schritt < 0) {
      schritt = sizeof(schrittfolge);
    } else {
      schritt = schritt % sizeof(schrittfolge);
    }
  }
  if (schrittZaehler == 0) {
    for (byte i = 0; i < sizeof(pinMotor); i++) {
      digitalWrite(pinMotor[i], LOW);
    }
    return true;
  }
  return false;
}

Ähnliche Themen:

Anleitung Endlicher Automat mit millis()

BlinkwithoutDelay - Die Nachtwächtererklärung

millis() Überlauf umgehen einschließlich Definition einer Klasse

Zustandsautomaten für Fortgeschrittene

Fußgängerampel Erweiterung mit den Vorschlägen von jurs unter Verwendung der FSM-Library SM.h!

Ein Automat ohne delay()

Zur Anschauung ein Bild des Kohlekranfunktionsmodells (Januar 2016):

Da sieht man auch schön wie unübersichtlich die switch/case Variante ist :slight_smile:

Für kleine Dinge ok, aber es wird irgendwann problematisch, da der ganze Code an einer Stelle steht.

Jeder Sketch ist zu was nütze, und sei es als abschreckendes Beispiel :wink:

Ich glaube nicht, daß switch-case das Problem ist. Das Problem ist, daß er den Code für die Zustände in die Case Zweige packt statt sie in Funktionen auszulagern.

Den Switch Case durch den Aufruf von Funktionspointern zu ersetzen macht den Code nur vordergründig übersichtlicher. Wenn man das tut, dann sieht man nicht mehr welche Funktionen gerufen werden sondern nur noch wie die Signaturen aussehen. Ob das soviel besser ist wage ich zu bezweifeln. Andererseits ist es damit (also CPS) sehr viel leichter die Zustände zu erweitern. Es hängt also wie immer vom Kontext ab was besser ist.

Schlimmer finde ich die Verzögerungsfunktionen. Die sind Singletons. Das kracht gewaltig wenn man mehr als einen Automaten abarbeiten will. Sowas muß man als Instanzen anlegen und korrekt verwalten.

Ach Leute, nun lasst mal die Kirche im Dorf.

agmue ist angetreten um zu erklären, wie man mittels Zustandsautomat ein System programmieren kann.
Und ich finde, das ist ihm sehr gut gelungen. Respekt!

Dass man im Code (wie in jedem Code) Sachen besser oder anders machen kann ist klar.
Aber das ist kein C-Tutorial, sondern eben nur eine Beschreibung, was ein Zustandsautomat ist und wie man ihn nutzen kann.

ich finde, solche Beiträge sollten ins Tutorial wandern.

agmue ist angetreten um zu erklären, wie man mittels Zustandsautomat ein System programmieren kann.
Und ich finde, das ist ihm sehr gut gelungen. Respekt!

Yes!
Und, ich glaube, er möchte auch Rückkopplung... :wink:

Dieses "endliche Automaten" Dingen ist ein Thema, über das wir hier im Forum noch tausende Male sprechen. Es wird in tausenden Varianten in Software gegossen werden.

Die sind Singletons.

Singletons sind böse.
Globale Variablen sind böse.
Überhaupt, alles was globale Räume verstopft und Seiteneffekte produzieren kann, ist böse. Und doch wird man sich mit bösen Dingen arrangieren müssen. Unsere begrenzen Systeme lassen einfach keine weit gehende Abstraktion und Dynamik zu. Jedes mal, wenn man sowas einsetzt, darf man denken: "Das könnte mich mal zum stolpern bringen."

Also reden!
Immer wieder, über Modelle und ihre Vor- und Nachteile.
Optimieren kann man nur in eine Richtung.
Es wird immer wieder auf Kompromisse hinaus laufen.

Ich freue mich immer über freundliche Reaktionen wie diese :slight_smile:

Danke für den Hinweis! Den habe ich jetzt sowohl im Kommentar als auch im Text ergänzt. Das ist wichtig zu berücksichtigen!

Instanzen gehören wohl zu OOP und wären daher für die Zielgruppe m. E. nicht geeignet. Aber wie ich schon Serenifly geantwortet habe: Wenn man mit den bekannten Möglichkeiten an Grenzen stößt, ist dies eine prima Motivation, sich neues Wissen anzueignen, um die Grenzen zu überwinden. Für mich habe ich mir das auf jeden Fall schon mal vorgemerkt!

guntherb:
agmue ist angetreten um zu erklären, wie man mittels Zustandsautomat ein System programmieren kann.

Und das für Änfänger! Danke, da fängt der Tag gut an :slight_smile: :slight_smile: :slight_smile:

Besonders Stolz, auch wenn es mit dem Thema nichts zu tun hat, bin ich auf die Beschleunigungs- und verzögerungsrampe. Der Kran scheint zu ächzen, um von der Haft- in die Gleitreibung zu kommen, obwohl der Motor - ich habe es versehendlich probiert - die Kranmasse locker herumwirbeln kann. Der Kran ist aber nur ein Funktionsmodell, um die Vor- und Nachteile von Servo, Schritt- und DC-Motor mit dem Modellbahner, der das dann auf seiner Anlage realisiert, zu besprechen.

combie:
Yes!
Und, ich glaube, er möchte auch Rückkopplung... :wink:

Stimmt! Denn so habe ich durch Dich den Endlichen Automaten für mich entdeckt, Danke!

Und Serenifly hatte bei einem anderen Thema vorgeschlagen, die Zeitverzögerung in eine Funktion zu verlagern, was ich nun erstmals realisiert habe. Daß er mich zu bösem Code verleitet hat, wurde mir erst beim Schreiben bewußt, also bin ich jetzt ein :smiling_imp: Macht auch mal Spaß!

[quote author=Udo Klein link=msg=2313079 date=1436685973]
Wenn man das tut, dann sieht man nicht mehr welche Funktionen gerufen werden sondern nur noch wie die Signaturen aussehen[/quote]
Stimmt. Aber wirklichen Überblick über alle Zustandsübergänge hat man sowieso nur durch ein Diagramm. Rein aus dem Code den Ablauf schnell zu verstehen geht mit keiner Variante gut.

Und Serenifly hatte bei einem anderen Thema vorgeschlagen, die Zeitverzögerung in eine Funktion zu verlagern, was ich nun erstmals realisiert habe

Ich habe hier bisher auch niemanden gesehen der mehrere Automaten gleichzeitig wollte. Jurs hatte es mal bei der Ampelschaltung kurz vorgeschlagen, falls man mehrere Ampeln möchte. Aber wenn man mal soweit ist, dann kann man sich auch mal damit auseinandersetzen eine kleine Klasse zu programmieren. Es reicht ja die eine Funktion in eine Klasse zu verpacken. Da gibt es auch noch ein anderes Stichwort: function object

Entweder in ein Objekt oder in eine Klasse. Beides geht. Objekte haben den Vorteil weniger Code zu erzeugen und damit Flash zu sparen. (Template) Klassen haben den Vorteil, daß man sie statisch mehrfach instanziieren kann. Das kostet zwar mehr Flash spart dann aber SRAM. Je nachdem was knapper ist, ist der eine oder der andere Weg besser.

Und dann wären da noch Koroutinen: Goto Considered Helpful | Blinkenlight. Man beachte die Verwendung von Goto Statements um ein Yield Makro zu bekommen :slight_smile:

@Serenify: das ist auch ein Beispiel bei dem jemand mehr als einen Automaten braucht.

  • Automat für den Parser
  • Automat für die Zeitschaltuhr
  • Automaten für Dekodergruppen / den Demodulator
  • Automat für die lokale Uhr
  • Automat für die Autosync Option

Die nächste Version wird noch mehr Automaten brauchen.

Sobald man anfängt mit Objekten zu arbeiten die mindestens ein Flag beinhalten, dann stellt sich sowieso die Frage wo die Grenze zum Automaten ist. D.h. ich habe bisher fast nie Software gesehen die nur einen Automaten beinhaltet.

Deine Software bewegt sich auch auf einem ganz anderen Level als das was die Leute machen die hier mit Fragen kommen. Da gibt es sehr vieles was sich mit einem einzelnen Automaten lösen lässt. Meistens geht es darum einen einzelnen Motor steuern oder ein paar LEDs mit einem Taster in einer bestimmten Reihenfolge leuchten lassen. Das sind eigentlich ganz einfache Sachen.

Bei derartigen Projekten bräuchte man dann mehrere Automaten wenn man mehrere unabhängige Motoren hat oder mehrere Ampeln mit unterschiedlichen Leuchtzeiten.

Dies sind auch meistens absolute Anfänger, die das erste mal von millis() hören. Da muss man nicht noch gleich Klassen/Objekte dazupacken wenn es nicht wirklich nötig ist. Das kann man ja problemlos erweitern. Wenn man mal eine nicht-blockierende Delay Funktion verstanden hat ist es auch kein so großer Schritt mehr mehrere Instanzen davon zu haben.

Serenifly:
Dies sind auch meistens absolute Anfänger, die das erste mal von millis() hören. Da muss man nicht noch gleich Klassen/Objekte dazupacken wenn es nicht wirklich nötig ist. Das kann man ja problemlos erweitern. Wenn man mal eine nicht-blockierende Delay Funktion verstanden hat ist es auch kein so großer Schritt mehr mehrere Instanzen davon zu haben.

Hooo...

Naja..
Ich sehe das etwas anders.
Man kann Menschen nicht unterstützen, beschützen, helfen, in dem man ihnen Wissen vor enthält.
Jeder Mensch lernt in seiner eigenen Geschwindigkeit. Zuviel Wissen wird automatisch ausgeblendet. Ein Aspekt der selektiven Wahrnehmung. Jeder von uns tut das so.

Wissen zurückhalten...
Sollte man mit Kindern ja auch nicht tun.
Sicherlich können sie nicht alles sofort und beim ersten mal vollständig verarbeiten. Aber davon gehört zu haben, bereitet den Boden für die nächste Saat.

Manchmal ist es unglaublich schwieriger falsch gelerntes wieder zu korrigieren, als gleich das richtige zu lernen. Denn das falsche übt sich fest.

Das Gefühl "Da ist noch was!" erzeugt Interesse.
Das Gefühl "Der hält Wissen zurück!" irritiert.
Man ist dann fix bei Macht und Machtlosigkeit.

combie:
Manchmal ist es unglaublich schwieriger falsch gelerntes wieder zu korrigieren, als gleich das richtige zu lernen. Denn das falsche übt sich fest.

Das ist aber keine Sache von falsch oder richtig. Es funktioniert beides. Eine einzelne Funktion halt mit Einschränkungen.

Und ich habe hier schon leider gesehen dass manche Leute damit überfordert sind eine ganz einfache Klasse mit einer Variablen und einre einzelnen Methode zu haben. Und nicht nur absolute Anfängern, sondern auch Leute die schon etwas weiter sind und mit einfacher prozedurale Programmierung klar kommen. Andere tun OOP als ungeeignet für den Arduino ab, anscheinend nicht wissend wie viel OOP in der Arduino Software steckt.
Wenn jemand programmieren kann, ja dann kann man etwas weiter ausholen. Aber einem Anfänger würde ich nicht zu viel auf einmal vor die Füße werfen. Die haben oft schon Probleme den einfachen Code zu verstehen.

Kommt halt auf den Zweck und die Zielgruppe an. In einem Tutorial kann man das mit Ausbau-Stufen erledigen. Man fängt erst mal mit der einfachsten Variante an und weist auf die Einschränkungen hin. In einem weiteren Teil kann man das dann ausbauen und verbessern.
Aber wenn jemand mit einem konkreten Problem kommt reicht auch erst mal nur die einfachere Version.

[quote author=Udo Klein link=msg=2313079 date=1436685973]Schlimmer finde ich die Verzögerungsfunktionen. Die sind Singletons. Das kracht gewaltig wenn man mehr als einen Automaten abarbeiten will. Sowas muß man als Instanzen anlegen und korrekt verwalten.
[/quote]
Gibt es denn irgendwo ein Beispiel, wie man das in OOP löst?
Bei mit ist das schon zu lange her, und ich habe keine Lust, mich komplett in OOP einzuarbeiten, nur eine "delay-Ersatzfunktion" in OOP schreiben zu können.

Nagut...
Obs dir hilft....

Hier mal der Code für einen Dimmer.
Inputs sind hier die analogen Pins, welche digital ausgewertet werden.
Taster über Pullups angeschlossen, also negative Logik

class Handler
{
  protected:
  static Handler* first;
  Handler* next = NULL;
  Handler()
  {
    if(first) next = first;
    first = this;
  }
  
  virtual void update() = 0;
  
  public:
  static void handle()
  {
      Handler* temp = first;
      while(temp)
      {
        temp->update();
        temp = temp->next;
      }
  }
};

Handler* Handler::first = NULL;

class Dim2Taster : public Handler
{
  protected:
  const byte upPin;
  const byte downPin;
  const byte pwmPin;
  int pwm;
  int dimDelay;
  unsigned long lastHit;
  void update()
  {
     if(millis() - lastHit > dimDelay)
     {
       int newPwm = pwm;
       if(!digitalRead(upPin))
       {
         newPwm++;
       }else if(!digitalRead(downPin))
       {
         newPwm--;
       }
       newPwm = constrain(newPwm,0,255);
       if(pwm != newPwm)
       {
         analogWrite(pwmPin,pwm=newPwm);
         lastHit = millis();
         Serial.print(pwmPin);
         Serial.print(" ");
         Serial.println(pwm);
       }
    }
  }

  
  public:
  Dim2Taster(int upPin, int downPin, int pwmPin, int dimDelay = 20) :upPin(upPin), downPin(downPin), pwmPin(pwmPin), pwm(0), dimDelay(dimDelay)
  {
    pinMode(upPin,INPUT_PULLUP);
    pinMode(downPin,INPUT_PULLUP);
    analogWrite(pwmPin,pwm=0);
    lastHit = millis();
  }
};



Dim2Taster LED1(A0,A1, 9);
Dim2Taster LED2(A2,A3,10);
Dim2Taster LED3(A4,A5,11);


void setup() 
{
  Serial.begin(9600);
}

void loop() 
{
  Handler::handle();
}

Vielen Dank!

Aber ein komplett unkommentierter Code ist dann schon echt hart.

ich hatte mit etwas erhofft, wie agmues "millisverzögerung", nur eben als class, so dass vielfache Aufrufe nicht zur Kollision führen.

Ich werde heute abend mal versuchen, zu verstehen was da passiert.

Wenn nicht, mach ichs halt weiter manuell mit if (millis()- ts > 500 )...

Ich werde heute abend mal versuchen, zu verstehen was da passiert.

Ich denke, da besteht Hoffnung.

Und zu den Kommentaren, da weiß ich gar nicht, was ich da hinschreiben sollte, außer meiner Adresse, Namen und Email...

Im Grunde ist die Funktion recht schlicht.

Die Klasse Handler bildet eine einfach verkettete Liste.
Jedes neu erzeugte Objekt (ein Kind von Handler) reiht sich bei der Erzeugung in die Liste ein.
Die Liste ist Singleton, oder besser, statisch.

Es werden 3 LED Objekte erzeugt.

In der Loop reicht ein Aufruf der handle Methode, damit die Liste durchlaufen wird. Von jeder LED wird die update() Methode aufgerufen.

Und das was du eigentlich sehen wolltest, steckt in der update().
Jede LED führt ihren eigenen unabhängigen 20ms Timer mit sich.

Auf irgendwelche Destructoren habe ich keine Rücksicht genommen, da dynamische Speicherverwaltung auf Arduinos sowieso böse ist.

Es sind also 3 aller einfachste endliche Automaten, welche quasi unabhängig von einander arbeiten.
Nach entfernen des Serial Gedöns, ist die setup leer.
Und in der loop nur ein Aufruf.

Es ist ein Beispiel für Kapselung. (vielleicht nicht das beste/schönste, aber konsequent)
Das hinzufügen von gleichartigen LEDs ist ein Kinderspiel.

Lagert man die Klassen in eine Lib aus, dann ist das Beispiel plötzlich superschlank.

1 Like

Und zu den Kommentaren, da weiß ich gar nicht, was ich da hinschreiben sollte

z.B. :
Wofür die Klasse Handler überhaupt, und insbesondere der singleton Handler::first gut ist
warum der Konstruktor Handler() protected ist und nicht public,
warum neue Handler vorne eingehängt werden und nicht hinten.

und so Sachen eben ... :wink:

michael_x:
warum der Konstruktor Handler() protected ist und nicht public,

Damit man kein Handler Objekt erstellen kann?

warum neue Handler vorne eingehängt werden und nicht hinten.

Das hat mich auch erst mal verwirrt :slight_smile:

warum der Konstruktor Handler() protected ist und nicht public,

Das ist ja mal eine gute Frage...

Ist "automatisch" so passiert, ohne nachzudenken. (so weit ist es schon 8) )
Ich schränke grundsätzlich die Sichtbarkeit von Eigenschaften und Methoden möglichst weit ein.
Hier führt es dazu, dass von Handler keine Instanz erzeugt werden kann.
update() ist übrigens auch protected. Kein Aufruf von außen sinnvoll, oder gar nötig.

warum neue Handler vorne eingehängt werden und nicht hinten.

Ob vorn oder hinten....
So ist es etwas schlanker/einfacher.
(zumindest aus meiner Sicht)