Sonntag, 19. Mai 2013

Eigentlich...

Eigentlich ist ein tolles Wort. Eigentlich lässt mich über so viele Dinge reden, die ich eigentlich gar nicht gemacht habe. Ein Beispiel?

Was habe ich heute gemacht? Ausgeschlafen, zu Marlen und Sascha gegangen, Katzen gehütet und mit meinem Arduino gespielt. Eine Ampelschaltung. Fertig.

Kurz.

Eigentlich könnte das alles viel länger sein. Denn eigentlich wollte ich an diesem verlängerten Pfingstwochenende ein defektes Apfelbook reparieren. Eigentlich sollte es diese Woche eintreffen. Scheinbar (auch ein tolles Wort) gibt es hier jedoch ein paar Verzögerungen. Da macht auch überhaupt nichts, eigentlich. Denn eigentlich war ich heute sowieso im Volkspark FHain verabredet. Inliner fahren. Nun hat es aber geregnet und im Regen ist es so richtig schlecht mit Inlinern. Eigentlich hatte ich also scheinbar nichts zu tun (Doublekill!).

Aber ich hab ja noch mein Arduino und seit ich ein Buch darüber lese, musste die praktische Seite des Projektes hinten anstellen. Bis heute.

Eigentlich ist so eine Ampelschaltung schnell gemacht, nimm 3 LEDs und schalte sie wie eine Ampel.

// Edit
Die hier gezeigt Logik ist Fehlerhaft, auch wenn der der Aufbau ansich zu funktionieren scheint.
Die Berichtigung gibts hier: http://ya365pb.blogspot.de/2013/05/lange-while.html


/*

Ampel Versuch

 */

// Wieso steht in den Bsp. immer int, reichen da nicht bytes? oO
byte red = 9; // Rot ist an Pin 9 usw.
byte yel = 8;
byte gre = 7;

// Pausen
int pred = 3000; // so lang ist rot (p(ause)red)
int pyel = 1000; // so lang ist gelb (p(ause)yel(low))
int pgre = 5000; // so lang ist grün (p(ause)gre(en))
int pswitch = 500; // schaltphasen

// the setup routine runs once when you press reset:
void setup() {                
  // die pins als output.
  pinMode(red, OUTPUT);
  pinMode(yel, OUTPUT);
  pinMode(gre, OUTPUT);
}

// the loop routine runs over and over again forever:
void loop() {
  digitalWrite(red, HIGH);
    delay(pred);
      digitalWrite(yel,HIGH);
      delay(pswitch);
    digitalWrite(red,LOW);
    delay(pyel);
  digitalWrite(yel,LOW);
  digitalWrite(gre,HIGH);
  delay(pgre);
  digitalWrite(gre,LOW);
  digitalWrite(yel,HIGH);
  delay(pyel);
 digitalWrite(yel,LOW);
  
}

Das ist aber selbst mir zu langweilig. In oben verlinktem Buch, gibts es aber ein schönes Beispiel, für eine Ampelschaltung mit angeschlossener Fußgängerampel. Also: Verkehrsampel macht Rot, Gelb, Grün. Wenn Fußgänger Knopf drückt (und nur dann), mache Verkehrsampel Rot und Fußgänger Grün. Soweit so schön, eigentlich. Denn für die Pausen wurde die delay() Funktion benutzt. Hier mein Code approach dazu:

/*

Ampel Versuch

 */

// reichen da nicht bytes? oO
byte red = 9; // Verkehrsampel Rot
byte yel = 8; // Verkehrsampel Gelb
byte gre = 7; // Verkehrsampel Grün
byte wred = 3; // Fußgängerampel Rot  (w(alker)red)
byte wgre = 2; // Fußgängerampel Grün (w(alker)gre(en))
byte but = 6; // Button Pin

// Pausen
int pred = 3000; // so lang ist rot
int pyel = 1000; // so lang ist gelb
int pgre = 5000; // so lang ist grün
int pswitch = 500; // schaltphasen
int psafety = 1000; // Puffer für Fußgänger, bis Verkehrsampel wieder auf grün springt
int butr = 0;

// the setup routine runs once when you press reset:
void setup() {                
  // die pins als output.
  pinMode(red, OUTPUT);
  pinMode(yel, OUTPUT);
  pinMode(gre, OUTPUT);
  pinMode(wred, OUTPUT);
  pinMode(wgre, OUTPUT);
  pinMode(but, INPUT);
}

