delay() vermeiden scheitert

Moin in die Runde,

ich lerne gerade C und die Besonderheiten des Loop(). Mein erstes Projekt ist ein Kamera Slider. Kommt der Stepper-getriebene Wagen an einer Position an, so sollen sequentiell bestimmte Dinge geschehen, bevor die Fahrt weiter geht. Diese sind:

  • Ein Delay, um Schwingungen des Schlittens zu beenden
  • Kamera triggern (auslösen an)
  • Kamera Trigger Time abwarten
  • Kamera triggern (auslösen aus)
  • Ein Delay, um die Latenz der Kameraauslösung zu berücksichtigen.

Mit „delay()“ (siehe auskommentierter Loop() im Code) überhaupt kein Problem. Ich möchte es aber gleich „richtig“ machen, und „delay()“s vermeiden (sämtliche Kamera-Trigger Beispiele, die ich finden konnte, nutzen leider delays). Ich kämpfe jetzt seit 2 Tagen mit diesem (mutmaßlich banalen) Problem. Der Trigger wird jedoch ca 1,5s betätigt, statt 0,5s, und auch das restliche Timing scheint völlig daneben. Ich bin mittlerweile ziemlich ratlos, warum. Hier der Code:

const byte triggerPin = 10; // Camera trigger

// Time periods of various delays.
const unsigned long carriageSettle = 500;
const unsigned long triggerOpen = 500;  // trigger needs 2 ms in MF mode, 500 ms in AF
const unsigned long camLatency = 500;

// Variable holding the timer values so far. One for each "Timer"
unsigned long carriageSettleTimer;
unsigned long triggerTimer;
unsigned long latencyTimer;

bool carriageActive = false;
bool triggerActive = false;
bool latencyActive = false;
bool sequenceActive = false;


void setup() {
  pinMode (triggerPin, OUTPUT);
  Serial.begin(9600);
}

void toggleTrigger() {
  if (digitalRead (triggerPin) == LOW)
    digitalWrite (triggerPin, HIGH);
  else
    digitalWrite (triggerPin, LOW);
}  // toggleTrigger

/*
void loop() {  // Mit delay()s klappt alles
  delay(1000); // other code execution
  
  delay(carriageSettle);
  toggleTrigger();
  delay(triggerOpen);
  toggleTrigger();
  delay(camLatency);
  
  delay(1500); // other code execution
}
*/

void loop() {
  delay(1000); // other code execution

  
  // Handling the Carriage settle time
  if(!sequenceActive){
    carriageSettleTimer = millis();
    sequenceActive = true;
    carriageActive = true;
  }
  if(carriageActive && (millis() - carriageSettleTimer >= carriageSettle)) {
    // carriageSettleTimer expired
//    Serial.println("Carriage");
    carriageActive = false;
    toggleTrigger();  // activate Trigger
//    Serial.println("Trigger open");
    triggerTimer = millis();
    triggerActive = true;    
  }
  if(triggerActive && (millis() - triggerTimer >= triggerOpen)){
    toggleTrigger();  // deactivate Trigger
//    Serial.println("Trigger closed");    
    triggerActive = false;
    latencyTimer = millis();
    latencyActive = true;    
  }
  if(latencyActive && (millis() - latencyTimer >= camLatency)){
    // Latency expired
    // Prepare for next cycle
//    Serial.println("Latency");    
    latencyActive = false;
    sequenceActive = false;
  }

  delay(1500); // other code execution
}

Die Sequenz wird durchlaufen, dies jedoch mit völlig falschem Timing. Bin für jeden Hinweis dankbar

Hast Du millis() verstanden?
siehe Nachtwächtererklährung hier im Form bzw blinkwitoutdelay- Beispiel in der IDe.

Grüße Uwe

Offensichtlich nicht richtig, sonst hätte ich wohl kein Problem. Die Zeit seit Power-Up. Mein Problem ist, dass ich erst nach Ablauf anderer Dinge eine Zeitvariable aktivieren kann.

