Bare-Bones mit GCC und C++

Autor / Redakteur: Frank Pilhofer* / Martina Hafner

Neue Optionen des GCC ermöglichen es, nicht benötigte oder schlicht nicht vorhandene Funktionalität der C- und C++-Laufzeitbibliotheken wegzulassen oder auf den eigenen Bedarf zurechtzuschneiden. So gelingt die Entwicklung von C++-Software auch für kleine Mikrocontroller wie z.B. einen Cortex-M0.

Firmen zum Thema

Frank Pilhofer ist Software-Entwickler im Bereich Embedded Systems Solutions bei der Zühlke Engineering GmbH.
Frank Pilhofer ist Software-Entwickler im Bereich Embedded Systems Solutions bei der Zühlke Engineering GmbH.
( Zühlke Engineering)

Objektorientierte Entwicklung mit C++ findet auch auf Embedded Systems immer mehr Anhänger. Vielfach bestehen allerdings Vorurteile, dass C++ zu komplex für den Einsatz auf Mikrocontrollern sei. Mit GCC steht ein kostenloser Compiler bereit, der alle gängigen Plattformen unterstützt.

Ursprünglich für Workstations gedacht, macht es der GCC dem Anwender nicht leicht, kompakte Software für Mikrocontroller zu entwickeln. Die Standard-C- und C++-Laufzeitumgebungen sind mit ihrer Fülle von Features nicht für geringen Speicherbedarf optimiert. Ein naiv übersetztes „Hello World“-Programm kommt schnell auf mehrere hundert Kilobytes, so dass sich das Vorurteil zunächst zu bestätigen scheint.

Herausforderungen

Die Standards C und C++ spezifizieren eine große Bibliothek an Standardfunktionen, die vom Compiler und der Laufzeitumgebung bereitgestellt werden müssen. Die Funktionen vom einfachen memset() bis zur STL und regulären Ausdrücken (in C++ 2011) füllen Megabytes. Doch zum Glück stellt schon der Linker sicher, dass nur die tatsächlich verwendeten Funktionen mitgelinkt werden. Die einfachste Möglichkeit um die Größe eines Programms zu reduzieren, ist also der Verzicht auf Funktionen der Standardbibliothek. Aber es ist ja nichts gewonnen, wenn die entsprechende Funktionalität dann an anderer Stelle repliziert werden muss.

Doch lässt der C-Standard einen gewissen Spielraum zu, was die Komplexität der Implementation betrifft. So sind z.B. unterschiedliche Implementationen von scanf() denkbar. Eine Version, die gepufferte Eingabe verwendet (also immer so viel Eingabedaten wie möglich liest), ist viel performanter, doch auf einem kleinen Mikrocontroller reicht eventuell der Speicher nicht, um die von der Implementierung benötigten Puffergrößen im RAM bereitzustellen.

Auch beinhaltet der Standard Features, die auf einem Mikrokontroller eher selten gebraucht werden, wie z.B. Internationalisierung („localization“) und Unicode („wide characters“). Und gerade hier kommt das Prinzip, dass nur gelinkt wird, was auch verwendet wird, an seine Grenzen. Denn eine standardkonforme Implementierung von printf() muss sowohl das eine als auch das andere unterstützen.

Ein einzelner Aufruf von printf() würde also bedeuten, dass die gesamte Funktionalität für Internationalisierung und Unicode mitgelinkt wird. Ein Verzicht ist hier wenig praktikabel, denn dann müsste man aufgrund solcher Abhängigkeiten auf zu viele nützliche Funktionen verzichten. Hier hilft es nur, eine Standardbibliothek einzusetzen, die von sich aus auf diese Features verzichtet und z.B. das printf-Format „%ls“ für wide strings nicht unterstützt.

