Experience Embedded

Professionelle Schulungen, Beratung und Projektunterstützung

Wer hat Angst vorm bösen "++"?

   
   

Autor: Matthias Bauer, redlogix Software & System Engineering GmbH

Beitrag - Embedded Software Engineering Kongress 2015

 

Manche Vorurteile halten sich hartnäckig. Zum Beispiel dieses: C++ ist für extrem ressourcenarme Systeme nicht geeignet. Dabei stimmt das schlichtweg nicht! Vielmehr bringt der Einsatz der richtigen C++-Sprachmittel gerade für Systeme mit extrem begrenzten Ressourcen unschätzbare Vorteile.

Indem der Compiler als Codegenerator genutzt wird, lassen sich aus generischen, flexibel konfigurierbaren Softwarekomponenten Programme realisieren, die keinerlei Runtime-Overhead gegenüber einer Speziallösung enthalten. Das ist gerade für Anwendungen interessant, die auf Plattformen mit sehr begrenzten Ressourcen (z.B. Codespeicher, RAM, Rechengeschwindigkeit, Energie) integriert werden.

Steht "++" für mehr Ressourcenbedarf?

Bei extrem oberflächlicher Betrachtung könnte man tatsächlich zu dem Schluss kommen, dass mit C++ entwickelte Software größeren Binärcode erzeugt als das Pendant in C.

Baut man zum Beispiel ein einfaches Hello-World-Programm in C und C++ mit der Entwicklungsumgebung Keil uVision für ein Cortex-M3-Target, dann sehen sich C++-Skeptiker zunächst bestätigt: Während das in C geschriebene Programm Codespeicher im niedrigen einstelligen Kilobyte-Bereich einnimmt, belegt das C++-Programm mehr als 30 kByte (siehe Abbildungen 1 & 2, PDF).

Für den Mehrverbrauch an Codespeicher ist jedoch nicht die Programmiersprache C++ verantwortlich. Durch die Verwendung mancher Teile der Standard Template Library handelt man sich Bibliothekscode ein, der nicht für Embedded-Anwendungen optimiert ist und zum Teil Features von C++ nutzt, die man in Anwendungen mit extrem begrenzten Ressourcen besser ausklammern sollte (z.B. Exception-Handling).

Bei der Verwendung der STL auf Embedded-Targets ist demnach aus mehreren Gründen Vorsicht geboten. Zum Beispiel machen die Collection-Klassen per Default von dynamischer Speicherallokierung Gebrauch, was auf Systemen ohne virtuellem Speichermanagement früher oder später zu Problemen durch Speicherfragmentierung führt.

Die Programmiersprache C++ an sich kann dagegen bedenkenlos eingesetzt werden, ohne dass dadurch mehr wertvolle Systemressourcen gebunden werden als durch den Einsatz von C. Bei C++ bezahlt man nämlich nur für die Features mit der Währung "Ressourcen", die man auch tatsächlich verwendet. Dazu sollte man natürlich genau wissen, welche Sprachfeatures ggf. Ressourcenmehrkosten verursachen.

Überblick über Ressourcenkosten einiger C++ Sprachfeatures

Sprachfeature

Ressourcenkosten

Einsatz­empfehlung (*)

Klassen ohne virtuelle Methoden

Kein Ressourcenmehrbedarf gegenüber normalen C-Funktionen, die auf Datenstrukturen arbeiten.

ohne Einschränkung

Vererbung

Keinerlei Ressourcenmehrbedarf. Eine Klasse, die Members von Basisklassen erbt  benötigt nicht mehr Code- oder Datenspeicher als wenn sie sämtliche Members selbst definiert hätte.

ohne Einschränkung

Verwendung von Zugriffsschutz für Klassenmembers (private, protected, public)

Kein Ressourcenmehrbedarf, da die Prüfung ausschließlich zur Compilezeit stattfindet. Durch das Konzept von Inline-Funktionen entstehen auch keine Kosten für öffentliche get- und set-Methoden zum Zugriff auf geschützte Member-Variablen.

ohne Einschränkung

Virtuelle Methoden