Nach 2 Tagen Kampf denke ich allerdings aktuell nur noch durch Watte.

Demokrit:

  • Ein Delay, um Schwingungen des Schlittens zu beenden
  • Kamera triggern (auslösen an)
  • Kamera Trigger Time abwarten
  • Kamera triggern (auslösen aus)
  • Ein Delay, um die Latenz der Kameraauslösung zu berücksichtigen.

Das ist eine klassische Ablaufsteuerung/Schrittkette.
Sollte also auch so im Programm niedergelegt werden.
(scheint mir so)

Hier solltest/könntest du Anregungen finden:

Ablaufsteuerung
Meine Standardantwort zu Ablaufsteuerungen:

Eine Stichworte Sammlung für Google Suchen:
Endlicher Automat,
State Machine,
Multitasking,
Coroutinen,
Ablaufsteuerung,
Schrittkette,
BlinkWithoutDelay,

Blink Without Delay
Der Wachmann

Multitasking Macros
Intervall Macro

Hi,

es ist richtig Delays zu vermeiden, damit der Sketch möglichst schnell durchläuft, und z.Bsp. Tasten abfragen kann.
Aber wenn Du beim Fotografieren genaue, oder zumindest immer gleiche Verschlusszeiten brauchst (10 Bilder mit je 1/320s o.ä.) dann würde ich lieber Delays nehmen.
Falls Dein Sketch etwas länger braucht, also z. Bsp. 50ms, dann wäre das genau Deine maximal mögliche Abweichung von Verschlusszeit zu Verschlusszeit. (1/320s = 3,125ms + 50ms = ~1/20s)

Gruß André

Für Zeitabstände verwendet man die Differenz eines gespeicherten millis()Wertes zum aktuellen millis()Wert.
Grüße Uwe

@SpaghettiCode
Die Steuerung steuert nicht die Belichtungszeit sondern den Moment des Auslösen des Fotos. Darum ist die Genauigkeit (auf die ms) der Zeiten nicht so wichtig.

Grüße Uwe

@ Uwe,
wenn es nur um den Zeitpunkt geht, hast Du natürlich Recht! Sorry. Ich war in Gedanken schon ein Stück weiter, da fotografieren mein anderes Hobby ist ...

Gruß André

Danke für das große Feedback. Ich tauche jetzt erst mal in die StateMachine ein. Ein heftiger Brocken, aber wahrscheinlich zelführend. Melde mich wieder, sobald es etwas zu vermelden gibt.

Das Konzept der StateMachine gefällt mir, und ich dachte, es verstanden zu haben. Alleine: Es funktioniert nicht. Im Serial Monitor werden zwar alle States durchlaufen, aber eine angeschlossene LED zeigt, dass die Intervalle nicht eingehalten werden. Es läuft viel zu schnell ab.

Der Code:

const byte triggerPin = 10; // Camera trigger

//Start time, used to calculate when this Timer has expired
unsigned long dummyTimerStart;
unsigned long carriageSettleTimer; 
unsigned long triggerTimer;
unsigned long latencyTimer;
unsigned long dummyTimerEnd;

unsigned long currentMillis;

//Interval/delay when/where this Timer expires
const unsigned long dummyStartInterval = 1000UL;    // 1000ms to simulate other code
const unsigned long carriageSettle = 500UL;         // 500ms
const unsigned long triggerOpen = 1000UL;           // trigger needs 2 ms in MF mode, 500 ms in AF
const unsigned long camLatency = 500UL;
const unsigned long dummyEndInterval = 1000UL;      // 1000ms to simulate other code

enum States
{
 StateDummyStart, StateCarriageSettle, StateTrigger, StateLatency, StateDummyEnd
};

States mState = StateDummyStart;

void setup() {
 pinMode (triggerPin, OUTPUT);
//  Serial.begin(57600);
}

void loop() {
 currentMillis = millis();
 StateMachine();    
}