Schließlich, wenn wir auf einem Mikrocontroller ohne Betriebssystem – eben Bare-Bones – unterwegs sind, fehlen die üblichen Systemaufrufe, auf denen z.B. die GNU Libc aufbaut. Am prominentesten fehlen Ein-/Ausgabefunktionen wie read() und write(), die von einem Betriebssystem wie Unix oder Windows als Kernelfunktion bereitgestellt werden. Hier ist der Entwickler gefragt, entsprechende Funktionen im Board Support Package zu implementieren.

Es zeigt sich, dass es verschiedene Stellschrauben gibt, mit denen die Größe der Standardbibliothek angepasst werden kann. Es kommt darauf an, diese Schrauben zu kennen und passend zu beeinflussen.

GCC, Newlib und Co: Die Teile des Puzzles

Toolchain: Von ARM wird das Paket „GNU Tools for ARM Embedded Processors [1] bereitgestellt, eine Version der GNU Compiler Collection, die alle Cortex-M und Cortex-R Prozessorfamilien unterstützt. Dieses für Windows, Linux und Mac OS frei verfügbare Paket enthält alles, was für die Entwicklung benötigt wird: Compiler für C und C++, Linker, diverse Tools sowie die C- und C++-Standardbibliotheken.

Debugging: Zum Flashen und Debuggen des Programms benötigt der GNU Debugger noch die Unterstützung des Open On-Chip Debuggers OpenOCD [8]. Noch komfortabler geht das Debugging mit einer IDE wie Qt Creator [9] oder Eclipse CDT [10].

Standardbibliothek: Die mit der Toolchain mitgelieferte C-Standardbibliothek beruht auf der speziell für Embedded Systems gedachten „Newlib“ [2], die bereits deutlich leichtgewichtiger ist als die sonst unter Linux eingesetzte GNU libc oder die unter Windows eingesetzte msvcrt.

Noch kleiner wird es mit der ebenfalls mitgelieferten „newlib-nano“, in der einige Funktionen wie z.B. malloc() noch einmal durch weniger komplexe Äquivalente ersetzt sind. So schrumpft malloc() selber von über 3000 auf nur mehr circa 500 Zeilen. Diese Version der Standardbibliothek wird mit Hilfe der Linker-Option „--specs=nano.specs“ verwendet.

Eine weitere Alternative ist die „e_stdio“ des Contiki-Projektes [5] mit einer gegenüber Newlib-Nano noch mal eingedampften weil selten gebrauchter Features beraubten Implementierung der Standard-I/O-Funktionen.

Bare-Bones: Bei der Bare-Bones-Entwicklung muss ein gewisser Unterbau geschaffen werden, auf dem die C-Standardbibliothek aufsetzen kann. Die Newlib verwendet zu diesem Zweck die „libgloss“-Bibliothek als Betriebssystemabstraktion.

Für Bare-Bones-Systeme bringt Newlib weiterhin die „libnosys“ mit. In dieser Bibliothek existieren alle Funktionen als leere Stubs, die lediglich einen Fehlerstatus an die Libc zurückmelden. Praktischerweise dokumentiert Newlib für jede Funktion der C-Standardbibliothek, welche Funktionen der Betriebssystemabstraktion bereitgestellt werden müssen.

Die libnosys wird mitgelinkt, wenn die Linker-Option „--specs=nosys.specs“ verwendet wird. Die Angabe dieser Option ist in unserem Fall allerdings nicht mehr nötig, weil die obigen „nano.specs“ die „nosys.specs“ bereits beinhalten.

Fertig?: Damit sind die nötigen Voraussetzungen geschaffen, um ein einfaches „Hello World“ zu übersetzen. Es entsteht ein Programm mit circa 8 kB Programmcode, bestehend aus printf() und dessen Abhängigkeiten. Allerdings ist dieses Programm noch nicht lauffähig, denn GCC weiß ja noch gar nichts über die Architektur unseres Mikrocontrollers.