Typischerweise (compilerabhängig) wird zusätzlicher Codespeicher für Methodensprungtabellen und Datenspeicher für die Verwaltung eines Zeigers darauf benötigt.
Wollte man virtuelle Methoden in C „zu Fuß“ nachbilden, würden das mindestens ebenso hohe Ressourcenkosten verursachen.

mit Bedacht

Exceptions

Codespeicher und Datenspeicher (von Compiler zu Compiler deutlich unterschiedlich effiziente Implementierung)

eher nicht verwenden

Verwendung von Templates

Werden vollständig zur Compilezeit ausgewertet, daher kein zusätzlicher Ressourcenbedarf.

Ihr unbedachter Einsatz kann jedoch zu erheblichem Codespeicher-Mehrbedarf führen.

mit Bedacht

 

Insbesondere Templates machen die Sprache C++ extrem interessant für Embedded-Anwendungen. Durch sie lässt sich der C++-Compiler als Codegenerator verwenden, der aus generischen und damit flexibel einsetzbaren Softwarekomponenten zur Compilezeit hocheffizienten Code erzeugt. Das Konzept der Codegenerierung lässt sich sogar bis auf Treiberebene gewinnbringend einsetzen. Dazu betrachten wir einen Treiber für Digitaleingänge, der z. B. bei jeder steigenden Pulsflanke aus dem Interrupt-Kontext eine Applikationsfunktion aufrufen soll.

Die ISR-Routine eines solchen Treibers besteht in der Regel aus zwei Teilen:

  1. Ein hardwareabhängiger Teil prüft, ob der Interrupt durch den betreffenden Hardwarebaustein (in unserem Fall Input-Pin) ausgelöst wurde (falls mehrere Quellen den Interrupt auslösen können) und setzt den Interrupt ggf. zurück.
  2. Aufruf einer Callback-Routine, die nicht Teil des Treibers ist, sondern applikationsspezifische Funktionalität enthält (siehe Abbildung 3, PDF).

 

Fallbeispiel: Ein Treiber mit ISR in C

Die ISRs von in C implementierten Treibern rufen die applikationsspezifischen Callbacks typischerweise über Funktionszeiger. Die relevanten Codefragmente eines solchen Treibers sehen dann so aus (siehe Abbildung 4, PDF).

Bei diesem Ansatz braucht die Callback-Routine dem Treiber erst zur Laufzeit bekannt gemacht werden, was oftmals überflüssige Flexibilität ermöglicht. In den meisten Fällen wird bereits zum Compile-Zeitpunkt festgelegt, welche Callback-Routine der Treiber rufen soll.

Derselbe Treiber mit C++

Realisiert man den Treiber mit der Programmiersprache C++, kann man auch völlig ohne Funktionszeiger beliebige Callback-Funktionen in den Treiber einhängen. Dazu nutzt man C++-Templates und das Konzept der sogenannten Template-Spezialisierung. Ein in C++ realisierter Treiber kann demnach wie folgt aussehen (siehe Abbildung 5, PDF).

Dort, wo der Treiber einen Callback ausführen möchte, ruft er einfach die statische Methode invokeCallback() des Klassentemplates TDriverIsrCallback auf. Durch Spezialisierung des Klassentemplates TDriverIsrCallback kann man nun für den jeweiligen Treiber festlegen, was in dieser statischen Methode getan werden soll, d.h. von dort aus die gewünschte applikationsspezifische Callback-Routine aufrufen (siehe Abbildung 6, PDF).

Wenn der Compiler den Code der Treiberklasse parst, muss ihm die Spezialisierung noch nicht zur Verfügung stehen. Die Spezialisierung kann demnach außerhalb des Treibercode-Moduls erfolgen, damit der Treiber generisch bleibt und keine Abhängigkeit zum Applikationscode hat. Als Bindeglied zwischen den Applikationsmodulen und Treibern dient das Code-Modul mit der gezeigten Templatespeziali­sierung.

Da der Compiler den Aufruf von invokeCallback() als Inline-Methode wegoptimiert, entsteht nach der Codegenerierung Binärcode, als wäre die Callback-Routine direkt im Treiber aufgerufen worden, also wie hier gezeigt (siehe Abbildung 7, PDF).

