Titelbild: OpenClipart-Vectors auf Pixabay.

Man muss dem Titel (zitiert aus einem Tweet von Filipe Fortes) hinzufügen, dass der Detektiv an einem Gedächtnisverlust leidet. Andernfalls könnte der Fall leicht gelöst werden. Ähnlich beim Debugging: Wenn ich nur wüsste, welche fiesen Dinge ich im Quellcode versteckt habe, könnte ich sie einfach entfernen – aber ich weiß es einfach nicht. In diesem Blogbeitrag werden wir einen Blick darauf werfen, welche Art von Werkzeugen man verwenden kann, um die im Schrank versteckten Skelette zu finden (speziell für eingebettete Systeme).

Debuggen mit print-Anweisung

Wenn man mit der Arduino IDE arbeitet, stellt man schnell fest, dass sie sehr einfach erlernbar, aber auch sehr begrenzt ist. Insbesondere unterstützt die IDE keine Art von Debugging (zumindest in der Version 1.X). Die einzige Möglichkeit, herauszufinden, was im Code passiert, besteht darin, print-Anweisungen in das Programm zu schreiben, die u.U. Variablenwerte anzeigen. Damit kann man überprüfen, ob der Kontrollfluss und die Werte der Variablen den Erwartungen entsprechen.

Was tut man aber, wenn die MCU keine serielle Schnittstelle hat oder diese Schnittstelle von genau dem Programm verwendet wird, das man debuggen möchte? Eine Möglichkeit ist es, einen (ansonsten ungenutzten) Ausgangspin zu nutzen, um mit einer LED den internen Zustand zu visualisieren. Das gibt jedoch nur sehr begrenzte Einblicke in das Innenleben des Programms.

Stattdessen könnte man einen Pin als zusätzlichen seriellen Ausgang verwenden. Man kann dazu meine TXOnlySerial-Bibliothek verwenden. Diese benötigt nur einen Pin und verbraucht weniger Flash und RAM als die SoftwareSerial-Bibliothek. Man braucht jedoch einen FTDI-Adapter o.ä., um die serielle Leitung an den Computer anschließen zu können.

Ein weiterer Trick, den ich verwende, besteht darin, Debugging-Druckanweisungen als Makro zu definieren. Wenn ich mit dem Debuggen fertig bin, definiere ich diese Anweisungen einfach als leer, was bedeutet, dass ich am Ende nicht alle Debug-Print-Anweisungen löschen muss. Die Definitionen stehen alle in eine Header-Datei (debug.h), die wie folgt aussieht:

/* Makros debuggen */
#ifdef DEBUGGEN
#define DEBDECLARE() TXOnlySerial deb(DEBTX)
#define DEBINIT() deb.begin(19200)
#define DEBPR(str) deb.print(str)
#define DEBPRF(str,frm) deb.print(str,frm)
#define DEBLN(str) deb.println(str)
#define DEBLNF(str,frm) deb.println(str,frm)
#else
#define DEBDECLARE()
#define DEBINIT()
#define DEBPR(str)
#define DEBPRF(str,frm)
#define DEBLN(str)
#define DEBLNF(str,frm)
#endif

In dem Arduino-Sketch muss man nur TXOnlySerial.h einbinden, man muss das konstante Symbol DEBTX (den Pin, den man als serielle Ausgabezeile verwenden will) definieren, eine Zeile mit DEBDECLARE() haben, DEBINIT() im Setup einfügen und dann kann man alle oben genannten Anweisungen verwenden. Das könnte dann wie folgt aussehen:

#define DEBUG // auskommentieren, wenn er nicht mehr benötigt wird
...
#define DEBTX 8 // wird als Debugging-Ausgabe verwendet
 <TXOnlySerial.h>#include
#include "debug.h"
...</TXOnlySerial.h>
DEBDECLARE();

void setup() {
 DEBINIT();
 ...
}

void loop() {
 ...
 DEBPR(F("Schleife"));
 ...
}

