Der Mikrocontroller in der Praxis: aktuelle MCU-Landschaft, Toolchains, Peripherie, Interrupts und DMA, Bussysteme, FreeRTOS und Zephyr, Linker und Bootloader, Debugging, Stromsparen und die C-Fallstricke, die jeden einmal erwischen.
01 / 11Start & Landkarte
Firmware ist der Teil, in dem alles zusammenläuft. Die Elektronik gibt vor, was möglich ist, die Regelungstechnik, was passieren soll — und hier wird daraus ein Gerät, das startet, arbeitet, sich meldet und sich aktualisieren lässt. Es ist auch der Teil, in dem die meiste Entwicklungszeit steckt.
Jedes Detail selbst; nur sinnvoll für kritische Stellen
Ein pragmatischer Weg: Idee im Ökosystem prüfen, fürs fertige Gerät auf das Hersteller-SDK wechseln, Register nur dort direkt, wo es nötig ist (enge ISRs, exaktes Timing). Das Referenzhandbuch des eigenen Controllers einmal durchblättern — nicht lesen, nur wissen, was drinsteckt.
Wie ein Projekt typischerweise verläuft
Board zum Leben erwecken
Versorgung prüfen, Takt konfigurieren, eine LED blinken lassen, serielle Ausgabe zum Laufen bringen. Erst wenn das steht, lohnt alles andere.
Peripherie einzeln in Betrieb nehmen
Jeden Sensor, jeden Aktor für sich testen — mit einem minimalen Programm. Wer alles gleichzeitig aufbaut, sucht Fehler in einem Suchraum, der zu groß ist.
Struktur festlegen
Was läuft im Interrupt, was im Hauptprogramm, was periodisch? Wo liegen die geteilten Daten? Diese Entscheidung im Nachhinein zu ändern ist teuer.
Funktion aufbauen
Zustandsautomaten statt verschachtelter Wartezeiten. Alles Zeitgesteuerte über einen zentralen Systemtick.
Robust machen
Watchdog, Brownout, Plausibilitätsprüfungen, definiertes Verhalten bei Sensorausfall, Fehlerprotokoll im Flash.
Wartbar machen
Versionsnummer, Diagnoseausgabe, Update-Weg, Dokumentation der Pinbelegung.
Grundlage dieses Moduls
Elecia White, Making Embedded Systems, 2. Auflage, O'Reilly.
Joseph Yiu, The Definitive Guide to Arm Cortex-M (verschiedene Ausgaben je Kern).
Referenzhandbuch und Datenblatt des jeweiligen Controllers — die einzige verbindliche Quelle.
02 / 11Mikrocontroller: Familien & Auswahl
Die Wahl des Controllers entscheidet über Monate Arbeit. Nicht die Rechenleistung ist der übliche Engpass, sondern Peripherie, Werkzeuge, Verfügbarkeit und Dokumentation. Ein Controller mit doppelter Taktfrequenz und schlechtem SDK ist die schlechtere Wahl.
Die Familien im Überblick
Familie
Kern
Takt
Speicher
Funk
Stärke
AVR (ATmega, ATtiny)
8 Bit, eigener Kern
bis 20 MHz
2–256 kB Flash
—
Sehr einfach zu verstehen, tolerant, Arduino-Ursprung. Für Neuentwürfe meist zu schwach
RP2040 / RP2350
2× Cortex-M0+ bzw. 2× M33 oder 2× RISC-V (Hazard3)
133 / 150 MHz
264 / 520 kB SRAM, externer QSPI-Flash
W-Varianten mit WLAN/BLE
PIO-Blöcke für frei programmierbare Schnittstellen, exzellente Dokumentation, sehr günstig
STM32
Cortex-M0+ bis M7, M33, M55
32–800 MHz
16 kB – 4 MB
bei WB/WBA-Serie
Riesige Auswahl, sehr viel Peripherie, industrielle Verfügbarkeit. STM32N6 mit 800 MHz Cortex-M55 und NPU als Spitze
ESP32-Familie
Xtensa oder RISC-V
80–400 MHz
bis 768 kB SRAM, PSRAM möglich
immer
Funk eingebaut, sehr preiswert, riesige Community. ESP32-P4 (400 MHz RISC-V) für Multimedia, C5 mit 5-GHz-WLAN, C6 mit Wi-Fi 6 und Thread
nRF52 / nRF54
Cortex-M4F / M33 (nRF54H20 zusätzlich mit RISC-V-Koprozessoren)
64–320 MHz
bis 2 MB Flash, 1 MB RAM
BLE, Thread, Matter, Zigbee
Der Maßstab bei Funk mit sehr geringem Stromverbrauch; nRF54L15 mit 128 MHz M33, 1,5 MB NVM
SAM / PIC / AVR-DA (Microchip)
Cortex-M0+ bis M7, PIC-Kerne
32–300 MHz
breit gefächert
teils
Sehr lange Verfügbarkeit, gute Analogperipherie
CH32V / GD32V
RISC-V
48–144 MHz
16–256 kB
teils
Extrem günstig (unter 20 Cent), wachsende Werkzeugunterstützung
i.MX RT (NXP)
Cortex-M7 / M33
bis 1 GHz
externer Speicher
—
„Crossover“ — Rechenleistung wie ein Anwendungsprozessor bei Mikrocontroller-Determinismus
Welche Familie wofür
Familie
Leistung
Peripherie
Funk
Sparsam
Einstieg & Doku
Günstig
AVRATmega, ATtiny
1 von 5
2 von 5
1 von 5
3 von 5
5 von 5
3 von 5
StarkEinfach zu durchschauen, 5-V-tolerant, Arduino-Ursprung: jede Frage ist schon beantwortet.Schwach8 Bit, wenig Speicher, kein Funk; für Neuentwürfe meist zu schwach.Nimm es fürKleinstaufgaben, Lernen am Register, 5-V-Umfeld.
RP2350Raspberry Pi Pico 2
3 von 5
4 von 5
2 von 5
3 von 5
5 von 5
5 von 5
StarkPIO für beliebige Schnittstellen, das beste Einsteigerhandbuch, sehr günstig, ARM oder RISC-V.SchwachKein Flash im Chip, Funk nur in der W-Variante, ADC mittelmäßig.Nimm es fürErstes Projekt, viele parallele Schnittstellen, LED- und Encoder-Aufgaben.
STM32Cortex-M0+ bis M55
5 von 5
5 von 5
2 von 5
4 von 5
3 von 5
3 von 5
StarkRiesige Auswahl, hochauflösende Timer, schnelle ADCs, industrielle Verfügbarkeit.SchwachGroßes, uneinheitliches Ökosystem; CubeMX und HAL wollen eingearbeitet werden.Nimm es fürMotorsteuerung, Regelung, Messtechnik, Produkte mit langer Laufzeit.
ESP32-FamilieS3, C3, C6, P4
4 von 5
3 von 5
5 von 5
2 von 5
4 von 5
5 von 5
StarkWLAN und Bluetooth eingebaut, viel RAM, riesige Community, sehr billig.SchwachAktiv stromhungrig, Deep Sleep braucht Sorgfalt, Analogteil schwach.Nimm es fürAlles mit Netz: Sensoren im WLAN, MQTT, Displays, Home Assistant.
nRF52 / nRF54Nordic
3 von 5
3 von 5
5 von 5
5 von 5
3 von 5
2 von 5
StarkDer Maßstab bei BLE, Thread und Matter mit sehr geringem Stromverbrauch.SchwachZephyr-basiertes SDK mit steiler Lernkurve; teurere Boards.Nimm es fürBatteriegeräte über Monate, Wearables, Funksensoren.
CH32V / GD32VRISC-V, Cent-Klasse
2 von 5
2 von 5
1 von 5
3 von 5
2 von 5
5 von 5
StarkUnter 20 Cent, brauchbare Peripherie, wachsende Werkzeugunterstützung.SchwachDokumentation dünn, Werkzeuge noch im Aufbau, kleine Community.Nimm es fürKostengetriebene Kleinserie, wenn Zeit für Einarbeitung da ist.
Punkte sind Einordnungen typischer Vertreter (5 = sehr gut), keine Messwerte. Sie helfen bei der Vorauswahl — entschieden wird am Datenblatt.
Auswahl nach Anwendungsfall
Erstes Projekt, Lernen
Raspberry Pi Pico 2 oder ein ESP32-Board. Beide billig, gut dokumentiert, mit MicroPython und C nutzbar. Der Pico hat das mit Abstand beste Handbuch für Einsteiger.
Etwas mit WLAN oder Bluetooth
ESP32-C6 oder ESP32-S3. Funk, Speicher und Werkzeuge in einem, sehr viele fertige Bibliotheken. Für Batteriebetrieb über Monate lieber nRF52840 oder nRF54L15.
Motorsteuerung, Regelung
STM32G4 oder STM32F4 — hochauflösende Timer, mehrere schnelle ADCs, Komparatoren, teils mit Hardware-Beschleuniger für Regelalgorithmen.
Viele parallele Schnittstellen
RP2350 mit seinen PIO-Blöcken: acht kleine Zustandsautomaten, die beliebige Protokolle takten — WS2812, DVI, mehrere UARTs, Encoder, alles ohne CPU-Last.
Batterie, Jahre Laufzeit
nRF54L, STM32U5 oder MSP430. Entscheidend ist nicht der aktive Strom, sondern der Ruhestrom und wie schnell der Controller wieder einschläft.
Bildverarbeitung, KI am Gerät
ESP32-P4, STM32N6 (Cortex-M55 mit NPU) oder gleich ein Linux-System. Vorher genau rechnen: Kamera und Modell bestimmen den Speicherbedarf, nicht die Taktfrequenz.
Die Kriterien in der richtigen Reihenfolge
Peripherie zählen
Wie viele PWM-Kanäle, ADC-Kanäle, UARTs, SPI, CAN? Und: Lassen sich diese Funktionen gleichzeitig auf die vorhandenen Pins legen? Die Alternate-Function-Tabelle im Datenblatt entscheidet häufiger über die Wahl als die Rechenleistung.
Speicher großzügig ansetzen
Ein Netzwerkstack, TLS und ein Dateisystem brauchen schnell 100 kB RAM. Wer knapp wählt, zahlt später mit einem Neuentwurf. Eine Nummer größer kostet meist wenige Cent.
Werkzeuge und Dokumentation prüfen
Gibt es ein vollständiges Referenzhandbuch? Läuft das SDK auf dem eigenen Betriebssystem? Gibt es Beispiele für die benötigte Peripherie? Ein Forum mit aktiven Antworten?
Verfügbarkeit prüfen
Lieferzeit und Lebenszyklus bei Mouser, Digi-Key oder LCSC ansehen. Hersteller geben oft Longevity-Zusagen — für alles, was länger als ein Jahr leben soll, ist das relevant.
Erst dann Rechenleistung
Für Regelung, Kommunikation und Bedienung reichen 48–160 MHz fast immer. Fließkomma-Einheit lohnt sich, sobald Regelalgorithmen oder Signalverarbeitung ins Spiel kommen.
Entwicklungsboards, die sich bewährt haben
Board
Preis
Besonderheit
Raspberry Pi Pico 2 / 2 W
≈ 5–8 €
RP2350, wahlweise ARM- oder RISC-V-Kerne, PIO, sehr gutes Handbuch
Strommessung eingebaut, hervorragend für Batterieoptimierung
Teensy 4.1
≈ 30 €
i.MX RT1062 mit 600 MHz, Arduino-kompatibel, sehr schnell
Arduino Uno R4 / Nano ESP32
≈ 20–25 €
Für den Einstieg mit vertrauter Umgebung, aber moderner Hardware
Ein Board mit eingebautem Debugger ist die beste Investition am Anfang. Ein Nucleo-Board oder ein Debug-Adapter (ST-Link, J-Link EDU, CMSIS-DAP) kostet wenig und macht Breakpoints, Speicheransicht und Fault-Analyse möglich — siehe Debuggen & Messen.
Verfügbarkeit und Lebenszyklus: Produktseiten bei Mouser, Digi-Key und LCSC.
03 / 11Toolchain & Build
Vom C-Quelltext zur Datei im Flash sind es vier Schritte: Präprozessor, Compiler, Assembler, Linker. Wer weiß, welcher Schritt gerade meckert, findet den Fehler in Minuten — die meisten unverständlichen Meldungen kommen vom Linker.
Der Weg vom Quelltext zum Gerät
Präprozessor
Löst #include, #define und bedingte Übersetzung auf. Fehlerbild: fehlende Header, falsche Include-Pfade, doppelte Makros. Ergebnis ansehen mit gcc -E.
Compiler
Erzeugt je Übersetzungseinheit Assembler. Fehlerbild: Syntax, Typen, unbekannte Bezeichner. Warnungen ernst nehmen — sie zeigen fast immer echte Probleme.
Assembler
Erzeugt Objektdateien (.o). Selten die Fehlerquelle.
Linker
Fügt Objektdateien und Bibliotheken zusammen und legt sie nach dem Linker-Skript im Speicher ab → Speicherbild & Linker.
Umwandeln und Flashen
objcopy macht aus der ELF-Datei ein .bin oder .hex. Geflasht wird per Debug-Adapter, Bootloader oder Massenspeicher-Emulation (Tabelle unten).
Linker-Meldung
Bedeutet
Übliche Ursache
undefined reference to …
Symbol wird benutzt, aber nirgends definiert
Quelldatei nicht im Build, Bibliothek nicht eingebunden, C++-Namensdekoration ohne extern "C"
multiple definition of …
Symbol mehrfach definiert
Variable im Header statt in einer .c-Datei — dort nur extern deklarieren
region FLASH overflowed
Programm größer als der Speicherbereich
Zu viel Code oder zu große Konstanten; mit -Os und --gc-sections prüfen
region RAM overflowed
Statische Daten passen nicht ins RAM
Große Puffer, Tabellen ohne const, zu groß dimensionierte Stacks
Zwei Befehle, die man kennen sollte:arm-none-eabi-size firmware.elf zeigt die Belegung (text = Code und Konstanten im Flash, data = initialisierte Variablen, bss = uninitialisierte; Flash = text + data, RAM = data + bss). arm-none-eabi-nm --size-sort -S firmware.elf listet die größten Symbole — der schnellste Weg zur Frage, warum das Programm plötzlich 40 kB größer ist.
Die SDK-Landschaft
SDK
Für
Build
Charakter
Arduino Core
fast alles
Arduino CLI, PlatformIO
Einfachster Einstieg, riesige Bibliothekssammlung. Auf modernen Cores (ESP32, RP2350, STM32) liegt darunter das Hersteller-SDK — beides lässt sich mischen
ESP-IDF
ESP32-Familie
CMake, idf.py
Vollständig, FreeRTOS eingebaut, Konfiguration über menuconfig, sehr gute Dokumentation
STM32Cube (HAL/LL)
STM32
CMake oder Makefile, CubeIDE
CubeMX erzeugt Initialisierungscode aus einer grafischen Pinbelegung — spart Zeit, erzeugt viel Code
Pico SDK
RP2040 / RP2350
CMake
Schlank, sauber dokumentiert, mit PIO-Assembler für eigene Schnittstellen
nRF Connect SDK
Nordic nRF
West (Zephyr)
Basiert auf Zephyr; steile Lernkurve, dafür sehr vollständig für Funkanwendungen
Zephyr
herstellerübergreifend
West, CMake, Kconfig, Devicetree
Portabel über Hunderte Boards, LTS-Zweig für Produkte. Der Preis ist die Einarbeitung in das Konfigurationssystem
MicroPython / CircuitPython
ESP32, RP2, nRF, STM32
—
Interaktiv über REPL, ideal zum Ausprobieren von Sensoren. Für zeitkritische Aufgaben zu langsam und zu wenig deterministisch
Konfiguration: Kconfig und Devicetree
Kconfig — welche Software
Legt fest, was übersetzt wird: Netzwerkstack ja oder nein, Log-Stufe, Puffergrößen. Aus dem Linux-Kernel übernommen, in Zephyr und ESP-IDF verwendet.
Devicetree — welche Hardware
Beschreibt, was vorhanden ist: welcher Sensor an welchem I²C-Bus mit welcher Adresse, welcher Pin eine LED ist. Der Code greift über Aliase zu, statt Pinnummern fest einzutragen.
Der Aufwand lohnt, sobald eine Firmware auf mehreren Boardvarianten laufen soll: statt #ifdef-Ketten ändert man eine Overlay-Datei. Für ein einzelnes Bastelprojekt ist es Überbau, für ein Produkt mit drei Hardwareständen die einzige saubere Lösung.
Flashen und Debuggen
Weg
Werkzeug
Debuggen
Anmerkung
SWD / JTAG
ST-Link, J-Link, CMSIS-DAP, Raspberry Pi Debug Probe
ja
Der Standardweg: Breakpoints, Speicheransicht, Live-Variablen → Debuggen & Messen
USB-DFU
dfu-util
nein
Bootloader im ROM, kein Adapter nötig
UART-Bootloader
stm32flash, esptool
nein
Bei ESP32 der Normalfall; Boot-Pins beim Start beachten
Massenspeicher
Datei auf ein Laufwerk kopieren
nein
Pico im BOOTSEL-Modus, viele mbed-Boards
OpenOCD / probe-rs
quelloffen
ja
Sehr viele Adapter und Ziele; probe-rs ist deutlich einfacher einzurichten
Compiler- und Linker-Schalter
Schalter
Wirkung
Wann
-Og
Optimierung, die Debuggen erlaubt
Während der Entwicklung
-O2
Standardoptimierung
Für die Auslieferung
-Os
Auf Größe optimieren
Wenn der Flash knapp wird
-Wall -Wextra
Warnungen einschalten
Immer
-ffunction-sections -fdata-sections
Jede Funktion in eine eigene Sektion
Zusammen mit -Wl,--gc-sections: entfernt ungenutzten Code
-g3
Volle Debug-Information
Immer — sie landet in der ELF-Datei, nicht im Flash
-Wl,-Map=firmware.map
Erzeugt die Speicherkarte des Linkers
Wenn Flash oder RAM knapp werden: zeigt, welches Objekt wie viel belegt
--specs=nano.specs
Kleine C-Bibliothek (newlib-nano)
Spart mehrere Kilobyte gegenüber der vollen printf-Implementierung
Optimierungsstufe ändert Verhalten — aber nie korrekten Code. Wenn ein Programm mit -O0 läuft und mit -O2 nicht, liegt fast immer ein echter Fehler vor: eine Variable, die eine ISR ändert, ohne volatile; ein Zugriff über einen Zeiger falschen Typs; eine Warteschleife ohne Nebenwirkung. Details unter C-Praxis & Fallstricke.
Die Peripherie ist der eigentliche Grund, einen Mikrocontroller zu nehmen. Timer, DMA und Wandler arbeiten unabhängig von der CPU weiter — richtig eingesetzt erledigt die Hardware Aufgaben, für die Software zu langsam und zu ungenau wäre.
GPIO richtig konfigurieren
Einstellung
Optionen
Wann was
Richtung
Eingang, Ausgang, Alternate Function, Analog
Analog schaltet die Digitalstufe ab und spart Strom — für ADC-Pins immer
Ausgangstyp
Push-Pull, Open-Drain
Open-Drain für I²C, Wired-AND, Pegelanpassung nach oben
Pull-Widerstand
Pull-up, Pull-down, keiner
Eingänge nie floaten lassen. Interne Pull-ups liegen bei 20–50 kΩ
Flankensteilheit
Low, Medium, High Speed
So langsam wie möglich — steile Flanken erzeugen Störungen und Überschwingen
Treiberstärke
2–20 mA je nach Controller
Gesamtstrom über alle Pins beachten, nicht nur den je Pin
Der Strom durch einen GPIO-Pin ist begrenzt — der über die ganze Portgruppe auch. Ein STM32 verträgt oft 20 mA je Pin, aber nur 80–120 mA über den gesamten Baustein. Acht LEDs mit je 20 mA direkt am Controller sind bereits jenseits der Spezifikation. Ab wenigen Milliampere gehört ein Transistor oder Treiberbaustein dazwischen.
Timer: der vielseitigste Block
Zeitbasis
Zähler plus Vorteiler erzeugen einen periodischen Interrupt. Der Systemtick für alles Zeitgesteuerte.
PWM (Compare)
Der Ausgang wechselt bei einem Vergleichswert. Auflösung und Frequenz teilen sich denselben Zähler: bei 100 MHz Takt sind volle 16 Bit nur bis ≈1,5 kHz möglich, bei 100 kHz PWM bleiben rund 10 Bit.
Input Capture
Der Zählerstand wird bei einer Flanke am Eingang festgehalten — ohne Interrupt-Jitter. Für Frequenz-, Pulsweiten- und Drehzahlmessung deutlich genauer als Software.
Encoder-Modus
Der Timer wertet zwei Quadratursignale direkt aus und zählt vor und zurück. Kein Interrupt, keine verlorenen Schritte — für Drehgeber die einzige richtige Lösung.
Komplementäre Ausgänge mit Totzeit
Advanced-Timer erzeugen zwei zueinander invertierte PWM-Signale mit einstellbarer Totzeit — Voraussetzung für jede Halbbrücke. Dazu ein Break-Eingang, der bei Überstrom die Ausgänge in Hardware sofort abschaltet.
Verkettung
Ein Timer triggert den nächsten oder den ADC. Damit lässt sich zum Beispiel exakt in der Mitte der PWM-Einschaltphase messen — der Standardtrick bei Motorstromregelung.
DMA — Daten bewegen ohne CPU
Der DMA-Controller kopiert selbstständig zwischen Peripherie und Speicher. Der Prozessor wird erst am Ende (oder zur Hälfte) benachrichtigt.
Anwendung
Ohne DMA
Mit DMA
ADC mit 100 kSa/s
100 000 Interrupts pro Sekunde — die CPU macht nichts anderes mehr
Ringpuffer im Hintergrund, mit Idle-Interrupt für Paketende
WS2812-LEDs
Bit-Banging blockiert alles
PWM oder SPI mit DMA, exaktes Timing ohne CPU-Last
Die drei DMA-Fallen: (1) Der Puffer muss dort liegen, wo der DMA-Controller hinkommt — bei manchen Controllern erreicht er das schnelle CCM-RAM nicht. (2) Bei aktivem Daten-Cache (Cortex-M7) muss vor dem Lesen invalidiert und nach dem Schreiben zurückgeschrieben werden, sonst sieht die CPU alte Daten. (3) Ein Puffer auf dem Stack ist ungültig, sobald die Funktion verlassen wird — DMA-Puffer gehören nach static oder in einen globalen Bereich.
ADC in der Praxis
Abtastzeit lang genug wählen
Der interne Sample-and-Hold-Kondensator braucht Zeit zum Laden. Bei hochohmiger Quelle die längste Abtastzeit einstellen — sonst hängt der Messwert vom vorher gemessenen Kanal ab.
Referenz klären
Wird gegen V_DDA gemessen, wandert der Messwert mit der Versorgung. Für absolute Messungen eine externe Referenz oder die interne Referenzspannung mit dem Werkskalibrierwert nutzen.
Über mehrere Werte mitteln
Oversampling mit 16 Werten bringt zwei zusätzliche effektive Bits. Viele Controller können das in Hardware.
Zeitpunkt festlegen
Bei PWM-getriebenen Lasten in der Mitte der Ein- oder Ausschaltphase messen, per Timer getriggert. Zufällig gemessene Werte sind bei geschalteten Lasten wertlos.
Interne Kanäle nutzen
Temperatursensor, Referenzspannung und Versorgungsüberwachung sind meist eingebaut — nützlich für Selbstdiagnose ohne Zusatzbauteile.
Weitere Blöcke, die oft übersehen werden
Block
Was er leistet
Wofür
Watchdog
Löst Reset aus, wenn er nicht regelmäßig zurückgesetzt wird
Pflicht in jedem Gerät, das unbeaufsichtigt läuft. Fenster-Watchdog erkennt auch zu häufiges Zurücksetzen
RTC
Uhrzeit mit Batteriepufferung, Weckfunktion
Zeitstempel, geplantes Aufwachen aus dem Tiefschlaf
Komparator
Analoge Schwelle in Hardware, ohne CPU
Überstromabschaltung in Nanosekunden, Nulldurchgangserkennung
CRC-Einheit
Prüfsumme in Hardware
Firmware-Integrität, Protokolle — deutlich schneller als Software
TRNG
Echter Zufallsgenerator
Schlüssel, Nonces, eindeutige Kennungen
Eindeutige ID
96–128 Bit Seriennummer im Chip
Geräteidentifikation, MAC-Adresse ableiten
PIO (nur RP2040/RP2350)
Kleine, frei programmierbare Zustandsautomaten an den Pins
Beliebige Protokolle in Hardware — der interessanteste Peripherieblock der letzten Jahre
Quellen
Referenzhandbuch des jeweiligen Controllers — Timer- und DMA-Kapitel sind gerätespezifisch und lassen sich nicht verallgemeinern.
STMicroelectronics AN4776 (Timer-Übersicht) und AN4031 (DMA-Nutzung).
Ein Interrupt unterbricht das laufende Programm — genau dort, wo man es am wenigsten erwartet. Deshalb sind fast alle sporadischen, nicht reproduzierbaren Fehler in Firmware Interrupt-Probleme: geteilte Daten ohne Schutz, zu lange Routinen oder eine Prioritätsvergabe, die niemand durchdacht hat.
Der Ablauf
Ereignis tritt auf
Peripherie setzt ein Flag und meldet es dem Interrupt-Controller (NVIC bei Cortex-M).
Prozessor sichert den Kontext
Bei Cortex-M werden acht Register automatisch auf den Stack gelegt — deshalb ist die Latenz deterministisch (12 Takte bei M3/M4/M7, 15 beim M0+) und es braucht keinen Assembler-Vorspann.
Handler läuft
Der Vektor zeigt auf die Funktion. Sie sollte kurz sein und keine blockierenden Aufrufe enthalten.
Flag löschen
Sonst wird die Routine sofort erneut aufgerufen. Bei manchen Controllern muss nach dem Löschen zurückgelesen werden, damit der Schreibvorgang die Peripherie erreicht hat, bevor der Handler endet.
Kontext wiederherstellen
Rücksprung ins unterbrochene Programm. Bei Tail-Chaining wird direkt der nächste anstehende Interrupt bedient, ohne zwischendurch zurückzukehren.
Prioritäten
Ebene
Beispiel
Regel
Höchste
Überstromabschaltung, Not-Halt, Motor-Break
Muss immer durchkommen, Routine extrem kurz
Hoch
Regelkreis-Tick, Encoder, ADC-Abschluss
Feste Periode, harte Frist
Mittel
Kommunikation: UART, SPI, CAN, DMA
Puffer füllen und leeren, keine Verarbeitung
Niedrig
Tastenabfrage, Display, Protokollierung
Kann warten; gehört meist ohnehin ins Hauptprogramm
Bei ARM Cortex-M ist die niedrigere Zahl die höhere Priorität. Priorität 0 ist die höchste. Und: Nur die oberen Bits des Prioritätsregisters sind implementiert — bei vier Bit sind das 16 Stufen, die Werte 0, 16, 32 usw. Wer 0, 1, 2 schreibt, bekommt möglicherweise dieselbe Stufe. Bei Verwendung eines RTOS gibt es zusätzlich eine Grenze, unterhalb derer keine RTOS-Aufrufe erlaubt sind (configMAX_SYSCALL_INTERRUPT_PRIORITY bei FreeRTOS).
Geteilte Daten sicher übergeben
Was volatile leistet
Es verhindert, dass der Compiler Zugriffe wegoptimiert oder in Register zwischenspeichert. Ohne volatile kann eine Schleife, die auf ein von der ISR gesetztes Flag wartet, endlos laufen, weil der Compiler das Flag nur einmal liest.
Was volatilenicht leistet
Es macht Zugriffe nicht atomar. Ein volatile uint32_t auf einem 8-Bit-Controller wird in vier Schritten gelesen — der Interrupt kann mittendrin zuschlagen. Und es ordnet keine Speicherzugriffe über mehrere Variablen hinweg.
Muster
Wann
Umsetzung
Einzelnes Flag
ISR meldet ein Ereignis
volatile bool; in der Hauptschleife prüfen und zurücksetzen
Kritischer Abschnitt
Mehrere zusammengehörige Werte
Interrupts kurz sperren (__disable_irq() / BASEPRI), Daten kopieren, wieder freigeben. So kurz wie irgend möglich
Ringpuffer
Datenstrom von der ISR ins Hauptprogramm
Ein Schreiber, ein Leser, Größe als Zweierpotenz. Dann sind ohne jede Sperre nur zwei Indizes nötig
Doppelpuffer
Ganze Datensätze, z. B. vom ADC per DMA
ISR füllt Puffer A, Hauptprogramm liest Puffer B, dann Tausch
Atomare Operation
Zähler erhöhen
Bei Cortex-M3 aufwärts LDREX/STREX bzw. die C11-Atomics; alternativ Interrupts kurz sperren
RTOS-Queue
ISR an Task
xQueueSendFromISR() — nie die normale Variante aus einer ISR aufrufen
Der Ringpuffer — das Muster, das man einmal richtig baut
#define RB_SIZE 256 /* Zweierpotenz! */
static volatile uint8_t rb[RB_SIZE];
static volatile uint16_t rb_w = 0, rb_r = 0;
/* nur aus der ISR aufgerufen */
static inline void rb_put(uint8_t c)
{
uint16_t next = (rb_w + 1u) & (RB_SIZE - 1u);
if (next != rb_r) { /* voll -> Zeichen verwerfen */
rb[rb_w] = c;
rb_w = next;
}
}
/* nur aus dem Hauptprogramm aufgerufen */
static inline int rb_get(void)
{
if (rb_r == rb_w) return -1; /* leer */
uint8_t c = rb[rb_r];
rb_r = (rb_r + 1u) & (RB_SIZE - 1u);
return c;
}
Warum das ohne Sperren funktioniert: Es gibt genau einen Schreiber für rb_w (die ISR) und genau einen für rb_r (das Hauptprogramm). Jeder liest nur den Index des anderen. Solange die Indizes einzeln atomar geschrieben werden — bei 16 Bit auf einem 32-Bit-Controller gegeben —, kann nichts durcheinandergeraten. Die Zweierpotenz macht aus dem Modulo eine UND-Verknüpfung.
Typische Fehler und ihre Symptome
Symptom
Wahrscheinliche Ursache
Prüfung
Programm hängt in der ISR fest
Interrupt-Flag nicht gelöscht
Debugger anhalten, Rücksprungadresse ansehen
Werte springen sporadisch
Mehrbyte-Variable ohne Schutz geteilt
Kritischen Abschnitt einbauen und beobachten, ob es verschwindet
Alles wird langsam, wenn Daten ankommen
Verarbeitung in der ISR statt im Hauptprogramm
GPIO-Pin am Anfang und Ende der ISR umschalten, am Oszilloskop messen
Hard Fault nach längerer Laufzeit
Stacküberlauf — ISR verschachteln sich tiefer als vorgesehen
Stack mit Muster vorbelegen und prüfen, wie weit es überschrieben wurde
Eine Aufgabe kommt nie dran
Eine hochpriore Quelle löst dauernd aus
Interruptzähler je Quelle mitzählen und ausgeben
Fehler nur mit Optimierung
Fehlendes volatile
Mit -O0 gegentesten; verschwindet der Fehler, ist es fast sicher das
Das nützlichste Messwerkzeug: Am Anfang der ISR einen GPIO-Pin auf High setzen, am Ende auf Low. Am Oszilloskop oder Logikanalysator sieht man dann sofort, wie lange die Routine läuft, wie oft sie aufgerufen wird und ob sich Interrupts verschachteln. Diese zwei Zeilen Code beantworten mehr Fragen als jede Simulation.
Quellen
Joseph Yiu, The Definitive Guide to Arm Cortex-M — Kapitel zu NVIC, Prioritäten und Latenz.
ARM, Cortex-M Programming Guide to Memory Barrier Instructions (DAI0321).
Vier Busse decken fast alles ab: I²C für viele langsame Sensoren an zwei Leitungen, SPI für hohe Datenraten auf kurzer Strecke, UART für Punkt-zu-Punkt und Debug-Ausgaben, CAN für störfeste Verbindungen über mehrere Meter. Die Wahl folgt aus Distanz, Anzahl der Teilnehmer und Datenrate.
Der Vergleich
Bus
Leitungen
Teilnehmer
Rate
Distanz
Stärke
Schwäche
I²C
2 (SDA, SCL) + GND
viele, per Adresse
100 k / 400 k / 1 M / 3,4 M
< 1 m
Wenige Pins, Adressierung eingebaut
Empfindlich, blockierbar, Adresskonflikte
SPI
4 (MOSI, MISO, SCK, CS)
je Teilnehmer ein CS
bis 100 MHz
< 30 cm
Sehr schnell, einfach, Vollduplex
Viele Pins, keine Rückmeldung, keine Adressierung
UART
2 (TX, RX)
genau 2
bis ~12 Mbit/s
< 5 m (mit RS-485 bis 1200 m)
Denkbar einfach, überall vorhanden
Nur Punkt zu Punkt, Baudraten müssen passen
CAN / CAN FD
2 differentiell + GND
bis 110
1 Mbit/s (FD bis 8 Mbit/s Daten)
bis 40 m bei 1 Mbit/s
Sehr störfest, Arbitrierung ohne Kollisionsverlust, Fehlererkennung eingebaut
Transceiver nötig, Abschlusswiderstände, mehr Einarbeitung
RS-485
2 differentiell
bis 32 (mehr mit modernen Treibern)
bis 10 Mbit/s
bis 1200 m
Sehr robust, große Reichweite
Halbduplex, Richtungsumschaltung, Protokoll (meist Modbus) selbst
USB
4 (D+, D−, V_BUS, GND); SuperSpeed zusätzlich zwei Paare
Host zu Geräten
12 M / 480 M / 5 G
< 5 m
Stromversorgung inklusive, am PC überall
Aufwendiger Protokollstapel, Host nötig
1-Wire
1 (+ GND)
viele
15 kbit/s (Overdrive 125 k)
bis 100 m
Minimale Verdrahtung, Speisung über die Datenleitung möglich
Sehr langsam, striktes Timing
Welcher Bus wofür
Bus
Datenrate
Reichweite
Störfest
Teilnehmer
Wenig Pins
Einfach
I²C2 Leitungen, Adressen
2 von 5
1 von 5
1 von 5
4 von 5
5 von 5
4 von 5
StarkZwei Leitungen für viele Sensoren, Adressierung eingebaut, jede Bibliothek kann es.SchwachUnter einem Meter, blockierbar, Adresskonflikte; Pull-ups wollen dimensioniert sein.Nimm es fürSensoren, Displays, EEPROMs auf derselben Platine.
SPI4 Leitungen, CS je Gerät
5 von 5
1 von 5
2 von 5
3 von 5
2 von 5
5 von 5
StarkSehr schnell, Vollduplex, trivial zu implementieren.SchwachEin Chip Select je Teilnehmer, keine Rückmeldung, nur kurze Leitungen.Nimm es fürDisplays, Flash, ADCs, alles mit hoher Datenrate auf dem Board.
UARTTX, RX
3 von 5
3 von 5
2 von 5
1 von 5
4 von 5
5 von 5
StarkDenkbar einfach, überall vorhanden, mit RS-485-Treiber weit.SchwachNur Punkt zu Punkt, Baudraten müssen passen, keine Fehlerprüfung.Nimm es fürDebug-Ausgabe, GPS, Funkmodule, zwei Controller miteinander.
CAN / CAN FDdifferentiell, Arbitrierung
3 von 5
4 von 5
5 von 5
5 von 5
4 von 5
2 von 5
StarkSehr störfest, Arbitrierung ohne Kollisionsverlust, Fehlererkennung in Hardware.SchwachTransceiver und Abschlusswiderstände nötig; das Protokoll darüber entwirft man selbst.Nimm es fürMehrere Baugruppen über Meter: Roboterarm, Fahrzeug, Maschine.
RS-485differentiell, halbduplex
3 von 5
5 von 5
5 von 5
3 von 5
4 von 5
3 von 5
StarkBis 1200 m, robust, günstige Transceiver.SchwachHalbduplex mit Richtungsumschaltung; Protokoll (meist Modbus) selbst.Nimm es fürGebäude, Feld, Sensoren über lange Leitungen.
Punkte sind Einordnungen typischer Vertreter (5 = sehr gut), keine Messwerte. Sie helfen bei der Vorauswahl — entschieden wird am Datenblatt.
I²C — worauf es ankommt
Pull-ups richtig dimensionieren
Beide Leitungen brauchen einen Pull-up nach V_CC — einmal im ganzen Bus, nicht je Modul. 4,7 kΩ bei 100 kHz, 2,2 kΩ oder weniger bei 400 kHz. Zu viele Module mit eigenen Pull-ups ergeben zusammen einen zu kleinen Widerstand und überlasten die Ausgänge.
Adresskonflikte prüfen
7-Bit-Adressen, oft nur teilweise einstellbar. Zwei gleiche Sensoren am selben Bus brauchen eine Adressbrücke oder einen I²C-Multiplexer (TCA9548A). Ein Scan-Programm, das alle 128 Adressen durchprobiert, gehört in jedes Projekt.
Kabellänge begrenzen
Die Buskapazität darf 400 pF nicht überschreiten. Bei längeren Strecken wird die Flanke zu flach — dann hilft ein Buspuffer (P82B715) oder ein Wechsel auf einen anderen Bus.
Blockierten Bus lösen
Wenn ein Teilnehmer mitten in einer Übertragung zurückgesetzt wird, kann er SDA dauerhaft auf Low halten. Standardlösung: neun Takte auf SCL erzeugen und dann eine Stopp-Bedingung senden. Diese Routine gehört in die Initialisierung.
Pegel beachten
3,3-V- und 5-V-Geräte am selben Bus brauchen einen Pegelwandler mit MOSFET (BSS138-Schaltung) — normale Pufferbausteine funktionieren bei Open-Drain nicht.
SPI — worauf es ankommt
Punkt
Detail
Modus (CPOL/CPHA)
Vier Kombinationen. Steht im Datenblatt des Bausteins; falscher Modus liefert scheinbar zufällige Daten. Modus 0 ist am verbreitetsten
Bitreihenfolge
Meist MSB zuerst, manche Bausteine LSB
Chip Select
Je Teilnehmer eine eigene Leitung, aktiv Low. Manche Bausteine brauchen zwischen zwei Übertragungen zwingend eine steigende Flanke
Taktfrequenz
Datenblattgrenze beachten, bei langen Leitungen deutlich darunter bleiben
Terminierung
22–47 Ω in Reihe zum Takt reduzieren Überschwingen erheblich
Bezeichnungen
MOSI/MISO heißen in neueren Datenblättern SDO/SDI oder COPI/CIPO. Immer prüfen, aus wessen Sicht benannt wurde
CAN — der unterschätzte Bus
Warum er so robust ist
Differentiell übertragen, Arbitrierung ohne Datenverlust (die niedrigere Kennung gewinnt und sendet ohne Wiederholung weiter), CRC-15 in Hardware, automatische Fehlererkennung mit Abschaltung defekter Knoten. Ein CAN-Netz läuft in Umgebungen weiter, in denen I²C längst aufgegeben hat.
Was man braucht
Einen CAN-Controller (in vielen Controllern eingebaut) plus einen Transceiver (MCP2551, TJA1051, SN65HVD230) und 120 Ω an beiden Enden der Leitung — nicht in der Mitte, nicht nur an einem Ende.
CAN FD und CAN XL
CAN FD erhöht die Nutzdaten von 8 auf 64 Byte und die Datenphase auf bis zu 8 Mbit/s. CAN XL geht weiter auf bis zu 2048 Byte und 20 Mbit/s und ist auf Koexistenz mit Ethernet ausgelegt.
Für eigene Projekte
Sobald mehrere Baugruppen über Meter verteilt sind — Roboterarm, Fahrzeug, Maschine — ist CAN die deutlich bessere Wahl als ein verlängerter I²C-Bus. Der Mehraufwand ist ein Transceiver je Knoten.
Fehlersuche an Bussen
Logikanalysator anschließen
Ein 8-Kanal-Gerät für unter 20 € mit PulseView dekodiert I²C, SPI, UART, CAN und 1-Wire fertig. Das ist bei Busproblemen das mit Abstand wirksamste Werkzeug — es zeigt, was wirklich auf der Leitung liegt, statt was man vermutet.
Erst die Pegel messen
Erreicht High wirklich V_CC? Ist die Flanke steil genug? Ein Oszilloskop zeigt zu schwache Pull-ups sofort als abgerundete Anstiege.
Adresse und Modus prüfen
Bei I²C: Scan durchführen. Bei SPI: CPOL/CPHA gegen das Datenblatt prüfen. Zwei Drittel aller Probleme sind hier erledigt.
Masse prüfen
Zwei Baugruppen ohne gemeinsame Masse kommunizieren nicht — oder scheinbar doch, mit sporadischen Fehlern. Die Masseleitung ist keine Nebensache, sie ist der Rückweg jedes Signals.
Auf das Minimum reduzieren
Einen einzigen Teilnehmer, kurze Leitung, niedrigste Geschwindigkeit. Läuft das, schrittweise erweitern — die Stelle, an der es kippt, ist die Antwort.
Ein RTOS löst genau ein Problem: mehrere Aufgaben, die unabhängig voneinander warten müssen. Es löst kein Problem mit Rechenleistung, Timing-Genauigkeit oder Codequalität — und es fügt eine Fehlerklasse hinzu, die es vorher nicht gab.
Brauche ich überhaupt eines?
Ja, wenn …
mehrere Aufgaben blockierend warten (Netzwerk, Benutzer, langsame Peripherie)
Aufgaben sehr unterschiedliche Zeitanforderungen haben
ein Netzwerkstack oder USB-Stack eingebunden wird, der ohnehin Tasks erwartet
das Projekt über Jahre wächst und mehrere Personen daran arbeiten
Nein, wenn …
zwei oder drei periodische Aufgaben genügen — dann reicht ein Zeitscheiben-Hauptprogramm
alles interruptgetrieben und kurz ist
der RAM knapp ist: jede Task braucht einen eigenen Stack
harte Zeitgarantien im Mikrosekundenbereich nötig sind — dafür ist der Interrupt zuständig, nicht der Scheduler
Die Alternative: kooperativer Zeitscheiben-Ablauf
while (1) {
uint32_t now = tick_ms();
if (now - t1 >= 1) { t1 = now; regelung_tick(); } /* 1 kHz */
if (now - t10 >= 10) { t10 = now; tasten_tick(); } /* 100 Hz */
if (now - t100 >= 100){ t100 = now; display_tick(); } /* 10 Hz */
kommunikation_tick(); /* so oft es geht */
}
Voraussetzung: Jede _tick()-Funktion kehrt sofort zurück und ist als Zustandsautomat gebaut. Damit lassen sich erstaunlich komplexe Geräte bauen — deterministisch, ohne Stackprobleme, mit einem einzigen Stack. Die Differenzbildung now - t1 ist überlaufsicher, solange beide Werte denselben vorzeichenlosen Typ haben.
Die Bausteine eines RTOS
Baustein
Wofür
Fallstrick
Task
Eigenständiger Ablauf mit eigenem Stack und Priorität
Stackgröße wird fast immer zu klein geschätzt
Queue
Daten zwischen Tasks übergeben, blockierend
Kopiert die Daten — bei großen Strukturen besser Zeiger übergeben
Semaphore (binär)
Signalisierung, typisch von ISR an Task
Kein Ersatz für einen Mutex: keine Prioritätsvererbung
Mutex
Gegenseitiger Ausschluss beim Zugriff auf eine Ressource
Nie aus einer ISR verwenden. Prioritätsvererbung aktivieren
Zählender Semaphor
Begrenzte Anzahl gleichartiger Ressourcen
—
Event Group / Flags
Auf mehrere Bedingungen gleichzeitig warten
Sparsamer als mehrere Semaphore
Software-Timer
Verzögerte oder periodische Aufrufe
Läuft in einer eigenen Task — deren Priorität und Stack beachten
Task Notification
Direkte, sehr schnelle Signalisierung an eine Task
Bei FreeRTOS deutlich schneller und sparsamer als ein Semaphor
FreeRTOS und Zephyr
Merkmal
FreeRTOS
Zephyr
Umfang
Scheduler plus Synchronisierung, sonst nichts
Vollständiges Betriebssystem mit Treibermodell, Netzwerkstack, Dateisystem, Bluetooth
Größe
5–10 kB Flash
ab ~10 kB, realistisch 50–200 kB mit Konnektivität
Konfiguration
Eine Header-Datei
Kconfig plus Devicetree
Hardware-Abstraktion
keine — man nutzt das Hersteller-SDK
einheitliche Treiber-APIs über alle Boards
Einstieg
an einem Nachmittag
mehrere Tage bis Wochen
Wann sinnvoll
Man hat bereits ein SDK und braucht nur den Scheduler
Mehrere Plattformen, Funk, langfristige Pflege, professionelles Umfeld
FreeRTOS ist in ESP-IDF fest eingebaut — wer mit ESP32 arbeitet, benutzt es ohnehin. Das nRF Connect SDK baut auf Zephyr auf. Beide sind quelloffen; Zephyr steht unter Apache 2.0, FreeRTOS unter MIT.
Stackgrößen bestimmen
Großzügig anfangen
1–2 kB je Task als Startwert. Zu klein ist der häufigste Grund für unerklärliche Abstürze.
Muster hineinschreiben
FreeRTOS füllt neue Stacks mit einem Füllwert. uxTaskGetStackHighWaterMark() liefert, wie viel nie benutzt wurde.
Nach realer Last messen
Nicht im Leerlauf, sondern unter Volllast mit allen Fehlerpfaden — gerade Fehlerbehandlung mit printf braucht überraschend viel.
Reserve lassen
Auf den gemessenen Spitzenwert 30–50 % aufschlagen. Bei FreeRTOS auf Cortex-M laufen ISRs auf einem eigenen Stack (MSP) — dessen Größe getrennt festlegen und prüfen.
Überlauf erkennbar machen
configCHECK_FOR_STACK_OVERFLOW = 2 und einen Hook implementieren, der den Task-Namen ausgibt. Ohne das sucht man den Fehler blind. Wo eine MPU vorhanden ist, schützt sie den Stack in Hardware.
Vorsicht mit printf und Fließkomma in Tasks: Eine printf-Implementierung mit Fließkomma-Unterstützung braucht schnell 1–2 kB Stack. Wer in fünf Tasks je 512 Byte veranschlagt hat, bekommt einen Absturz, der erst unter bestimmten Bedingungen auftritt. Für Diagnoseausgaben lohnt sich eine schlanke Alternative wie tinyprintf oder ein binäres Protokoll.
Aufteilung, die sich bewährt hat
Task
Priorität
Aufgabe
Regelung
hoch
Feste Periode über einen Timer, kurze Rechenzeit, keine blockierenden Aufrufe
Sensorik
mittel
Liest Sensoren, füllt einen gemeinsamen Zustand, filtert
Kommunikation
mittel
Verarbeitet Protokolle, blockiert an Queues
Bedienung / Anzeige
niedrig
Darf jederzeit unterbrochen werden
Diagnose / Protokoll
niedrigste
Schreibt in den Flash, wenn sonst nichts zu tun ist
Idle
—
Legt den Controller schlafen (Tickless Idle) — hier entsteht die Ersparnis bei Batteriebetrieb
Zwischen dem Einschalten und der ersten Zeile in main() passiert mehr, als die meisten vermuten: Vektortabelle, Takt, Kopieren initialisierter Variablen, Nullen des BSS-Bereichs, Konstruktoren. Wer diesen Weg kennt, versteht Linker-Fehler, Bootloader und Updates ohne Zauberei.
Der Speicher beim Start
Sektion
Inhalt
Liegt im
Beim Start
.isr_vector
Vektortabelle: Anfangs-Stackzeiger und Adressen aller Handler
Flash, ganz vorn
Der Prozessor liest daraus die ersten beiden Worte
.text
Programmcode
Flash
Wird direkt von dort ausgeführt
.rodata
Konstanten, Zeichenketten, Tabellen
Flash
Bleibt dort
.data
Initialisierte globale Variablen
Werte im Flash, Variablen im RAM
Startup-Code kopiert vom Flash ins RAM
.bss
Globale Variablen ohne Anfangswert
RAM
Startup-Code nullt den Bereich
Heap
Dynamisch angeforderter Speicher
RAM
Wächst zu höheren Adressen
Stack
Lokale Variablen, Rücksprungadressen, ISR-Kontext
RAM
Wächst zu niedrigeren Adressen
Stack und Heap wachsen aufeinander zu. Treffen sie sich, überschreibt der eine den anderen — ohne Fehlermeldung, mit beliebig verzögerten Folgen. Deshalb gilt: kein malloc nach der Initialisierung, besser gar keins. Statische Puffer fester Größe sind vorhersagbar, und der Linker meldet schon beim Bauen, wenn der Speicher nicht reicht.
Der Startup-Ablauf
Reset
Der Prozessor lädt den Stackzeiger aus dem ersten Wort der Vektortabelle und springt an die Adresse im zweiten (Reset-Handler).
Takt einstellen
Interner Oszillator, PLL, externer Quarz. Bei ESP32 und ähnlichen SoCs erledigt das ein ROM-Bootloader vorab.
.data kopieren, .bss nullen
Zwei Schleifen im Startup-Code. Ab hier stimmen die Anfangswerte globaler Variablen.
Laufzeitumgebung starten
Bei C++ die Konstruktoren globaler Objekte, bei C die newlib-Initialisierung.
main() aufrufen
Ab hier läuft der eigene Code. main() darf nie zurückkehren — der Startup-Code endet in einer Endlosschleife.
Deshalb ist eine globale Variable mit Anfangswert „teuer“: Ihr Wert belegt Flash und RAM. Eine große Nachschlagetabelle gehört nach const, damit sie in .rodata bleibt und gar nicht erst kopiert wird.
Das Linker-Skript
MEMORY
Beschreibt die physischen Bereiche: Flash-Anfang und -Größe, RAM-Anfang und -Größe, gegebenenfalls weitere wie CCM-RAM oder externes SDRAM.
SECTIONS
Legt fest, welche Sektion in welchen Bereich kommt und welche Symbole der Startup-Code dafür braucht — Anfang und Ende von .data und .bss, Obergrenze des Stacks.
Anpassen, wenn …
Was zu tun ist
ein Bootloader davor liegt
Die Anwendung beginnt mit Versatz statt bei Flash-Anfang, und die Vektortabelle muss zur Laufzeit umgesetzt werden: bei Cortex-M3/M4/M7/M33 über das VTOR-Register. Cortex-M0 hat kein VTOR — dort spiegelt man die Tabelle ins RAM oder nutzt ein herstellereigenes Remap-Register
ein Bereich den Reset überleben soll
Etwa für einen Absturzbericht: eigene Sektion anlegen, die der Startup-Code weder kopiert noch nullt — in .bss wird sie gelöscht
Code aus dem RAM laufen soll
Zeitkritische Funktionen sind dort schneller; beim Schreiben in den Flash darf ohnehin kein Code aus dem Flash ausgeführt werden
Bootloader und Updates
Variante
Prinzip
Bewertung
ROM-Bootloader
Fest im Chip, meist über UART, USB-DFU oder I²C erreichbar
Kostet nichts, ist nicht anpassbar, aber als Rettungsanker immer da
Eigener Bootloader, ein Bereich
Löscht die Anwendung und schreibt die neue
Einfach — ein Abbruch macht das Gerät unbrauchbar
A/B-Verfahren
Zwei vollständige Anwendungsbereiche im Wechsel
Der sichere Standard: Bei Fehlschlag wird der alte Stand wieder aktiviert. Kostet doppelten Flash
Externer Flash als Zwischenspeicher
Update wird dorthin geladen und dann kopiert
Spart internen Flash, braucht einen zusätzlichen Baustein
MCUboot
Fertiger, quelloffener Bootloader mit Signaturprüfung und A/B
In Zephyr und im nRF Connect SDK integriert; für ernsthafte Projekte die naheliegende Wahl
Ein Update, das nicht scheitert
Vollständig übertragen, dann prüfen
Erst das ganze Abbild in den inaktiven Bereich schreiben, danach Prüfsumme und Signatur verifizieren. Nie während der Übertragung aktivieren.
Umschalten atomar gestalten
Ein einziges Flag oder ein Metadatenblock entscheidet, welcher Bereich startet. Dieser Schreibvorgang muss so klein sein, dass er nicht halb passieren kann.
Bestätigung durch die Anwendung
Die neue Firmware bestätigt sich nach dem ersten erfolgreichen Start selbst. Bleibt die Bestätigung aus, fällt der Bootloader beim nächsten Reset auf den alten Stand zurück.
Rollback-Schutz
Ein Zähler in geschütztem Speicher verhindert, dass eine alte Version mit bekannter Lücke eingespielt wird → Security & Kryptografie.
Versorgung absichern
Beim Schreiben in den Flash braucht der Controller stabile Spannung: Brownout-Erkennung aktivieren, bei Batteriebetrieb vorher den Ladezustand prüfen → Stromsparen.
GNU ld-Handbuch, Kapitel Scripts; ARM, Cortex-M Generic User Guide — Vektortabelle und VTOR.
Joseph Yiu, The Definitive Guide to Arm Cortex-M — Startup, Speichermodell, Ausnahmebehandlung.
09 / 11Debuggen & Messen
Vier Werkzeuge decken praktisch alles ab: Debug-Adapter für Breakpoints und Speicheransicht, serielle Diagnoseausgabe, Logikanalysator für Busse, GPIO-Pin am Oszilloskop für Zeitmessungen. Das allgemeine Vorgehen — halbieren, messen, eine Sache auf einmal — steht unter Lernpfad → Debugging & Fehlersuche.
Debug-Schnittstellen
Schnittstelle
Pins
Leitungen
Kann
SWD
2
SWDIO, SWCLK (+ GND, möglichst nRESET)
Anhalten, Einzelschritt, Speicher lesen und schreiben, Breakpoints, Flashen — der Standard bei Cortex-M
JTAG
4
TCK, TMS, TDI, TDO (+ GND, optional nTRST)
Wie SWD, zusätzlich Boundary Scan und Verkettung mehrerer Bausteine
SWO / ITM
1
SWO, zusätzlich zu SWD
Diagnoseausgabe mit sehr geringer Rückwirkung, deutlich schneller als UART
ETM / Trace
5
TRACECLK + bis zu 4 Datenleitungen
Lückenloser Befehlsverlauf. Nur bei größeren Kernen und mit teurerem Adapter
RTT (SEGGER)
0
keine zusätzliche — läuft über SWD/JTAG
Zwei Ringpuffer im RAM, im laufenden Betrieb vom Debugger gelesen. Schnell, kaum Rückwirkung
Vier Pins reservieren, immer: SWDIO, SWCLK, GND und Reset auf einer Stiftleiste oder als Tag-Connect-Pads. Der Platz ist minimal, der Nutzen enorm — am fertigen Gerät lassen sie sich nicht nachrüsten. Wer die Pins anderweitig belegt, sollte wissen, dass viele Controller SWD nach dem Umkonfigurieren erst nach einem Reset wieder freigeben.
Hard Fault analysieren
Ein Hard Fault ist kein Zufall, sondern eine präzise Fehlermeldung — man muss sie nur lesen.
1 · Fault-Register auswerten
Bei Cortex-M sagt das CFSR, welche Art von Fehler vorlag; MMFAR und BFAR enthalten die betroffene Adresse.
2 · Gesicherten Kontext finden
Der Prozessor legt beim Fault acht Register auf den Stack (R0–R3, R12, LR, PC, xPSR). Der PC zeigt auf den auslösenden Befehl.
3 · Adresse in Code übersetzen
arm-none-eabi-addr2line -e firmware.elf 0x0800xxxx liefert Datei und Zeile.
4 · Handler nachrüsten
Ein Hard-Fault-Handler, der die Register über die serielle Schnittstelle ausgibt oder in einen reset-festen Bereich schreibt, macht auch Fehler im Feld auswertbar → Speicherbild.
Fault-Typ
Häufige Ursache
Bus Fault (PRECISERR)
Zugriff auf eine ungültige Adresse — Nullzeiger oder Peripherie ohne aktivierten Takt
Usage Fault (UNALIGNED)
32-Bit-Zugriff auf eine ungerade Adresse, oft durch Zeiger-Umdeutung auf einen Bytepuffer
Usage Fault (INVSTATE)
Sprung an eine Adresse ohne gesetztes Thumb-Bit — typischerweise ein kaputter Funktionszeiger
Memory Manage Fault
MPU-Verletzung, häufig Stacküberlauf bei geschütztem Stack
Escalated Hard Fault
Ein Fault innerhalb eines Fault-Handlers oder ein abgeschalteter Handler
Logging, das im Betrieb funktioniert
Regel
Begründung
Log-Stufen ERROR/WARN/INFO/DEBUG
Zur Übersetzungszeit abschaltbar, damit der Auslieferungsstand weder Flash noch Rechenzeit verbraucht
Zeitstempel in jeder Zeile
Millisekunden seit dem Start. Ohne Zeitbezug lassen sich Abläufe nicht rekonstruieren
Nicht blockierend ausgeben
Blockierendes printf über UART mit 115 200 Bd braucht für 80 Zeichen rund 7 ms — genug, um einen Regelkreis zu ruinieren. Ringpuffer plus DMA oder RTT verwenden
Nichts aus der ISR ausgeben
Formatieren und Warten gehören nicht in einen Interrupt: Ereignis in den Puffer legen, im Hauptprogramm ausgeben → Interrupts
Absturzberichte speichern
Fault-Register und die letzten Zeilen in einen reset-festen Bereich schreiben und beim nächsten Start ausgeben. Bei Fehlern alle drei Tage die einzige Chance
Messen statt vermuten
Frage
Werkzeug
Vorgehen
Wie lange läuft diese Funktion?
GPIO-Pin plus Oszilloskop
Pin am Anfang setzen, am Ende löschen. Zeigt auch Jitter und Verschachtelung
Zeigt sofort, ob eine Optimierung greift oder eine Variable wegoptimiert wurde
Sporadische Fehler in Firmware
Reset-Ursache auslesen
Fast jeder Controller speichert sie in einem Register: Watchdog, Brownout, Software-Reset, Pin. Diese Information beim Start ausgeben — sie beantwortet die halbe Frage.
Bedingung erzwingen
Ein Fehler, der einmal pro Woche auftritt, ist nicht zu debuggen: Last erhöhen, Temperatur ändern, Kommunikation fluten, Takt verstellen.
Nebenläufigkeit verdächtigen
Sporadisch heißt in Firmware meist: gemeinsame Variable ohne Schutz, fehlendes volatile, zu kleiner ISR-Stack, Prioritätsumkehr → Interrupts & Nebenläufigkeit.
Hardware ausschließen
Versorgung am Oszilloskop ansehen, Verbindungen nachmessen, zweites Exemplar probieren. Erstaunlich viele „Softwarefehler“ sind kalte Lötstellen oder Spannungseinbrüche.
David Agans, Debugging: The 9 Indispensable Rules — kein Embedded-Buch, aber das beste zum Vorgehen.
10 / 11Stromsparen & Batteriebetrieb
Bei Batteriebetrieb entscheidet nicht der aktive Strom, sondern der Ruhestrom. Ein Gerät, das 99,9 % der Zeit schläft, wird vom Schlafstrom bestimmt — 10 µA statt 1 mA bedeuten Faktor 100 in der Laufzeit. Die erste Frage lautet deshalb immer: Wie oft muss es überhaupt wach sein?
Die Betriebsarten
Modus
Typischer Strom
Was bleibt an
Aufwachzeit
Aktiv (Run)
2–100 mA
alles
—
Sleep / WFI
0,5–10 mA
Peripherie und RAM, nur die CPU steht
wenige Takte
Stop / Deep Sleep
2–50 µA
RAM erhalten, wenige Aufwachquellen
10 µs – 1 ms
Standby
0,3–3 µA
nur RTC und wenige Pins; RAM verloren außer im Backup-Bereich
wie ein Neustart
Shutdown
unter 100 nA
fast nichts
Neustart
Datenblattwerte gelten unter Laborbedingungen. Dazu kommen in der Praxis: Ruhestrom des Reglers, Pull-Widerstände, LEDs, Sensoren im Dauerbetrieb und Leckströme über falsch konfigurierte Pins. Ein Controller mit 2 µA Standby nützt nichts, wenn der Regler daneben 5 mA zieht.
Wo der Strom tatsächlich hingeht
Verbraucher
Typischer Beitrag
Gegenmittel
Spannungsregler
AMS1117 rund 5 mA · MCP1700 1,6 µA · TPS7A02 25 nA
Ruhestrom I_q im Datenblatt zum Auswahlkriterium machen. Bei sehr kleiner Last ist ein LDO oft sparsamer als ein Schaltregler → Spannungsversorgung
Pull-Widerstände
4,7 kΩ an 3,3 V dauerhaft Low ≈ 700 µA
Im Schlaf abschalten, hochohmiger auslegen oder den Ruhepegel umdrehen
Floatende Eingänge
einige µA je Pin, stark streuend
Ein offener CMOS-Eingang lässt beide Transistoren teilweise leiten: ungenutzte Pins auf Analog oder mit Pull konfigurieren
Sensoren
im Betrieb einige hundert µA, im Schlaf unter 1 µA
Schlafmodus aktiv setzen — oder die Versorgung über einen MOSFET ganz abschalten, dann entfällt auch der Leckstrom
Funk
WLAN-Sendespitze 100–350 mA, BLE 5–15 mA
Nicht die Spitze zählt, sondern die Gesamtzeit im Funkbetrieb. Ein fehlgeschlagener Verbindungsaufbau kostet mehr als hundert erfolgreiche Sendungen
Status-LED
5 mA dauerhaft
Mehr als alles andere zusammen: nur blitzen lassen — 10 ms alle 5 s sind sichtbar und kosten fast nichts
Ungenutzte Peripherie
je Block einige zehn µA bis mA
Takt der nicht benutzten Baugruppen abschalten; ADC, Komparator und Debug-Einheit werden gern vergessen
Laufzeit rechnen
Batterielaufzeit abschätzen
I_mittel = (I_aktiv · t_aktiv + I_schlaf · t_schlaf) / (t_aktiv + t_schlaf)Beispiel: 20 mA für 50 ms alle 60 s, dazwischen 5 µA → I_mittel = (20 · 0,05 + 0,005 · 59,95)/60 = 21,7 µA. Aus einer 2000-mAh-Zelle ergibt das rechnerisch über zehn Jahre — begrenzt dann durch die Selbstentladung, nicht durch die Schaltung.
Messen, nicht schätzen
Methode
Bereich
Bewertung
Multimeter in Reihe
mA
Für Mikroampere zu ungenau; der Bürdenwiderstand verfälscht und kann beim Aufwachen sogar einen Reset auslösen
Shunt plus Oszilloskop
µA bis A
Zeigt den zeitlichen Verlauf — genau das, was man braucht. 1 Ω und ein Differenztastkopf reichen oft
Spezialgerät (Nordic PPK2, Joulescope, Otii)
nA bis A, dynamisch
Der richtige Weg: löst nA bis hunderte mA in einem Durchgang auf und integriert die Energie automatisch
Langzeittest mit Kondensator
Mittelwerte
Bekannte Kapazität laden, Spannungsabfall über Stunden messen. Billig und erstaunlich brauchbar
Strommesszange
ab einigen mA
Für die kleinen Ströme im Schlafbetrieb unbrauchbar
Der typische Befund beim ersten Messen: Das Gerät wacht viel häufiger auf als geplant — ein nicht abgeschalteter Timer, ein ständig auslösender Sensor-Interrupt oder der RTOS-Tick, der den Controller tausendmal pro Sekunde weckt. Genau dagegen gibt es configUSE_TICKLESS_IDLE → RTOS.
Batterietypen
Typ
Spannung
Kapazität
Spitzenstrom
Selbstentladung
Anmerkung
CR2032 (Lithium-Knopfzelle)
3,0 V
210–240 mAh
nur wenige mA
rund 1 % pro Jahr
Bei Funkspitzen bricht die Spannung ein — Pufferkondensator (10–100 µF) zwingend
Kältefest bis −40 °C, erste Wahl für Langzeitgeräte
Li-Ion 18650
3,0–4,2 V
2500–3500 mAh
mehrere A
2–3 % pro Monat
Braucht Schutzschaltung und Ladeelektronik; nicht unter 0 °C laden
LiFePO₄
2,5–3,6 V
je nach Bauform
hoch
wenige % pro Monat
Sehr viele Zyklen, thermisch gutmütig, geringere Energiedichte
NiMH (LSD-Typ)
1,2 V
1900–2500 mAh
hoch
klassisch bis 20 % pro Monat, LSD rund 1 %
Nur Low-Self-Discharge-Zellen taugen für Geräte, die lange liegen
Supercap
2,5–5,5 V
gering
sehr hoch
Tage bis Wochen
Für Überbrückung und Stromspitzen, nicht für Dauerbetrieb
Bei zweistelligen Laufzeiten in Jahren rechnet man gegen die Chemie, nicht gegen die Schaltung: Eine Zelle mit 1 % Selbstentladung pro Jahr ist nach zehn Jahren rechnerisch bei 90 % — jede weitere Optimierung des Schlafstroms bringt dann nichts mehr. Ebenso wichtig: Kapazitätsangaben gelten für kleine Entladeströme und Raumtemperatur; bei Kälte und Stromspitzen bleibt deutlich weniger übrig.
Quellen
Nordic Semiconductor, Power Profiler Kit II — Messgerät und lesenswerte Anwendungshinweise.
Texas Instruments, Achieving Ultra-Low Power in MCU Applications und die MSP430-Anwendungsberichte.
STMicroelectronics AN4991 und AN5001 — Low-Power-Modi bei STM32.
Datenblätter von Energizer, Panasonic und Varta mit Entladekurven und Innenwiderstand über Temperatur.
C ist in Embedded-Systemen deshalb Standard, weil es nichts versteckt. Genau das ist auch das Problem: Der Compiler darf unter bestimmten Annahmen alles wegoptimieren, was nach diesen Annahmen unmöglich ist. Wer die Annahmen kennt, schreibt Code, der auch mit -O2 noch tut, was er soll.
Die Schlüsselwörter, die zählen
Schlüsselwort
Bedeutung
Wann verwenden
volatile
Jeder Zugriff muss wirklich stattfinden
Hardwareregister, von ISR geänderte Variablen. Nicht als Ersatz für Atomarität
const
Wird nicht verändert
Nachschlagetabellen — landet dann in .rodata statt im RAM
static (Dateiebene)
Nur in dieser Übersetzungseinheit sichtbar
Standard für alles, was nicht ausdrücklich exportiert wird. Erlaubt dem Compiler bessere Optimierung
static (in Funktion)
Behält den Wert zwischen Aufrufen, liegt nicht auf dem Stack
DMA-Puffer, Zustandsvariablen; Vorsicht bei Wiedereintritt
inline / static inline
Vorschlag zum Einbetten
Kurze Zugriffsfunktionen in Headern
restrict
Zusicherung, dass zwei Zeiger nicht auf dasselbe zeigen
In Rechenschleifen — kann erhebliche Beschleunigung bringen
_Atomic / stdatomic.h
Garantiert unteilbare Operationen
Zähler, die von ISR und Hauptprogramm verändert werden
Feste Breiten und Typen
Immer stdint.h
uint8_t, int16_t, uint32_t statt char, short, int. Die Breite von int ist plattformabhängig — auf AVR 16 Bit, auf ARM 32 Bit. Code, der davon abhängt, bricht beim Portieren.
bool aus stdbool.h
Klarer als int mit 0 und 1, und der Compiler kann besser prüfen.
size_t für Größen und Indizes
Vorzeichenlos und passend zur Plattform. Vergleiche mit vorzeichenbehafteten Typen vermeiden.
Aufzählungen für Zustände
typedef enum statt magischer Zahlen. Der Compiler warnt bei switch, wenn ein Fall fehlt.
Hardwareregister ansprechen
/* Falsch: der Compiler darf den Zugriff wegoptimieren */
uint32_t *reg = (uint32_t *)0x40020014;
/* Richtig: volatile, damit jeder Zugriff wirklich erfolgt */
#define GPIOA_ODR (*(volatile uint32_t *)0x40020014u)
/* Bit setzen, ohne die anderen zu stören */
GPIOA_ODR |= (1u << 5);
GPIOA_ODR &= ~(1u << 5);
/* Bitfeld schreiben: erst löschen, dann setzen */
#define MODE_MASK (3u << 10)
#define MODE_OUT (1u << 10)
GPIOA_MODER = (GPIOA_MODER & ~MODE_MASK) | MODE_OUT;
Read-Modify-Write ist nicht atomar.reg |= bit; besteht aus drei Schritten. Unterbricht ein Interrupt dazwischen und ändert dasselbe Register, geht die Änderung verloren. Viele Controller bieten dafür Set/Reset-Register (BSRR bei STM32) oder Bit-Banding (nur Cortex-M3/M4) — beides erledigt die Operation in einem einzigen Schreibzugriff und ist damit unterbrechungsfest.
Fallen, die immer wieder zuschlagen
Falle
Was passiert
Richtig
Zeiger auf lokale Variable zurückgeben
Der Speicher ist nach dem Rücksprung ungültig
static, Puffer vom Aufrufer, oder Wert kopieren
strcpy ohne Längenprüfung
Pufferüberlauf
snprintf mit Größenangabe; strncpy terminiert nicht garantiert
sizeof auf einen Zeiger
Liefert die Zeigergröße, nicht die Arraygröße
Länge separat übergeben; sizeof(arr)/sizeof(arr[0]) nur im Gültigkeitsbereich des Arrays
Ganzzahldivision mit Rundungserwartung
7/2 ist 3
Vorher multiplizieren, Festkomma nutzen oder bewusst runden
Absicht mit /* fallthrough */ kennzeichnen; -Wimplicit-fallthrough aktivieren
Zeiger-Umdeutung auf Puffer
Nicht ausgerichteter Zugriff oder Verletzung der Aliasing-Regeln
memcpy in eine passende Struktur — moderne Compiler optimieren das weg
Rückgabewert ignoriert
Fehler bleiben unbemerkt
Jeden Rückgabewert prüfen oder bewusst mit (void) verwerfen
malloc im laufenden Betrieb
Fragmentierung, unbestimmte Laufzeit, irgendwann kein Speicher mehr
Statische Puffer oder ein Pool fester Blockgröße
Muster, die sich bewährt haben
Modul mit klarer Schnittstelle
Eine .h-Datei mit den öffentlichen Funktionen, alles andere static in der .c-Datei. Kein Zugriff auf interne Variablen von außen.
Init, Tick, Handler
Jedes Modul bekommt dieselbe Struktur: xyz_init(), xyz_tick() (zyklisch, kehrt sofort zurück) und gegebenenfalls einen ISR-Anteil. Sehr leicht zu verstehen und zu testen.
Fehlercodes statt stiller Fehler
Ein einheitlicher Aufzählungstyp als Rückgabewert. Ein zentraler Fehlerzähler mit letztem Code erleichtert die Diagnose erheblich.
Hardware hinter einer dünnen Schicht
Nicht wegen Portabilität, sondern damit sich Logik ohne Hardware testen lässt — siehe Digitaltechnik → Software.
Konfiguration an einer Stelle
Pinbelegung, Zeitkonstanten und Grenzwerte in einer config.h. Spart Suchen und macht Hardwarevarianten beherrschbar.
Statische Zusicherungen
_Static_assert(sizeof(frame_t) == 16, "Rahmengröße"); — prüft Annahmen zur Übersetzungszeit statt im Feld.
C++ auf dem Mikrocontroller
Nützlich
constexpr — Berechnungen zur Übersetzungszeit, kostet zur Laufzeit nichts
Aufzählungsklassen (enum class) — typsicher
Referenzen statt Zeiger, wo kein Nullwert vorkommen kann
Templates für typsichere Puffer und Einheiten
RAII für Sperren und Ressourcen
Vorsicht
Ausnahmen (exceptions) — meist abgeschaltet, kosten Flash und Determinismus