Schlanke Embeded-Entwicklung mit Small C++

Seite 4: Objektorientierung

Inhaltsverzeichnis

Bei den AVRs ist ein GPIO durch den Port definiert (je nach konkretem Modell PORTA bis PORTL) und das Bit innerhalb des Ports. Im Beispiel wird PORTB, GPIO 5 verwendet. Zu jedem Port gibt es (im Wesentlichen) drei Register: das PORT-Register, in das der jeweilige Output-Wert (0 oder 1) geschrieben wird, das PIN-Register, aus dem der Input-Wert gelesen wird, und das DDR-Register, das festlegt, ob der entsprechende Pin ein Input oder ein Output ist. Im normalen Betrieb werden für GpioOut nur das PORT-Register und das entsprechende Pin-Bit benötigt, also sind als (private) Daten der Klasse die Adresse des Port-Registers und das Bit definiert. Damit sieht die Klasse mit einer Member-Funktion zum Setzen des Outputs wie folgt aus:

class GpioOut
{
public:
void set(bool state)
{
if (state)
{
*port |= bitVal;
} else {
*port &= ~bitVal;
}
}

private:
volatile uint8_t *port;
uint8_t bitVal;
};

Bleibt noch der Konstruktor. Die AVR Libc liefert für die verschiedenen AVR-Modelle Makrodefinitionen für die Register, also zum Beispiel PORTB, DDRB und PINB (die so ja auch schon im bisherigen Programm verwendet wurden). Diese Definitionen sollen weiterverwendet werden. Dazu bietet es sich an, eine entsprechende Hilfsstruktur zu definieren und ein Makro, das diese Hilfsstruktur füllt:

struct GpioDef
{
volatile uint8_t *portAddr;
volatile uint8_t *dirAddr;
volatile uint8_t *pin_addr;
uint8_t bit;
};
#define GPIO(port, bit) GpioDef({&PORT##port, &DDR##port, &PIN##port, bit})

Damit lässt sich der Pin mit GPIO(B, 5) definieren. Der Konstruktor bekommt jetzt also als Parameter eine GpioDef und den Initialwert:

GPIO::GpioOut(GpioDef def, bool initState)
: port(def.portAddr)
, bitVal(_BV(def.bit))
{
set(initState);
*def.dirAddr |= bitVal;
}

Im Programm lassen sich die Funktionen init(), ledOn() und ledOff() streichen und dafür ein entsprechendes globales Objekt definieren:

GpioOut led(GPIO(B, 5), true);

Und ledHalfCycle() wird einfacher:

template <int ms>
void ledHalfCycle(bool on)
{
led.set(on);
delay_ms(ms);
}

Damit sieht das Programm schon etwas mehr nach C++ aus, aber nach dem Kompilieren kommt das böse Erwachen: Das Binärprogramm ist auf 460 Byte gewachsen – und das ohne bessere Funktionalität, nur durch etwas Modularisierung. Ist C++ für so kleine Prozessoren vielleicht doch keine so gute Idee?

Eine erneute Analyse des generierten Codes zeigt, dass der Zuwachs an Code zu einem wesentlichen Teil auf Kosten des Konstruktors für das globale Objekt geht. Der GCC produziert relativ viel Infrastruktur für die dynamische Initialisierung (lies: Konstruktoren) globaler Variablen. Wer entsprechende globale Objekte haben will, muss einmal für die Infrastruktur zahlen.

Um nur die LED blinken zu lassen, kann man aber auf das globale Objekt verzichten und die LED stattdessen in main() definieren und sie den Funktionen als Argument via Referenz übergeben. Globale Variablen sind ja eigentlich sowieso keine gute Idee. (Auf so kleinen Systemen wie dem AVR sind in der Regel globale Variablen zwar besser als solche auf dem Stack, aber das ist ein anderes Thema …)