Das Debuggen von klassischen AVRs in der Arduino IDE 2 ist endlich Realität geworden! Es hat eine Weile gedauert, diese Funktion zu implementieren, aber jetzt ist es nur noch ein Kinderspiel, das Debugging zu aktivieren und den Debugger zu benutzen.

Debuggen in der Arduino IDE 2

Seit der ersten Beta-Version im Februar 2021 bietet die Arduino IDE 2 Debugging-Funktionen, allerdings nur für eine sehr begrenzte Anzahl von Arduino-Boards. Außerdem gab es keine Möglichkeit, das System für die Interaktion mit anderen Boards zu konfigurieren. Inzwischen hat sich das aber geändert, und nach einigem Rumprobieren konnte ich eine Lösung finden, die es einem Benutzer relativ einfach macht, einen klassischen AVR-Chip zu debuggen.

Für die Ersteinrichtung ist etwas Aufwand erforderlich. Man muss einen Arduino UNO vorbereiten, der als Debugging-Probe dient (die Hardware-Schnittstelle zum Targetchip). Außerdem muss man zwei zusätzliche Boardmanager-URLs eingeben und die entsprechenden Pakete installieren:

Dann musst man jedes Mal, wenn man den Debugger starten will, Folgendes tun:

  • Verbinde den Hardware-Debugger mit dem Targetboard mit Hilfe von Drahtbrücken, mit einem modifizierten ICSP-Kabel oder einem Shield und einem ICSP-Kabel.
  • Verbinde den Host über ein USB-Kabel mit dem Hardware-Debugger.
  • Lade den Sketch, den du debuggen möchtest.
  • Wähle im Werkzeug-Menü das Board oder den Chip aus, auf dem der Sketch ausgeführt werden soll.
  • Kompiliere das Programm, indem du auf die Schaltfläche Überprüfen (links oben) klickst nachdem du Für Debugen optimieren im Sketch-Menü aktiviert hast.
  • Starte das Debugging, indem du auf die Debug-Schaltfläche in der oberen Zeile klickst.

Eine Schnellstart-Anleitung liefert die Details und auf der Arduino-Website gibt es ein kurzes Tutorial zur Arbeit mit dem Arduino IDE 2 Debugger. Ich wünsche viel Spaß beim Debuggen!

Diejenigen, die jetzt noch wissen wollen, wie dw-link in die Arduino IDE 2 und andere Frontends integriert wurde, sollten weiter lesen.

Einbinden eines Hardware-Debuggers

Wenn ein Hardware-Debugger integriert werden soll, muss die IDE oder GUI mindestens die folgenden Fakten kennen:

  • Wo die Binärdatei, normalerweise eine ELF-Datei, des zu debuggenden Programms zu finden ist. Eine IDE weiß das normalerweise schon.
  • Wo die Quelldateien zu finden sind. Das ist notwendig, damit die GUI oder IDE den Quellcode anzeigen kann. IDEs wissen das in der Regel schon.
  • Wo man den GDB-Debugger findet, in unserem Fall avr-gdb.
  • Wo der Debug-Server zu finden ist. Das ist ein Programm, das mit dem Hardware-Debugger über USB oder eine serielle Leitung verbunden ist und mit dem GDB-Debugger über einen TCP/IP-Port kommuniziert. Ein solcher Server ist vom Hardware-Debugger abhängig.
  • Die Kommunikationsverbindung zum Hardware-Debugger. Das kann ein serielles Gerät sein. In diesem Fall brauchen wir keinen Debug-Server. Alternativ kann es sich um einen TCP/IP-Port auf dem lokalen oder auf einem entfernten Rechner handeln.
  • Eine Möglichkeit zu wissen, dass der Debug-Server bereit ist, Verbindungen von GDB zu akzeptieren. Oft wird dies als regulärer Ausdruck angegeben, und die IDE wartet darauf, dass ein matchender Ausdruck in der Ausgabe des Debug-Servers erscheint. Manchmal verwenden IDEs auch einfach ein Timeout.
  • Zum Schluss sind oft noch ein paar erste GDB-Befehle nötig, um die Debugsession zu starten.