Der Nachteil des Debuggens mit print-Anweisungen besteht darin, dass man, wenn man den Wert einer anderen Variablen sehen möchten, den Arduino-Sketch ändern, kompilieren, hochladen und dann das Programm erneut starten muss. Es wäre viel schöner, wenn man an einer bestimmten Stelle im Programm anhalten und dann den Programmstatus überprüfen könnte. Und dann vielleicht sogar Variablenwerte ändern könnte, bevor man mit der Ausführung fortfährt. Und dafür sind echte Debugger da.

Vorbereiten der Arduino IDE 1.X für das Debuggen

Wenn man avr-gdb, den GNU-Debugger für AVR-MCUs, zum Debuggen von Arduino-Sketches verwenden möchte, muss man ihn zuerst installieren, da er nicht mehr Teil der Arduino-Software ist. Auf einem Mac ist avr-gdb als Homebrew-Paket verfügbar. Unter Linux kann man einfach das Paket avr-gdb (das gdb-avr genannt wird) mit einem Paketmanager laden. Unter Windows muss man die gesamte AVR-Toolchain von irgendwoher herunterladen, die dann den Debugger enthält. WinAVR ist wahrscheinlich eine gute Wahl, entweder in der „offiziellen“ Version, die mehr als 10 Jahre alt ist, oder einer neueren Version von Zak’s Electronics Blog ~*. Alternativ kann man sich die Toolchain von der Microchip-Website (nach Registrierung) herunterladen.

Dann muss man (auf allen Plattformen) einige Konfigurationsdateien der Arduino IDE anpassen, um Zugriff auf die ELF-Dateien (die Debugging-Informationen enthalten) zu erhalten. Außerdem muss man die gcc-Optimierungsstufe von -Os (d.h. Speicherplatz minimieren) in -Og (d.h. Debug-freundlich optimieren) ändern. Das ist nicht unbedingt notwendig, aber sehr ratsam, da ansonsten der erzeugte Code nicht mehr der Abfolge von Zeilen im Quellcode-Programm entspricht.

Zuerst muss man die Datei platform.local.txt dem Verzeichnis hinzufügen, in dem sich die entsprechende Datei platform.txt befindet. Wenn man die Board-Informationen mit dem Board Manager installiert hatte, dann findet man den entsprechenden Ordner je nach Betriebssystem wie folgt:

  • macOS: ~ /Library/Arduino15/packages/corename/hardware/avr/version/
  • Linux: ~/.arduino15/packages/corename/hardware/avr/version/
  • Windows: C:\user\USERNAME\AppData\Local\Arduino15\packages\corename\hardware\avr\version

Hier sollte dann die platform.local.txt Datei mit folgendem Inhalt hinzugefügt werden:

recipe.hooks.savehex.postsavehex.1.pattern.macosx=cp "{build.path}/{build.project_name}.elf" "{sketch_path}" recipe.hooks.savehex.postsavehex.1.pattern.linux=cp "{build.path}/{build.project_name}.elf" "{sketch_path}" recipe.hooks.savehex.postsavehex.1.pattern.windows=cmd /C copy "{build.path}{build.project_name}.elf" "{sketch_path}"

Nach dem Neustart der IDE werden die ELF-Dateien in den Sketch-Ordner kopiert, wenn man im Menü Sketch die Option Kompilierte Binärdatei exportieren auswählt.

Nun wollen wir dem Board-Menü eine Option hinzufügen, mit der wir zwischen -Os und -Og wechseln können. Um dies zu tun, fügt man die Zeile

menu.debug=Debug-Compiler-Flag

am Anfang der Datei boards.txt hinzu, die im selben Verzeichnis liegt wie platform.local.txt. Außerdem muss man für die Boards, für die man Debug-Informationen generieren möchte, die folgenden Zeilen einfügen:

board.menu.debug.disabled=Debug deaktiviert
board.menu.debug.disabled.build.debug=
board.menu.debug.enabled=Debug aktiviert
board.menu.debug.enabled.build.debug=-Og

board.build.extra_flags = {build.debug}