Bare-Bones Bottom Up: Bevor wir ein erstes minimalistisches Programm auf unserem Mikrocontroller laufen lassen können, müssen wir erst einmal in ein paar Untiefen des Controllers und des GCC-Compilers absteigen. Eine Vektor-Tabelle muss angelegt und per Linker Skript an die richtige Adresse gelinkt werden. Der Einsprungpunkt muss das Datensegment und die C/C++-Runtime initialisieren bevor main() aufgerufen wird. Speicher für Stack und Heap muss bereitgestellt werden. Der Newlib-Stub „_write“ muss implementiert werden, um unsere Konsolenausgaben zur richtigen Peripherie zu schicken. Aber letzten Endes landet man bei einem Programm bestehend aus ca. 9 kB Code und ca. 4 kB RAM.

Nach dieser Vorarbeit ist der Schritt von C zu C++ gering und besteht im Wesentlichen darin, das Programm mit „c++“ statt „gcc“ zu kompilieren und zu linken. Ein „Hello World“ in C++ ist ca. 7 kB größer als das Äquivalent in C, weil einige zusätzliche Funktionen der C++-Runtime mitgelinkt werden.

Polymorphie, Exceptions, Templates und Bare Bones: Passt das?

Standard-C++-Bibliothek: Grundsätzlich steht dem Entwickler jetzt die gesamte Sprachvielfalt von C++ und fast die gesamte C++-Bibliothek zur Verfügung. Nicht unterstützt sind lediglich einige Erweiterungen des C++ Standards von 2011 wie die Thread Support Library, die zusätzliche Unterstützung eines Betriebssystems benötigen würden.

Allerdings ist die GNU C++-Bibliothek (libstdc++) nicht unbedingt für kleinen Speicherbedarf (sowohl gemessen am ausführbaren Code als auch am RAM) optimiert. Wir erinnern uns, dass die GNU C-Bibliothek (libc) auch aus diesem Grund durch die Newlib ersetzt wurde. Ein entsprechendes platzsparendes Äquivalent der libstdc++ gibt es noch nicht.

Damit die Programme klein bleiben, sollte daher mit den C++-Bibliotheksfunktionen vorsichtig umgegangen werden. So schlägt die „Input/output Library“ (z.B. „cout“) schnell mit mehreren hundert Kilobytes zu Buche. Der einfache C++-String („std::string“) kommt auf moderatere ca. 10 kB.

Namespaces: C++ Namespaces haben keinen Einfluss auf die Größe des kompilierten Programms und können daher nach Bedarf eingesetzt werden. Nur die Debug-Informationen werden ein klein bisschen größer, aber die landen ja nicht auf dem Mikrocontroller.

Polymorphie: Polymorphie und damit verwandte Features wie virtuelle Methoden und abstrakte Klassen haben nur einen minimalen Einfluss auf die Größe des Programms und können daher auch nach Bedarf verwendet werden.

Etwas „teurer“ ist die Funktion „dynamic_cast“, die RTTI (Run-Time Type Information) voraussetzt.

Exceptions: Auch Exceptions können gegen einen geringen Aufpreis von wenigen Kilobytes verwendet werden. Richtig eingesetzt ermöglichen Exceptions eine sauberere Programmstruktur, und der „Aufpreis“ an Abhängigkeiten relativiert sich, weil alternative Strukturen zur Fehlerbehandlung auch zusätzlichen Code bedeuten. Gegenargumente sind, dass durch Exceptions der Programmfluss und die Laufzeit im Fehlerfall etwas intransparenter werden.

Dies sind aber Argumente, die je nach Projekt abgewogen werden sollten – aus technischer Sicht spricht erst einmal nichts gegen Exceptions.

Allerdings ist in der ARM-Toolchain die Unterstützung von Exceptions in der Standard-C++-Bibliothek abgeschaltet. Das bedeutet, dass undefiniertes Verhalten auftreten kann, wenn im Rahmen einer C++-Bibliotheksfunktion eine Exception geworfen wird, also z.B. beim Einfügen eines Elements in einen Vektor.