// the loop routine runs over and over again forever:
void loop() {
  if (butr == HIGH) {
  
  digitalWrite(red, HIGH);
  // Start Fußgänger ampel
  // Bei Verkehr Rot = Fußgänger grün
    digitalWrite(wred, LOW);
    digitalWrite(wgre, HIGH); 
  // Wartezeit für Verkehrsampel
  delay(pred);
  // Verkehrsampel fertig, aber erst Fußgängerampel auf Rot
    digitalWrite(wgre, LOW);
    digitalWrite(wred, HIGH);
    delay(psafety); // Pause für Fußgänger, die noch auf der Straße sind.
  // weiter mit Verkehrsampel   
      digitalWrite(yel,HIGH);
      delay(pswitch);
    digitalWrite(red,LOW);
    delay(pyel);
  digitalWrite(yel,LOW);
  digitalWrite(gre,HIGH);
  delay(pgre);
  digitalWrite(gre,LOW);
  digitalWrite(yel,HIGH);
  delay(pyel);
 digitalWrite(yel,LOW);
  }
  
  else {
    for (byte i=0; i < 5; i++) {
    digitalWrite(red, HIGH);
    digitalWrite(wred, LOW);
    delay(pswitch);
    digitalWrite(red, LOW);
    digitalWrite(wred, HIGH);
    } // for zu
  } // else zu
} // alles zu

Die blinkende else Abfrage am Ende ist nur zum debuggen, damit ich weiß wann das Board "denkt", dass der Knopf nicht gedrückt wurde. Für die Ampel selbst ist sie egal.

Eigentlich ganz nett so. Funktioniert ja scheinbar. Die delay() Funktion hat aber einen entscheidenden Nachteil und dieser wurde eigentlich auch bereits in einem vorherigen Kapitel im Buch beschrieben und gelöst. Die delay() Funktion ist ähnlich wie sleep in der Bash. Hier hält das Skript also tatsächlich für eine definierte Zeit an. Nachteil? Wenn das Skript wartet, kann es den Status des Buttons gar nicht überprüfen. Der Fußgänger kann also lange auf dem Knopf herum drücken, solange das Skript nicht an der "richtigen" Stelle ist, erkennt es den Knopfdruck nicht.

Die Lösung wurde, wie bereits erwähnt, eigentlich schon mehrere Kapitel vorher gegeben. (mit millis() statt delay() arbeiten) Daher verwunderte es mich doch, dass hier wieder delay() benutzt wurde und ich entschied mich, es besser zu machen.

Ein Bash Skript oder ein Windows cmd Skript zu schreiben, dass fiel mir bisher immer recht leicht. Stapelverarbeitung, Schritt für Schritt, da glänzt mein Hirn. Die delay() Funktion und die damit verbundene Logik ging daher ähnlich leicht von der Hand. Mit der Entscheidung aus diesem gemachten Nest auszubrechen besiegelte ich unwissentlich meinen restlichen Samstag, denn eine Logik, welche immer und immer wieder von oben beginnt, egal ob sie unten fertig ist, bringt mehr Stolpersteine mit sich als mir bewusst war. Und da ich mit der "echten" Programmierung bisher auf Kriegsfuß stand, fehlt(e) mir da eindeutig die Erfahrung.

Legen wir los. Das Arduino Board Zählt die Zeit die vergangen ist, seit das Programm läuft. Die Funktion heißt millis() und gibt die Zeit in Millisekunden zurück.

Wir setzen uns also irgendwo einen Zeitstempel und vergleichen diesen mit der aktuellen Zeit. Die Differenz daraus ist die verstrichene Zeit, welche wir statt delay()  nutzen können.

Ich setze zu Beginn des Programms die Variable 'time' und prüfe mit ihr, wie weit wir schon in der Zeit gereist sind und ob es Zeit ist die Ampel zu schalten. Einfach indem ich sie von der aktuellen Zeit millis() abziehe und das Ergebnis mit meiner Rotpause vergleiche. 

if (millis() - time > pred)    // Prüfe ob die abgelaufene Zeit länger ist als die Zeit die Rot sein soll.

