Go Down

Topic: Anleitung: Endlicher Automat mit millis() (Read 17786 times) previous topic - next topic

agmue

Jun 07, 2015, 10:45 am Last Edit: Jun 07, 2015, 10:50 am by agmue
Idee: Im Forum taucht immer wieder der Rat auf: „Ersetze dalay() durch millis()." und „Das kann man mit einem Endlichen Automaten lösen." Hier ein Beispiel, das beides vereint.

Aufgabenstellung: Es sollte sich um eine allgemein bekannte Situation handeln, die mit einfachen Mitteln nachvollzogen werden kann. Meine Wahl fällt auf eine Ampelschaltung, weil sie jeder kennt. Asynchron dazu soll ein Blaulicht blitzen.

Material: Neben einem Arduino, ich verwende einen UNO, vier LEDs mit je einem Vorwiderstand.

Quellen: Viele nette Menschen, die ihr Wissen im Internet zur Verfügung stellen so wie in diesem Forum.

Sketch 1 mit delay()
Zunächst sehen wir uns den Sketch an, wie ihn jeder Anfänger schnell schreibt:
Code: [Select]
void setup() {
  // Definiert die Pins als Aus- oder Eingang
  pinMode(2, OUTPUT);
  pinMode(3, OUTPUT);
  pinMode(4, OUTPUT);
}

void loop() {
  // Ampelschaltung
  digitalWrite(2, HIGH);
  digitalWrite(3, LOW);
  digitalWrite(4, LOW);
  delay(3000);
  digitalWrite(2, HIGH);
  digitalWrite(3, HIGH);
  digitalWrite(4, LOW);
  delay(1000);
  digitalWrite(2, LOW);
  digitalWrite(3, LOW);
  digitalWrite(4, HIGH);
  delay(3000);
  digitalWrite(2, LOW);
  digitalWrite(3, HIGH);
  digitalWrite(4, LOW);
  delay(1000);
}

Sollte der Arduino nichts weiter machen, wären wir fertig. Die Funktion delay() unterbricht das Programm für eine definierte Zeit, während der keine anderen Aktionen möglich sind. Lange for- und while-Schleifen können genauso blockierend sein. Eine weitere zeitkritische Funktion wie ein Blaulicht oder eine Reaktion auf Taster ist auf diese Weise nicht zu realisieren. Dies geht nur, wenn loop() nicht blockiert wird.

Sketch 2 mit Konstanten
Ohne weiteren Kommentar kann man nur durch Verstehen des Codes erschließen, dass die rote LED an Pin 2 angeschlossen werden soll. Wollen wir die LED später mit einem anderen Anschluss verbinden, weil wir beispielsweise auf eine anderen Arduino umgestiegen sind, so müssten wir das ganze Programm durchsuchen, um die relevanten Stellen zu finden. Bei langen Programmen kann dies mühsam und fehlerträchtig sein. Daher werden Konstanten definiert, die mit ihrem Namen gleich auch ihre Bedeutung verraten.
Code: [Select]
// Ampel 1 Belegung der Ausgaenge
const byte RotPin = 2;
const byte GelbPin = 3;
const byte GruenPin = 4;
//
const boolean ein = HIGH;
const boolean aus = LOW;
const int ZEITROTPHASE = 3000;
const int ZEITGELBPHASE = 1000;

void setup() {
  // Definiert die Pins als Aus- oder Eingang
  pinMode(RotPin, OUTPUT);
  pinMode(GelbPin, OUTPUT);
  pinMode(GruenPin, OUTPUT);
}

void loop() {
  // Ampelschaltung
  digitalWrite(RotPin, HIGH);
  digitalWrite(GelbPin, LOW);
  digitalWrite(GruenPin, LOW);
  delay(ZEITROTPHASE);
  digitalWrite(RotPin, HIGH);
  digitalWrite(GelbPin, HIGH);
  digitalWrite(GruenPin, LOW);
  delay(ZEITGELBPHASE);
  digitalWrite(RotPin, LOW);
  digitalWrite(GelbPin, LOW);
  digitalWrite(GruenPin, HIGH);
  delay(ZEITROTPHASE);
  digitalWrite(RotPin, LOW);
  digitalWrite(GelbPin, HIGH);
  digitalWrite(GruenPin, LOW);
  delay(ZEITGELBPHASE);
}



