Forum: Mikrocontroller und Digitale Elektronik Richtiger Umgang mit zeitintensiven Methoden/Funktionen


von Christian S. (christian_s593)


Lesenswert?

Hallo,

Vor einiger Zeit war ich an einem Mikrocontroller-Projekt (Atmega328), 
wobei einige programmiertechnische Probleme aufkamen. Ich wollte kurz 
mein Lösungsansatz vorstellen und mich würde interessieren, wie eure 
Problemlösungen aussehen würden.
Das Projekt ist bereits abgeschlossen, jedoch meiner Meinung nach an 
einigen Stellen unschön gelöst.

Als Beispiel wird ein GSM-Modul an einer Stelle gestartet:

gsmPower(ON);

Wobei ein Enum mit den Wert ON oder OFF übergeben wird.
Damit wäre es eigentlich getan, wenn die Startprozedur nicht über 2 
Sekunden dauern würde.
Deshalb war meine Überlegung, es gibt ein Wert zurück, wobei 1 für 
Fertig, 0 für in Bearbeitung und -1 für ein Fehler stehen. (An der 
Stelle sind eigentlich keine Fehler zu erwarten, an anderen jedoch schon 
öfter, wie z.B. kein Empfang.) Zusätzlich wird bei einem Fehler "errno" 
gesetzt, damit man bei ein Abbruch nachvollziehen kann, wo es nicht 
klappte.

Die Auswertung lief dann so:

...
case GSM_START:
switch(gsmPower){
 case -1: state = ERROR_HANDLER; break;
 case 0: state = GSM_START; break;
 case 1: state = GSM_LOGIN; break;
}
break;
case GSM_LOGIN: .......

Hier sieht man auch gleich wohin es geführt hat: Zu einem riesigen 
switch-case Konstrukt. Anzumerken ist, dass auch die Funktion gsmPower 
selber eine State Machine ist.
Und hier kam irgendwann der Knackpunkt, dass es ein Timeout geben sollte 
und der Vorgang abgebrochen wird. Am Ende hab ich es so gelöst, dass 
jede Funktion ein eigenen Timeout besitzt und garantiert, dass 
irgendwann abgebrochen wird, wobei dann ein "-1" zurückgegeben wird.
Wenn jedoch eine andere "zeitgleiche" Funktion den Abbruch hervorruft, 
wissen die anderen Funktionen davon nichts. Somit musste ich jede 
mögliche Funktionen im state "ERROR_HANDLER" diese Information 
übergeben, dass bei erneuten Start alle Statemachines wieder im 
Urzustand sind.

Schlussendlich funktionierte es so, jedoch war es zum Schluss 
unübersichtlich und auch leserlich waren diese switch-case Konstrukte 
nie.

Vor allem die C++ Experten unter euch könnten ja mal einen naiven C 
Programmierer wie mir erklären, wie man sowas in Klassen und Objekte 
steckt. Bin für komplett neue Ansätze offen, damit ich es beim nächsten 
Projekt besser weiß.

von Dr. Sommer (Gast)


Lesenswert?

Kann grad nicht viel schreiben, aber schau mal nach:
- State pattern
- Observer pattern

von Dieter F. (Gast)


Lesenswert?

Arduino ?

von c-hater (Gast)


Lesenswert?

Christian S. schrieb:

> Vor allem die C++ Experten unter euch könnten ja mal einen naiven C
> Programmierer wie mir erklären, wie man sowas in Klassen und Objekte
> steckt.

Ich bin zwar, weiss Gott, kein C++-Experte (weil ich andere, viel 
sauberere OO-Sprachen bevorzuge), aber im Bezug auf ein Konstrukt aus 
miteinander interagierenden Statemachines bringt ein objektorientierter 
Ansatz de facto "nur" einen einzigen Vorteil: er unterstützt die saubere 
Trennung von Stati und Ereignissen bereits konzeptionell.

