Immer kürzer werdende Produktentwicklungszeiten und steigende Qualitätsanforderungen an die Software verlangen geradezu nach einer Soft-ware-Architektur, in der einzelne Module bereits getestet sind und durch einige wenige Parameter an neue Erfordernisse angepasst werden können. Tipps zum guten Gelingen.
Wird über konfigurierbare Software im Embedded-Bereich gesprochen, wird meist an komplexe Systeme im Automotive-Umfeld, wie z.B. AUTOSAR, gedacht. Für kleine bis mittlere Projekte im Verbraucher- oder Industrie-Bereich gilt der Aufwand als zu hoch, als dass es sich lohnen würde, das System konfigurierbar auszulegen. Dabei wird oft vergessen, dass die verwendete Architektur die Komplexität mitbestimmt.
Die Architektur ist in solchen Systemen meist weniger aufwendig, weshalb es sich in der Praxis gezeigt hat, dass es durchaus lohnenswert ist, auch kleinere Software-Systeme konfigurierbar anzulegen. Gerade im Hinblick auf die Software-Qualität und die Projektlaufzeit, welche heutzutage immer wichtiger wird, kann es ein entscheidender Vorteil sein, schwer handhabbare Teile der Software, wie Treiber und komplexere Applikationsmodule, derart auszulegen, dass diese in verschiedenen Applikationen verwendet werden können. Im günstigsten Fall ist dann nur noch eine Neukonfiguration bei der Wiederverwendung nötig.
Den Entwicklern steht dann mit der Zeit eine Bibliothek funktionsfähiger und geprüfter Software-Module zur Verfügung. Vorteilhafterweise werden parallel dazu die entsprechenden Unit- und Software-Modultests ebenfalls im Software-Management-System abgelegt. Test-Suiten müssen dann nicht mehr mit jedem Projekt neu erstellt, sondern können ebenfalls wiederverwendet werden. Da sich Treiber und Applikationsfunktionen einer gewissen Evolution erfreuen, u.a. um die Generalität und Konfigurierbarkeit erhalten zu können, ist es jedoch erforderlich, die Bibliotheksmodule entsprechend den Erfordernissen anzupassen. Dieser Aufwand ist jedoch im Vergleich geringer als eine Neuentwicklung. Insbesondere Design-, Test- und Dokumentationsaufwand verkürzen sich für diese Module erheblich, was sich positiv auf die Projektlaufzeit und Kosten auswirkt. Entwickler können sich somit auf die Implementierung neuer Applikationsmodule und die Software-Integration konzentrieren.
Auch werden sich neue Projektmitarbeiter einfacher und schneller in das System einarbeiten. Dazu ist eine tool-unterstützte Konfiguration hilfreich. Da es meist keine Konfigurations-Tools für eine selbstgewählte Architektur gibt, müssen diese ebenfalls neu entwickelt werden. Die Komplexität solcher Tools hängt natürlich stark von den Wünschen der Benutzer sowie den Möglichkeiten der zu konfigurierenden Software ab. Selbst- verständlich wird man die Frage stellen müssen, wie weit der Aufwand getrieben werden sollte, um solch ein Stadium zu erreichen. Wie sollte also vorgegangen werden, um die gewünschte Flexibilität und Software-Qualität zu erreichen, um laufende Projekte oder gar ein neues Projekt nicht zu gefährden?
Wiederverwendungs-„Overkill“ vermeiden
Die Erfahrung zeigt, dass es weder sinnvoll noch notwendig ist, ein Projekt von Anfang an so zu planen, dass alle Module konfigurierbar sind. Dies führt fast immer zum Exitus, so dass zukünftige Versuche nicht mehr angegangen werden. Die Auswahl eines Software-Moduls, welches konfigurierbar sein soll, sollte mit Bedacht durchgeführt werden. Je nach Erfahrung des Entwicklers sind dabei unterschiedliche Vorgehensweisen denkbar. Meist wird mit einem kleinen Modul angefangen, um das Prinzip zu verstehen und sich mit den zu verwendenden Methoden vertraut zu machen.
Ebenfalls ohne besonderes Risiko ist es möglich, ein bestehendes Modul nach und nach mit Konfigurationsoptionen auszustatten. Unabdingbar ist es jedoch, sich mit den Möglichkeiten und Grenzen des verwendeten Präprozessors auseinanderzusetzen. Insbesondere mit spezifischen Befehlen der Compiler-Toolchain sollte sparsam umgegangen werden, um eine eventuell spätere Portierung auf einen anderen Hersteller nicht unnötig zu erschweren.
Anhand zweier Beispiele soll im Folgenden die grundsätzliche Vorgehensweise verdeutlicht werden. Die vorgestellte Methode zur Software-Konfiguration benutzt ausschließlich Präprozessordirektiven. Um ein Software-Modul konfigurierbar zu gestalten, genügen oft einfache Anweisungen wie z.B. #define, #if (Ausdruck) bzw. #if -Konstante mit dem dazugehörigen #else und #endif. Ein einfaches Beispiel zeigt eine Benachrichtigung des Anwenders, falls eine Funktion aufgerufen wird. Üblicherweise wird die Definition von Konstanten in Header-Dateien eingetragen.
Datei: foo.h
#define DEBUG_FUNCTION
Diese Header-Datei kann - je nach Architektur - in beliebige Source-Dateien eingebunden werden, um diesen Dateien die Konstante DEBUG_FUNCTION bekannt zu machen.
Um dem Anwender anzuzeigen, dass eine Funktion aufgerufen wurde, muss in der C-Datei die Konstante abgefragt und der Code zum Versenden der Nachricht eingeführt werden (Listing 1).
Soll die Nachricht nicht ausgegeben werden, so muss die Definition in der Header-Datei auskommentiert werden. Die Konstante existiert dann nicht mehr und der Debugcode wird beim Compilieren nicht eingebunden.
Dieses Verfahren funktioniert zwar problemlos, lässt sich jedoch hinsichtlich Lesbarkeit und Fehlertoleranz noch verbessern.
Hierzu definiert man in einer Header-Datei, die nach Möglichkeit systemweit eingebunden werden kann, die zwei Konstanten ENABLE und DISABLE, denen zusätzlich noch Integer-Werte zugewiesen werden:
Datei: std_define.h
#define DISABLE 1
#define ENABLE 2
Jetzt kann man in foo.h die Definition der Debug-Konstanten für jedermann lesbar und verständlich implementieren. Um die neuen Symbole verwenden zu können, muss natürlich std_define.h in foo.h eingebunden werden. Zum Aktivieren der Funktionsüberwachung wird das Symbol DEBUG_FUNCTION auf den Wert ENABLE und zum Deaktivieren auf den Wert DISABLE gesetzt (Listing 2). In der C-Datei darf dann die #ifdef-Konstante nicht mehr verwendet werden. Stattdessen muss man den Präprozessor einen Ausdruck mit #if (Ausdruck) berechnen lassen.
Ist das Ergebnis wahr, dann wird der Code zwischen #if und #endif bzw. #if und #else ausgeführt - ganz analog einer if-Anweisung in C (Listing 3).
Es sollte beachtet werden, dass viele Präprozessoren Symbole, die nicht existieren, mit dem Wert Null initialisieren. Einem Symbol sollte daher nicht der Wert Null zugewiesen werden, da ansonsten nicht erkannt wird, ob die Definition tatsächlich existiert. Folgender Ausdruck wird daher mit TRUE evaluiert, falls DISABLE den Wert Null hat und DEBUG_FUNCTION in foo.h nicht definiert ist.
#if (DEBUG_FUNCTION ==
DISABLE)
Neben diesen einfachen Konfigurationsmöglichkeiten können auch ganze Berechnungen mit Konstanten durchgeführt werden. Dies ist besonders zur Initialisierung von Timern oder Baudratengeneratoren nützlich. Bei vorgegebener Taktfrequenz des Prozessors muss der Anwender dann zur Initialisierung der Schnittstellengeschwindigkeit diese nur noch in eine Header-Datei eintragen. Die Berechnung und Zuweisung auf die korrekten Register übernehmen dann, wie in Listing 4 dargestellt, Ausdrücke und Makros in den jeweiligen Software-Modulen.
Zu erwähnen ist, dass hierbei nur die Konfigurationsdatei uart_cfg.h geändert werden muss. Es ist nicht notwendig, den C-Quellcode zu ändern. Dadurch werden mögliche Fehler in der Funktionalität vermieden. Der Entwickler kann sich darauf verlassen, einen funktionierenden Code zu erhalten und die durchzuführenden Testsequenzen können wesentlich kürzer ausfallen. Weiterführende Techniken benötigen zusätzlich ein angepasstes Design. Die Praxis hat gezeigt, dass es möglich ist, einen I/O-Treiber derart zu gestalten, dass die zugehörigen Quelldateien prozessorunabhängig sind. Die Anpassung an neue Prozessoren oder Projekte erfolgt hierbei ebenfalls nur durch Ändern der entsprechenden Konfigurationsdateien. Diese müssen sich nicht einmal im I/O-Treiber-Modul selbst befinden. Dadurch ist es möglich, solche Module, voll funktionsfähig und getestet, als Library-Module den Entwicklern zur Verfügung zu stellen.