Sketch 2 mit Konstanten
Ohne weiteren Kommentar kann man nur durch Verstehen des Codes erschließen, dass die rote LED an Pin 2 angeschlossen werden soll. Wollen wir die LED später mit einem anderen Anschluss verbinden, weil wir beispielsweise auf eine anderen Arduino umgestiegen sind, so müssten wir das ganze Programm durchsuchen, um die relevanten Stellen zu finden. Bei langen Programmen kann dies mühsam und fehlerträchtig sein. Daher werden Konstanten definiert, die mit ihrem Namen gleich auch ihre Bedeutung verraten.

Sketch 3 mit Endlichem Automaten und millis()
Stell Dir vor, wir verabreden uns zu einem Treffen in fünf Minuten. Was passiert?
1. Du schaust auf die Uhr, um die aktuelle Zeit zu ermitteln und merkst Dir diese als die Zeit, an der wir uns verabredet haben.
2. Du wartest, bis die aktuelle Zeit auf Deiner Uhr abzüglich der Zeit, an der wir uns verabredet haben, mit den fünf Minuten übereinstimmt. Wir treffen uns.

Zu 1.: Zeit_der_Verabredung = aktuelle_Zeit

Zu 2.: Wenn aktuelle_Zeit abzüglich Zeit_der_Verabredung gleich fünf Minuten dann "Hallo!"

Deine Uhr zeigt die Sekunden, Minuten und Stunden seit Mitternacht an. millis() zeigt die Millisekunden seit Reset an. Für die Verabredung "in fünf Minuten" ist das aber nicht relevant, wann die Zeitzählung startet. Deine Uhr könnte Winterzeit zeigen, meine Sommerzeit oder auch vollkommen falsch gehen, wir würden uns dennoch in fünf Minuten treffen, weil es sich um eine relative Zeitangabe handelt.

Das jetzt in millis (Intervall = fünf Minuten):

Zu 1.: Zeit_der_Verabredung = millis();

Zu 2.: if (millis() - Zeit_der_Verabredung == Intervall) {Serial.println("Hallo");}

Für den (schlechten) Fall, Dein Programm sollte länger als eine Millisekunde für loop() benötigen, schreibt man besser:

Zu 2.: if (millis() - Zeit_der_Verabredung >= Intervall) {Serial.println("Hallo");}

Die Ampelphasen ROT, ROTGELB, GRUEN, GELB sind die Zustände des Endlichen Automaten. Die Variable zustand durchläuft alle Phasen, wobei durch Bedingungen, hier die Zeit, die Zustandsänderungen ausgelöst werden. Mittels enum werden die Zustände beginnend bei 0 fortlaufend durchnummeriert.
Code: [Select]
// Ampel Belegung der Ausgaenge
const byte RotPin = 2;
const byte GelbPin = 3;
const byte GruenPin = 4;
//
const boolean ein = HIGH;
const boolean aus = LOW;
//
const int ZEITROTPHASE = 3000;
const int ZEITGELBPHASE = 1000;
unsigned long ampelMillis;
unsigned long ampelIntervall;
//
enum ZUSTAENDE {ROT, ROTGELB, GRUEN, GELB};
byte zustand = ROT;

void setup() {
  // Definiert die Pins als Ausgang
  pinMode(RotPin, OUTPUT);
  pinMode(GelbPin, OUTPUT);
  pinMode(GruenPin, OUTPUT);
}

void loop() {
  // Ampelschaltung
  if (millis() - ampelMillis >= ampelIntervall) {
    switch (zustand) {
      case ROT:
        digitalWrite(RotPin, HIGH);
        digitalWrite(GelbPin, LOW);
        digitalWrite(GruenPin, LOW);
        zustand = ROTGELB;
        ampelMillis = millis();
        ampelIntervall = ZEITROTPHASE;
        break;
      case ROTGELB:
        digitalWrite(RotPin, HIGH);
        digitalWrite(GelbPin, HIGH);
        digitalWrite(GruenPin, LOW);
        zustand = GRUEN;
        ampelMillis = millis();
        ampelIntervall = ZEITGELBPHASE;
        break;
      case GRUEN:
        digitalWrite(RotPin, LOW);
        digitalWrite(GelbPin, LOW);
        digitalWrite(GruenPin, HIGH);
        zustand = GELB;
        ampelMillis = millis();
        ampelIntervall = ZEITROTPHASE;
        break;
      case GELB:
        digitalWrite(RotPin, LOW);
        digitalWrite(GelbPin, HIGH);
        digitalWrite(GruenPin, LOW);
        zustand = ROT;
        ampelMillis = millis();
        ampelIntervall = ZEITGELBPHASE;
        break;
    }
  }
}

