Delay in Unterfunktion vermeiden

OP #7364922
Lesenswert?

Hallo zusammen,

ich habe ein schnelles Hauptprogramm (durchlauf kleiner 5ms, passt für meinen Zweck). Ab und an rufe ich eine Funktion auf, die mir ein paar Daten per UART raushaut um ein Gerät fernzusteuern. Das Problem: Das Gerät ist langsam im verstehen, wenn ich zwei oder drei Befehle sende, muss dazwischen immer ein 500ms Delay.

Aktuell mach ich das so:

1
void PS_on(uint8_t status)
2
{
3
 EA_Init();
4
 HAL_Delay(500);
5
 EA_set_output(status);
6
}

Das ist natürlich doof für das Hauptprogramm, weil dann Eingaben liegen bleiben. Wie macht man sowas besser? Die Funktion mit einer static variable versehen? Also alle 5ms aufrufen und den zweiten Teil im counter aufrufen? Was wenn ich mal 5 Befehle schicken will und 5 Delays ersetzen möchte? Das wird doch dann schnell unübersichtlich.

1
void PS_on(uint8_t status)
2
{
3
 EA_Init();
4
 static uint8_t count = 0;
5
 if(count == 100)
6
   {
7
   EA_set_output(status);
8
   count = 0;
9
   }
10
}

Danke!

Beitrag #7364923 wurde von einem Moderator gelöscht.
Moderator (Firma: Titel) Persönliche Seite #7364927
Lesenswert?

Paul B. schrieb:

Wie macht man sowas besser?

Nennt sich hochtrabend "Zustandsautomat".

In der Praxis heißt das: es gibt kein delay() im Programm. Die Hauptschleife wird schnellstmöglich durchlaufen und Zeiten werden durch Abfragen eines Timerticks (= ein ms-Zähler) und Berechnen von Zeitdifferenzen ausgewertet.

Etwa so:

1
int tiNow, tiLastset;
2

3
void main()
4
{
5
   EA_Init();
6
   for (;;) {  // Hauptschleife andauernd schnellstmöglich durchlaufen
7
      tiNow = get_timertick();
8
      if (tiNow-tiLastset > 500) {
9
         EA_set_output(status);
10
         tiLastset = tiNow;
11
      }
12
   }
13
}

Und dann ist es ganz einfach, noch 10 weitere Zeiten einzubauen:

1
int tiNow, tiLastset;
2
int tiL1, tiL2....
3

4
void main()
5
{
6
   EA_Init();
7
   for (;;) {  // Hauptschleife dauernd schnellstmöglich durchlaufen
8
      tiNow = get_timertick();
9

10
      if (tiNow-tiLastset > 500) {
11
         EA_set_output(status);
12
         tiLastset = tiNow;
13
      }
14

15
      if (tiNow-tiL1 > 333) {
16
         mach_was_alle_drittelsekunde();
17
         tiL1 = tiNow;
18
      }
19

20
      if (tiNow-tiL2 > ...) {
21
         ....();
22
         tiL2 = tiNow;
23
      }
24

25
   }
26
}

Und jetzt ist es ganz einfach, nboch weitere Funktionalität dazuzubauen: da kommt einfach ein weiterer if-Zweig dazu.

1
int tiNow, tiLastset;
2
int tiL1, tiL2....
3

4
void main()
5
{
6
   EA_Init();
7
   for (;;) {  // Hauptschleife dauernd schnellstmöglich durchlaufen
8
      tiNow = get_timertick();
9

10
      if (tiNow-tiLastset > 500) {
11
         EA_set_output(status);
12
         tiLastset = tiNow;
13
      }
14

15
      if (tiNow-tiL1 > 333) {
16
         mach_was_alle_drittelsekunde();
17
         tiL1 = tiNow;
18
      }
19

20
      if (tiNow-tiL2 > ...) {
21
         ....();
22
         tiL2 = tiNow;
23
      }
24
   
25
      if (eingang == aktiv) {
26
         reagiere_auf_eingang();
27
      }
28

29
      if (Zeichen_an_serieller_Schnittstelle_angekommen()) {
30
        zeichen = Hole_Zeichen_von_serieller_Schnittstelle_ab();
31
      }
32
      
33
      :
34
      :
35
      :
36

37
   }
38
}
#7364946
Lesenswert?

Paul B. schrieb:

Das Problem: Das Gerät ist langsam im verstehen, wenn ich zwei oder drei Befehle sende, muss dazwischen immer ein 500ms Delay.

Sowas gehört sich nicht. Derjenige, der das verzapft hat, muß ein blutiger Anfänger gewesen sein. Befehle werden direkt nach dem Empfang geparst. Benötigt die Befehlsausführung längere Zeit, dann kann der Master Statusabfragen senden, die mit "Busy" beantwortet werden.

Wie schon gesagt wurde, sendet man längere Sequenzen als Zustandsmaschine. Jeder Befehl ist dann ein Zustand. Die Befehle werden in den UART-FIFO gestellt, d.h. die Zustandsmaschine muß nicht bei jedem Byte warten. Feste lange Wartezeiten kann man über einen Sheduler ablaufen lassen bzw. über einen durchlaufenden Timer. Sinnvoller ist allerdings, wenn der Slave abgefragt werden kann, ob er wieder bereit ist. Feste lange Wartezeiten sind immer nur eine Notlösung, wenn das Protokoll schon vergurkt ist.

#7364951
Lesenswert?

Peter D. schrieb:

Das Problem: Das Gerät ist langsam im verstehen, wenn ich zwei oder drei Befehle sende, muss dazwischen immer ein 500ms Delay.

Sowas gehört sich nicht. Derjenige, der das verzapft hat, muß ein blutiger Anfänger gewesen sein.

Es wird dich vielleicht überraschen, aber es muss immer eine maximale Geschwindigkeit bzw minimale Periodendauer geben. Ob die jetzt 500ms, 500µs oder 500ns ist, ist erst einmal unerheblich und sagt keineswegs etwas über die Erfahrung des Entwicklers aus.

Moderator (Firma: Titel) Persönliche Seite #7364955
Lesenswert?

MaWin O. schrieb:

aber es muss immer eine maximale Geschwindigkeit bzw minimale Periodendauer geben. Ob die jetzt 500ms, 500µs oder 500ns ist, ist erst einmal unerheblich und sagt keineswegs etwas über die Erfahrung des Entwicklers aus.

Aber wie der dann die diese Aufgabe mit der langsamen Hardware löst, das sagt durchaus was aus.

Denn wenn dieses langsame Gerät halbwegs brauchbar realisiert ist, dann kann man es immer anpingen, aber es meldet einfach über die serielle Schnitte zurück, dass es derzeit noch keine neuen Befehle empfangen kann.

OP #7364969
Lesenswert?

Danke für die ausführlichen Antworten!

Lothar M. schrieb:

1
> void main()
2
> {
3
>    EA_Init();
4
>    for (;;) {  // Hauptschleife dauernd schnellstmöglich durchlaufen
5
>       tiNow = get_timertick();
6
>       if (tiNow-tiLastset > 500) {
7
>          EA_set_output(status);
8
>          tiLastset = tiNow;
9
>       }

Das Programm schickt den "Init" Befehl in Endlosschleife und alle 500ms den set_Output Befehl. Soweit so gut. Aber damit habe ich doch immer noch das Problem, dass alle ich das Gerät quasi überlaste.

Was ich suche ist ja nicht der zyklische Aufruf, dass mache ich im Hauptprogramm so wie es in der von Falk verlinkten Seite gezeigt ist. Da läuft ein 1ms Timer und fragt die Flags ab.

Evetuell ist das Beispiel auch schlecht gewählt, lasst uns daher etwas einfacheres nehmen.

Angenommen ich habe eine Funktion die mir 5 LEDs mit 500ms verzögerung anschaltet und wieder ausschaltet.

1
void LEDs()
2
{
3
LED 1 an
4
Delay 500
5
LED 2 an
6
Delay 500
7
LED 3 an 
8
....
9
}

Diese Funktion rufe ich nach einem Tasterdruck EINMAL auf, den ich in der Hauptschleife abfrage. Es wird also nicht zyklisch ausgeführt sondern nur bei Bedarf. Wie löse ich das Delay auf. Ich steh etwas auf dem Schlauch. Muss ich dann jedes mal den Status der vorherigen LED abfragen?

1
if (Tik > 500 & LED 1 == 1)
2
{
3
LED 2 an 
4
} 
5
usw?

Danke für eure Mühe!

#7364972
Lesenswert?

MaWin O. schrieb:

Es wird dich vielleicht überraschen, aber es muss immer eine maximale Geschwindigkeit bzw minimale Periodendauer geben.

Die maximale Geschwindigkeit sollte hautsächlich nur durch das Interface begrenzt sein. D.h. die Kommunikation und die Steuerung sollten parallel ablaufen und sich nicht gegenseitig blockieren können. Die Empfangspuffer sollten so groß sein, daß mindestens ein Steuerbefehl und ein Abfragekommando Platz haben. Ich mache es auch immer so, daß ich ADC-Werte reihum auslese, d.h. Abfragen können sofort mit dem letzten Meßwert aus dem RAM beantwortet werden. Und wenn eine Spannungsrampe gefahren werden soll, dann kann ich dabei ständig den Busy-Status abfragen.

Moderator (Firma: Titel) Persönliche Seite #7365012
Lesenswert?

Paul B. schrieb:

Das Programm schickt den "Init" Befehl in Endlosschleife

Nein. Die HW wird natürlich nur 1x beim Powerup initialisiert. Muss man ja nicht laufend machen.

und alle 500ms den set_Output Befehl. Soweit so gut. Aber damit habe ich doch immer noch das Problem, dass alle ich das Gerät quasi überlaste.

  1. Dann musst du eben 600ms oder 1000ms oder noch länger warten.
  2. Was ist das für ein lahmes "Gerät"?

Paul B. schrieb:

Angenommen ich habe eine Funktion die mir 5 LEDs mit 500ms verzögerung anschaltet und wieder ausschaltet. .... Diese Funktion rufe ich nach einem Tasterdruck EINMAL auf, den ich in der Hauptschleife abfrage. Es wird also nicht zyklisch ausgeführt sondern nur bei Bedarf. Wie löse ich das Delay auf. Muss ich dann jedes mal den Status der vorherigen LED abfragen?

Nein, du machts einen "Zustandszähler" und wenn der auf 0 stehts, dann fragst du den Taster ab. Wenn der Taster gedrückt ist, schaltest du den Zähler um 1 weiter.

Wenn der Zustand 1 ist, dann schaltest du die 1. LED ein. Wenn der Zustand 1 ist und die Zeit abgelaufen ist, dann schaltest du den Zunstand um 1 weiter.

Wenn der Zustand 2 ist, dann schaltest du die 2. LED ein. Usw. usf.

In SW sieht das z.B. so aus:

1
int zustand = 0; 
2
int tinow, tilast;
3
main()
4
   Init_HW();
5
   for(;;) {
6
      tinow = gettick();
7

8
      if (zustand==0) { // auf Taster warten
9
         tilast=tinow;
10
         if (taste_gedrückt()) {
11
            zustand=1;
12
            tilast=tinow;
13
         }
14
      }
15
      
16
      if (zustand==1) { // LED1 an
17
         LED1on();
18
         if (tinow-tilast > 500) {
19
            zustand=1;
20
            tilast=tinow;
21
         }
22
      }
23

24
      if (zustand==2) { // zusätzlich LED2 an
25
         LED2on();
26
         if (tinow-tilast > 500) {
27
            zustand=3;
28
            tilast=tinow;
29
         }
30
      }
31

32
      if (zustand==3) { // zusätzlich LED3 an
33
         LED3on();
34
         if (tinow-tilast > 500) {
35
            zustand=4;
36
            tilast=tinow;
37
         }
38
      }
39

40
      if (zustand==4) { // alles wieder aus
41
         LED1off();
42
         LED2off();
43
         LED3off();
44
         if (tinow-tilast > 500) {
45
            zustand=0;
46
         }
47
      }
48

49
   }
50
}
Moderator (Firma: Titel) Persönliche Seite #7365031
Lesenswert?

J. S. schrieb:

Anstelle der if-Kette würde ich das switch-case verwenden.

Mir fallen da auch noch ein paar Möglichkeiten ein, aber jetzt erst mal einen logischen Schritt nach dem anderen. Zuerst geht es allem voran um die Denkweise, dass man etwas nur dann macht, wenn es nötig ist und den Rest der Rechenzeit für was anderes verwendet. Denn die mainloop wird ist jetzt ja zigtausend mal pro Sekunde druchlaufen. Da kann man locker noch weitere Abfragen udn Zustandsautomaten dazuhängen.

Und wie man das hinterher tatsächlich implementiert, das bleibt dem jeweiligen Wissensstand überlassen.

Gast #7365055
Lesenswert?

Paul B. schrieb:

Das wird doch dann schnell unübersichtlich.

Ich mache gleichzeitig folgendes: Kartoffeln kochen, Wäsche bügeln, Musik geniessen, Akkus laden, das Wetter erleben, und vieles mehr...

Wo soll ich da das "delay" hin schreiben? Ich habe das niemals gebraucht, genau so wie das "goto".

"delay" ist von vorgestern und sollte verboten werden.

OP #7365091
Lesenswert?

Danke für die Erklärung!

Lothar M. schrieb:

  1. Was ist das für ein lahmes "Gerät"?

Ein Netzteil, welches sich per UART fernsteuern lässt. Lasse ich das Delay weg, nimmt er nur den ersten Befehl.

Lothar M. schrieb:

In SW sieht das z.B. so aus:

Ist es wirklich üblich solche Zustandsautomaten Monster in die main zu packen? Ich dachte immer mal will so viel wie möglich in Funktionen auslagern. Ich dachte immer man mancht nur in der main die Abfrage des Tasters und springt dann in die Funktion. Wenn ich den Zustandsautomaten in die Funktion packe, dann müsste ich sie ja mehrmals aufrufen damit ich alle Zustände durchspringen kann? Ich seh den Wald wahrscheinlich vor lauter Bäumen nicht.
Gibts irgendwo auf GitHub ein schönes Beispiel wo man sich mal ein etwas komplexeres Projekt ansehen kann wo die Sache gut gelöst wurde.

Thomas R. schrieb:

"delay" ist von vorgestern und sollte verboten werden.

Ein Beispiel aus deinem Funduds wäre nett und würde zur Lösung/Erklärung mehr beitragen als das Verteufeln von delay :).