Der springende Punkt ist allerdings: OO unterstützt das wirklich nur, 
konsequent nutzen muss man dieses Feature schon in eigener 
Verantwortung. Es läuft darauf hinaus, Statuswechsel tatsächlich ganz 
konsequent immer nur in Form von "Ereignissen" zu propagieren. Sprich: 
Polling irgendwelcher Stati ist komplett tabu. Stati anderer Objekte 
dürfen nur in Ereignishandlern einmalig(!) abgefragt werden.

"Unübersichtliche" switch-case-Konstrukte wird man dadurch aber 
natürlich nicht los, sie wandern nur in die privaten Methoden der 
verwendeten Klassen. Das Ziel ist halt: Codekapselung. Es soll nicht 
mehr wichtig sein, was im Detail im Inneren jeder einzelnen Statemachine 
passiert. Entscheidend soll nur sein, dass sich an ihrem Zustand etwas 
geändert hat, was dann über ein Ereignis der Statemachine (also des 
Objektes) an alle gemeldet wird, die sich für diesen Sachverhalt 
interessieren und deshalb einen Ereignishändler an eben diese 
Statemachine gehängt haben.

Das Ziel muß neben der Trennung in Stati und Ereignisse natürlich immer 
sein, die Zahl der Ereignisse so gering wie möglich zu halten. Dadurch 
wird der Graph überschaubarer und die Vorteile dieses Blackbox-Konzeptes 
werden umso deutlicher, je konsequenter die Kapselung der Stati und die 
Verringerung der Zahl der jeweils nach aussen relevanten Ereignisse 
durchgezogen wird. Wenn man die Sache nun noch hierarchisch betreibt, 
also die vielen kleinen Statemachines eines Subsystems wieder in eine 
Meta-Statemachine kapselt, hat man OO richtig verstanden.

Du wohl noch nicht...

von Dr. Sommer (Gast)


Lesenswert?

c-hater schrieb:
> "Unübersichtliche" switch-case-Konstrukte wird man dadurch aber
> natürlich nicht los,
Das ist eigentlich der Sinn des State Patterns...

Btw, der Plural von "Status" ist "Status" ;-)
http://www.duden.de/rechtschreibung/Status

von Christian S. (christian_s593)


Lesenswert?

Dr. Sommer schrieb:
> Kann grad nicht viel schreiben, aber schau mal nach:
> - State pattern
> - Observer pattern

Interessante Stichwörter, vor allem das Erstere.

Dieter F. schrieb:
> Arduino ?

Frage nicht ganz vollständig, aber ich benutze weder die IDE noch die
Hardware aus vielerlei Hinsicht. (Zumindest bei diesen Projekt)

c-hater schrieb:
> Das Ziel muß neben der Trennung in Stati und Ereignisse natürlich immer
> sein, die Zahl der Ereignisse so gering wie möglich zu halten. Dadurch
> wird der Graph überschaubarer und die Vorteile dieses Blackbox-Konzeptes
> werden umso deutlicher, je konsequenter die Kapselung der Stati und die
> Verringerung der Zahl der jeweils nach aussen relevanten Ereignisse
> durchgezogen wird. Wenn man die Sache nun noch hierarchisch betreibt,
> also die vielen kleinen Statemachines eines Subsystems wieder in eine
> Meta-Statemachine kapselt, hat man OO richtig verstanden.
>
> Du wohl noch nicht...

Nein, das hab ich wohl noch nicht. Aber ich arbeite mich da langsam ein. 
In der Vergangenheit war es für mich wichtig, dass Projekte schnell 
fertig werden und funktionieren und dann nutzt man halt gewohnte Mittel 
(ANSI C).
Jedoch hab ich bei diesen Beispiel schon erkannt, dass OO hier der 
Ansatz sein könnte.
Die Möglichkeit die FSM zu kapseln und hierarchisch Anzuordnen hab ich 
sogar genutzt, jedoch nicht so häufig, wie es möglich gewesen wäre. Die 
relativ flache Hierarchie hatte eben den Vorteil des einfacheren 
Datenaustausch innerhalb der Statemachine (z.B. ein Abbruch).
Der Spaghetti-Code wird ja nicht besser, wenn eine Information von oben 
in jede kleine Statemachine runter muss und zusätzlich irgendwelche 
unsolicited codes wieder den Weg nach oben schaffen müssen.