Die Vorstellungskraft ist wichtiger als Wissen, denn Wissen ist begrenzt. (Albert Einstein)

agmue

#1
Jun 07, 2015, 10:51 am Last Edit: Oct 20, 2015, 06:17 pm by agmue
Sketch 4 mit  Blaulicht
Letztlich wird noch der Code für das Blaulicht ergänzt, der eine andere Variante der Verwendung von millis() zeigt.
Code: [Select]
// Ampel Belegung der Ausgaenge
const byte RotPin = 2;
const byte GelbPin = 3;
const byte GruenPin = 4;
// Blaulicht Belegung der Ausgaenge
byte BlauPin = 5;
//
const boolean ein = HIGH;
const boolean aus = LOW;
//
const int ZEITROTPHASE = 3000;
const int ZEITGELBPHASE = 1000;
const int BLAUPHASE = 100;
unsigned long ampelMillis;
unsigned long ampelIntervall;
unsigned long blauMillis;
//
enum ZUSTAENDE {ROT, ROTGELB, GRUEN, GELB};
byte zustand = ROT;

void setup() {
  // Definiert die Pins als Ausgang
  pinMode(RotPin, OUTPUT);
  pinMode(GelbPin, OUTPUT);
  pinMode(GruenPin, OUTPUT);
  pinMode(BlauPin, OUTPUT);
}

void loop() {
  // Blaulicht
  if (millis() - blauMillis >= 750) {
    blauMillis = millis();
    digitalWrite(BlauPin, ein);
  }  else if (millis() - blauMillis >= 150) {
    digitalWrite(BlauPin, aus);
  }    else if (millis() - blauMillis >= 100) {
    digitalWrite(BlauPin, ein);
  }      else if (millis() - blauMillis >= 50) {
    digitalWrite(BlauPin, aus);
  }
  //
  // Ampelschaltung
  if (millis() - ampelMillis >= ampelIntervall) {
    switch (zustand) {
      case ROT:
        digitalWrite(RotPin, HIGH);
        digitalWrite(GelbPin, LOW);
        digitalWrite(GruenPin, LOW);
        zustand = ROTGELB;
        ampelMillis = millis();
        ampelIntervall = ZEITROTPHASE;
        break;
      case ROTGELB:
        digitalWrite(RotPin, HIGH);
        digitalWrite(GelbPin, HIGH);
        digitalWrite(GruenPin, LOW);
        zustand = GRUEN;
        ampelMillis = millis();
        ampelIntervall = ZEITGELBPHASE;
        break;
      case GRUEN:
        digitalWrite(RotPin, LOW);
        digitalWrite(GelbPin, LOW);
        digitalWrite(GruenPin, HIGH);
        zustand = GELB;
        ampelMillis = millis();
        ampelIntervall = ZEITROTPHASE;
        break;
      case GELB:
        digitalWrite(RotPin, LOW);
        digitalWrite(GelbPin, HIGH);
        digitalWrite(GruenPin, LOW);
        zustand = ROT;
        ampelMillis = millis();
        ampelIntervall = ZEITGELBPHASE;
        break;
    }
  }
}


Entsprechend der Aufgabenstellung sollen Ampel und Blaulicht unabhängig voneinander ausgeführt werden. Da loop() häufig durchlaufen wird, ist dies näherungsweise gewährleistet.

Eine anschauliche Erklärung, wie millis() verwendet werden.

Ich habe dies geschrieben aus Dank an die aktiven Forumsmitglieder und in der Hoffnung, es möge jemandem nutzen!  :)

Diejenigen, die meine Texte hier im Forum gründlich lesen und mich korrigieren, bitte ich, mit diesem Text ebenso zu verfahren.
Die Vorstellungskraft ist wichtiger als Wissen, denn Wissen ist begrenzt. (Albert Einstein)

Doc_Arduino

Hallo,

Deine Mühen sind sehr löblich. Aber Du mußt dir bewußt sein, dass der Thread irgendwann nach hinten rutscht und verschwindet. Ich hab schon ähnliches durch. Du kannst den für dich fertig machen und bei Gelegenheit rausholen. Oder Du schreibst irgendwann ein Buch.
Tschau
Doc Arduino '\0'

Messschieber auslesen: http://forum.arduino.cc/index.php?topic=273445
EA-DOGM Display - Demos: http://forum.arduino.cc/index.php?topic=378279

tinaki

hi
das ist wirklich ein ganz tolles tutorial - leider steige ich trotzdem noch nicht ganz durch, aber ich werde es die nächste tage immer wieder lesen und mal nachbauen, bis ich es eben raffe,
eine frage habe ich, vielleicht hat jemand eine lösung:

ich habe einen einfachen staubsugerroboter programmiert, der in bahnen den raum durchfährt, ausschert, dreht und wieder die nächste bahn abfährt. nun muss man sich das so vorstellen:
er fährt parallel zur wand entlang und checkt, ahh ich bin in der rechten raumecke, also schere ich nun diagonal rückwärts im 45 gard winkel zur raummitte hin aus, drehe dann und fahre zurück.
um das zu realisieren habe ich die Grundbewegungen im Void angelegt, also vorwärtsfahren(); Auscheren (), Turn() etc.

die zeit wie lange er was macht also zB Rückwärts fährt habe ich über delay gelöst - und -WUNDER- stoße jetzt auf diesen artikel weil es eine millis lösung braucht, denn wenn er rückwärts ausschert reicht es nicht zu den motoren zu sagen: mach mal 800 millsec delay DENN DANN kann er nicht mehr auf den button reagieren der an der Rückseite angebracht ist. also muss ich das über millis lösen.

kann ich das auch im void lösen und nicht im loop so dass bei der Funktion Rückwärts immer einen bestimmte  zeit gemeint ist? geht das? oder muss das dann immer in den loop gebaut werden.

ich werde sicher noch eine zeit brauchen bis ich das beispiel ganz verstehe und das in meinem code einfügen kann und bin für jeden tipp dankbar.

agmue

#4
Jun 18, 2015, 12:12 pm Last Edit: Jun 19, 2015, 09:39 pm by agmue
hi
das ist wirklich ein ganz tolles tutorial
Danke, das freut mich  :)
Wenn Du zu dieser Anleitung einen Verbesserungsvorschlag hast, dann werde ich den gerne berücksichtigen. Dann wäre hier die richtige Stelle.

Die Frage zum Staubsauger fände ich in einem eigenen Thema besser aufgehoben. Über einen spezifischen Thementitel wirst Du auch von den richtigen Leuten gelesen, ggf. auch von mir. Denn die generelle Frage lautet: Wie wird delay() in einem Unterprogramm einer Funktion ersetzt. Möglicherweise muß die Programmstruktur Richtung switch (zustand) verändert werden. Spannend  :)
Die Vorstellungskraft ist wichtiger als Wissen, denn Wissen ist begrenzt. (Albert Einstein)

tinaki

genau das war auch mein erster gedanke, ich hatte einen switch case gebastelt, der aber lief nur mit dem distance messer und nicht auch in verbindung mit den buttons- aber mann muss dazu sagen, ich bin totaler anfänger und dies ist mein erstes projekt. Genau das ist die frage: Wie wird delay() in einem Unterprogramm ersetzt ? Ein unterprogramm, welches man dann dann verschiedenen Bewegungen wie VORWÄRTS; TURN(); BACK;  etc zuordnen kann, mit veränderbaren Zeiten (dauer)
- ok also ich kann das ja neu posten ABER bevor ich jetzt was neues aufmache:
Gibt es vielleicht noch mehr Leute da draußen die das Lesen?
hat jemand eine idee?

Serenifly

Nehmen wir erst mal einen besseren Namen für ein Unterprogramm in der strukturierten Programmierung: Funktion

Funktionen können Rückgabewerte haben. Man kann eine Funktionen einen bool zurückgeben lassen. False wenn die Zeit noch nicht abgelaufen ist. True wenn sie abgelaufen ist. Dann ruft man die Funktion ständig auf und weiß in der aufrufenden Funktion ob die Zeit vorbei ist und etwas gemacht wurde.
Dann brauchst du noch etwas Logik damit der eigentliche Code nur einmal ausgeführt und sonst der "Delay" Teil.

Hier musst du auch lernen was "static" macht:
http://www.arduino.cc/en/Reference/Static
Lokale statische Variablen behalten ihren Wert von einem Funktionsaufruf zum nächsten.


Aber lies dich generell in das Thema endliche Automaten ein. Was du da machst ist eine Paradebeispiel dafür. Das kann man nämlich auch besser lösen und die Verzögerung in einen extra Zustand, d.h. eine eigene Funktion auslagern. Dann wechselt man Funktion1 -> Verzögerung -> Funktion2

tinaki

super! ich verstehe: ich mache die ganze zeit eine anfrage, darunter fällt auch die zeit und die buttons sind auch dabei.
static versteh ich noch nicht ganz  kommt viellicht noch.

