Bedienen und Beobachten: 1. Aufgabe mit dem Fridolin

In der ersten Aufgabe werden die Grundlagen für Folgeprojekte mit dem Fridolin gelegt. 

Da der Fridolin ein Fahrerloses Fahrzeug ist muss alles automatisiert werden. Im ersten Schritt werden drei elementare Funktionen realisiert:

  1. Anzeigen der aktuellen Aufgabe auf dem Display und auf dem seriellen Monitor am Rechner
  2. Bedienen des Fridolin mit den drei Tastern, um die Zustände in 3 zu wählen
  3. 2 einfache Daueraufgaben: LED an - aus oder blinken - aus (Simulation mit Factorio)
Damit wird die unterste Stufe der Automatisierung erreicht: Bedienen und Beobachten! Ich erläutere hier meine Lösung. Eine ganz andere Lösung hat Meister Edel, der den Fridolin gebaut hat, geschrieben. Diese steht hier zum Download bereit: Erste-Lösung-vom-Meister


It works! Beiden Lösung ist gemeinsam, dass delay() nicht genutzt wird. Das wird in vielen Arduino Seiten diskutiert. Am besten mit Google "arduino mulittasking" suchen. Meine Lösung ist deutlich komplizierter als die vom Meister Edel. 

Vorüberlegungen

Skizze meiner Programmstruktur
Erst einmal habe ich überlegt, wie die Aufgabe in sinnvolle Teile zerlegt werden kann, um daraus dann die Lösung zu bauen. Dieser Ansatz ein System zu betrachten nennt sich "konstruktivistisch-technomorph". [1, Kap. 2.2.4 S.36-39] Musst Du nicht wissen, kannst Du aber. 😉 


  1. DATEI new.try
    Versionsdokumentation, Definitionen, Verwaltung bedingte Compilierung, setup(), loop() und
    die entscheidende Hauptroutine: LED_control()
  2. DATEI initialisierung
    Konfigurationen und Kalibrieren, falls notwendig, sowie Testen aller Komponenten
    wird alles in setup() aufgerufen.
  3. DATEI basicIO
    low level Routinen, die direkt mit der Hardware kommunizieren. Alle andern Teile rufen die auf.
  4. DATEI TaskManagement
    hier sammle ich alle Aufgaben, die "gleichzeitig" ablaufen sollen. Momentan nur Blinken der LED.
Hier ist der Stand vom 04.12.2021: NewTry_v1.1.1

Taster Auslesen

Die drei Taster werden simplem mit Digital Read ausgelesen. Also simpel auf HIGH und LOW abfragen. Aber was mache ich mit Taster 3? Mal soll er einschalten mal Ausschalten. Meister Edel hat das so gelösst:
/------------------------ T3 Abfragen ----------------------------
    T3_a = digitalRead(T3);      // Taster 3 abfragen und Zustand auf Variable übertragen
      if(T3_z == 0 && T3_a == LOW)   // Fallende Flanke auswerten


Er merkt sich den Zustand des Tasters 3 im letzten Zyklus und schaut nach ob er gedrückt wurde. [2] Das habe ich anders gemacht. Ich habe dafür eine kleine Routine, die jedes mal alle Taster fragt und sich das merkt: 
/*
 *   Taster[0][0] = T1 0->1  Taster[1][0] = T1 down  Taster[2][0] = T1 1->0
 *   Taster[0][1] = T2 0->1  Taster[1][1] = T2 down  Taster[2][1] = T1 1->0
 *   Taster[0][2] = T3 0->1  Taster[1][2] = T3 down  Taster[2][2] = T1 1->0
*/
/*
 int ReadTaster(){
   for (int i=0;i<=2;i++){
     Taster[0][i]=false; // Negative Flanke aus
     Taster[2][i]=false; // Positive Flanke aus
     if(digitalRead(TasterPin[i])==LOW) {
       if(!Taster[1][i]){Taster[2][i]=true;}  // Taster[2][i] = Ti 1->0
       }
     else{
       if(Taster[1][i]){Taster[0][i]=true;}   // Taster[0][i] = Ti 0->1  
      }
     Taster[1][i]=!digitalRead(TasterPin[i]); // Taster[1][i] = Ti down
   }
 }
*/
Sieht erst mal kompliziert aus, funktioniert aber. Aber danach sin in dem kleinen Array Taster[3][3] alle Möglichkeiten als Wahrheitswert gespeichert. Das kann ich dann irgendwo aufrufen, wo ich das grad mal brauche.

Ausgabe auf dem Display und serial Monitor

Beim Display einfach nur den Cursor auf  die richtige Position setzen und ausgeben. Dafür habe ich ein Mako definiert. Der Compiler setzt automatisch den gesamten Text ein, wenn ich SHOW hinschreibe.
  #define SHOW(x,y,s)   lcd.setCursor (x,y); lcd.print(s);
Das habe ich auch für den Serial Montor gemacht. Da gibt es dann die Variante ohne Zeilenvorschub SAX und mit Zeilenvorschub SAYln.
  #define SAY(S)   (Serial.print(S))
  #define SAYln(S) (Serial.println(S))
Natürlich könnte ich das auch immer in den Code schreiben. Meins ist kürzer und habe auch noch einen anderen Vorteil.

Der Fridolin wird ja selbstständig fahren. Dann ist er nicht mehr mit dem Rechner verbunden und kann auch nichts mehr ausgeben. Dann ist der Code Serial.print(S) nuztzlos und kostet nur Zeit. Besser wäre ein Variante, bei der das gar nicht läuft.