Gast #7365131
Lesenswert?

Paul B. schrieb:

Thomas R. schrieb:

"delay" ist von vorgestern und sollte verboten werden.

Ein Beispiel aus deinem Funduds wäre nett und würde zur Lösung/Erklärung mehr beitragen als das Verteufeln von delay :).

In diesem Thread gibt es schon genug gute Beiträge und niemand verwendet "delay", nur du verwendet diesen Mist.

#7365140
Lesenswert?

Paul B. schrieb:

Wenn ich den Zustandsautomaten in die Funktion packe, dann müsste ich sie ja mehrmals aufrufen damit ich alle Zustände durchspringen kann? Ich seh den Wald wahrscheinlich vor lauter Bäumen nicht.

MaWin O. schrieb:

Mit asynchroner Programmierung schreibe ich es ganz einfach fast so hin, wie der Threadersteller es geschrieben hat. Ganz ohne globales Blockieren.

Asynchrone Programmierung, also die Schachtelung von Rückrufschnipseln, ist eine mögliche alternative Notation zu Zustandsautomaten. MaWin O., wäre schön dazu von dir ein Beispiel zu sehen, vielleicht ähnlich dem Automaten von Lothar ... ?

LG, Sebastian

OP #7365142
Lesenswert?

Thomas R. schrieb:

In diesem Thread gibt es schon genug gute Beiträge und niemand verwendet "delay", nur du verwendet diesen Mist.

Zu denen ich noch Fragen habe, was üblich und "state of the art" ist. Von denen du bis jetzt übrigens keine beantworten konntest.

Worin siehst du den Mehrwert deiner Beiträge? Zwei an der Zahl, zweimal kein fachlicher Gewinn für die Diskussion. Was tribt dich an, inhaltlose Kommenentare zu Themen abzugeben, von denen du offenbar genauso viel weißt wie ich. Delay ist Scheiße, hab ich für mich auch festgestellt und deswegen gibts den Thread hier.

#7365180
Lesenswert?

Sebastian W. schrieb:

Asynchrone Programmierung, also die Schachtelung von Rückrufschnipseln, ist eine mögliche alternative Notation zu Zustandsautomaten. MaWin O., wäre schön dazu von dir ein Beispiel zu sehen, vielleicht ähnlich dem Automaten von Lothar ... ?

Ja, gerne. Das sieht praktisch wie der Originalcode vom Threadersteller aus. Außer, dass dem delay()-Aufruf noch ein .await() oder etwas ähnliches angehängt wird (je nach Sprache). Und die Funktion wird meist auch noch mit dem keyword async oder ähnlichem dekoriert. Das ist aber eigentlich unwichtig. Das Wichtige ist: Der Programmablauf bleibt lesbar und linear.

