'BlinkWithoutDelay' mal etwas anders ...

Hallo,
Das ‘BlinkWithoutDelay’ ist ja ein vielzitiertes Beispiel, wenn es darum geht zeitliche Abläufe ohne delay() zu steuern.

Was mich persönlich daran immer stört, dass meiner Meinung nach das viele Hantieren mit den millis() im Sketch das ganze etwas unübersichtlich macht. Ich habe für mich daher eine eigene Methode, das gesamte millis()-Handling in einer Klasse zu kapseln.

Gut, Programmiertechniken und -methoden sind immer auch etwas Geschmachssache, und nicht jeder wird das so sehen wie ich. Ich möchte es aber hier doch mal vorstellen, vielleicht gefällt es dem einen oder anderen ja auch besser so.

Letzendlich geht es immer darum, eine Zeit zu starten, und dann abzufragen ob sie abgelaufen ist - eigentlich wie bei einer Sanduhr bzw. einem Kurzzeitwecker in der Küche ;).

Meine Klasse hat also 2, nein eigentlich 3 Methoden:
void start( long laufzeit ) startet die Zeit in ms.
bool abgelaufen() true wenn die Zeit abgelaufen ist.
long restzeit() gibt die noch verbleibende Laufzeit in ms an.

Da die laufzeit als ‘long’ definiert ist, aber natürlich immer positiv sein muss, ist die längste mögliche laufzeit auf ca 24 Tage begrenzt. Das dürfte in der Praxis aber keine relevante Einschränkung sein.

Das ganz einfache Beispiel ‘Blinkewithoutdelay’:

// ############## Klassendefinition des Zeitzählers ######################
class Sanduhr
{
  public:
    Sanduhr(){ timervalue = (long)millis(); }
    void start(  long wert ) { timervalue =  (wert>1?wert:1) + (long)millis(); }
    bool abgelaufen() { return ( ( (long)millis() - timervalue) > 0 ); }
    long restzeit()   { if ( abgelaufen() ) return 0; else return timervalue - (long)millis(); } 
  private:
    long timervalue;
};
// ############## Ende Klassendefinition ############################


Sanduhr zeit1;
byte ledState = LOW;
void setup() {
    //
    zeit1.start( 2000 );
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
    if (zeit1.abgelaufen()) {
        // Zeitzähler neu starten
        zeit1.start(1000);
    
        // if the LED is off turn it on and vice-versa:
        if (ledState == LOW) {
          ledState = HIGH;
        } else {
          ledState = LOW;
        }
    
        // set the LED with the ledState of the variable:
        digitalWrite(LED_BUILTIN, ledState);
    }
}

Interessanter und lohnender wird das Ganze natürlich, wenn man mehrere unabhängige Zeitzähler benötigt. Hier blinken 3 Led’s in unterschiedlichem Takt. Das Beispiel zeigt auch eine Anwendung der Methode ‘restzeit’:

// ############## Klassendefinition des Zeitzählers ######################
class Sanduhr
{
  public:
    Sanduhr(){ laufzeit = (long)millis(); }
    void start(  long wert ) { laufzeit =  (wert>1?wert:1) + (long)millis(); }
    bool abgelaufen() { return ( ( (long)millis() - laufzeit) > 0 ); }
    long restzeit()   { if ( abgelaufen() ) return 0; else return laufzeit - (long)millis(); } 
  private:
    long laufzeit;
};
// ############## Ende Klassendefinition ############################

//Blinken von 3 Leds in unregelmäßigem Rhythmus

Sanduhr Blinkzeit[3];
int intervall[] = { 800, 899, 750 };
// Ports für die Led's
byte ledP[] = {8,9,10};
bool startLed[sizeof(ledP)];   // Flag für den Startzyklus der Led

void setup() {
    for ( byte i=0; i<sizeof(ledP); i++ ) {
        pinMode(ledP[i], OUTPUT);
        startLed[i] = true;
    }
}

void loop() {

    for ( byte i=0; i<sizeof(ledP); i++ ) {
        if ( Blinkzeit[i].abgelaufen() ) {
            // Zeitzähler neu starten
            Blinkzeit[i].start( intervall[i] );
            startLed[i]=true;
            // Led startet im Zustand 'AUS'
            digitalWrite(ledP[i], LOW );
        } else if ( startLed[i] && Blinkzeit[i].restzeit() < (intervall[i]/3) ) {
            // Led nach 2/3 der Intervallzeit einschalten.
            digitalWrite(ledP[i], HIGH );
            startLed[i]=false;
        }
        
    }
}