Die Verwendung von Exceptions in eigenem Programmcode ist von dieser Einschränkung aber nicht betroffen.

Templates: Der Name ist Programm: Templates ermöglichen die Wiederverwendung von Code-Schablonen. Auch Templates sind zunächst einmal harmlos, solange sie bewusst eingesetzt werden. Zum Problem werden Templates erst, wenn sie zu oft instanziiert werden.

Das „zu oft“ ist dabei natürlich auch sehr projektspezifisch. So könnte zum Beispiel eine Template für eine Liste von Datenobjekten sinnvoll sein. Wenn diese Template aber dann für viele ähnliche Datentypen verwendet wird, sollte diese Abstraktion neu überdacht werden.

Performance und andere Vorurteile: C++ hat immer noch einen Ruf, von der Performance hinter C herzuhinken. So wird gerne behauptet, dass insbesondere virtuelle Methoden und Exceptions langsam seien. Diese und andere Vorurteile wurden bereits in einem lesenswerten Report des C++-Standardkomitees entkräftet [7].

Natürlich ist zum Aufruf einer virtuellen Methode eine zusätzliche Indirektion über die „vtable“ einer Klasse nötig. Und natürlich entsteht Aufwand, wenn beim Auftreten einer Exception der passende Exception Handler gesucht werden muss. Das alles muss aber auch im Vergleich zu alternativen Implementierungen ähnlicher Konzepte z.B. zur Fehlerbehandlung gesehen werden. Durch die Verwendung dieser C++-Features entsteht kein zusätzlicher Aufwand, sondern nur „anderer.“

Lizenzmodelle von Open Source-Software

Gerade im Embedded-Bereich gibt es noch viele Missverständnisse, was die Lizenzmodelle von Open Source-Software angeht. Die kurze Antwort ist, dass alle Software, die in diesem Artikel vorgestellt wurde, auch im kommerziellen Umfeld zur Entwicklung von potentiell kostenpflichtiger Software genutzt werden darf, ohne dass diese Software von einer offenen Lizenz „infiziert“ wird.

Grundsätzlich darf jede Open Source-Software intern zur Entwicklung eingesetzt werden. Es zeichnet Open Source ja gerade aus, dass die Verwendung von Software nicht eingeschränkt ist. Von Interesse ist nur die explizite oder auch implizite Weitergabe von Software. Bei der Weitergabe von Software werden Kopien angefertigt, so dass das Copyright greift.

Bei der Entwicklung und Vertrieb von Software für einen Mikrocontroller wird ja beispielsweise der Compiler (z.B. GNU GCC) gar nicht weitergegeben, sondern nur das entstandene Kompilat – ein lizenzrechtlich völlig unproblematischer Vorgang.

Bei der Auslieferung von Software werden allerdings Programmcode und Funktionsbibliotheken weitergegeben, die explizit und implizit mit unserem Programm mitgelinkt werden. In unserem Fall sind dies die C-Standardbibliothek Newlib und die C++-Standardbibliothek GNU libstdc++. Eine Sonderrolle nimmt noch die Bibliothek libgcc ein, die implizite Funktionen des GNU C Compilers bereitstellt– zum Beispiel für Soft-Float oder auch Funktionen zur 64-Bit-Integer-Arithmetik. Hier ist also ein Blick auf die Lizenzen angebracht.

Dabei stellt sich heraus, dass Newlib eine Kollektion von Software vieler verschiedener Autoren ist. Die in den Dateien „COPYING.NEWLIB“ und „COPYING.LIBGLOSS“ gesammelten Einzellizenzen erlauben allesamt die kommerzielle Wiederverwendung, fordern aber teilweise – die sogenannte „3 Clause BSD License,“ dass auch bei einer Weitergabe in kompilierter Form ein Copyright-Vermerk in der Dokumentation der Software auftauchen muss. Um diesen Lizenzbedingungen Genüge zu tun, reicht es also, diese beiden Dateien in das ausgelieferte Softwarepaket zu übernehmen.

