C++ auf dem AVR: avr-classes

Hintergrund

Frage:  Warum sollte man einen 8-Bit-Mikrocontroller mit C++ programmieren? Ist das nicht einfach nur unnötige Grausamkeit?
Antwort:  Zuerst war es Neugier, dann Faszination und jetzt ist es eine Klassenbibliothek, die ich in anderen AVR-Projekten nutze.

Tatsächlich bedeutet der Einsatz von C++ gegenüber C einen gewissen Mehraufwand für den Mikrocontroller. Dieser schlägt vor allem bei exzessivem Einsatz virtueller Methoden zu Buche, also bei Anwendung der Techniken und Tricks, die objektorientierte Programmierung so mächtig machen. Während der Umsetzung meiner Bibliothek habe ich jedoch beobachtet, dass dieser Mehraufwand viel geringer ausfällt als ich anfangs erwartet hatte.

Auf dieser Seite berichte ich von meinen Erfahrungen und stelle die avr-classes-Bibliothek vor. Eine ausführlichere Beschreibung der Klassen und Methoden ist als Doxygen-Dokumentation verfügbar. Um nicht jedem die Installation von Doxygen zuzumuten steht die generierte HTML-Dokumentation auch als separater Download bereit.

Auf dem AVR verfügbare C++-Features

Folgende Tabelle gibt einen Überblick zu den C++-Features, die durch die AVR-GCC-Toolchain verfügbar gemacht werden. Dabei beziehe ich mich auf den Entwicklungsstand von 2019 (Atmel Studio 7, AVR-GCC 5.4.0), der aber im Wesentlichen seit 2016 unverändert scheint. Weiterhin habe ich angegeben, welche der verfügbaren Features in avr-classes auch verwendet werden.

Feature Verfügbar Verwendet Kommentar
C++-Standard C++14 C++11 Verwendet werden nullptr, override und Lambda-Expressions; siehe auch C++ Standards Support in GCC.
Konstruktoren, Destruktoren ja ja Keine Einschränkungen.
Überladene Funktionen und Operatoren ja ja Keine Einschränkungen.
Default-Parameter ja ja Keine Einschränkungen.
Vererbung ja ja Keine Einschränkungen.
Virtuelle Methoden ja ja Erhöht RAM-Verbrauch (2 Byte pro Methode pro Instanz der betreffenden Klasse), sollte daher mit Bedacht eingesetzt werden.
Mehrfachvererbung ja ja Wird in avr-classes zur Implementierung mehrerer Interfaces (rein virtuelle Methoden) genutzt.
Templates ja ja Die STL ist nicht verfügbar, aber eigene Templates können benutzt werden.
Namespaces ja ja Keine Einschränkungen.
Dynamischer Speicher jein nein Die avr-libc stellt ein malloc und free bereit, aber die Operatoren new und delete müssen selbst bereitgestellt werden; siehe auch avr-libc: Memory Areas and Using malloc().
Exceptions nein --- Es muss explizit mit -fno-exceptions übersetzt werden; siehe auch avr-libc: Can I use C++ on the AVR?
Run-time Type Information nein --- Damit gibt es kein dynamic_cast.

Um dynamischen Speicher habe ich absichtlich einen Bogen gemacht, weil ich die Gefahr der Speicherfragmentierung kategorisch ausschließen wollte. Dennoch gibt es viele Komponenten, die z.B. dynamische Registrierung und Deregistrierung zur Laufzeit erlauben. Dies ist durch die Verwendung statischer Arrays realisiert, deren Größe als Template-Parameter angegeben wird. Dies ist ein recht passabler Kompromiss zwischen statischer Dimensionierung und einfacher Konfigurierbarkeit durch den Anwender.

Abstraktion und Abhängigkeiten

Auf oberster Ebene ist die Klassenbibliothek in drei verschiedene Projekte aufgeteilt:

Das Abhängigkeitsdiagramm zeigt die zu Idee, die der Aufteilung in Projekte zugrunde liegt:

Die Klassen aus core sind generisch und haben keine Abhängigkeiten zu den anderen Projekten (wohl aber untereinander). Viele von ihnen stellen nur Interfaces in Form von Klassen mit rein virtuellen Methoden bereit, die von spezifischen Klassen aus ham32a implementiert werden. Ähnlich dazu hängen die Klassen aus devices nur von den allgemeinen Komponenten aus core ab, nicht aber von hardwarespezifischen Klassen.

Die Projekte werden zu statischen Bibliotheken übersetzt. Theoretisch könnte somit die plattformspezifische Bibliothek gegen eine andere getauscht werden, ohne die Bibliotheken core und devices neu übersetzen zu müssen. Praktisch funktioniert das nicht ganz, weil auch bei der Übersetzung von core und devices eine konkrete Zielplattform (z.B. -mmcu=atmega32a) angegeben werden muss, da die verschiedenen Versionen des AVR-Cores unterschiedliche Instruktionen unterstützen.

