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.

Anbieter 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.

(ID:43690572)