Das regele ich über bedingte Compilierung. D.h. ich sage dem Compiler ob ich den serial Monitor haben will. Das regelt der Text trace: #define trace Wenn mein Programm so läuft, wie ich es haben will. kommt trace weg und voila der serial Monitor ist raus.
ifdef trace
  #define SAY(S)   (Serial.print(S))
  #define SAYln(S) (Serial.println(S))
#else
  #define SAY(S)    // ersetzt SAY(S) durch nix
  #define SAYln(S)  // ersetzt SAY(S) durch nix
#endif
Nebenbei benutze ich SAYln, um auf dem Rechner das Programmablauf zu beobachten. Bei jeder Routine füge ich am Angang SAYln("NameDerRoutine ....") und am Ende SAYln("NameDerRoutine DONE"). Dann kann ich beobachten ob alles so läuft wie geplant. 

Hauptprogramm

Weil ich viele Funktionen in andere Dateien ausgelagert habe ist das recht kurz. 
/*
 * Implementiert eine Zustandsmaschine
 * active 0: LED ist aus
 * active 1: LED ist aktiv je nach Modus
 * Modus 0: LED ist Dauericht
 * Modus 1: LED is im Blinkmodus
 */
   byte Modus=0; 
   bool active=false; // Funktion an oder aus
   
void LED_control() { 
  SAYln("LED_control ....");
  if (!active){
    SAYln("LED aus");
    showfast(0,0," LED aus   ");
    digitalWrite(LED_BUILTIN,0);
    StartTask(nextWakeUpLED);     // Timer für LED setzen
  }
  else {
    if (Modus==0){
      SAYln("LED an");
      showfast(0,0," LED an    ");      
      digitalWrite(LED_BUILTIN,1);
    }
    else {
      SAYln("LED blinkt");
      showfast(0,0," LED blinkt");      
      LED_blink();    // Default = blitzt kurz auf = Programm läuft
    }
  }
  SAYln("LED_control done");
}

void loop() {
  currentTime = millis();  // Am Anfang jeden Durchlaufes die Zeit retten
  ReadTaster();            // In j-e-d-e-m Zylkus alle Taster aufrufen

  // aus den Tastern den neuen Zustand ermitteln
  if((Taster[0]&3)==2) {active=true;   }
  if((Taster[1]&3)==2) {active=false;  }
  if((Taster[2]&3)==2) {active=!active;}
  if(((Taster[0]&3)==2) & ((Taster[1]&3)==3)) {Modus=1;}
  if(((Taster[0]&3)==2) & ((Taster[2]&3)==3)) {Modus=0;} 


  // die LED Routine aufrufen
  LED_control(); 
}

Das UnterProgramm LED_blink ist ein kleine Routine, aus der Datei Taskmanagement:

/*
 * LED blinken lassen als Multitasking-Test
 *      
 * Meldungen 
 *      return= 102: LED-Toggle
 *      return=-102: Sheduler zu früh
 */
int LED_blink(){
  if(currentTime<=nextWakeUpLED) {
    return -102;
    } else {
     if (digitalRead(LED_BUILTIN)) {
      nextWakeUpLED=nextWakeUpLED+waitLEDoff;
     }
    else{
      nextWakeUpLED=nextWakeUpLED+waitLEDon;
    }
    toggle_LED();
    return 102;
  }
}
Das hat Meister Edel im Prinzip genau so gelöst.  

Optimierungen

Die Tasterroutine ist gar nicht optimal. Um 9 Wahrheitswerte zu speichern werden 9 Bytes verschwendet. Und dann weis ich nur die letzten beiden Zustände. In der kleineren und schnelleren Version bekommt jeder Taster ein Byte und speichert die letzten 8 Zustände in den 8 Bits. 

In jedem Durchlauf wird der Zustand und die Position der Taster auf dem LCD-Display ausgegeben. Aber die Ausgabe auf dem dem LCD-Display ist echt langsam. Damit wird das ganze Programm langsam; aber es wird meistens dasselbe ausgegeben. 

Ich habe eine kleine Routine zwischengeschaltet, die sich merkt, was angezeigt wird. Bei jeder Ausgabe wird erst nachgesehen, ob sich was ändert. Ändert sich nichts, dann wird auch nichts aufs Display ausgegeben. Schön schnell!

Aber es geht doch auch ohne die Optimierung! Guter Einwand!

Taster und Display sind für Menschen; die sind nun mal langsam. Aber die Routinen werden permanent sehr schnell aufgerufen und kosten so viel Rechenzeit. Das ist Verschwendung! 

Merke: Der wahre Logistiker hasst Verschwendung jeglicher Art!    

Update 9.12.2021

Ich habe den Code ein wenig aufgeräumt und das Tracing im Serial Monitor mit einem Parameter versehen. Damit wird eingestellt wie geschwätzig das Programm ist. Die Version 1.1.2 kannst Du hier runterladen. Doku wie immer in den Kommentaren.

Quellen

  1. Plum U (2004), "Eine systemtheoretische Betrachtung der Produktentwicklung". Thesis at: Technischen Universität München - Fakultät für Maschinenwesen. München, June, 2004. [BibTeX] [URL]
  2. Das geht hier, zu Glück. Im allgemeinen ist das nicht so einfach. Es sind mechanische Kontakte, die ein wenig "hüpfen", wenn sie geschlossen werden. Das nennt man Prellen. Wäre das bei unseren Tastern der Fall, müsste das noch abgefangen werden.  






























Kommentare