Gast #7365189
Lesenswert?

Hallo Leute,

ohne jetzt den Thread komplett gelesen zu haben (evtl. wurde meine Antwort schon gegeben):

In der "Automatisierungstechnik" (ich weiß, das haben wir hier auch) wird das so gehandhabt, dass eine Funktion erstellt wird, die in jedem Durchlauf der Hauptschleife aufgerufen wird. Diese hat als Argument ein "Start"-Signal. Ein statischer Pegel oder eine Flanke dieses Parameters triggert in der Funktion die durchzuführende Aktion. Dabei kehrt die Funktion sehr schnell zurück. Liegen in der Funktion dann Ergebnisse bzw. Daten vor, signalisiert die Funktion dies über ein Bit "Auftrag erledigt". Das übergeordnete Programm kann dann die Daten, die die Funktion erzeugt hat, ohne Verzögerung abholen und verwenden.

ciao

Marci

(Firma: Torrox.de) #7365220
Lesenswert?

Paul B. schrieb:

Ist es wirklich üblich solche Zustandsautomaten Monster in die main zu packen? Ich dachte immer mal will so viel wie möglich in Funktionen auslagern.

Wenn das gezeigte Beispiel den gesamten Funktionsumfang zeigen würde, wäre ich mit der Größe der main() Funktion einverstanden. Ansonsten haben sich enums bewährt, dann muss man den Kommentar bei der Nutzung der magic numbers nicht immer wiederholen ;-)

Warum sollte man das nicht in Funktionen auslagern können? Bei dem ARM Cortex, die ich hauptsächlich nutze, gibt es ein Intrinsic, dass die CPU schlafen läßt bis irgend ein Interrupt ausgelöst hat.

Wenn jetzt jedes Modul, seine Interrupts lokal händelt und z.B. nötige Informationen in Modul-Lokalen Variablen, Queues etvl. mit dem main thread teilt, dann könnte z.B. jedes Modul eine Funktion zur Verfügung stellen, dass die aktuelle Zeit nimmt, und den Zeitpunkt, zu dem das Modul etwas zu tun hat, zurück geben lassen:

1
int main()
2
{
3
    time_pnt_t next = time_now();
4

5
    for ( ;;  )
6
    {
7
      next = module1( next );
8
      next = time_min( next, module2( next ) );
9
      next = time_min( next, module3( next ) );
10

11
      time_wait_till( next );
12

13
      __WFI();
14
      next = time_now();
15
    }
16
}

time_pnt_t wäre ein Typ, der einen Zeitpunkt mit der nötigen Auflösung speichern kann, der überlaufen kann und trotzdem zuverlässig, zweifelsfrei den Abstand zwischen zwei geplanten Ereignissen berücksichtig. time_now() liefert die aktuelle Zeit, time_min() liefert den früheren von 2 Zeitpunkten. time_wait_till() setzt einen Timer auf, der __WFI() zum gg. Zeitpunkt wieder aufweckt.

#7365229
Lesenswert?

Thomas R. schrieb:

Ich habe das niemals gebraucht, genau so wie das "goto".

In Assembler kann man mit goto (JMP) oder goto mit Flag-Bedienungen viel Gutes anstellen. In anderen Programmiersprachen wurde goto durch andere Konstrukte ersetzt, ohne dass es offensichtlich ist. ZB der break aus der switch oder while oder for Anweisung ist nichts anderes als ein goto.

#7365257
Lesenswert?

Sebastian W. schrieb:

wäre schön dazu von dir ein Beispiel zu sehen, vielleicht ähnlich dem Automaten von Lothar ... ?

ich hätte da ein Beispiel für asynchrones Eventhandling:

1
#include "mbed.h"
2

3
EventQueue queue;
4
InterruptIn user_button(BUTTON1);
5

6
void EA_Init() {
7
  // some init stuff
8
}
9

10
void EA_set_output(uint8_t status) {
11
  // some output stuff
12
}
13

14
void PS_on(uint8_t status)
15
{
16
   queue.call(EA_Init);
17
   queue.call_in(500ms, &EA_set_output, status);
18
}
19

20
int main()
21
{
22
  user_button.rise( []() {
23
    PS_on(42);
24
  });
25

26
  queue.dispatch_forever();
27

28
  return 0;
29
}

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren