Reihe: Embedded World – Die unsichtbaren Gehirne verstehen (Teil 10)
Debuggen im Verborgenen – Die Kunst, Unsichtbares sichtbar zu machen
Von DerSchneider
Einleitung: Die Suche im Nebel
Stellen Sie sich vor, Sie schreiben ein Programm für Ihren PC. Es stürzt ab. Sie haben einen Bildschirm, eine Tastatur, eine Maus. Sie können Breakpoints setzen, Variablen inspizieren, den Call-Stack durchsuchen. Der Computer selbst erzählt Ihnen, was falsch läuft.
Stellen Sie sich jetzt vor, Sie programmieren einen Mikrocontroller in einer laufenden Maschine. Es gibt keinen Bildschirm, keine Tastatur, keine Maus. Das Programm läuft – oder eben nicht. Aber es sagt Ihnen nichts. Es ist ein stummer, unsichtbarer Geist im Silizium, und Sie müssen herausfinden, warum er sich nicht so verhält, wie Sie es wollen.
Das ist die Realität des Embedded-Debuggings. Es ist die Kunst, Unsichtbares sichtbar zu machen, mit begrenzten Mitteln aus winzigen Hinweisen die Ursache eines Fehlers zu erschließen. Dieser Artikel führt in diese Kunst ein. Wir lernen die Werkzeuge und Techniken kennen, mit denen Embedded-Entwickler ihre unsichtbaren Kollegen zum Reden bringen.
Hauptteil
1. Die einfachste Methode: Serial over Debug
Beginnen wir mit der einfachsten und zugleich mächtigsten Debugging-Methode: der seriellen Ausgabe. Fast jeder Mikrocontroller hat eine UART-Schnittstelle. Schließt man sie an einen USB-Seriell-Wandler an, kann man Nachrichten an den PC senden.
c
printf("Sensorwert: %d\n", wert);
Das ist das Embedded-Äquivalent zu System.out.println() oder console.log(). Einfach, aber oft genial effektiv. Man sieht, welche Werte das Programm verarbeitet, welche Pfade es nimmt, wo es hängen bleibt.
Die Grenzen: Es braucht die UART-Hardware, es beeinflusst das Timing (ein printf kann Tausende von Taktzyklen dauern) und es funktioniert nicht, wenn der Fehler genau in der Kommunikation liegt.
Trotzdem: In mindestens 80 Prozent aller Fälle reicht die serielle Ausgabe, um einen Fehler zu finden. Sie ist das Schweizer Taschenmesser des Embedded-Debuggings.
2. Die nächste Stufe: LED-Debugging
Was, wenn keine UART vorhanden ist oder die Schnittstelle selbst Fehler macht? Dann hilft die älteste Debugging-Methode der Welt: die LED.
Man nehme eine LED, schließe sie an einen freien Pin an und lasse sie in bestimmten Situationen blinken:
c
// Fehlerfall signalisieren
if (fehler) {
while(1) {
led_ein();
delay(100);
led_aus();
delay(100);
}
}
// Programmfortschritt anzeigen
void funktion1() {
led_gruen_ein();
// ... Arbeit ...
led_gruen_aus();
}
void funktion2() {
led_rot_ein();
// ... Arbeit ...
led_rot_aus();
}
Die LED ist der Urvater aller Debugging-Werkzeuge. Sie ist immer da, kostet nichts und verrät oft genug: Läuft das Programm überhaupt? Kommt es in diese Schleife? Bleibt es an dieser Stelle hängen?
Manche Entwickler haben ganze Kodierungen entwickelt – zwei LEDs, die binär den Zustand anzeigen, oder eine LED, deren Blinkmuster eine Fehlernummer codiert.
3. Der Profi: In-Circuit-Debugger (JTAG/SWD)
Für ernsthafte Debugging-Arbeiten kommt man um einen Debugger nicht herum. Die meisten Mikrocontroller haben eine spezielle Debug-Schnittstelle – JTAG (Joint Test Action Group) oder SWD (Serial Wire Debug).
Mit einem Debugger kann man:
- Das Programm anhalten (Breakpoints setzen)
- Variablen lesen und verändern
- Den Call-Stack ansehen
- Das Programm Schritt für Schritt ausführen
- Speicherbereiche inspizieren
Das ist so, als hätte der Mikrocontroller plötzlich einen Bildschirm und eine Tastatur. Man kann live hineinschauen, während das Programm läuft – oder es genau dort anhalten, wo der Fehler auftritt.
Die Hardware kostet wenig (ein ST-Link oder J-Link ist schon für unter 20 Euro zu haben), und die Entwicklungsumgebungen (Eclipse, VS Code mit entsprechenden Plugins) unterstützen das hervorragend.
4. Die Zeitmaschine: Der Logikanalysator
Manche Fehler sind zeitkritisch. Ein Interrupt kommt zu spät, ein Signal ist zu kurz, zwei Ereignisse überlappen sich unerwartet. Solche Fehler sieht man mit einem Debugger nicht, weil der das Programm anhält und damit das Timing zerstört.
Hier kommt der Logikanalysator ins Spiel. Er zeichnet digitale Signale über die Zeit auf und zeigt sie als Wellenform an. Man sieht genau, wann welcher Pin seinen Zustand ändert, wie lang Impulse sind, ob die zeitlichen Abläufe stimmen.
Ein einfacher Logikanalysator (oft für unter 10 Euro als USB-Stick erhältlich) kann Wunder wirken:
- Ist der Taster wirklich entprellt?
- Kommt das SPI-Signal mit der richtigen Frequenz?
- Stimmen die Timing-Vorgaben des Datenblatts?
- Überschneiden sich zwei Interrupts unerwartet?
Die aufgezeichneten Signale lügen nicht. Sie zeigen die physikalische Realität, nicht die Programm-Logik.
5. Das Fenster in die Vergangenheit: Der Oszilloskop
Wo der Logikanalysator nur digitale Signale (HIGH/LOW) kennt, zeigt das Oszilloskop die tatsächlichen Spannungsverläufe. Das ist wichtig, wenn es um analoge Probleme geht:
- Rauschen auf der Stromversorgung
- Überschwinger auf Signalleitungen
- Langsame Anstiegszeiten
- Fehlerhafte Pegel
Ein Oszilloskop ist teurer und komplexer als ein Logikanalysator, aber für bestimmte Probleme unersetzlich.
6. Die Kunst des Printf, wenn kein Printf geht
Oft hat man keine UART frei oder die Ausgabe stört das Timing zu sehr. Dann muss man kreativ werden:
Ringspeicher im RAM: Man legt einen Puffer im RAM an und schreibt Debug-Informationen hinein. Wenn das Programm anhält (oder gezielt gestoppt wird), liest man den Puffer aus.
c
#define DEBUG_SIZE 100
typedef struct {
unsigned long time;
int event;
int value;
} debug_entry_t;
debug_entry_t debug_buffer[DEBUG_SIZE];
int debug_index = 0;
void debug_log(int event, int value) {
debug_entry_t *e = &debug_buffer[debug_index];
e->time = millis();
e->event = event;
e->value = value;
debug_index = (debug_index + 1) % DEBUG_SIZE;
}
Nach einem Absturz kann man den Puffer über den Debugger auslesen und sieht, was vor dem Absturz passiert ist.
GPIO als Logikanalysator: Man nutzt einen freien Pin, setzt ihn vor wichtigen Aktionen und löscht ihn danach. Mit einem Logikanalysator kann man dann die Dauer und Reihenfolge der Aktionen messen.
7. Der Klassiker: Heisenbugs
Die fiesesten Fehler in Embedded Systems sind die, die verschwinden, wenn man sie sucht – sogenannte Heisenbugs (nach Werner Heisenberg, der zeigte, dass Messung das Gemessene verändert).
Klassische Beispiele:
- Ein Fehler tritt nur auf, wenn der Debugger nicht angeschlossen ist.
- Ein Fehler verschwindet, sobald man eine LED zur Anzeige einbaut.
- Ein Fehler tritt nur auf, wenn das Programm im Release-Modus läuft, nicht im Debug-Modus.
Die Ursache ist fast immer ein Timing-Problem. Der Debugger verlangsamt das Programm, die LED-Ausgabe dauert ihre Zeit, der Release-Modus optimiert Code anders. Plötzlich sind die zeitlichen Bedingungen andere, und der Fehler zeigt sich nicht mehr.
Die Lösung: So nah wie möglich an der Realität debuggen. Logikanalysator statt Printf, Ringspeicher statt serielle Ausgabe, genaue Analyse der Timing-Anforderungen.
8. Fallstricke und typische Fehler
Die Erfahrung zeigt: Bestimmte Fehler treten immer wieder auf. Eine kleine Phänomenologie:
Vergessene Initialisierung: Ein Timer läuft nicht, weil das entsprechende Register nie gesetzt wurde. Das Programm wartet ewig auf ein Ereignis.
Falsche Register: Man schreibt ins falsche Register oder verwechselt die Bitpositionen. Der Pin tut nicht, was er soll.
Interrupt-Konflikte: Zwei Interrupts stören sich gegenseitig. Ein niederpriorer Interrupt wird nie bedient, weil ein hochpriorer dauernd kommt.
Stack-Overflow: Der Stack wächst in den Variablenbereich (oder umgekehrt). Das Programm zeigt merkwürdiges, scheinbar zufälliges Verhalten.
Race Conditions: Zwei Tasks greifen gleichzeitig auf dieselbe Variable zu. Mal geht’s gut, mal nicht.
Optimierungsfallen: Der Compiler optimiert Code weg, den er für überflüssig hält (z.B. Zugriffe auf Hardware-Register, die er nicht als solche erkennt). Das Schlüsselwort volatile ist oft die Lösung.
9. Ein Praxisbeispiel: Der flackernde Taster
Erinnern Sie sich an das Taster-Beispiel aus Artikel 3? Der Taster prellt, und wenn man nicht entprellt, flackert die LED.
Das Debugging mit verschiedenen Methoden:
- LED-Debugging: Man lässt eine LED parallel zum Taster leuchten. Sie flackert – das zeigt das Prellen.
- Logikanalysator: Man schließt den Analysator an den Taster-Pin. Das Bild zeigt mehrere Impulse in wenigen Millisekunden.
- Software-Entprellung: Man implementiert eine einfache Entprellung und prüft mit dem Logikanalysator, ob das Prellen verschwunden ist.
Jede Methode hat ihren Beitrag geleistet, und zusammen ergeben sie ein vollständiges Bild.
10. Prävention ist besser als Debugging
Die beste Debugging-Strategie ist, Fehler gar nicht erst zu machen. Einige Grundregeln:
- Defensive Programmierung: Immer Prüfungen einbauen, auf NULL-Zeiger testen, Bereichsüberschreitungen vermeiden.
- Assertions: An kritischen Stellen Bedingungen prüfen und bei Verletzung Alarm schlagen.
- Code-Reviews: Ein zweites Paar Augen sieht oft, was man selbst übersehen hat.
- Modulare Tests: Jedes Modul einzeln testen, bevor man es integriert.
- Versionskontrolle: Immer wissen, was sich geändert hat.
Trotzdem: Fehler werden passieren. Die Kunst ist, sie systematisch zu finden – nicht durch wildes Herumprobieren, sondern durch methodisches Vorgehen mit den richtigen Werkzeugen.
Fazit und Ausblick
Debuggen in Embedded Systems ist eine Detektivarbeit. Die Spuren sind oft klein, die Zeugen schweigen, und der Täter ist unsichtbar. Aber mit den richtigen Werkzeugen – von der einfachen LED über den Debugger bis zum Logikanalysator – und einer methodischen Vorgehensweise lassen sich auch die hartnäckigsten Fehler finden.
Wir haben die Werkzeuge kennengelernt. Aber Werkzeuge allein genügen nicht. Man muss auch wissen, wo die besonderen Gefahren lauern – gerade in sicherheitskritischen Systemen, wo ein Fehler nicht nur ärgerlich, sondern lebensbedrohlich sein kann.
Mit diesem Thema – der Sicherheit in Embedded Systems – beschäftigen wir uns im nächsten Artikel.
Kommentar abschicken