Mehrere Schwellenwerte und LED Anzeige

Es geht um die Gesamtbeschleunigung von kurzen Erschütterungen, gemessen mit einem Beschleunigungssensor. Auf WOKWI durch ein Schiebepotentiometer ersetzt. Hier für's Prinzip mit nur drei Schwellenwerten, später sollen es sechs sein. Beim Überschreiten von einem der drei Schwellenwerte soll nur die entsprechende LED kurz aufleuchten, die anderen nicht. Das funktioniert, aber nur, wenn man den Schieberegler sehr schnell bewegt, andernfalls bleibt die gelbe LED an, oder auch eine andere. Wo liegt der Fehler? Danke!

const int pinAccel = A0; // simulierte Gesamtbeschleunigung
const int pinsLED[] = {11, 12, 13}; // gelb, orange, rot
const int threshold0 = 300;
const int threshold1 = 600;
const int threshold2 = 900;
const unsigned long timeWait = 150;
const unsigned long timeLEDOn = 300;

int mAV_EMA = 0;
int previousThreshold = -1;
unsigned long timerStart;
unsigned long timerLED;

void setup()
{
  Serial.begin(9600);

  for (byte i = 0; i < 3; i++)
  {
    pinMode(pinsLED[i], OUTPUT);
  }
}

void loop()
{
  unsigned long timeNow = millis();

  mAV_EMA = analogRead(pinAccel);

  int threshold = detectThreshold(mAV_EMA);

  if (threshold >= 0) // falls 0 überschritten
  {
    previousThreshold = threshold; // kann 0, 1 oder 2 sein
    timerStart = timeNow;//millis(); // Zeitpunkt merken

    while ((millis() - timerStart) < timeWait) // warten
    {
      mAV_EMA = analogRead(pinAccel);
      threshold = detectThreshold(mAV_EMA);

      if (threshold > previousThreshold) // gibt es einen Höheren?
      {
        previousThreshold = threshold; // falls ja, merken
        timerStart = timeNow; // Zeitpunkt merken
      }
    }
    Serial.print("Warten vorbei "); Serial.println(previousThreshold);

    digitalWrite(pinsLED[previousThreshold], HIGH); // nach Ablauf der Wartezeit nur die entsprechende LED einschalten
    timerLED = timeNow; // Zeitpunkt merken
  }

  if ((millis() - timerLED) >= timeLEDOn) // nach Ablauf der LED-an-Zeit wieder ausschalten
  {
    digitalWrite(pinsLED[previousThreshold], LOW);
  }

  delay(10);
}

int detectThreshold(int accel)
{
  if (mAV_EMA >= threshold2) return 2;
  if (mAV_EMA >= threshold1) return 1;
  if (mAV_EMA >= threshold0) return 0;
  return -1;
}

Was mir auffällt ist, daß timeNow innerhalb while nicht aktualisiert wird.

timerStart = millis(); // timeNow; ändert nichts am Verhalten.

Moin @Lagom,

auf jeden Fall hast Du ein Problem mit dem Ausschalten der LEDs:

Die Variable previousThreshold hat beim Start des Programms den Wert -1 und wird als Index für das Array der Led-Pins genutzt.

Insgesamt soll die passende Led für 300 ms leuchten, während aber bereits die nächste Auswertung läuft, in der die Variable previousThreshold einen abweichenden Wert erhalten kann. Dadurch indiziert sie I.d.R. nicht mehr die zuvor eingeschaltete Led.

Du könntest z.B. nach Anzeige eines lokalen Maximums die Auswertung für die Anzeigezeit unterbinden. Eine andere Möglichkeit wäre es, beim Überschreiten einer Schwelle kurz zu blinken und erst wieder auszulösen, wenn diese Schwelle zuvor unterschritten wurde. Also ähnlich wie beim Softwareseitigen Debouncing.

1 Like

Hallo Lagom

Hier kommt ein Stück Pseudo Code zum Vermessen eines Eingangssignals.
Dieser Pseudo Code muss nur noch mit "Leben" gefüllt werden.
Hierbei ist zu Bedenken, dass der Code n-mal, hier 6, benötigt wird.
Ich empfehle darum den Code objekt-orientiert zu programmieren.