Das ist alles, was nötig ist, um den Ball ins Rollen zu bringen. Bei ARM-Prozessoren muss man normalerweise noch ein paar weitere Dinge angeben. AVRs sind jedoch so einfach, dass man nur die oben genannten Angaben braucht. Schauen wir uns drei verschiedene Beispiele an.

Einbindung von dw-link in die PlatformIO IDE

Die Einbindung des dw-link Debuggers in der PlatformIO IDE war relativ einfach. Man muss nur alles in der Konfigurationsdatei platformio.ini angeben. PlatformIO weiß bereits, wo die Quell- und ausführbaren Dateien zu finden sind und bringt seine eigene (sehr veraltete) Version von avr-gdb mit. Der relevante Teil dieser Datei könnte also wie folgt aussehen:

debug_tool = custom
debug_server = dw-server.py ;; <-- muss aufrufbar sein (z.B. in /usr/local/bin)
	-p 3333
;;debug_port = /dev/cu.usbmodem211101 ;; <-- anstelle von dw-server angeben 
debug_init_cmds = 
	define pio_reset_halt_target
	     monitor reset
	end
        define pio_reset_run_target
	     monitor reset
             continue
	end
	set serial baud 115200 ;; <-- wird nur für die serielle Leitung benötigt
        target remote $DEBUG_PORT 
	monitor version
	monitor mcu
	$LOAD_CMDS
	$INIT_BREAK
debug_build_flags = 
	-Og
	-g3
	-fno-lto

In der ersten Zeile steht, dass das debug_tool ein benutzerdefiniertes Tool ist. In der folgenden Zeile wird der debug_server angegeben, der in unserem Fall dw-server.py heißt und am TCP/IP-Port 3333 lauscht. Dieser Server ist einfach eine Brücke von der seriellen Leitung zu TCP/IP mit der zusätzlichen Fähigkeit, einen dw-link Hardware-Debugger zu erkennen. Anstelle des Servers können wir auch die serielle Leitung angeben, an die der dw-link Debugger angeschlossen ist, indem wir das Attribut debug_port dafür verwenden.

Nachdem wir festgelegt haben, wie der Hardware-Debugger mit dem GDB-Debug-Programm verbunden ist (hier über den Port 3333), muss konfiguriert werden, wie GDB gestartet werden soll, was unter debug_init_cmds beschrieben wird. Zunächst werden zwei neue GDB-Befehl definiert, pio_reset_halt_target und pio_reset_run_target. Der erste Befehl wird direkt nach dem Start aufgerufen, der zweite, wenn ein Neustart angefordert wird. Danach wird im Skript die serielle Baudrate auf 115200 gesetzt, was nur notwendig ist, wenn die serielle Leitung verwendet wird. Dann wird mit dem Befehl target remote eine Verbindung zum Debug-Server oder Hardware-Debugger hergestellt. Das Argument $DEBUG_PORT gibt an dieser Stelle entweder eine serielle Leitung oder einen TCP/IP-Port an.

Die folgenden zwei monitor-Befehle liefern Informationen für den Benutzer. Schließlich wird die ausführbare Datei auf die MCU hochgeladen und ein erster Haltepunkt gesetzt. Danach wird die Ausführung gestartet.

Als letzter Teil der Konfiguration werden die Compiler-Flags für die Erstellung der ausführbaren Datei für eine Debug-Sitzung angegeben. Normalerweise optimiert der Compiler den Quellcode, um den Programmplatz zu minimieren, wodurch der erzeugte Maschinencode verschoben wird. Darüber hinaus versuchen die üblichen Compiler-Optimierungen, so weit wie möglich auf Variablen zu verzichten. Das bedeutet, dass der generierte Code nur noch lose mit dem Quellcode verbunden ist. Das Anfordern eines Haltepunkts an einer bestimmten Stelle führt oft dazu, dass mehrere Zeilen nach der angeforderten Stelle abgebrochen werden und lokale Variablen möglicherweise nicht zugänglich sind. Mit der Compiler-Option -Og versucht der Compiler, weniger aufdringlich zu sein. Mit -O0 würde man auf alle Compiler-Optimierungen verzichten, was manchmal notwendig sein kann. Allerdings wird der Code dadurch erheblich aufgebläht. Die Option -g3 versucht, so viele symbolische Informationen über Variablen, Funktionen usw. wie möglich in die ausführbare Datei zu schreiben. Schließlich verlangt -fno-lto, dass die Link-Zeit-Optimierungen nicht angewendet werden. Auch hierdurch wird der Code aufgebläht, aber es kann notwendig sein, um objektorientierte Programme zu debuggen.

