Einleitung: Warum C++ in Embedded kein Masochismus, sondern eine Superkraft ist
\n\nWenn man „Embedded-Entwicklung“ hört, stellt man sich meist einen gestandenen Ingenieur mit Lötkolben vor, der in purem C schreibt und auf das Datenblatt schwört. Oder das Gegenteil – einen Enthusiasten, der digitalWrite() auf einem Arduino nutzt und sich freut, dass eine LED blinkt. Aber zwischen diesen beiden Welten gibt es eine goldene Mitte – Embedded-Entwicklung mit C++.
Ja, genau das C++, das mit Templates und virtuellen Funktionen erschreckt. Tatsächlich sind moderne Mikrocontroller (ARM Cortex-M, RISC-V) durchaus in der Lage, C++-Code zu verarbeiten. Mehr noch: Der sinnvolle Einsatz von OOP, Kapselung und sogar Templates kann Ihren Embedded-Code sauberer, sicherer und schneller machen.
\n\nIn diesem Artikel schauen wir uns an, wie man effizienten C++-Code für Mikrocontroller schreibt, umgehen die Fallstricke mit virtuellen Tabellen und lernen, mit der Hardware auf Registerebene zu arbeiten – aber im C++-Stil. Los geht's?
\n\n1. Grundlagen: Was Embedded C++ von normalem C++ unterscheidet
\n\nDie Hauptregel der Embedded-Entwicklung: Ressourcen sind begrenzt. Sie haben keine Gigabyte RAM und keinen Gigahertz-Prozessor. Sie haben 32 KB Flash und 8 KB RAM. Jedes Byte zählt. Daher geht es bei C++ in Embedded nicht um „Ich schreibe eine Klasse mit 10 Vererbungsebenen“, sondern um Kontrolle über Abstraktionen.
\n\nWas Sie ohne Bedenken verwenden können:
\n- \n
- Klassen und Strukturen – zum Gruppieren von Daten und Funktionen. Das ist (speichertechnisch) kostenlos, wenn es keine virtuellen Funktionen gibt. \n
- Namensräume – um keine Präfixe wie
hal_gpio_init()zu erzeugen. \n - constexpr und Templates – Berechnungen zur Compile-Zeit, spart Taktzyklen. \n
- Referenzen und Übergabe als konstante Referenz – sicherer als Zeiger. \n
Was Sie ohne dringenden Bedarf besser nicht anfassen sollten:
\n- \n
- Dynamischer Speicher (new/delete) – der Heap fragmentiert, der Allokator kann versagen. In Bare-Metal-Projekten tabu. \n
- Ausnahmen (try/catch) – erfordern Runtime-Unterstützung, nehmen viel Platz ein. \n
- Virtuelle Funktionen – jede virtuelle Funktion erzeugt eine Tabelle (vtable) in RAM/Flash, und der Aufruf erfolgt über doppelte Indirektion. Aber manchmal sind sie gerechtfertigt (Treiber). \n
- RTTI (typeid) – wird gar nicht benötigt. \n
Der Kern ist einfach: Der C++-Compiler erzeugt genauso effizienten Code wie C, wenn Sie keine „schweren“ Features verwenden. Überprüfen Sie den Disassembler – und das Glück ist auf Ihrer Seite.
\n\n2. Praxis: Arbeit mit Registern im C++-Stil
\n\nIm klassischen Embedded C schreiben wir so etwas wie:
\n#define GPIOA_BASE 0x40020000\n#define GPIOA_MODER (*(volatile uint32_t*)(GPIOA_BASE + 0x00))\nGPIOA_MODER |= (1 << 10); // Setzt Bit 10 (Ausgang)\n\nDas funktioniert, sieht aber archaisch aus. In C++ können wir einen praktischen Wrapper ohne Overhead erstellen.
\n\nBeispiel: Klasse für die Arbeit mit einem Pin
\n#include <cstdint>\n\nclass GpioPin {\npublic:\n // Kopieren verbieten\n GpioPin(const GpioPin&) = delete;\n GpioPin& operator=(const GpioPin&) = delete;\n\n // Aufzählung für Modi\n enum class Mode : uint32_t {\n Input = 0b00,\n Output = 0b01,\n AltFunc = 0b10,\n Analog = 0b11\n };\n\n // Konstruktor: Nimmt die Portadresse und die Pin-Nummer (0-15)\n GpioPin(volatile uint32_t* port_base, uint8_t pin)\n : port_(port_base), pin_(pin) {\n // Kann den Pin sofort konfigurieren\n }\n\n // Betriebsmodus setzen\n void setMode(Mode mode) {\n volatile uint32_t* moder = port_ + 0x00; // Offset des MODER-Registers\n *moder &= ~(0b11 << (pin_ * 2));\n *moder |= (static_cast<uint32_t>(mode) << (pin_ * 2));\n }\n\n // High-Pegel setzen\n void high() {\n volatile uint32_t* bsrr