aber was meinst du damit:
"Aber lies dich generell in das Thema endliche Automaten ein. Was du da machst ist eine Paradebeispiel dafür. Das kann man nämlich auch besser lösen und die Verzögerung in einen extra Zustand, d.h. eine eigene Funktion auslagern. Dann wechselt man Funktion1 -> Verzögerung -> Funktion2"

gibt es dafür ein beispiel? 1000 dank!!

Serenifly

gibt es dafür ein beispiel?
Schau mal an in welchem Thread du bist. Ist im ersten Post erklärt.

Das ist leicht anders als was du willst. Da wird jeder Zustand zeitgesteuert. Aber ist auch kein Problem nur einen Zustand WARTEN zu haben und nur in diesem den Zustandsübergang per Zeit zu machen. Aber das Grundgerüst ist das gleiche.

Endliche Automaten kann man auch auf andere Arten realisieren. Aber switch/case ist am einfachsten und reicht völlig aus wenn sich die Zustände in Grenzen halten

agmue

#9
Jun 20, 2015, 03:46 pm Last Edit: Jun 20, 2015, 03:57 pm by agmue
Beispiele von mir zum Thema Endlicher Automat mit millis() und Schalterzustand:
fade millis
LED zeiversetzt schalten
Arduino UNO, 2Schalter, 1LED
Die Vorstellungskraft ist wichtiger als Wissen, denn Wissen ist begrenzt. (Albert Einstein)

Jomelo

@agmue
Ich hab mich auch mal an so etwas versucht: http://forum.arduino.cc/index.php?topic=165552.0
In der neusten LCDMenuLib ist es auch enthalten.

Aber solange es die Delay Funktion in der Arduino Umgebung gibt wird dieses als erste Wahl bevorzugt.

Diesen Code in einer Lib zu verstecken wäre aber auch nicht fair  :smiley-mr-green:
Code: [Select]

#undef delay()
#define delay(x)
while(!success){try++;}

Kapitano

 An agmue

Herzlichen dank suchte schon eine kleine Ewigkeit genau das.
weil das delay ein biserl verteufelt ist.

mfg. Kapitano

agmue

@Jomelo: Gegenüber dem, was Du machst, spiele ich nur in der Kreisklasse  8)

@Kapitano: Danke, freue mich :)
Die Vorstellungskraft ist wichtiger als Wissen, denn Wissen ist begrenzt. (Albert Einstein)

RudiDL5

#13
Apr 27, 2016, 08:05 am Last Edit: Apr 27, 2016, 11:50 am by RudiDL5
@agmue:
Es ist echt prima, dass du hier so eine tolle Anleitung gegen dieses schon oft und zu Recht verwünschte "delay(x)" geschrieben hast.

Ich kann mich noch gut an meine Anfänge gegen Ende 2014 erinnern, als ich mich selbst auf ein Ampelprogramm für eine Modelleisenbahn gestürzt habe. Dabei hat mir das delay(x) fast Finger und Ohren gleichzeitig gebrochen... damals habe ich händeringend nach Lösungen gesucht. Diese habe ich dann Mitte 2015 auch mit "millis()" gefunden und setze delay(x) heute nur noch bei kleinen Dreizeilern ein, wenn ich "mal eben" schnell etwas testen möchte. Oder innerhalb "void setup()" für eine bewusste Verzögerung nach einer Art "Splash-Screen" fürs LCD-Display.

Wäre es für deine Anleitung nicht eine geeignete Ergänzung, wenn man deine Ampel jetzt mit einer Anforderung für Fußgänger erweitert? Hier fast vor der Haustür beobachte ich nämlich täglich an einer Kreuzung, wie die Ampeln brav den Verkehr regeln. Wenn aber nun jemand über die Straße möchte drückt er einen Knopf, die Ampel zeigt blinkend "Warten" an, und in der nächsten Rot-Phase für die Autos darf der Fußgänger dann losspurten. Das wäre in meinen Augen doch eine ideale Erweiterung - für dein Tutorial - und auch für die Modelleisenbahn - oder ähnliche Gebiete?!

LG, Rudi

Kapitano

@Jomelo: Gegenüber dem, was Du machst, spiele ich nur in der Kreisklasse  8)

@Kapitano: Danke, freue mich :)
@agmue

Bitte kannst du mir einen tipp geben wo ich #include <SM.h> diese Datei herunterladen kann suche schon stunden im netz.
bin ja noch in den Kinderschuhen und muss alles ausprobieren und lernen. (Arduino ca. 4 Monate und absolut keine Vorkenntnisse).
danke im voraus
mfg. Kapitano

Go Up