//** pseudo code **

switch (pinAccel)
{
case 0 ... threshold0:
  {
    reset all triggers
  }
  break;
case threshold1 ... threshold2:
  {
    if (trigger1 is not set)
    {
      set trigger1
      start LED1
    }
  }
  break;
case threshold2 ... threshold3:
  {
    if (trigger2 is not set)
    {
      reset LED1
      set trigger2
      start LED2
    }
  }
  break;
case threshold3 ... threshold4:
  {
    if (trigger3 is not set)
    {
      reset LED1
      reset LED2
      set trigger3
      start LED3
    }
  }
  break;
}

.
.
.
and so on

** pseudo code end

Danke, ja, mit einer state machine funktioniert es (am Testaufbau), außer man läßt den Schieberegler stehen, oder zieht ihn langsam zurück, siehe WOKWI - was bei den echten Erschütterungen, um die es mir geht (Boxschläge wie jab, crochet, left hook, right cross), nicht vorkommen kann (über 150 Millisekunden kam auch kein Anfänger mit weniger explosivem Schlag). Ich möchte es nur gerne auch mit if-Konstruktionen hinbekommen. Lerneffekt ; )

const int pinAccel = A0; // simulierte Gesamtbeschleunigung
const int pinsLED[] = {11, 12, 13}; // gelb, orange, rot
const unsigned long timeLEDOn = 300;

const int threshold0 = 300;
const int threshold1 = 600;
const int threshold2 = 900;
const unsigned long timeWait = 150;

enum {START, WAIT, STOP} state;

int previousThreshold = -1;
unsigned long timerStart;

void setup()
{
  Serial.begin(9600);

  for (byte i = 0; i < 3; i++)
  {
    pinMode(pinsLED[i], OUTPUT);
  }
}

void loop()
{
  unsigned long timeNow = millis();

  int mAV_EMA = analogRead(pinAccel); // float für Beschleunigungssensor

  int threshold = detectThreshold(mAV_EMA);

  switch (state)
  {
    case START:
      if (threshold >= 0) // falls 0 überschritten
      {
        previousThreshold = threshold; // kann 0, 1 oder 2 sein
        timerStart = timeNow; // Zeitpunkt merken
        state = WAIT; // in loop() sofort zu WAIT
      }
      break; // ab in den loop()

    case WAIT:
      if (threshold > previousThreshold) // gibt es einen Höheren?
      {
        previousThreshold = threshold; // falls ja, merken
        timerStart = timeNow; // Zeitpunkt merken
      }

      if (timeNow - timerStart >= timeWait) // Wartezeit vorbei?
      {
        digitalWrite(pinsLED[previousThreshold], HIGH); // nur die entsprechende LED einschalten
        timerStart = timeNow; // Einschaltzeitpunkt merken
        state = STOP; // in loop() sofort zu STOP
      }
      break; // ab in den loop()

    case STOP:
      if (timeNow - timerStart >= timeLEDOn) // LED-an-Zeit vorbei?
      {
        digitalWrite(pinsLED[previousThreshold], LOW); // LED wieder ausschalten
        state = START; // Alles wieder von vorne
      }
      break; // ab in den loop()
  }

  delay(10);
}

int detectThreshold(float mAVEMA)
{
  if (mAVEMA >= threshold2) return 2;
  if (mAVEMA >= threshold1) return 1;
  if (mAVEMA >= threshold0) return 0;
  return -1;
}

Ein paar Hinweise:

  • Hast Du Messwertreihen der später einzusetzenden Sensoren verfügbar? Häufig wird mit Annahmen gearbeitet, die in der Realität dann nicht zutreffen oder es gibt zumindest unerwartete Abweichungen, die die Algorithmik nicht abfängt. So erspart man sich, viel Zeit in die falsche Richtung zu investieren.
  • Insgesamt spielen Abtastrate, Auflösung und dabei ggf. auch Messfehler eine Rolle bei der Auswertung. In der digitalen Welt haben wir keine kontinuierliche Messkurve sondern nur zeitlich getaktete Abtastwerte zur Verfügung. Je nach dem realen Prozess können gegenüber der Abtastung sehr schnelle Prozesse zwischen den Messungen "verschwinden" oder bei unangepasster Abtastrate ein nicht zutreffender Verlauf der realen Prozesse ermittelt werden.

