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ß.
Kann grad nicht viel schreiben, aber schau mal nach: - State pattern - Observer pattern
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...
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
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.
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.
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.
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...
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.
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.
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/
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.
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.
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.
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.
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.
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?"
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.
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
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.