Wenn es bereits eine Zeile mit der Option extra_flags für dieses Board gibt, dann hängt man einfach {build.debug} an. Das muss man für alle Boards tun, die man debuggen möchte. Wenn man nun die IDE neu startet, gibt es im Menü Werkzeuge die Option Debug Compiler Flag, die standardmäßig deaktiviert ist.

Anstatt die Datei boards.txt zu ändern, kann man alternativ die Arduino CLI verwenden und einfach eine zusätzliche build-Option angeben:

arduino-cli compile --build-property "build.extra_flags=-Og" ...

Jetzt kann man ELF-Dateien mit der richtigen Optimierungsstufe generieren, indem man zuerst Debug Compiler Flag: "Debug Enabled" im Menü Wekzeuge auswählt und dann im Menü Sketch die Option Kompilierte Binärdatei exportieren selektiert. Aber was kann man jetzt mit einer ELF-Datei anfangen?

EDIT: Für die Modifikation der Datei boards.txt habe ich mittlerweile ein Python-Skript geschrieben, dass die Optionen automatisch für alle Boards einfügt. Einfach das Python-Skript in den entsprechenden Ordner kopieren und dort aufrufen (vorausgesetzt, man hat Python3 installiert).

Debuggen der MCU

Es gibt grundsätzlich drei verschiedene Methoden, wenn es um eingebettetes Debugging geht:

  1. Man kann einen MCU-Simulator verwenden, eine Remoteverbindung mit dem Debugger herstellen und mit dem Simulator interagieren.
  2. Man kann einen Debug-Stub verwenden, d. h. eine Bibliothek, die mit dem Code des eingebetteten Systems verknüpft ist und mit dem Remote-Debugger kommunizieren kann.
  3. Schließlich kann man auch einen Hardware-Debugger verwenden, der eine Verbindung zwischen der On-Chip-Debugging-Hardware (OCD) der MCU auf der einen Seite und einem Debugger auf dem Desktop-Computer auf der anderen Seite herstellt.

Simulation

Für AVR-MCUs finden sich professionelle Simulatorlösungen in den IDEs von Microchip: Microchip Studio 7 (ehemals Atmel Studio 7) und MPLAB X, ersteres nur für Windows, letzteres für Windows, Linux und macOS (aber nur Intel-Chips). Das Interessante ist, dass die IDEs nichts kosten!

Die beiden Open-Source-Alternativen sind simulavr und simavr. Ersteres scheint weniger vollständig und erweiterbar, letzteres könnte eine bessere Dokumentation haben. Ich habe nur Letzteres ausprobiert, aber ich habe nie versucht, neue externe Geräte zu erstellen. Was interessant aussieht, ist, dass man simavr einen Strom von Eingabeereignissen geben kann, dass die entsprechenden Ausgaben aufgezeichnet werden können und ähnlich wie bei einem Logikanalysator visualisiert werden können. Das könnte ein großartiges Werkzeug zum Entwerfen von Komponententests sein. Übrigens enthält der Fork von simavr von gatk555 einen „Getting started guide to simavr“, der hilfreich ist, wenn man simavr zum ersten Mal verwendet. Basierend auf simavr gibt es zwei Erweiterungen, die auch sehr umfangreiche GUIs bereitstellen: PICSimLab und simutron. Ich habe keines von beiden ausprobiert, aber sie sehen interessant aus.

Im Allgemeinen bin ich kein großer Fan von Simulatorlösungen, da nicht klar ist, wie viel von der echten MCU der Simulator abdeckt. Mit anderen Worten, etwas, das am Simulator arbeitet, funktioniert möglicherweise nicht auf der echten MCU und umgekehrt. Also, warum sollte ich mir die Mühe machen, Zeit zu investieren, um zu verstehen, wie man mit dem Simulator arbeitet?

Wenn man simavr ausprobieren möchte, sollte man sich den Blogbeitrag von Lars Kellogg-Stedman zum Debuggen eines Programms mithilfe von simavr anschauen.

Stub-Lösungen