Das Lizenzmodell für die GNU libstdc++ und libgcc ist die „GNU Lesser General Public License“ (LGPL), die zusätzliche Anforderungen stellt, die eventuell eine Weitergabe des Quelltextes erfordern würden. Allerdings gilt darüber hinaus noch die „GCC Runtime Library Exception:“

When you use GCC to compile a program, GCC may combine portions of certain GCC header files and runtime libraries with the compiled program. The purpose of this Exception is to allow compilation of non-GPL (including proprietary) programs to use, in this way, the header files and runtime libraries covered by this Exception.

Diese Ausnahmebedingung erlaubt es uns wiederum, auch kommerzielle Software mit dem GCC zu entwickeln und zu vertreiben.

C++ auf einem Mikrocontroller: Zusammenfassung und Fazit

Dass der Einsatz von C++ statt C auch auf einem Mikrocontroller Sinn macht, hat dieser Artikel gar nicht in Frage gestellt. Stattdessen wurde widerlegt, dass C++ zu groß oder zu komplex für einen Mikrocontroller ist. Ein aktuelles Projekt des Autors besteht aus ca. 10,000 Zeilen C++ und passt in 40 k ROM und 4 k RAM. Es gibt also keine technischen Probleme, die einem Einsatz von C++ widersprechen würden.

Auch zeigt sich, dass der Einsatz von Open Source-Software kein Problem ist. Für größere Projekte, bei denen Lizenzkosten vernachlässigbar sind, mag es weiterhin sinnvoll sein, eine Entwicklungsumgebung und dazugehörigen Support einzukaufen. Für kleinere Labormuster oder Einzelentwicklungen, bei denen solche Lizenzkosten prohibitiv wären, sind GCC&Co. eine Option. Der Einsatz der Debugging-Tools ist im „Bare Metal“-Bereich noch etwas hakelig, aber vielversprechend. Dank der stark wachsenden Homebrew-Szene ist hier eine weitere Verbesserung zu erwarten.

Als Nebeneffekt erfährt man beim Aufsetzen eines BSP mit GCC viel über die untersten Schichten der Software, und über die Verzahnung der Puzzleteile wie Compiler, Standardbibliothek und Prozessor.

Literatur- und Quellenverzeichnis

[1] GNU Tools for ARM Embedded Processors, https://launchpad.net/gcc-arm-embedded/

[2] The Newlib Homepage, https://sourceware.org/newlib/

[3] PM0215, „STM32F0xxx Cortex-M0 Programming Manual“, http://www.st.com/st-web-ui/static/active/en/resource/technical/document/programming_manual/DM00051352.pdf

[4] RM0360, „Reference Manual, STM32F030x4/x6/x8 advanced ARM-based 32-bit MCUs“, http://www.st.com/st-web-ui/static/active/en/resource/technical/document/reference_manual/DM00091010.pdf

[5] Contiki: The Open Source OS for the Internet of Things, http://www.contiki-os.org/

[6] ISO/IEC 14882, „Information technology – Programming languages – C++“.

[7] C++ TR 18015, „Technical Report on C++ Performance“, Februar 2006. Siehe http://www.open-std.org/jtc1/sc22/wg21/docs/18015.html

[8] Open On-Chip Debugger, http://openocd.sourceforge.net/

[9] QT Creator, http://qt-project.org/wiki/Category:Tools::QtCreator

[10] Eclipse CDT, http://www.eclipse.org/cdt/

* Frank Pilhofer ist seit 2010 Software-Entwickler im Bereich Embedded Systems Solutions bei der Zühlke Engineering GmbH. Mit freundlicher Genehmigung wurde dieser Beitrag dem Tagungsband Embedded Software Engineering Kongress 2014 entnommen.

(ID:43690572)