Stellenweise sind das vielleicht Probleme gewesen, wo ein kleines OS 
oder Task Scheduler mit Message Queue geholfen hätte. Jedoch ist das 
wahrscheinlich der Kanonenschuss auf Spatzen.

von A. S. (Gast)


Lesenswert?

Es gibt 2 Ansätze: eventgetrieben oder SPS-loop.

Du hast die SPS-loop gewählt, die auch oft gut und sinnvoll ist.

Im Detail kann man sicher immer was verbessern, aber prinzipiell wird 
der Kontrollfluss durch OOP dabei nicht besser erkennbar.

Wenn, dann könnten parallele Tasks hier sinnvoll sein (z.b. eigene für 
GSM)

Das Problem (Events oder zyklisch) bleibt trotzdem bestehen.

In erster Näherung: wenn die meisten (Re)Aktionen kurz sind, dann 
Events. Wenn sie hingegen länger dauern, asynchron oder per Timer 
abgebrochen werden können sollen oder mehrere zeitgleich interagieren 
(nicht nacheinander) dann loop.

von Christian S. (christian_s593)


Lesenswert?

Achim S. schrieb:
> Es gibt 2 Ansätze: eventgetrieben oder SPS-loop.
>
> Du hast die SPS-loop gewählt, die auch oft gut und sinnvoll ist.

Was genau meinst du mit Event-getrieben?
Mir selbst ist nur das Schema mit den Task bekannt. Ein Scheduler prüft, 
ob ein ein Task ausführbereit ist (Zeit abgelaufen oder Ereignis 
eingetreten) der dann die Funktionen (Task) ausführt. Die wiederum 
melden den Scheduler, die Zeit, Bedingung und die Funktion, die als 
nächstes ausgeführt werden soll.
Oder meinst du mit Event-getrieben noch was ganz anderes?

Bei dem Projekt kam noch hinzu, dass es am Ende der Main-Loop noch 
entscheiden sollte, ob es 50ms in Idle oder anderen Sleepmode oder 8sec 
in Powerdown geht. Je nachdem welche Hardware benutzt wurde, gab es 
Semaphoren, die es regelten.
Somit war die Loop-Zeit bekannt und für Programmabschnitte konstant.
Ich hab zu Anfang den Fehler gemacht, in den State Machines die Zahl der 
Aufrufe als Zeitgeber zu nutzen, hab aber irgendwann doch eine Funktion 
ähnlich wie "millis()" erstellt.

von c-hater (Gast)


Lesenswert?

Achim S. schrieb:

> Im Detail kann man sicher immer was verbessern, aber prinzipiell wird
> der Kontrollfluss durch OOP dabei nicht besser erkennbar.

Das ist nur insofern richtig, wenn man die Gesamtkomplexität überblicken 
will. Nur ist eben genau das bei jedem nichttrivialen Projekt für 
Menschen sowieso praktisch völlig unmöglich.

Nehmen wir mal an, wir hätten es nur mit 20 Eingängen zu tun, die nur 
die Zustände 0 oder 1 annehmen können, dann hat man schon 2^20 mögliche 
Zustände der Statemachine, also rund 1 Million. Es gibt wohl nur sehr 
wenige Menschen, die wirklich jede dieser Kombination durchdenken 
können, also daraufhin abklopfen: Könnte diese Situation überhaupt 
auftreten, wenn ja, was muss ich daraufhin am Status ändern?