Debug-Stubs sind die Hardware-Debugger des armen Mannes. Sie ermöglichen es, die auf der MCU laufende Software zu debuggen, ohne dass man in teure Hardware investieren muss. Aber natürlich hat alles seinen Preis.

Ein Debug-Stub ist eine Software, im Falle des Arduino-Frameworks eine Software-Bibliothek, die mit einem Debugger interagiert, z. B. über die serielle Leitung, was bedeutet, dass das Programm, das man debuggt, die serielle Leitung nicht verwenden kann. Darüber hinaus benötigt ein solcher Stub in der Regel ein paar mehr Ressourcen, wie z.B. einen der Interrupts der MCU und/oder den Watch-Dog-Timer. Und zu guter Letzt nimmt der Stub die knappsten Ressourcen von MCUs in Anspruch, nämlich RAM und Flash. Es gibt also tatsächlich einen erheblichen Preis zu zahlen. Man das kompensieren, indem man die Software auf einer MCU mit mehr Ressourcen debuggt, z. B. wenn man für ein ATmega328 entwickelt, kann man einen ATmega1284 zum Debuggen verwenden. Dann hat man jedoch ein ähnliches Problem wie bei Simulatoren.

Der einzige Debug-Stub für AVR8-Prozessoren, der vom Arduino-Framework unterstützt wird, ist avr_debug von Jan Dolinay. Er unterstützt ATmega328, ATmega1284 und ATmega2560. Um reibungslos zu laufen, benötigt er außerdem einen modernen Optiboot-Bootloader, was in jeder Hinsicht eine ausgezeichnete Idee ist. Ich werde das Setup und die Verwendung des Stubs im nächsten Blogbeitrag dieser Serie beschreiben.

Hardware-Debugger

Hardware-Debugger sind die Königsklasse im Bereich der Debugging-Tools. Sie kommunizieren auf der einen Seite mit einem Software-Debugger auf dem Desktop-Computer, und auf der anderen Seite steuern sie die On-Chip-Debugging-Hardware (OCD) auf dem Chip. Wenn man einen Hardware-Debugger für die klassischen ATtinys und die kleinen ATmegas (z.B. 328) kaufen möchten, dann gibt es nur wenige Kandidaten:

  • AVR Dragon (eingestellt),
  • Atmel-ICE (90€ inkl. Porto),
  • MPLAB Snap (35€ + Porto).

Der Grund dafür, dass es nur wenige Alternativen gibt, ist, dass diese spezielle MCU-Familie die debugWIRE-Schnittstelle zum Debuggen verwendet. Diese Schnittstelle verwendet nur die RESET-Leitung als Kommunikationsleitung, was bedeutet, dass man keinen anderen wertvollen Pin opfern muss. Andere Debugging-Schnittstellen, z. B. JTAG, verwenden viel mehr. Hier müssen man 5 Datenleitungen reservieren. Im Gegensatz zu debugWIRE, einem proprietären, nicht offengelegten Protokoll, ist JTAG standardisiert und aus diesem Grund gibt es eine Reihe von verschiedenen und auch billigen Hardware-Debuggern und es gibt auch viele Softwarelösungen, einschließlich Open-Source-Lösungen. Wenn man also größere ATmegas- oder ARM-Prozessoren debuggen möchte, hat man eine große Auswahl und es scheint der Fall zu sein, dass es auch viele verschiedene Alternativen in Bezug auf die Software gibt. Es gibt insbesondere openOCD, das eine gdbserver-Schnittstelle zur Verfügung stellt, d.h. eine Schnittstelle zum GDB-Debugger. Ich habe es noch nicht ausprobiert und werde daher nicht darüber schreiben. Stattdessen werde ich in einem der kommenden Blogbeiträge darüber berichten, wie man den Atmel-ICE-Debugger auf MCUs verwenden kann, die debugWIRE unterstützen.

EDIT: Inzwischen habe ich ein Arduino-Programm namens dw-link implementiert, das einen Arduino Uno oder Nano in einen Hardware-Debugger verwandelt.