Einbindung von dw-link in Gede

Gede ist keine IDE, sondern einfach eine grafische Benutzeroberfläche für GDB. Im Gegensatz zu anderen GUI-Frontends konnte ich dw-link mit Gede zum Laufen bringen (nach einigen Anpassungen). Wenn man Gede startet, wird das folgende Fenster angezeigt.

Im Gegensatz zu PlatformIO müssen bei Gede das Projektverzeichnis, die ausführbare GDB-Datei und die ELF-Datei des zu debuggenden Programms angegeben werden. Im folgenden Block muss der Debugging-Modus angegeben werden, d.h. das Starten eines Programms auf dem Host, die Verbindung zu einem Debug-Server für Remote-Debugging, die Verwendung einer seriellen Schnittstelle für Remote-Debugging, das Öffnen eines Core-Dumps oder die Verbindung zu einem laufenden Prozess. Für uns sind nur die zweite und dritte Option relevant. In beiden Fällen solltest du das Kontrollkästchen Download aktivieren, damit der Code in die MCU geladen wird, bevor das Debugging beginnt.

Gede verfügt über eine Standard-Startsequenz für das Remote-Debugging, in der ein erster Haltepunkt festgelegt werden kann und Haltepunkte aus früheren Durchläufen erneut gesetzt werden können. Außerdem kann man im Bereich Befehle (siehe oben im Fenster) zusätzliche Initialisierungsbefehle hinzufügen, die ich nicht verwendet habe. Wenn du auf OK klickst, wird die Gede-GUI gestartet, die wie folgt aussieht.

Ein letzter Hinweis: Man muss eine aktuelle avr-gdb-Version verwenden, da Gede auf den Befehl target extended-remote angewiesen ist, der in älteren avr-gdb-Versionen (vor 10.2) offenbar nicht richtig funktioniert.

Dw-link in der Arduino IDE 2 zum Laufen bringen

Das hört sich alles sehr einfach an, aber ich musste ein bisschen herumprobieren und auch den Quellcode anpassen. Im Gegensatz dazu waren meine ersten Versuche, dw-link in der Arduino IDE 2 zum Laufen zu bringen, völlig erfolglos. Außerdem gab es anfangs keine Dokumentation, die dabei geholfen hätte, einen neuen Hardware-Debugger mit der IDE zu verbinden.

Deqing Sun war der erste, der ein Arduino-Paket entwickelte, das das Debuggen eines Arduino UNO in der Arduino IDE 2 ermöglichte. Er fand heraus, dass das Debugging in der Arduino IDE 2 auf der cortex-debug-Erweiterung für VSCode basiert und dass der Debug-Server ein OpenOCD-Server sein sollte. Sein Paket enthielt deshalb zwei ausführbare Dateien namens arm-none-eabi-gdb und dwdebug. Erstere ist ein umbenanntes avr-gdb und letztere ist eine leicht gehackte Version von dwire-debug, die als OpenOCD-Server fungiert. Außerdem gab es ein paar Änderungen in der Datei platform.txt:

debug.executable={build.path}/{build.project_name}.elf
debug.toolchain=gcc
debug.toolchain.path={runtime.tools.DWDebugTools.path}/linux
debug.toolchain.path.windows={runtime.tools.DWDebugTools.path}/win
debug.toolchain.path.macosx={runtime.tools.DWDebugTools.path}/macosx

debug.server=openocd
debug.server.openocd.path={debug.toolchain.path}/dwdebug
#spielt keine Rolle
debug.server.openocd.scripts_dir={debug.toolchain.path}/dwdebug
#unwichtig
debug.server.openocd.script={debug.toolchain.path}/dwdebug