Man muss sich darüber hinaus auch veranschaulichen, das fast jeder 
Konfigurationsdialog einer größeren Software im Schnitt bereits 
ALLEINE ungefähr diese Komplexität besitzt. Das ist nicht zufällig so, 
sondern stellt ungefähr die Grenze dessen dar, was für Menschen noch so 
einigermaßen überschaubar ist. Glaubt man jedenfalls, wenn man sich 
allerdings anschaut, wieviele Logikfehler bereits in solchen Dialogen 
anzutreffen sind, sind normale Programmierer auch mit dieser Komplexität 
bereits einigermassen überfordert.

Tja, das eigentliche Problem ist nun (da die Komplexität nunmal da ist 
und der Mensch nunmal so beschränkt ist): wie kann ich trotzdem solche 
hochkomplexen Probleme einigermaßen brauchbar implementieren?

Und das geht zuverlässig nur durch Kapselung. Es läuft im Prinzip darauf 
hinaus, die Wirkung von Eingängen immer möglichst lokal zu halten. 
Schaffe ich also ein Objekt, was 10 binäre Eingänge so verarbeitet, das 
für den Rest des Gesamtsystems daraus nur noch ein auszuwertendes 
binäres Ereignis übrig bleibt, habe ich die Komplexität des 
Gesamtproblems schonmal enorm verringert, nämlich um den Faktor 2^10. 
Wenn ich bereit bin, darauf zu scheißen, was diese Objekt genau tut, 
solange es offensichtlich tut, was es soll. Erst, wenn es nicht das tut, 
was es soll, muss ich in die Abgründe dieser einzelnen Statusmaschine 
abtauchen, die dann aber mit ihren 2^10=1024 Stati immer noch relativ 
leicht überschaubar ist.

Darauf läuft es eigentlich hinaus: zerlege ein Problem in überschaubare 
Teilprobleme. Das war eigentlich schon immer das Grundkonzept des 
Programmierens, OOP lieferte nur endlich die passenden Werkzeuge 
dafür...

von Christian S. (christian_s593)


Lesenswert?

c-hater schrieb:
> Darauf läuft es eigentlich hinaus: zerlege ein Problem in überschaubare
> Teilprobleme. Das war eigentlich schon immer das Grundkonzept des
> Programmierens, OOP lieferte nur endlich die passenden Werkzeuge
> dafür...

Genau das Zerlegen in Teilprobleme. Damit hatte ich auch manchmal meine 
Schwierigkeiten. Hier ein kleiner Auszug der Struktur des Logins:
...
├ GSM_POWER_ON
├ LOGIN
│ ├ CHECK_SIM_CARD_INSERT
: ├ SIM_CARD_PIN
: │ ├ CHECK_PIN_IS_NEEDED
  │ ├ LOAD_PIN_EEPROM
  │ ├ PIN_TEST (*)
  │ ├ PIN_USER_ENTRY
  │ │ ├ WAIT_BUTTON_PRESSED
  │ │ ├ WAIT_BUTTON_RELEASED
  │ │ ├ STORE_DIGIT
  │ │ ├ CHECK_NUM_DIGITS
  │ │ └ PIN_TEST (*)
  │ └ UPDATE_PIN_EEPROM
  ├ CHECK_GSM_READY
  ├ SEND_GSM_SETTINGS
  └ CHECK_NETWORK_READY

Das ist nur Login in vereinfachter Form (Timeouts, Fehler und 
Wiederholungen fehlen)
Da kam nämlich die Frage auf, Kapsel ich wirklich die Pin-Eingabe als 
eigene State Machine oder lass ich das zusammen, was zusammen gehört.
Bei (*) sieht man schon, was das unschöne an der Kapselung war.

Insgesamt ist das Programm sehr sequenziell, und da die Nebenfunktionen 
sehr überschaubar sind, war ich manchmal versucht, alles sequenziell 
runterzuschreiben und die Nebenanwendungen im Timerinterrupt zu 
erledigen. Wahrscheinlich auch ein praktikabler Ansatz.