//                    StateMachine()
//======================================================================
void StateMachine()
{
 switch (mState)
 {
   //******************************************
   case StateDummyStart:
     if (CheckTimer(dummyTimerStart, dummyStartInterval, true)) {
//        Serial.println("StartDummy over");
       mState = StateCarriageSettle;
     }

     break;

   //******************************************
   case StateCarriageSettle:
     if (CheckTimer(carriageSettleTimer, carriageSettle, true)) {
//        Serial.println("Carriage settle over");       
       digitalWrite (triggerPin, HIGH);
//        Serial.println("Open trigger");        
       mState = StateTrigger;
     }

     break;

   //******************************************
   case StateTrigger:
     if (CheckTimer(triggerTimer, triggerOpen, true)) {
       digitalWrite (triggerPin, LOW);
//        Serial.println("Close trigger");        
       mState = StateLatency;
     }

     break;

   //******************************************
   case StateLatency:
     if (CheckTimer(latencyTimer, camLatency, true)) {
//        Serial.println("Latency over");        
       mState = StateDummyEnd;
     }

     break;

   //******************************************
   case StateDummyEnd:
     if (CheckTimer(dummyTimerEnd, dummyEndInterval, true)) {
//        Serial.println("End Dummy over");        
       mState = StateDummyStart; //now switching to State StateCarriageSettle
     }

     break;
  
   //******************************************
   default:
     // default code goes here
     break;

 }

} // END StateMachine

// Timer for actions needed 'after' a period of time.
boolean CheckTimer(unsigned long &lastMillis, const unsigned long Interval, boolean restart) {

 // lastMillis = the time this "timer" was (re)started
 // Interval   = interval/delay needed.
 // restart    = do we restart this timer again and again?

 // has this timer expired?
 if (currentMillis - lastMillis >= Interval) {
 
   //should this timer start again?
   if (restart == true) {
     lastMillis = currentMillis;  //get ready for the next iteration
   }

   //This Timer did expire
   return true;
 }

 return false;

} // END CheckTimer()

Hallo,

du musst dir für die Sache lastmillis für jeden Zustand separat merken und vergleichen.
Sonst setzt irgendein gültiger Zustand die gemerkte Zeit auf aktuell für den nächsten ...
Die Parameterübergabe true ist übrigens überflüssig.

Das überflüssige "true" leuchtet mir ein. Den Hauptsatz jedoch verstehe ich nicht. Die Zähler sind doch separiert, und zum nächsten State geht es erst nach Ablauf des vorigen. Klappt jedoch nicht, wie ich zugeben muss.

Hallo,

war vorhin auf dem falschen Pfad. Du musst dir last_ms nicht für jedes einzeln merken sondern man muss es für den nächsten Vergleich nachziehen sprich aktualisieren. Sonst sehen alle anderen die alten last_ms und sind immer gültig und rasseln deswegen durch. Habs mal umgebaut.

// https://forum.arduino.cc/index.php?topic=584509.0

const byte triggerPin = 10; // Camera trigger

unsigned long currentMillis;
unsigned long last_ms;

enum t_state {
  START, SETTLE, TRIGGER, LATENCY, END
};
t_state mState = START;

//Interval/delay when/where this Timer expires
const unsigned int dummyStartInterval = 1000;    // 1000ms to simulate other code
const unsigned int carriageSettle = 500;         // 500ms
const unsigned int triggerOpen = 1000;           // trigger needs 2 ms in MF mode, 500 ms in AF
const unsigned int camLatency = 500;
const unsigned int dummyEndInterval = 1000;      // 1000ms to simulate other code



void setup() {
  Serial.begin(9600);
  pinMode (triggerPin, OUTPUT);
}

void loop() {
  currentMillis = millis();
  StateMachine();
}