Natürlich habe ich das auch ausprobiert, aber es hat bei mir nicht funktioniert. Das Wichtigste, was ich übersehen hatte, war, dass die gehackte Version von dwire-debug eine Zeile ausgibt, die der Arduino IDE 2 den Start des OpenOCD-Servers signalisiert, damit die IDE den GDB-Debugger starten kann. Dies war die folgende Zeile:

Info : Listening on port 50000 for gdb connection

Als ich vor kurzem erneut versuchte, dw-link mit der Arduino IDE 2 zu verbinden, fand ich den entsprechenden Kommentar in dem dwire-debug Programm und konnte dann tatsächlich alles zum Laufen bringen. Noch besser: Ich stellte fest, dass es inzwischen eine Dokumentation darüber gibt, wie man die Debug-Schnittstelle angibt. Und damit sehen die Zusätze in der platform.txt in den Paketen ATTinyCore und MiniCore jetzt wie folgt aus.

# Optimierungsflags für das Debugging
compiler.optimization_flags=-Os
compiler.optimization_flags.release=-Os
compiler.optimization_flags.debug=-Og -g3

...

# Debugger configuration (general options)
# ----------------------------------------
debug.executable={build.path}/{build.project_name}.elf
debug.toolchain=gcc
debug.toolchain.path={runtime.tools.dw-link-tools.path}

debug.server=openocd
debug.server.openocd.path={debug.toolchain.path}/dw-server
#doesn't matter, but should be specified so that cortex-debug is happy
debug.server.openocd.script=doesnotmatter
debug.cortex-debug.custom.gdbPath={debug.toolchain.path}/bin/avr-gdb
debug.cortex-debug.custom.objdumpPath={runtime.tools.avr-gcc.path}/avr-objdump
debug.cortex-debug.custom.serverArgs.0=-s
debug.cortex-debug.custom.serverArgs.1=noop
debug.cortex-debug.custom.serverArgs.2=-p
debug.cortex-debug.custom.serverArgs.3=50000
debug.cortex-debug.custom.postLaunchCommands.0=monitor mcu {build.mcu}
debug.cortex-debug.custom.postLaunchCommands.1=Setup brechen
debug.cortex-debug.custom.preRestartCommands.0=Einrichtung unterbrechen

Der erste Block von drei Zeilen behandelt die Compiler-Optionen, um die Compiler-Optimierungen Debugging-freundlicher zu machen (wie oben beschrieben). Dies kann durch die Auswahl des Eintrags Für Debugging optimieren im Sketch-Menü ausgelöst werden.

Das Attribut debug.executable gibt den absoluten Pfad an, unter dem die ELF-Datei des Sketches zu finden ist, den wir debuggen wollen. Wenn dieses Attribut nicht angegeben wird, ist die Debug-Schaltfläche in der IDE ausgegraut. Das Attribut debug.toolchain muss immer den Wert gcc haben. Und das Attribut debug.toolchain.path gibt den Pfad an, in dem die Werkzeuge zu finden sind, die in unserem Fall die Programme dw-server und avr-gdb sind.

Dann geben wir, wie oben beschrieben, vor, einen OpenOCD-Server zu starten und geben mit debug.server.openocd.path an, wo dieser zu finden ist. Die folgende Zeile, debug.server.openocd.script=doesnotmatter, ist notwendig, damit cortex-debug zufrieden ist, aber in unserem Fall bewirkt sie genau gar nichts. Danach geben wir die Pfade an, unter denen avr-gdb und avr-objdump zu finden sind. Ich weiß immer noch nicht, wozu der Aufruf von avr-objdump in unserem Kontext gut sein soll, aber ohne diese Angabe erhalten wir eine Fehlermeldung, die aber folgenlos bleibt.

Schließlich fügen wir der cortex-debug-Erweiterung einige benutzerdefinierte Argumente hinzu. Die serverArgs signalisieren dem dw-server, dass kein externes Programm gestartet werden soll und dass der TCP/IP-Port, auf den gelauscht werden soll, 50000 ist. Das bedeutet, dass ich das ursprüngliche dw-server Python-Skript nicht hacken musste. Ich musste nur unbekannte Optionen ignorieren, einen Port mit der Option -p angeben und die Option -s blockieren, indem ich den früher in der Behehlszeile angegebenen Wert mit einem Schlüsselwort überschrieb, das signalisierte, dass kein Programm gestartet werden sollte. Der Aufruf von dw-server durch die Arduino IDE 2 sieht nun wie folgt aus:

/Users/nebel/Library/Arduino15/packages/MiniCore/tools/dw-link-tools/1.3.0/dw-server -c "gdb_port 50000" -c "tcl_port 50001" -c "telnet_port 50002" -s /Users/nebel/GitHub/dw-link/examples/blinkmodes -f "/Applications/Arduino IDE.app/Contents/Resources/app/plugins/cortex-debug/extension/support/openocd-helpers.tcl" -f doesnotmatter -s noop -p 50000

Dabei ist alles bis einschließlich -f doesnotmatter von cortex-debug erzeugt und wird vom dw-server igngnoriert.

Wie bereits erwähnt, sucht die cortex-debug-Erweiterung in der Ausgabe des Debug-Servers eine Zeile, die zu einem Match mit einem angegebenen regulären Ausdruck führt, um zu entscheiden, wann der Server bereit ist, eine TCP/IP-Verbindung zu akzeptieren. Ich habe einfach die Ausgabe von dw-sever geändert, aber man könnte den regulären Ausdruck auch mit dem cortex-debug-Attribut overrideGDBServerStartedRegex anpassen.

Die letzten drei Zeilen betreffen die GDB-Start- und Neustartbefehle. Das erste der postLaunchCommands stellt sicher, dass der in der Arduino IDE ausgewählte MCU-Typ mit dem angeschlossenen MCU-Typ übereinstimmt. Dadurch wird verhindert, dass man Code, der für eine MCU erstellt wurde, auf einer MCU debuggt, die nicht mit dieser MCU kompatibel ist. Der zweite Befehl setzt einen temporären Haltepunkt am Anfang der setup-Funktion. Und dieser temporäre Haltepunkt wird auch eingerichtet, wenn ein Neustart durchgeführt wird, wie die letzte Zeile anweist.

Zusätzlich zu den Änderungen in der Datei platform.txt habe ich auch einige Änderungen in der Datei boards.txt vorgenommen, um eine feinere Steuerung der Debug-Optimierungsoption zu ermöglichen und die Kompatibilität mit früheren Versionen zu erhalten. D.h. man kann weiterhin mit ader Arduino IDE 1 Code erzeugen, den man dann mit einem externen Debugger bearbeiten kann.

Schließlich müssen auch die ausführbaren Dateien als Tools bereitgestellt werden. Ich habe mich umgesehen und einigermaßen aktuelle Versionen von avr-gdb für Windows, 64-Bit Mac und AARCH64/X86_64-Linux gefunden. Da ich auch Zugang zu Rechnern mit diesen Architekturen habe, konnte ich mit Pyinstaller eigenständige Binärdateien für das Python-Skript dw-server erstellen. Es ist mir jedoch nicht gelungen, 32-Bit-ARM- oder i686-Linux-Versionen zu erstellen. Interessanterweise kann man die Arduino IDE 2 aber auch nicht für 32-Bit-Architekturen installieren. Es ist also wahrscheinlich kein großer Verlust, wenn die entsprechenden Tools für diese Architekturen nicht erzeugt wurden.

Und zum Schluss: Nachdem alles installiert war und ich alles nochmal testete, stellte sich heraus, dass der Windows Defender das compilierte Python-Skript als Virus wahrnahm und kommentarlos löschte. Mit einer etwas anderen Art der Compilation, durch Erzeugung eines ganzen Ordners statt einer Binärdatei, lief es dann zum Schluss aber.

Zusammenfassung

Der Hardware-Debugger dw-link kann jetzt mit der PlatformIO IDE, mit Gede und schließlich mit der Arduino IDE 2 verwendet werden. Ich hoffe, dass ich nicht nur die Verwendung des Debuggers angenehmer gemacht habe, sondern auch anderen Einblicke geben konnte, wie man einen Hardware-Debugger in eine dieser IDEs oder GUIs zu integrieren kann.

Views: 4