Mit anderen Worten:

  • Wie lange dauern die zu messenden Prozesse?
  • Wie groß sind die zeitlichen Abständen zwischen zwei zu messenden Events?

Hieraus lassen sich die erforderliche Abtastrate und die folgende Auswertung ermitteln.

Z.B. könnte das Ergebnis sein, dass man eine Folge von Messwerten mit festem Zeittakt in ein Array (noch zu definierender Größe) schreiben und die Auswertung anhand des Werteverlaufs durchführten muss, um zu gesicherten Ergebnissen zu kommen.

Das A&O sind in jedem Fall reale Messwerte ...

Gruß
ec2021

Über 500 Erschütterungen gemessen. Ein trainierter Hobbyboxer schafft drei Schläge pro Sekunde, Erschütterungsdauer unter 150 Millisekunden. Der Beschleunigungssensor steckt nicht in einem speed bag, sondern einer target mat. Das Ganze funktioniert per state machine gut, aber nun möchte ich es in eine reine if-statement basierte Lösung übersetzen.

Darf man fragen warum?

Switch case wurde erfunden, um if Orgien zu vermeiden.

1 Like

Dem schließe ich mich an.
Der Controller selbst kennt nur Bedingte Sprünge (BRNE usw., wenn's ein avr ist). Ob man die Logik per if - else oder switch - case menschenlesbar darstellt, ist eigentlich wurscht.

Eine "Orgie" wird nicht nötig sein, die Menge an Code beider Lösungen dürfte sehr ähnlich sein. Aber der Lerneffekt ist erheblich, wenn man beide Lösungen nebeneinander stellt.

Stimmt, das kannst Du hier sehen:

https://wokwi.com/projects/407465097953318913

/*
  Wokwi: https://wokwi.com/projects/407465097953318913
  Forum: https://forum.arduino.cc/t/mehrere-schwellenwerte-und-led-anzeige/1295931

  Wowki (State Machine): https://wokwi.com/projects/406583203546521601

  2024/08/28
  ec2021

*/

// ASE Edgar Bonet concept
const int pinAccel = A0; // simulierte Gesamtbeschleunigung
const int pinsLED[] = {11, 12, 13}; // gelb, orange, rot
const unsigned long timeLEDOn = 300;

const int threshold0 = 300;
const int threshold1 = 600;
const int threshold2 = 900;
const unsigned long timeWait = 150;

enum {START, WAIT, STOP} state;

int previousThreshold = -1;
unsigned long timerStart;
boolean detectStart = true;
boolean startFound = false;
boolean maximumFound = false;

void setup()
{
  Serial.begin(115200);

  for (byte i = 0; i < 3; i++)
  {
    pinMode(pinsLED[i], OUTPUT);
  }
}

void loop()
{
  unsigned long timeNow = millis();

  int mAV_EMA = analogRead(pinAccel); // float für Beschleunigungssensor

  int threshold = detectThreshold(mAV_EMA);

  // Entspricht dem "START" der State Machine
  if (detectStart && threshold >= 0) // falls 0 erstmalig überschritten
  {
    previousThreshold = threshold; // kann 0, 1 oder 2 sein
    timerStart = timeNow; // Zeitpunkt merken
    startFound = true;
    detectStart = false;
    maximumFound = false;
  }


  // Entspricht dem "WAIT" der State Machine
  if (startFound) {
    if (threshold > previousThreshold) // gibt es einen höheren Wert nach erstmaligem Überschreiten?
    {
      previousThreshold = threshold; // falls ja, merken
      timerStart = timeNow; // Zeitpunkt merken
    }
    if (startFound && timeNow - timerStart >= timeWait) // Wartezeit vorbei?
    {
      digitalWrite(pinsLED[previousThreshold], HIGH); // nur die entsprechende LED einschalten
      timerStart = timeNow; // Einschaltzeitpunkt merken
      startFound = false;
      maximumFound = true;
    }
  }
  // Entspricht dem "STOP" der State Machine
  if (maximumFound && timeNow - timerStart >= timeLEDOn) // LED-an-Zeit vorbei?
  {
    digitalWrite(pinsLED[previousThreshold], LOW); // LED wieder ausschalten
    maximumFound = false;
    detectStart = true;
  }
  delay(10);
}

int detectThreshold(float mAVEMA)
{
  if (mAVEMA >= threshold2) return 2;
  if (mAVEMA >= threshold1) return 1;
  if (mAVEMA >= threshold0) return 0;
  return -1;
}

Letztlich nur eine "verkappte" State Machine. Kann man für "didaktische Zwecke" sicher so machen, allerdings bleibt die State-Machine-Lösung wohl übersichtlicher und bei Erweiterungen/Anpassungen weniger fehleranfällig.

Ein trainierter Hobbyboxer schafft drei Schläge pro Sekunde, Erschütterungsdauer unter 150 Millisekunden.

Beim aktuellen Timing der State Machine (und des Nachbaus mit If) sieht das m.E. so aus:

image

(1) = Schwellwert wurde überschritten (0..2)
(2) = Innerhalb der folgenden 150 ms wird kein höherer Schwellwert vermessen; die zugehörige Led wird eingeschaltet
(3´) = Nach Ablauf von 300 ms wird die Led ausgeschaltet

  • In den Phasen START und WAIT werden die Erschütterungen gemessen, in die Klassen -1 bis 2 umgesetzt und verarbeitet.
  • In der Phase STOP wird auf das Ende der Led-EInschaltdauer gewartet; zwar wird weiter gemessen und in die Werte -1 bis 2 umgerechnet; die Werte werden jedoch nicht weiter genutzt.

Ein weiterer Schlag (Links-Rechts-Kombination) in der Phase STOP bleibt - wenn ich mich nicht täusche - unberücksichtigt.

Abhilfe würde hier schaffen, die Led-Behandlung von der Messung und Auswertung zu entkoppeln ...

Und hier mal eine solche Möglichkeit (bei der die State Machine dann auch kaum noch erforderlich wäre):

(siehe https://wokwi.com/projects/407469487863527425 )

Sketch
/*
  Wokwi: https://wokwi.com/projects/407469487863527425
  Forum: https://forum.arduino.cc/t/mehrere-schwellenwerte-und-led-anzeige/1295931

  Wowki (State Machine): https://wokwi.com/projects/406583203546521601

  2024/08/28
  ec2021

*/

// ASE Edgar Bonet concept
const int pinAccel = A0; // simulierte Gesamtbeschleunigung
const int pinsLED[] = {11, 12, 13}; // gelb, orange, rot
const unsigned long timeLEDOn = 300;

struct ledType  {
  byte pin;
  byte state;
  unsigned long startTime;
  void on(unsigned long start) {
    digitalWrite(pin, HIGH);
    startTime = start;
    state = HIGH;
  };
  void off() {
    digitalWrite(pin, LOW);
    state = LOW;
  };
  void autoOff(unsigned long actTime) {
    if (!state) return;
    if (actTime - startTime > timeLEDOn) {
      off();
    }
  };
};

ledType leds[] = {
  {11, LOW, 0},
  {12, LOW, 0},
  {13, LOW, 0}
};

const int noOfLeds = sizeof(leds) / sizeof(ledType);

const int threshold0 = 300;
const int threshold1 = 600;
const int threshold2 = 900;
const unsigned long timeWait = 150;

enum {START, WAIT, STOP} state;

int previousThreshold = -1;
unsigned long timerStart;

void setup()
{
  Serial.begin(9600);

  for (auto led : leds)
  {
    pinMode(led.pin, OUTPUT);
  }
}

void loop()
{
  unsigned long timeNow = millis();

  int mAV_EMA = analogRead(pinAccel); // float für Beschleunigungssensor

  int threshold = detectThreshold(mAV_EMA);

  switch (state)
  {
    case START:
      if (threshold >= 0) // falls 0 überschritten
      {
        previousThreshold = threshold; // kann 0, 1 oder 2 sein
        timerStart = timeNow; // Zeitpunkt merken
        state = WAIT; // in loop() sofort zu WAIT
      }
      break; // ab in den loop()

    case WAIT:
      if (threshold > previousThreshold) // gibt es einen Höheren?
      {
        previousThreshold = threshold; // falls ja, merken
        timerStart = timeNow; // Zeitpunkt merken
      }

      if (timeNow - timerStart >= timeWait) // Wartezeit vorbei?
      {
        switchLedOn(previousThreshold, timeNow);
        state = START; // in loop() sofort zu START
      }
      break; // ab in den loop()
  }
  handleLeds();
  delay(10);
}

int detectThreshold(float mAVEMA)
{
  if (mAVEMA >= threshold2) return 2;
  if (mAVEMA >= threshold1) return 1;
  if (mAVEMA >= threshold0) return 0;
  return -1;
}

void switchLedOn(int No, unsigned long startTime) {
  if (No > -1 && No < noOfLeds) {
    leds[No].on(startTime); // nur die entsprechende LED einschalten
  }
}

void handleLeds() {
  for (auto led : leds)
  {
    led.autoOff(millis());
  }
}

Wie gesagt, nur als Anschauungsobjekt ....

Gruß
ec2021

Danke, ich denke, das wird es wohl sein, wenn man es ohne state machine sozusagen "zu Fuß" mit if-Konstruktion usw. machen will. Und Fußarbeit ist beim Boxen wichtig ; )

Das geschilderte Verhalten ist auch in Deiner State Machine-Lösung zu finden...

Es liegt daran, dass das Warten auf die Abschaltung der Led ein eigener,"state" ist, der keine Behandlung der Messwerte zulässt, während er aktiv ist.

Schau Dir gerne meinen letzten Code auf Wokwi an, dort sind state machine und das Ausschalten der LEDs getrennt. Das lässt sich natürlich auch auf die if-Lösung umsetzen. Habe ich aber nicht getan. Die weiter oben gepostete if-Umsetzung ist eine 1:1 Umsetzung der State Machine mit dem gleichen Verhalten (300 ms Wartezeit).

Die Art der Umsetzung ist letztlich egal... Der wesentliche Vorteil von State Machines ist die Übersichtlichkeit.

Viel Erfolg!
ec2021

P.S.: Um regelmäßige Aktivitäten einzubauen, kann man diese vor bzw. nach der State Machine gleich mit einbauen; hier nur das Prinzip:

enum  States {STATE1, STATE2, STATE3};
States state = STATE1;

void setup() {
}

void loop() {
  stateMachine();
}

void stateMachine(){
  performEveryLoopBefore();
  switch(state){
    case STATE1:
        state = STATE2;
      break;
    case STATE2:
        state = STATE3;
      break;
    case STATE3:
        state = STATE1;
      break;
   default:
      state = STATE1;
  }
  performEveryLoopAfter();
}


void performEveryLoopBefore(){
  // Vor jedem State Machine-Durchlauf
}


void performEveryLoopAfter(){
  // Nach jedem State Machine-Durchlauf
}

Beides zusammen (vor- und nachher) wäre in diesem Beispiel natürlich nicht besonders sinnvoll, da außer der stateMachine() in loop() nichts Weiteres läuft. Das kann im Einzelfall anders sein.

Das Ganze könnte genauso auch so aussehen, hier mit einer separaten Funktion doSomeThingElse(); das ist aber alles "Geschmackssache":

enum  States {STATE1, STATE2, STATE3};
States state = STATE1;

void setup() {
}

void loop() {
  performEveryLoopBefore();
  stateMachine();
  performEveryLoopAfter();
  doSomethingElse();
}

void stateMachine(){
   switch(state){
    case STATE1:
        state = STATE2;
      break;
    case STATE2:
        state = STATE3;
      break;
    case STATE3:
        state = STATE1;
      break;
   default:
      state = STATE1;
  }
}


void performEveryLoopBefore(){
  // Vor jedem State Machine-Durchlauf
}


void performEveryLoopAfter(){
  // Nach jedem State Machine-Durchlauf
}

void doSomeThingElse(){
  // Bei jedem Loop
}

In den performEvery..() Funktionen kann man u,a, Vorbereitungen oder Nachbereitungen bei einem Zustandswechsel unterbringen oder - wie z.B. das zeitgesteuerte Ausschalten der Leds - von den Zuständen unabhängige Prozesse.

Etwas Zeit gehabt. Die if-Konstruktion funktioniert fast. Die LEDs werden nur eingeschaltet, wenn das Signal steigt. Wenn man den Schieberegler nicht zu schnell bewegt, leuchtet nur die LED des überschrittenen Schwellenwerts. Bewegt man ihn aber zu schnell, bleibt die dem Schwellenwert zugehörige LED an. Für zweckdienliche Hinweise wäre ich dankbar.

const int pinAccel = A0; // simulierte Gesamtbeschleunigung
const int pinsLED[] = {11, 12, 13}; // gelb (0), orange (1), rot (2)
const int threshold0 = 300;
const int threshold1 = 600;
const int threshold2 = 900;
const unsigned long timeWait = 150;
const unsigned long timeLEDOn = 300;

int mAV_EMA = 0;
bool signalRises = false;
int previousThreshold = -1;

unsigned long timerStart;
unsigned long timerLED;

void setup()
{
  Serial.begin(9600);

  for (byte i = 0; i < 3; i++)
  {
    pinMode(pinsLED[i], OUTPUT);
  }
}

void loop()
{
  mAV_EMA = analogRead(pinAccel);
  int threshold = getThreshold(mAV_EMA); // kann 0, 1 oder 2 sein

  if (threshold >= 0) // falls 0 überschritten
  {
    timerStart = millis(); // Zeitpunkt merken

    while ((millis() - timerStart) < timeWait) // warten
    {
      mAV_EMA = analogRead(pinAccel);
      threshold = getThreshold(mAV_EMA);

      if (threshold > previousThreshold) // gibt es einen Höheren?
      {
        previousThreshold = threshold; // falls ja, merken
        signalRises = true; // Signal steigt
        timerStart = millis(); // Zeitpunkt merken
      }

      if (((millis() - timerStart) >= timeWait) && signalRises) // nur bei steigendem Signal
      {
        digitalWrite(pinsLED[previousThreshold], HIGH); // nach Ablauf der Wartezeit nur die entsprechende LED einschalten
        timerLED = millis(); // Einschaltzeitpunkt merken
      }
    }

    signalRises = false;
    previousThreshold = threshold;
  }

  if ((millis() - timerLED) >= timeLEDOn) // nach Ablauf der LED-an-Zeit wieder ausschalten
  {
    digitalWrite(pinsLED[previousThreshold], LOW);
  }

  delay(10);
}

int getThreshold(int accel)
{
  if (mAV_EMA >= threshold2) return 2;
  if (mAV_EMA >= threshold1) return 1;
  if (mAV_EMA >= threshold0) return 0;

  return -1;
}

Da Du zum Ausschalten der Led stets die Variable previousThreshold benutzt, wird nur jeweils die zuletzt geschaltete Led behandelt.

Du musst das Abschalten der LED unabhängig von der Variablen previousThreshold programmieren...

Habe ich aber weiter oben schon detailliert beschrieben ... und Lösungen gepostet...

P.S.: Wenn Du eine schnelle, einfache Lösung suchst:

  • Schalte einfach erst alle Led aus, bevor die relevante Led eingeschaltet wird. Da der Einschaltzeitpunkt eh neu zugewiesen wird, passt er sowieso nicht mehr zum vorherigen Einschaltvorgang ...

Am Rande:
Magische Zahlen in Programmen machen irgendwas mit dem Karma.

Alternativ:

  for (byte pin:pinsLED)
  {
    pinMode(pin, OUTPUT);
  }

Leitsatz: Wo kein Index, da auch kein Index Überlauf.
Weder versehentlich, noch absichtlich.

Auch mich befremdet dieses Vorhaben.

Warum?

Im Endeffekt wird es sowieso eine Statemachine werden.
Egal wie gut man das auch zu verbergen sucht.

Zum Code von Post #17 siehe Post #4

Die Funktion

if ((millis() - timerLED) >= timeLEDOn) // nach Ablauf der LED-an-Zeit wieder ausschalten
  {
    digitalWrite(pinsLED[previousThreshold], LOW);
  }

wird auch hier nach jedem Neustart mit dem Array-Index -1 aufgerufen...