Aber zurück: In der OO-Programmierung landet man mit der Abstraktion 
doch auch an den Punkt, wo man die Methode GSM.Login(); aufruft und man 
als Rückgabewert -1, 0 oder 1 bekommt je nachdem, ob die Funktion fertig 
ist, oder noch einige male Aufgerufen werden muss.

Und darum ging es mir bei der Anfangsfrage, gibt es andere Möglichkeiten 
oder Wege zeitintensive Funktionen, auf die nicht gewartet werden kann, 
umzusetzen.

von Dr. Sommer (Gast)


Lesenswert?

Christian S. schrieb:
> Aber zurück: In der OO-Programmierung landet man mit der Abstraktion
> doch auch an den Punkt, wo man die Methode GSM.Login(); aufruft und man
> als Rückgabewert -1, 0 oder 1 bekommt je nachdem, ob die Funktion fertig
> ist, oder noch einige male Aufgerufen werden muss.

Bei OOP würde man die Funktion eher nur 1x aufrufen und dabei einen 
Callback übergeben der aufgerufen wird wenn's fertig ist (Observer 
Pattern). Die GSM interne Verarbeitung könnte dann in Interrupts oder 
einem separaten Thread geschehen. Wenn man sowieso mit Threads arbeitet 
kann man die Login Funktion auch einfach blockierend machen (kehrt erst 
zurück wenn fertig).

Das ganze ist aber weniger eine Frage von OOP. OOP beschäftigt sich mehr 
mit Strukturieung von Daten und weniger mit Abläufen.

von Max (Gast)


Lesenswert?

Wenn du eine Funktion in eine Statemachine umwandeln möchtest, damit du 
andere Dinge parallel bearbeiten kannst, dann solltet du dir mal das 
Konzept "Protothreads" anschauen.

Das ganze basiert auf C Macros und baut im Hintergrund genau so eine 
"switch/case" Hölle, wie du es in deinem ersten Post beschrieben hast. 
Dein eigentliche Code sieht aber um einiges lesbarer aus.

http://dunkels.com/adam/pt/

von Christian S. (christian_s593)


Lesenswert?

Max schrieb:
> Wenn du eine Funktion in eine Statemachine umwandeln möchtest, damit du
> andere Dinge parallel bearbeiten kannst, dann solltet du dir mal das
> Konzept "Protothreads" anschauen.
>
> Das ganze basiert auf C Macros und baut im Hintergrund genau so eine
> "switch/case" Hölle, wie du es in deinem ersten Post beschrieben hast.
> Dein eigentliche Code sieht aber um einiges lesbarer aus.
>
> http://dunkels.com/adam/pt/

Danke für die Info, Protothreads hab ich schonmal gelesen, hab ich aber 
immer mit "richtigen" Multitasking wie Freertos in Verbindung gebracht.

Ein ähnliches Macro-Konstrukt steckt hinter das Menusystem von "Marlin" 
(3D Drucker Firmware). Mega simpel und leserlich hinzuschreiben, aber 
wehe es klemmt mal im Macro, da war Debuggen eine einzige Katastrophe.
Aber unterm Strich finde ich solche Macros garnicht schlecht.

von rmu (Gast)


Lesenswert?

https://github.com/ve3wwg/teensy3_fibers kommt ohne macro-Wahnsinn aus, 
und es gibt auch einen ARM Cortex M0 Port.

Kooperatives Multi-Tasking. Wenn eine Faser (mini-Task) nix zu tun hat 
ruft er yield() auf und der nächste ist dran. Kann so verschachtelte 
Schleifen viel übersichtlicher machen, man kann den Zustand der 
verschiedenen Statemachines in jeweils einer Fiber lokal halten und muss 
sich an anderen Stellen wo das uninteressant wäre nicht damit abgeben 
oder darauf aufpassen.

Bissl RAM braucht das Zeug schon, da jede Faser einen eigenen Stack hat.

von A. S. (Gast)


Lesenswert?

Christian S. schrieb:
> Was genau meinst du mit Event-getrieben?