void StateMachine()
{
  switch (mState) {
    case START:
        if (checkTimer(dummyStartInterval) ) {
          Serial.println(F("SETTLE"));
          last_ms = currentMillis;
          mState = SETTLE;
        }
        break;

    case SETTLE:
        if (checkTimer(carriageSettle) ) {
          digitalWrite (triggerPin, HIGH);
          Serial.println(F("TRIGGER"));
          last_ms = currentMillis;
          mState = TRIGGER;
        }
        break;

    case TRIGGER:
        if (checkTimer(triggerOpen) ) {
          digitalWrite (triggerPin, LOW);
          Serial.println(F("LATENCY"));
          last_ms = currentMillis;
          mState = LATENCY;
        }
        break;

    case LATENCY:
        if (checkTimer(camLatency) ) {
          Serial.println(F("END"));
          last_ms = currentMillis;
          mState = END;
        }
        break;

    case END:
        if (checkTimer(dummyEndInterval) ) {
          Serial.println(F("START"));
          last_ms = currentMillis;
          mState = START;
        }
        break;

    default: Serial.println(F("switch case ungültig")); // sollte nie eintreten
        break;

  }

} // END StateMachine


bool checkTimer(unsigned int interval)
{
  bool state = false;

  if (currentMillis - last_ms >= interval) {
    last_ms = currentMillis;
    state = true;
  }
  return state;
}

Hammer! Es klappt perfekt. Und das Beste daran - ich verstehe es sogar. Und ich war schon kurz davor, aufzugeben.

1000 Dank für Deine Hilfe

Hallo,

immer wieder schön wenn es auch nachvollzogen werden kann. Das ist das Wichtigste bei der Sache. :slight_smile:

Das ist das Wichtigste bei der Sache.

Ja!

Dennoch behagt mir folgendes nicht:

const unsigned int carriageSettle = 500;
Die Zeit wird mit unsigned long verglichen.
Also Äpfel, mit Birnen.
Ja, es gelingt hier.
Solange die Zeiten unter 63 Sekunden sind.

Hier mal eine Variante, welche etwas Speicherhungeriger ist, aber im Grunde das gleiche tut:

#include <TaskMacro.h>
// Macros aus: https://forum.arduino.cc/index.php?topic=415229.msg2859554

const byte triggerPin = 10; // Camera trigger

//Interval/delay when/where this Timer expires
const unsigned long dummyStartInterval = 1000UL;    // 1000ms to simulate other code
const unsigned long carriageSettle     =  500UL;    // 500ms
const unsigned long triggerOpen        = 1000UL;    // trigger needs 2 ms in MF mode, 500 ms in AF
const unsigned long camLatency         =  500UL;
const unsigned long dummyEndInterval   = 1000UL;      // 1000ms to simulate other code

bool fotoMerker = false; // Uebergabe Merker, startet den Fotoautomaten, verhindert Motorlauf


Task fotoAutomat()
{
  taskBegin();
  while(1)
  {
    // Schritt warte auf Uebergabemerker belegt (Foto Anforderung)
    taskWaitFor(fotoMerker);
    Serial.println("Fotoautomat gestartet "); 

    // Schritt warte carriageSettle
    taskPause(carriageSettle);
    Serial.println("carriageSettle abgelaufen "); 

    // Schritt Trigger begin
    digitalWrite(triggerPin, HIGH);

    // Schritt warte triggerOpen
    taskPause(triggerOpen);
    Serial.println("triggerOpen abgelaufen "); 

    // Schritt Trigger ende
    digitalWrite(triggerPin, LOW);

    // Schritt warte camLatency
    taskPause(camLatency);
    Serial.println("camLatency abgelaufen "); 


    // Schritt Foto fertig, Ubergabemerker zuruecksetzen
    fotoMerker = false;
    
    // und wieder von vorn
  }
  taskEnd();
}

Task motorAutomat()
{
  taskBegin();
  while(1)
  {
    // Schritt warte auf Uebergabemerker frei (Fotoautomat fertig)
    taskWaitFor(not fotoMerker);
    Serial.println("Motor fährt "); 

    // Schritt warte Motor fertig
    taskPause(dummyStartInterval+dummyEndInterval);
    Serial.println("Motor ist angekommen "); 

    fotoMerker = true;
    Serial.println("Nachricht an Fotoautomat gesendet "); 

    // und wieder von vorn
  }
  taskEnd();
}

