Schlanke Embeded-Entwicklung mit Small C++

Fazit

Gelungene Schrumpfkur

Damit schrumpft das Binärprogramm wieder auf 394 Byte, aber das ist immer noch massiv mehr als die Variante ohne GPIO-Klasse. Diese verursacht allein durch den Zugriff via Pointer einigen Overhead, aber irgendwie muss man sich das Register ja schließlich merken, um dann das richtige Bit zu setzen oder zu löschen. Aber statt das konkrete Register im Objekt zu speichern, geht das in C++ auch im Typ: Aus GpioOut wird einfach ein entsprechendes Template, das die Pin-Informationen als Template-Parameter enthält.

Allerdings kann ein Pointer nicht direkt als Template-Parameter dienen, daher ist ein wenig Trickserei (also Typen-Casting) angesagt. Der Port und das Pin-Bit sind nun nicht mehr als Daten im Objekt, sondern Template-Parameter:

template <uintptr_t port, uint8_t bit>
class GpioOut

Und eine kleine (private) Hilfsfunktion macht aus dem uintptr_t jeweils wieder einen Pointer:

constexpr uint8_t volatile *portReg() {
return reinterpret_cast<uint8_t volatile *>(port);
}

Für den Konstruktor wird die Tatsache genutzt, dass das DDR-Register in der Regel genau ein Byte vor dem PORT-Register kommt, wieder via eine Hilfsfunktion. Und die set()-Funktion sieht fast aus wie vorher:

template <uintptr_t port, uint8_t bit>
class GpioOut
{
public:
GpioOut(bool initState)
{
set(initState);
*ddrReg() |= bitVal(bit);
}

void set(bool state)
{
if (state)
{
*portReg() |= bitVal(bit);
} else {
*portReg() &= ~_BV(bit);
}
}
private:
constexpr uint8_t volatile *portReg() {
return reinterpret_cast<uint8_t volatile *>(port);
}
constexpr uint8_t volatile *ddrReg() {
return portReg() - 1;
}
};

Dabei ist bitVal() eine kleine Hilfsfunktion, die das Bit an der entsprechenden Stelle setzt:

constexpr uint8_t bitVal(uint8_t bit)
{
return 1 << bit;
}

_BV() ist ein Makro aus der AVR Libc, die genau dasselbe übernimmt. Interessanterweise erkennt der Compiler, dass *port Reg() &= ~_BV(bit) ein Bit in einem Register löscht, und macht aus dem ganzen Ausdruck einen einzigen Clear-Bit-Befehl (cbi). Kommt aber statt dem Makro _BV() die constexpr-Funktion bitVal() zum Einsatz, erkennt der Compiler das Muster nicht mehr. Das entsprechende Set-Bit zwei Zeilen vorher erkennt der Compiler dagegen auch mit bitVal().

Bleibt noch, das Programm auf das neue GpioOut umzuschreiben. Für den Typcast von der Portadresse auf uintptr_t wird wieder eine Hilfsfunktion verwendet:

constexpr uint16_t port2addr(uint8_t volatile &p)
{
return reinterpret_cast<uintptr_t>(&p);
}

Der Datentyp für das LED-Gpio sieht dann so aus:

typedef GpioOut<port2addr(PORTB), 5> MyLed;

Die Funktionen erhalten statt einem GpioOut einfach ein MyLed als Parameter, und in main() definiert man das entsprechende Objekt (das man jetzt ohne schlechtes Gewissen auf den Stack nehmen kann, da das Objekt die Größe null hat):

MyLed led(true); 

Und prompt ist das Binärprogramm wieder schön klein, nämlich nur noch 222 Byte. Die 6 Byte gegenüber dem bisher kleinsten Programm werden dadurch gespart, dass init() jetzt inline ist (als
Konstruktor von GpioOut).

Die Definition des Typs MyLed sieht etwas unschön aus, aber das ist rein kosmetisch und lässt sich gegebenfalls mit einem Makro aufbessern. Problematischer ist, dass die Funktionen ein MyLed als Parameter haben und dadurch nur für genau diese LED funktionieren. Aber auch das kann man ändern, indem der LED-Typ in die Template-Parameterliste der Funktionen mit aufgenommen wird. Das bleibt als Übung dem Leser überlassen (ohne Größenänderung des Binärprogramms).

Fazit

Bjarne Stroustrups Aussage vom Anfang des Artikels gilt also auch für ganz kleine Systeme. In der Regel sind mit C++-Mechanismen Programme möglich, die besser wartbar sind, ohne größere oder weniger effiziente Programme zu produzieren als entsprechende C-Programme. Das gilt allerdings nur für den Vergleich von C++ zu C. Wie die Analyse des generierten Codes zeigt, produziert der GCC in einigen Fällen suboptimalen Code.

Die 90 Byte, die das eigentliche Programm (ohne Initialisierungscode) hier braucht, ließe sich mit handoptimiertem Assembler vermutlich auf weniger als 60 Byte drücken. Aber das Programm wäre dann deutlich schlechter wartbar. Und das etwas speziellere C++-Know-how, das zur Optimierung nötig war, steckt in den Klassen und Hilfsfunktionen und lässt sich in einer (Header-)Library verstecken. Das eigentliche Programm ist dagegen "ganz normales" C++. (ane)

Detlef Vollmann
ist ein aktives Mitglied des C++-Standardisierungskomitees (hauptsächlich in der Unterkommission zur Concurrency). Er ist einer der (vielen) Autoren des C++ Performance Report und führte die "Futures" in C++11 ein. Er liefert Support und Schulung zu Embedded-Systemen und Concurrency in C++.