Jede Embedded-Software muss in der Praxis eine Vielzahl von verteilten und zentral koordinierten Aufgaben ausführen. Software Patterns stellen dabei nicht nur die konzeptionelle Integrität sicher, sondern ermöglichen auch die Skalierbarkeit über verschiedene Projekte hinweg. Im zweiten Teil unserer Beitragsreihe rücken wir die Manager mit ihren vielfältigen Funktionen in den Fokus.
Initialisierung (prozeduraler Ansatz)
Im prozeduralen Umfeld kommen Managermodule zum Einsatz, symbolisiert durch das „m“ vor den Namen. Im Architekturelement AEn ist der mAEnManager für seine Module m1…mN verantwortlich.
main() ruft init() vom mSoftwareManager auf. Diese Funktion ruft wiederum von allen mAEnManagern die initAEn() Funktionen in der erforderlichen Reihenfolge auf (Delegationen). Diese Funktionen initialisieren die Module aus deren Verantwortlichkeit. Das Prinzip der Delegation ist für die meisten weiteren Managerfunktionen anwendbar.
Bild 5: Manager Pattern für prozeduralen Ansatz
Im Vergleich zu einem objektorientierten Ansatz ist beim prozeduralen häufig keine Objektinstanziierung erforderlich. Somit könnten ggf. die create()Funktionen entfallen. Die aus der objektorientierten Programmierung bekannten Konstruktoren und Destruktoren von Klassen lassen sich in der prozeduralen Programmierung durch manuell aufzurufende Funktionen construct() und destruct() ersetzen.
Objektinstanziierung und Initialisierung (objektorientierter Ansatz)
main() instanziiert ein cSoftwareManager Ur-Objekt. Dieses instanziiert alle cAEnManager Objekte, die wiederum alle Objekte der Klassen c1…cN aus deren Verantwortung instanziieren. Abhängig von den angewandten Relationen (Assoziation, Aggregation) erzeugen die createX() Funktionen deren Objekte dynamisch und implementierungsabhängig auf dem Heap. Bei der durchgängig angewandten Komposition werden alle Objekte automatisch auf dem Stack angelegt, wobei die createX() Funktionen entfallen.
Die initX() Funktion erlaubt zusätzlich zum Konstruktor-Aufruf weitere Initialisierungen. Als Alternative ruft der Konstruktor die jeweilige initX() Funktion auf.
Bild 6: Manager Pattern für objektorientierten Ansatz
Ist die Architekturhierarchie tiefer als die hier dargestellte, erstreckt sich der oben beschriebene Ablauf über die gesamte Hierarchie.
Als Ergebnis dieses Schrittes sind alle Objekte instanziiert und grundinitialisiert.
Initialisierung der Relationen
Die Funktion buildXRelations() initialisiert die Relationen (Assoziation, Aggregation) zwischen den Objekten intern, aber auch über Architekturelement-Grenzen hinweg. Dazu gehören auch die Callback-Registrierungen. Typischerweise sind Zeiger mit entsprechender Objektaderesse initialisiert.
Bild 7: Manager Pattern um Relationsinitialisierung ergänzt
Häufig sind dazu Objektadressen aus nebenliegenden Architekturelementen erforderlich. Kennen sich diese AEnManager untereinander, so können sie Objektadressen übermitteln.
Konfiguration, Parametrierung, Datenbankzugriff
Nach der komplett abgeschlossenen Initialisierung detektiert die Software z.B., auf welcher Hardware die Ausführung stattfindet. Ergebnisabhängig liest der SoftwareManager die korrespondierenden Parameter aus einer lokalen oder in der Cloud befindlichen Datenbank.
Bild 8: Manager Pattern um Konfiguration ergänzt
Die Funktion configX() der AEnManager gibt die Parameter zu den zu konfigurierenden Klassen/ Modulen weiter. Eventuell ist configX() bereits vor initX() ausführbar und gibt die Parameter über die initX() Funktion weiter.
Allokation von Ressourcen
Die einzelnen Architekturelemente benötigen ggf. für den weiteren Betrieb zusätzliche individuelle Ressourcen. Besonders in einer funktional-sicherheitskritischen Embedded-Software ist es empfehlenswert, die erforderlichen Ressourcen bereits zu Beginn der Softwareausführung komplett anzulegen. Damit ist zum einen sichergestellt, dass alle Ressourcen zur Laufzeit verfügbar sind, zum anderen erfolgt dadurch gleichzeitig eine Laufzeit-Performanceverbesserung.
Bild 9: Manager Pattern um Ressourcen-Allokation ergänzt
Ressourcen sind z.B. Speicher oder bei Anwendung eines Betriebssystems bereits Betriebsmittel wie Mailboxen, Eventgruppen, Semaphore, Mutexe und Timer. Bedingung wäre, dass das Betriebssystem vor dem Bootvorgang dies bereits erlaubt. Falls nein, lässt sich die Reihenfolge der Funktionsaufrufe ändern oder der Funktionsaufruf allocateXResources() in die Architekturelement-spezifischen taskInit() verschieben.
Betriebsphase: Ohne Betriebssystem
Ein Bare-Metal-Applikation führt eine oder mehrere Funktionen kontinuierlich aus. Hier enthält die run() Funktion diese zentrale while(true) loop und führt in vorgegebener Reihenfolge kontinuierlich die runAEn() Funktionen aus.
Bild 10: Manager Pattern um Betriebsphase ergänzt
Boot-, Startup- und Betriebsphase: Mit Betriebssystem
Die start() Funktion des SoftwareMangers bootet das Betriebssystem. Anschließend schedult das Betriebssystem als erste Task die taskInit(). Diese kreiert taskOperation()und alle taskAEnInit()Tasks der AEnManager. Anschließend beendet sie sich selbst. Die taskAEnInit() kreiert taskARnOperation() und ggf. weitere Architekturelement-spezifische Tasks. Anschließend beendet sie sich ebenfalls. Nun beginnt die eigentliche Betriebsphase mit dem Scheduling der verbleibenden Tasks.
Bild 11: Manager Pattern für Startup und Betriebsphase
Die Manager-Taskfunktionen taskXOperation() enthalten einen Zustandsfolgeautomaten, der den aktuellen Zustand der Software bzw. der einzelnen Architekturelemente repräsentiert. Dieser Zustandsautomat ist im weiteren Verlauf des Kapitels vorgestellt.
Diagnose
Die Funktion executeDiagnostics() des SoftwareManagers startet die Diagnoseausführung und sammelt alle Diagnosedaten über die Aufrufe der in den AEnManagern enthaltenen executeAEnDiagnostics() Funktionen.
Bild 12: Manager Pattern um Diagnose ergänzt
Das Element Diagnostics speichert bzw. ermöglicht die Ausgabe der gespeicherten Daten.
Neustart
Die Funktion restart() des SoftwareManagers löst den Software-Neustart aus, indem die Aufrufe der in den AEnManagern enthaltenen restartAEn() Funktionen die einzelnen Architekturelemente neu aufstarten.
Bild 13: Manager Pattern um Neustart ergänzt
Zur feineren Unterteilung des Neustarts, aber auch des nachfolgend erklärten Herunterfahrens, dienen die gegenläufigen Funktionen zu denen, die die Manager während des Hochlaufs ausführen.
Herunterfahren
Die Funktion shutdown() des SoftwareManagers löst das Herunterfahren der Software aus, indem die Aufrufe der in den AEnManagern enthaltenen shutdownAEn() Funktionen die einzelnen Architekturelemente herunterfahren.
Bild 14: Manager Pattern um Herunterfahren ergänzt
Zentrale Fehlerbehandlung
Die Klassen c1…cN bzw. Module m1…mN erkennen ihre Fehler und melden diese ihrem AEnManager. Dieser leitet die Fehler direkt an den übergeordneten SoftwareManager weiter. Der SoftwareManager übernimmt zentral die Fehlerbehandlung, auch für potentiell auftretende Fehler in den AEnManagern.
Bild 15: Manager Pattern um zentrale Fehlerbehandlung ergänzt
Neben der richtigen Fehlerreaktion, z.B. mittels handleError() und recoverError(), übernimmt der SoftwareManager auch den Fehlereintrag in sein ErrorLogbook.
Dezentrale Fehlerbehandlung
Die Klassen c1…cN bzw. Module m1…mN erkennen ihre Fehler und melden diese ihrem AEnManager. Kann der AEnManager den Fehler bearbeiten, tut er dies und trägt den Fehler in sein lokales ErrorLogbook ein.
Bild 16: Manager Pattern um dezentrale Fehlerbehandlung ergänzt
Fatale Fehler, die der AEnManager nicht selbst bearbeiten kann, leitet er an den übergeordneten SoftwareManager weiter. Dieser übernimmt die Fatal-Fehlerbehandlung und trägt diese in sein ErrorLogbook ein.
Manager Zustandsfolge-Automat
Der SoftwareManager Zustandsfolge-Automat repräsentiert die möglichen Zustände der gesamten Software bzw. des Systems. Der AEnManager Zustandsfolge-Automat repräsentiert die möglichen Zustände des einzelnen Architekturelements.
Bild 17: Manager Zustandsfolge-Automat
Manager Ausführungsfluss
Bild 18: Manager Ausführungsfluss auf oberster Ebene
Als Zwischenzusammenfassung ist hier der Ausführungsablauf der einzelnen Funktionen auf Ebene des SoftwareManagers dargestellt. Dahinter verbergen sich die delegierenden Aufrufe der AEnManager. Dies ist nur eine Ablaufvariante von vielen möglichen.
Diskussionspunkte und Verbesserungen
Das bisher vorgestellte Manager Pattern lässt sich durch weitere Design- und Implementierungsdetails, aber auch Varianten verändern und ergänzen. Im Folgenden werden hierzu ein detaillierteres Grundkonzept und ein detaillierteres erweitertes Konzept vorgestellt.
Detailliertes Grundkonzept mit Fehlernotifikation
Im Common::Management sind das Fehler-Notifikationsinterface icbcErrorHandler (icbc == Interface Callback Class) und weitere Klassen für Diagnose und Fehlerbehandlung enthalten. Diese Elemente werden gleichermaßen in allen Managern verwendet.
Bild 19: Detailliertes Grundkonzept mit Fehlernotifikation
Das Applikationselement cN notifiziert über die notifyError() Funktion des Interfaces icbcErrorHandler die Fehler an den cAEnManager. Der cAEnManager wiederum notifiziert seine Fehler über das gleiche Interface an den cSoftwareManager.
Bei Anwendung der Komposition erfolgt die Objektinstanziierung als eingebettete Elemente/ Attribute:
Die Aufrufe der delegierten Funktionen müssen hier einzeln erfolgen, da keine gemeinsamen Typen definiert sind:
Der exemplarische C++ Programmcode des detaillierten Grundkonzepts ist im Download zu diesem Beitrag enthalten (siehe unten).
Detailliertes erweitertes Konzept mit Fehlernotifikation und Manager-Interface
Bild 20: Detailliertes erweitertes Konzept mit Fehlernotifikation und Manager-Interface
Bei Anwendung der Assoziation/ Aggregation erfolgt die Objektinstanziierung dynamisch und der Zugriff über Zeiger:
Die Aufrufe der delegierten Funktionen erfolgen in einer Schleife (einfach erweiterbar), da alle cAEnManager den gemeinsame Interfacetype icAEnManager (ic == Interface Class) implementieren und der cSoftwareManager über ein Zeigerarray darauf zugreift:
Der exemplarische C++ Programmcode des detaillierten erweiterten Konzepts ist im Download zu diesem Beitrag enthalten (siehe unten).
Resümee
In der Praxis muss jede Embedded-Software die hier vorgestellten verteilten und zentral zu koordinierenden Aufgaben ausführen. Anforderungsabhängig sind weniger oder mehr dieser Aufgabenart auszuführen. Um die konzeptionelle Integrität auch über mehrere Projekte zu wahren, eignet sich der Einsatz von Software Patterns. Wie bei jedem Pattern müssen der Softwarearchitekt, der Softwareentwickler bzw. das Softwareteam die hier exemplarisch vorgestellten Strukturen und Abläufe auf die individuellen Gegebenheiten in der Software anpassen.
Der 1. Teil des Beitrags beleuchtet die Grundlagen des Manager Pattern für die Koordination verschiedener zentraler Aufgaben der Software.
Holen Sie sich das richtige Wissen darüber, wie das Embedded Software Manager Pattern in Ihren Software-Architekturen und Ihrem Software-Design anzuwenden ist. Dabei lassen Sie das Konzept mit der immer steigenden Komplexität der Software, unabhängig von der Programmiersprache und dem Programmieransatz (prozedural / objektorientiert), dauerhaft mitwachsen. MicroConsult bietet Ihnen dazu professionelle Trainings und Coachings rund um die Themen Analyse, Design und Architektur uvm. an.
Weiterführende Informationen
MicroConsult-Download für diesen Beitrag, komplett und aktuell (zip)
MicroConsult Fachwissen zum Thema Embedded SW-Entwicklung
MicroConsult-Trainings zum Thema:
- Requirements Engineering und Management für Embedded-Systeme
- Software-Architekturen für Embedded- und Echtzeitsysteme
- Embedded C++: OO-Programmierung für Mikrocontroller mit C++/EC++
- Embedded C++ für Fortgeschrittene
- Embedded-Software-Design und Patterns mit C
- Interfacedesign – Analyse, Design und Architektur