Könntest du mir sagen, warum du long verwendest und nicht unsigned long?

Gut, Programmiertechniken und -methoden sind immer auch etwas Geschmachssache, und nicht jeder wird das so sehen wie ich. Ich möchte es aber hier doch mal vorstellen, vielleicht gefällt es dem einen oder anderen ja auch besser so.

Eine Variante mehr.... Finde ich OK. (ich muss sie nur noch verstehen und testen)

combie: Könntest du mir sagen, warum du long verwendest und nicht unsigned long?

Der Grund ist der Überlauf bei den millis(). Wenn man unsigned long verwendet, muss man sich die Startzeit und die Laufzeit merken um den Vergleich so abzufragen, dass es auch beim Überlauf funktioniert. Arbeitet man vorzeichenbehaftet, funktioniert es auch, wenn man sich nur den Ablaufzeitpunkt merkt. Das spart 4 Byte pro Instanz der Klasse. Und RAM ist beim Arduino in der Regel ein kostbares Gut ;)

Dass die Intervall-Zeit auf ca 24 Tage beschränkt ist, damit kann sicher jeder leben. Aber funktioniert es auch nach 24 Tagen Arduino-Laufzeit noch, wenn (long)millis() dabei ist, ins Negative umzukippen?

Du könntest auch alternativ die Methoden

class Sanduhr {
public:
  void start();
  bool abgelaufen(unsigned long dauer);
  unsigned long startzeit(); // entbehrlich ?
...
};

erfinden?

Dass "RAM ein kostbares Gut ist", damit hast du sicher recht.

michael_x: Dass die Intervall-Zeit auf ca 24 Tage beschränkt ist, damit kann sicher jeder leben. Aber funktioniert es auch nach 24 Tagen Arduino-Laufzeit noch, wenn (long)millis()dabei ist, ins Negative umzukippen?

ja, das funktioniert 'rund um die Uhr'. Es geht ja immer nur um Differenzen, und die funktionieren im gesamten Bereich, auch beim Umkippen ins Negative. Vorzeichenbehaftete Zahlen verhalten sich da wie ein lückenloser 'Zahlenkreis', bei dem die kleinste negative Zahl direkt an die größte positive angrenzt. Die betrachtete Differenz muss nur kleiner sein als der 'halbe' Zahlenkreis. Es muss deshalb auch sichergestellt sein, dass die 'start' Methode mindestens 1x in 24 Tagen aufgerufen wird, sonst funktioniert das 'abgelaufen' nicht mehr. Aber auch das dürfte für 99,9% der Anwendungen keine echte Einschränkung sein. Man kann das rel. leicht testen, wenn man in der Klasse statt millis() mal micros() verwendet. Dann läuft ja alles um den Faktor 1000 schneller, und die 'kritischen' Bereiche werden in einem überschaubaren Testzeitraum erreicht.

MicroBahner:
Es muss deshalb auch sichergestellt sein, dass die ‘start’ Methode mindestens 1x in 24 Tagen aufgerufen wird, sonst funktioniert das ‘abgelaufen’ nicht mehr. Aber auch das dürfte für 99,9% der Anwendungen keine echte Einschränkung sein.

Auf Kosten eines RAM-Bytes pro Instanz könnte man auch das noch ‘wasserdicht’ bekommen. Dann kann zwischen ‘abgelaufen’ erkannt und dem nächsten Start auch 1 Jahr oder mehr vergehen und ‘abgelaufen’ bleibt dann ‘abgelaufen’ :wink:

// ############## Klassendefinition des Zeitzählers ######################
class Sanduhr
{
  public:
    Sanduhr(){ laeuft = false; }
    void start(  long wert ) { ablaufzeit =  (wert>1?wert:1) + (long)millis(); laeuft = true; }
    bool abgelaufen() { 
        if ( laeuft && ( (long)millis() - ablaufzeit) > 0 ) laeuft = false;
        return ! laeuft; 
        }
    long restzeit()   { if ( abgelaufen() ) return 0; else return ablaufzeit - (long)millis(); } 
  private:
    long ablaufzeit;
    bool laeuft;
};
// ############## Ende Klassendefinition ############################