Diese Technik bringt folgende Vorteile gegenüber der Callback-Lösung per Funktionszeiger:

  • Weniger Datenspeicher, da kein Funktionszeiger gespeichert werden braucht
  • Weniger Codespeicher, da die Adresse der Callback-Funktion im Laufe der Treiberinitialisierung nicht mittels setCallbackFCT() gefüllt werden muss
  • Der Compiler kann die Applikations-Callback-Funktionen sogar "inlinen".

 

Der letzte Punkt ist besonders interessant: Für kurze Inline-Callback-Routinen platziert der Compiler deren Inhalt direkt in die Treiber-ISR, d.h. aus der ISR wird dann gar keine Funktion aufgerufen; das Sichern der Rücksprungadresse, Sprung und Rücksprung entfallen also gänzlich. Das kann bei sehr häufig auftretenden Interrupts durchaus ins Gewicht fallen.

Die etwas gewöhnungsbedürftige Schreibweise, um Callback-Routinen mittels Templatespezialisierung einzuhängen, kann mittels eines Makros deutlich vereinfacht werden. Im Embedded-Software-Baukasten redBlocks, wo die hier vorgestellte Technik zum Einsatz kommt, wird statt einer Template-Spezialisierung gemäß Abbildung 6 folgendes Makro genutzt (der zweite Parameter DigitalInputA0:: CBK_ON_INPUT_CHANGED erlaubt die Auswahl des Callbacks, falls ein Treiber mehr als eine Callback-Routine besitzt; siehe Abbildung 8, PDF).

Die Quadratur des Kreises - wiederverwendbar und kein Overhead?

Der gezeigte Implementierungsansatz erlaubt eine saubere Trennung von Treibern in einen hardwareunabhängigen High-Level- und einen hardwareabhängigen Low-Level-Teil. Der High-Level-Teil (z.B. zuständig für die Datenpufferung in einem UART-Treiber) kann ohne jegliche Modifikationen zusammen mit unterschiedlichen Low-Level-Treibern auf verschiedenen Hardware-Targets zusammenarbeiten und ist damit portabel und wiederverwendbar (siehe Abbildung 9, PDF).

Trotz der sauberen Modularisierung entsteht mit der in diesem Artikel vorgestellten Technik keinerlei Ressourcenoverhead. Beim Einsatz von Funktionszeigern hingegen wird durch die Aufteilung von Treibern in einen High-Level- und einen Low-Level-Teil eine unnötige Indirektion bei Callbacks eingeführt.

Zusammenfassung

Mit C++-Templates definiert der C++-Standard einen mächtigen Codegenerator, der ganz gezielt eingesetzt werden kann, um aus generisch wiederverwendbaren Softwarebausteinen hocheffizienten Code für ressourcenarme Embedded-Systeme zu erzeugen. Eine wartungsfreundliche, modulare Softwarearchitektur lässt sich so bis auf Treiberebene ohne zusätzliche Ressourcenkosten realisieren.

Die hier vorgestellte Technik kommt in der redBlocks-Komponentenbibliothek zum Einsatz und wird dort unter anderem dafür verwendet, um Embedded-Software in die SiL-Umgebung des redBlocks-WYSIWYG-Simulators zu integrieren und dort automatisiert testen zu können.

Mit Hilfe des redBlocks-Eval-Pakets (erhältlich via www.redblocks.de) kann die hier beschriebene Technik einfach nachvollzogen werden.

 

Beitrag als PDF downloaden

 


Implementierung - unsere Trainings & Coachings

Wollen Sie sich auf den aktuellen Stand der Technik bringen?

Dann informieren Sie sich hier zu Schulungen/ Seminaren/ Trainings/ Workshops und individuellen Coachings von MircoConsult zum Thema Implementierung /Embedded- und Echtzeit-Softwareentwicklung.

 

Training & Coaching zu den weiteren Themen unseren Portfolios finden Sie hier.


Implementierung - Fachwissen

Wertvolles Fachwissen zum Thema Implementierung/ Embedded- und Echtzeit-Softwareentwicklung steht hier für Sie zum kostenfreien Download bereit.

Zu den Fachinformationen

 
Fachwissen zu weiteren Themen unseren Portfolios finden Sie hier.