Task blinkAutomat() // blinkt vor sich hin, um zu zeigen, dass alle Automaten Zeit bekommen
{
  taskBegin();
  while(1)
  {
    digitalWrite(LED_BUILTIN,HIGH);
    taskPause(500);
    digitalWrite(LED_BUILTIN,LOW);
    taskPause(500);
  } 
  taskEnd();
}


void setup() 
{
  Serial.begin(9600);
  Serial.println("Start");
  pinMode(LED_BUILTIN,OUTPUT);
  pinMode(triggerPin,OUTPUT);
}

void loop() 
{
  fotoAutomat();
  motorAutomat();
  blinkAutomat();
}

Zusätzlich zum Fotoautomaten hat es noch:

  1. einen Motorautomaten, welche eine Schlittenfahrt simuliert
  2. und einen Blinkautomaten, welcher als Herzschlag fungiert, also zeigt, das der µC noch "lebt".

Die Synchronisation, die gegenseitige Verriegelung der Tasks, erfolgt über einen Merker

combie:
Ja!

Dennoch behagt mir folgendes nicht:

const unsigned int carriageSettle = 500;
Die Zeit wird mit unsigned long verglichen.
Also Äpfel, mit Birnen.
Ja, es gelingt hier.
Solange die Zeiten unter 63 Sekunden sind.

Ich nehme immer den Datentyp wo mein größtmöglicher Wert reinpasst den ich verwende. Warum soll man für kleine Werte den größten Datentyp verwenden? Würde im Umkehrschluss bedeuten man müsste immer long bzw. unsigned long verwenden. Halte ich für nicht Zielführend.

man müsste immer long bzw. unsigned long verwenden.

Immerhin wird (millis()-prev > interval) in unsigned long berechnet.
Aber im Grunde hast du recht und kannst dich drauf verlassen, dass dein short interval richtig erweitert wird.

Auch das alternative Konzept wirkt sehr aufgeräumt und überzeugend - und bildet wohl ebenfalls eine State Machine. Ich, als C-Neuling, möchte den Ball jedoch anfangs flach halten. Speicherbedarf ist bei meiner Anwendung ein sehr kritischer Faktor. Die (mehrsprachige) Menüsteuerung (LCD/Rotary Encoder) nagt schon gewaltig daran.

Zum Konzept der State Machine habe ich noch zwei Fragen.

  1. Ich plane mehrere Betriebsarten, deren Ablauf grundverschieden sein wird. (schrittweise, gleiten, Dolly Zoom Fahrt, Macro Mode). Sehe ich das richtig, dass man in diesem Fall unterschiedliche State Machines für die Betriebsarten definiert - oder ist es besser, nur eine State Machine zu definieren, die so abstrahiert ist, dass sie zu allen Modi passt?

  2. Ich nutze AccelStepper. Dieser benötigt ein "stepper.run();" im Loop. Gehört auch dieser in die State Machine, oder sollte man ihn außerhalb platzieren?

Zu 1: Da ich nicht weiß, wie sich Dolly Zoom Fahrt von Macro Mode unterscheidet, kann ich Dir keine Empfehlung geben. Die Frage ist, was "grundverschieden" aus Programmierersicht bedeutet.

Ganz allgemein: Eine Schrittkette, ich denke an switch/case, kann zwischen den Schritten frei "springen". Also Schritt 7 kann nach 3 kommen. Man kann also auch grundverschiedene Dinge in eine Schrittkette packen. Bei vielen Überschneidungen, also doch gleichen Schritten, würde ich das in Erwägung ziehen. Sonst wird es aber so lang und unübersichtlich wie Spagetticode, was man sicherlich nicht möchte.

Ich würde mit einer sprechenden Festlegung der Namen in enum und einer Schrittkette anfangen. Aufteilen geht immer noch.

Zu 2: Außerhalb!

Leider eines der nicht ordentlich beendeten Themen, aber möglicherweise kannst Du dennoch was abschauen: Steuerung eines Kamerasliders