ist das Ergebnis größer als 3000 (pred = 3000) dann mach etwas, nämlich Gelb dazu schalten. Nun 'time' reseten und wieder warten bis die nächste Pause abgelaufen ist. Und einfach so weiter... ja?

      if (millis() - time > pred)   // Prüfe ob die abgelaufene Zeit länger ist als die Zeit die Rot sein soll.
      {                                // falls dem so ist...
        digitalWrite(yel,HIGH);        // schalte Gelb dazu 
        time = millis()                // time reseten, müssen ja die neue pause zählen
        if (millis() - time > pyel  ) // Prüfe ob die abgelaufene Zeit länger ist als Rot und Gelb leuchten sollte)
          {                             // falls dem so ist...
            digitalWrite(red,LOW);      // schalte Rot ab
            digitalWrite(yel,LOW);      // schalte Gelb ab
            digitalWrite(gre,HIGH);     // und Grün an
          }

NEIN! Wäre eine Stapelverarbeitung, dann würde das so gehen... ist es aber nicht. Denn das Skript fängt ständig wieder oben an, es wartet nicht. In dem Moment als ich also 'time' auf 0 Setze, greift wieder die Erste if Abfrage.

"Sind schon 3 Sekunden rum damit ich Gelb dazu schalten kann? Ja -> Schalte Gelb dazu, setze 'time' auf 0. Sind schon 3 Sekunden rum, damit ich Gelb dazu schalten kann? Ja -> Schalte Gelb dazu, setze 'time' auf 0. Sind schon 3 Sekunden rum, damit ich Gelb dazu schalten kann?"

Meine Erste Idee war, statt relativen Zeiten (Pausen) absolute Zeiten zu setzen:

0 - 3 Sekunden: Rot
3 - 3,5 Sekunden: Rot & Gelb
3,5 - 4,5 Sekunden: Gelb
4,5 - 10 Sekunden: Grün

usw.

Das ist aber auch blöd, denn wenn man die Schaltpausen mal ändern möchte, muss man immer Umrechnen. Aber die Lösung ist recht simpel... wenn man drüber nachdenkt. ;) Denn statt absoluter Zahlen oder dem falschen Ansatz "sind denn schon 3 Sekunden rum? Ja -> reset", fragt man in der Zweiten Phase einfach "Sind denn schon 3 Sekunden + 0,5 Sekunden rum?" und in der Dritten "Sind denn schon 3 Sekunden + 0,5 Sekunden + 1 Sekunde rum?". Die Variable 'time' reseten wir auch noch, aber erst ganz zum Schluss. Das sieht dann so aus:


    if (millis() - time > pred)              // Prüfe ob die abgelaufene Zeit länger ist als die Zeit die Rot sein soll.
      {                                      // falls dem so ist...
        digitalWrite(yel,HIGH);              // schalte Gelb dazu
          if (millis() - time > pyel + pred) // Prüfe ob die abgelaufene Zeit länger ist als Rot und Gelb leuchten sollte (Pause Rot + Pause Gelb)
          {                                  // falls dem so ist...
            digitalWrite(red,LOW);           // schalte Rot ab
            digitalWrite(yel,LOW);           // schalte Gelb ab
            digitalWrite(gre,HIGH);          // und Grün an
          }
            if (millis() - time > pgre + pyel + pred) // Prüfe ob die abgelaufene Zeit länger ist als Pause Rot + Pause Rot & Gelb + Pause Gelb
            {                                         // falls dem so ist...
              digitalWrite(gre,LOW);                  // schalte Gelb aus
              digitalWrite(yel,HIGH);                 // und Grün an
            }                                         // usw.usf ;)
              if (millis() - time > pyel + pgre + pyel + pred)
              {
                digitalWrite(yel,LOW);               // wenn wir fertig sind, Gelb abschalten
                time = millis();                     // und die Zeit reseten!

Schön, dann haben wir es doch jetzt eigentlich, oder? Einfach diesen Code auch auf die Fußgängerampel anwenden und vorher abfragen ob jmd. den Knopf gedrückt hat oder nicht. Falls nein, schalte weiter die Verkehrsampel, falls ja schalte Verkehrsampel auf Rot und lass die Fußgänger über die Straße.

Aber wieder macht die endlose loop() Funktion diese Logik zunichte, denn wieder wartet sie nicht.

"Hat jemand den Knopf gedrückt? Nein -> prüfe ob die Zeit schon abgelaufen ist. Hat jemand den Knopf gedrückt? Nein -> prüfe ob die Zei.... Hat jemand den Knopf gedrückt? Nein -> Zeit ist abgel.... Hat jemand den Knopf gedrückt? Nein -> ...schalte auf Gelb... Hat jemand den K.... grün... Knopf?" Hilfe!

Man muss diese Stränge also irgendwie trennen, so dass sie trotz der Schleife nicht gleichzeit abgefragt und ausgeführt werden. Hat man diese Logik erst einmal auseinander gefummelt und die zwei Stränge Verkehrsampel und Fußgängerampel soweit getrennt, dass sie sich nicht mehr in die Quere kommen, kommt noch eine ganz kleine Kleinigkeit dazu. Wieder ist die scheinbar parallele Verarbeitung schuld. Die ganze Zeit wird abgefragt, "Knopf gedrückt, ja oder nein?" Wenn die Autos nun also gerade anfahren, weil seit 1 Sekunde Grün gezeigt wird, und ein Fußgänger sein Knöpfchen drückt, springt die Verkehrsampel *sofort* auf Rot und der Fußgänger hat Grün. Toll oder? Nö!

Wir benötigen also eine Logik ala "Hat jemand den Knopf gedrückt? Ja? Dann mach erst die Verkehrsampel zuende und gib danach für die Fußgänger frei" Diese Logik verhindert ausserdem, dass die Fußgänger dauergrün haben. Würde man blind den Button der Fußgänger abfragen und damit entscheiden ob Verkehr oder Fußgängerampel geschaltet wird, könnte man unweigerlich einen "immer nur Fußgänger" Zyklus provozieren.
Dazu habe ich die alte Variable "butr" (but(ton)r(read)) in safe umbenannt. Quasi "ist es safe jetzt Fußgänger gehen zu lassen?". Diese wird beim deklarieren auch gleich auf 0 gesetzt.  Und dann:

if (safe == 0) // falls safe 0 ist, prüfe den Knopf
{
  if (digitalRead(but) == HIGH) // wenn er gedrückt ist, setze safe auf 1
 {
   safe = 1;
 }
}
  if (safe == 0 | safe == 1) // Egal ob ein Fußgänger gedrückt hat oder nicht, lass den Verkehr mindestens 1x laufen
   {
Da sowohl bei 0 als auch bei 1 die Verkehrampel erstmal ihren Zyklus abarbeitet, bleibt die Fußgängerampel so lang Rot, bis sie an der Reihe ist. Das triggern wir in der letzten Phase der Verkehrsampel:
if (millis() - time > pyel + pgre + pyel + pred)
              {
                digitalWrite(yel,LOW);               // wenn wir fertig sind, Gelb abschalten
                time = millis();                     // und die Zeit reseten!
                if (safe == 1) safe = safe++;        // Falls ein Fußgänger gedrückt hatte und der Verkehr nun 1x durchgelaufen ist, mache aus safe eine 2 (1+1)
Wir setzen also safe auf 2, und das wiederrum ist das Startsignal für die Fußgängerampel:
else if (safe == 2) // wenn der Knopf gedrückt wurde (safe = 1) UND der Verkehr einmal durchlief (safe = 1 + 1) dann mache...
Uff. Geschafft. Der gesamte Code sieht so aus:
/*

Ampel Versuch

 */

// reichen da nicht bytes? oO
byte red = 9; // Verkehrsampel Rot
byte yel = 8; // Verkehrsampel Gelb
byte gre = 7; // Verkehrsampel Grün
byte sig = 5; // "Signal kommt"
byte wred = 3; // Fußgängerampel Rot
byte wgre = 2; // Fußgängerampel Grün
byte but = 6; // Button Pin
byte safe = 0; // war butr für buttonread

// Pausen
int pred = 3000; // 3 Sekunden Rot
int pyel = 1000 ; // 1 Sekunde Gelb
int pgre = 5000; // 5 Sekunden Grün
int psafety = 1000; // Puffer für Fußgänger, bis Verkehrsampel wieder auf grün springt
unsigned long time;

// the setup routine runs once when you press reset:
void setup() {
  // die pins als output.
  pinMode(red, OUTPUT);
  pinMode(yel, OUTPUT);
  pinMode(gre, OUTPUT);
  pinMode(sig, OUTPUT);
  pinMode(wred, OUTPUT);
  pinMode(wgre, OUTPUT);
  pinMode(but, INPUT);
  time = millis(); // einmal die aktuelle Zeit festlegen
}

// the loop routine runs over and over again forever:
void loop() {
if (safe == 0) // falls safe 0 ist, prüfe den Knopf
{
  if (digitalRead(but) == HIGH) // wenn er gedrückt ist, setze safe auf 1
 {
   safe = 1;
 }
}

  if (safe == 0 | safe == 1) // Egal ob ein Fußgänger gedrückt hat oder nicht, lass den Verkehr mindestens 1x laufen
   {
    digitalWrite(red, HIGH);
    digitalWrite(wred, HIGH);                // Fußgängerampel auch auf Rot und bleibt auch so 
    if (millis() - time > pred)              // Prüfe ob die abgelaufene Zeit länger ist als die Zeit die Rot sein soll.
      {                                      // falls dem so ist...
        digitalWrite(yel,HIGH);              // schalte Gelb dazu
          if (millis() - time > pyel + pred) // Prüfe ob die abgelaufene Zeit länger ist als Rot und Gelb leuchten sollte (Pause Rot + Pause Gelb)
          {                                  // falls dem so ist...
            digitalWrite(red,LOW);           // schalte Rot ab
            digitalWrite(yel,LOW);           // schalte Gelb ab
            digitalWrite(gre,HIGH);          // und Grün an
          }
            if (millis() - time > pgre + pyel + pred) // Prüfe ob die abgelaufene Zeit länger ist als Pause Rot + Pause Rot & Gelb + Pause Gelb
            {                                         // falls dem so ist...
              digitalWrite(gre,LOW);                  // schalte Gelb aus
              digitalWrite(yel,HIGH);                 // und Grün an
            }                                         // usw.usf ;)
              if (millis() - time > pyel + pgre + pyel + pred)
              {
                digitalWrite(yel,LOW);               // wenn wir fertig sind, Gelb abschalten
                time = millis();                     // und die Zeit reseten!
                if (safe == 1) safe = safe++;        // Falls ein Fußgänger gedrückt hatte und der Verkehr nun 1x durchgelaufen ist, mache aus safe eine 2 (1+1)
              }
      }
  }
  
  if (safe == 1)
  {
    digitalWrite(sig,HIGH);
  }
  
  else if (safe == 2) // wenn der Knopf gedrückt wurde (safe = 1) UND der Verkehr einmal durchlief (safe = 1 + 1) dann mache...
  {
    digitalWrite(red,HIGH); // erstmal alles auf Rot, damit auch keiner überfahren wird
    digitalWrite(wred,HIGH);// falls wir irgendwo einen Logikfehler haben.
    if (millis() - time > psafety)           // Prüfe ob die sicherheits Zeit vorbei ist
      {                                      // falls dem so ist...
        digitalWrite(wred,LOW);
        digitalWrite(sig,LOW);
        digitalWrite(wgre,HIGH);             // gib den Fußgängern Grün
      }
        if (millis() - time > psafety + pgre)// Prüfe ob abgelaufene Zeit länger als Safety + Grünphase ist.
        {                                    // falls ja
          digitalWrite(wred,HIGH);           // Fußgänger bekommen Rot
          digitalWrite(wgre,LOW);
        }
        if (millis() - time > psafety + pgre + psafety) // Wenn wieder Rot ist, warte noch die Safety Zeit ab (Safety + Grünphase + Safety)
        {
          time = millis();                              // Und resete die Zeit, damit es von vorn los gehen kann
          safe = 0;
        }
  }
delay(1);
} // alles zu





Und ja, ich bin mir im klaren darüber, dass eine if Schleife Abfrage eigentlich nicht die eleganteste Wahl ist.

Kommentare:

  1. Ich bin zwar noch nicht durch mit dme Blogpost, aber zu der Frage "// reichen da nicht bytes? oO":
    * Du hast Recht, dass man die niedrigen Zahlen mit einem Byte abbilden kann. Aber wenn ich das richtig verstehe, verlangen die Funktionen, die die PINs als Argumente haben, Variablen/Werte vom Typ INT.
    * Das Casten (also Umwandeln) von kleineren Typen zu Groeßeren funktioniert zwar verlustfrei, ist aber trotzdem doof. Finde ich.
    * Und ist es denn wirklich so, dass nur 255 Pins moeglich sind?

    AntwortenLöschen
  2. *Aber wenn ich das richtig verstehe, verlangen die Funktionen, die die PINs als Argumente haben, Variablen/Werte vom Typ INT.

    Wie kommst du darauf / woraus liest du das?

    255 Pins?! :D Ich hab 7 Digitale I/O und nochmal 6 Analoge Inputs zur Verfügung. Das sind wesentlich weniger als 255 :)

    AntwortenLöschen
  3. in der pin-docu nutzen sie auch INT, deswegen komme ich drauf [1]


    [1] http://arduino.cc/en/Tutorial/DigitalPins

    AntwortenLöschen