Dennoch ist die Aufteilung der Projekte sinnvoll, weil sie hilft die Abhängigkeiten bewusst zu gestalten, sodass letztendlich die Projekte austauschbar bleiben und lediglich neu übersetzt werden müssen, um auf eine anderen Controller umzusteigen. Ein Umstieg auf eine ganz andere Plattform (z.B. ARM) würde hingegen auch Umbauten in core erfordern, da viele der hardwarenahen Komponenten auf die AVR8-Plattform zugeschnitten sind.

Generell ist es die Applikation, die konkrete, plattformspezifische Klassen instanziiert, da diese für ein spezielles Zielsystem entwickelt werden kann, während die Bibliothek für alle möglichen Szenarien einsetzbar bleiben muss. Die allgemein gehaltenen Komponenten sehen die Instanzen der konkreten Klassen nur als Implementierung eines abstrakten Interfaces. Dies soll im Folgenden an einem Beispiel aus dem Bereich GPIO erklärt werden.

Dieses Diagramm zeigt nur einen Ausschnitt der GPIO-Komponenten, der gerade groß genug ist um die Idee darzustellen. Die wesentlichen Punkte sind:

Soweit sogut. Um also eine LED an Pin PB2 zu steuern muss also eine PortB-Instanz angelegt werden, als Ausgang konfiguriert werden (mindestens Pin 2 davon) und eine OutputPortPin-Instanz erzeugt werden, die PortB benutzt und weiß, dass sie Pin 2 steuert.

PortB MyPort;
MyPort.setDirection(OUTPUT, 1 << 2);
OutputPortPin MyLED(&MyPort, 2);
MyLED.setValue(true);

Die wahre Macht der Interfaces wird aber erst sichtbar, wenn man sich die weiteren Klassen ansieht.

Diese weiteren Klassen erlauben es, OutputPins auf Shift-Registern abzubilden:

PortB MyPort;
MyPort.setDirection(OUTPUT, 0x03);
OutputPortPin ClockPin(&MyPort, 0);
OutputPortPin DataPin(&MyPort, 1);
ShiftRegister74HC164 MyShiftRegister(&DataPin, nullptr, &ClockPin, nullptr);
OutputShiftRegisterPort MyShiftRegisterPort(&MyShiftRegister);
OutputPortPin MyLED1(&MyShiftRegisterPort, 0);
OutputPortPin MyLED2(&MyShiftRegisterPort, 1);
OutputPortPin MyLED3(&MyShiftRegisterPort, 2);
OutputPortPin MyLED4(&MyShiftRegisterPort, 3);

Das sieht jetzt nach einer Menge Code aus, aber man muss sich vor Augen halten was man dafür bekommt: man hat 4 OutputPortPin-Instanzen, die in Wirklichkeit auf einem über 2 Pins angeschlossenen Shift-Register liegen. Diese OutputPortPins kann man unabhängig voneinander wie normale Pins ansteuern. Außerdem kann man sie jeder Komponente, die ihrerseits nur einen OutputPin verlangt, als Ersatz für einen real vorhandenen Port-Pin unterjubeln. Zwar werden sich diese nicht in der gleichen Geschwindigkeit bedienen lassen wie echte Port-Pins, aber um etwa eine 7-Segment-Anzeige anzusteuern ist das egal.

Weiterhin existieren die Klassen PinArray und darauf aufbauend VirtualOutputPort, die zusammenhängende Ports anbieten, deren einzelne Pins beliebig verstreut sein können. Dies macht nicht nur das Ausbügeln einer etwas verkorksten Verdrahtung möglich, sondern stellt programmtechnisch sogar Ports mit 16, 32 oder krummen Breiten wie 27 Bits zur Verfügung.

Aber: bei all der Schwärmerei darf man nicht vergessen, dass am Ende der Ausführungskette eine 8-Bit-CPU steht, die zwar dank Emulationscode einen uint64_t über ein Dutzend Pins auf ein Array aus Shift-Registern ausgeben kann, aber dabei trotzdem ganz schön ins Schwitzen geraten wird. Nicht alles, was möglich ist, ist auch sinnvoll.

Features und Funktionen

Das avr-classes-Projekt ist noch relativ jung und wird als Hobby vorangetrieben, d.h. insbesondere auch nur um Features erweitert, die ich gerade im Moment brauche und haben will. Wenn sich jemand einbringen möchte, oder ein Feature besonders dringend vermisst, kann sich gerne bei mir melden.

Derzeit unterstützte Funktionen:

Derzeit unterstützte Controller:

Für die Zukunft geplante Funktionen:

An Ideen mangelt es also nicht direkt.

Downloads

Datum Version Datei Beschreibung
2020-01-02 1.0 avr-classes-1.0.zip Quellcode (Atmel Studio 7, C++)
avr-classes-doc-1.0.zip Doxygen-Dokumentation (HTML)

Zurück zur Hauptseite