SPS-Loop: Du läufst zyklisch durch Deinen "Switch" und machst jeweils 
ein paar Sekunden oder Millisekunden etwas (oder läufst sofort durch, 
wenn nichts zu tun ist). Kennzeichnend ist, dass Du jeweils auch auf 
verschiedene Ereignisse prüfst (Überwachungs-Timer abgelaufen, Cancel 
von woandersher, Messwert ungültig, ...) Nachteil ist, dass Du nicht 
runterprogrammieren kannst sondern Häppchen machen musst.

Eventbasiert: Du startest etwas "im Hintergrund" (z.B. Init) und wartest 
dann auf Events:
- Init-Fertig
- Cancel-Event
- Überwachungs-Timer-Event (der Timer wurde vorher ebenso gestartet)
Jeder Event startet dann andere Dinge.

Das entspräche einer Windows Nachrichtenschleife oder dem 
Linux-Select-Befehl.

Eventbasiert macht nur Sinn, wenn die Teilaufgaben im Hintergrund laufen 
können oder die Teilaufgaben kurz sind. Oder (wie in einem 
Windows-Programm) wenn die Sanduhr keine Rolle spielt, also z.B. wenn 
ein Prozess gestartet wird und der Benutzer darauf wartet.

von Christian S. (christian_s593)


Lesenswert?

rmu schrieb:
> Bissl RAM braucht das Zeug schon, da jede Faser einen eigenen Stack hat.

By default 8K of stack is reserved for the main fiber.

Der Overhead ist ja gigantisch, da kenn ich sparsamere FreeRTOS Ports. 
Wahrscheinlich kann es auch nach unten skalieren, aber meistens habe ich 
Controller auf dem Tisch von <4K RAM. Aber ja, dafür gibts auch 
brauchbare präemptive OS für.
Die Frage dabei wäre aber trotzdem, ob sowas nicht der Overkill ist, 
wenn man eigentlich gar nicht so viele Sachen parallel machen will, 
sondern nur viele Funktionen nacheinander aufruft, die Zeitintensiv sind 
und man in der Wartezeit lieber im Sleepmode ist.

von Christian S. (christian_s593)


Lesenswert?

Achim S. schrieb:
> Eventbasiert: Du startest etwas "im Hintergrund" (z.B. Init) und wartest
> dann auf Events:
> - Init-Fertig
> - Cancel-Event
> - Überwachungs-Timer-Event (der Timer wurde vorher ebenso gestartet)
> Jeder Event startet dann andere Dinge.

Setzt das nicht auch präemptives Multitasking voraus?
Stell ich mir ohne dass ein Task unterbrochen werden kann ziemlich 
sperrig vor.

von Einer K. (Gast)


Lesenswert?

rmu schrieb:
> Bissl RAM braucht das Zeug schon, da jede Faser einen eigenen Stack hat.
Und damit ist das Prinzip, bei kleinen AVR, meist schon aus dem Rennen.

Christian S. schrieb:
> Danke für die Info, Protothreads hab ich schonmal gelesen, hab ich aber
> immer mit "richtigen" Multitasking wie Freertos in Verbindung gebracht.

Hier wird das Thema beackert: 
Beitrag "Wer benutzt Protothreads?"

von Vincent H. (vinci)


Lesenswert?

Die allererste Antwort schlug bereits die Lösung vor. Dich nerven große 
switch/case Anweisungen, dann pack jeden State in eine Klasse. Genau das 
tut das "State Pattern".

Und wenn du nicht pollen willst, dann lässt du deine State Machine von 
einem Timer dispatchen, der von den States selbst mit dem richtigen 
Timeout Wert geladen wird.

Kein switch/case, kein blockieren, keine riesigen Funktionen.

von Ordner (Gast)


Lesenswert?

Dr. Sommer schrieb:

> Btw, der Plural von "Status" ist "Status" ;-)
> http://www.duden.de/rechtschreibung/Status

Neee, die Mehrzahl von "der Status" ist im Deutschen  "die Zustände" ;-)

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.