Das Titelbild ist von Hebi B. auf Pixabay
Dieser Blogbeitrag zeigt, wie man in 7 einfachen Schritten zu einer funktionierenden Debugging-Lösung mit einem gdb-Stub für einige 8-Bit-AVR-MCUs gelangt. Die einzige zusätzliche Hardware, die man benötigt, ist ein ISP-Programmiergerät, um einen neuen Bootloader zu brennen (wenn man mit einem sehr langsam laufenden Programm zufrieden ist, braucht man nicht einmal das).
1. Stub herunter laden
Zuerst muss man den gdb-stub von GitHub herunterladen. Man kann das Repository entweder als ZIP-Datei herunterladen und entpacken oder das GitHub-Repository klonen. Das Repository enthält eine recht umfangreiche Dokumentation im Ordner doc
. Es ist ein Dokument mit ungefähr 100 Seiten, das man wahrscheinlich zunächst nicht studieren möchte. Aber es kann sich in Zukunft als nützlich erweisen.
Man sollte daran denken, dass die serielle Kommunikationsleitung nicht verwendet werden kann (siehe vorheriger Beitrag), wenn man mit dem Stub debuggt. Wenn also eine serielle Kommunikation vom Programm benötigt wird, muss man auf dem ATmega328 die SoftwareSerial-Bibliothek
verwenden und zwei andere Pins als die seriellen Hardware-Pins 0 und 1 verwenden. Auf anderen MCUs kann man eine der alternativen seriellen Schnittstellen verwenden. Wenn man diese serielle Leitung an den Desktop-Computer anschließen möchte, benötigt man allerdings einen FTDI-Adapter.
Eine weitere Einschränkung ist, dass der Stub nur auf dem ATmega328(P), dem ATmega1284(P), dem ATmega1280 und dem ATmega2560 funktioniert. Darüber hinaus verwendet es ungefähr 5k Byte Flash und 500 Byte RAM.
2. Installation der Arduino-Bibliothek
Nun muss der Ordner arduino/library/avr-debugger
des heruntergeladenen GitHub-Repositorys in einen Arduino-Bibliotheksordner kopiert werden, der am besten avr-debugger
heißen sollte.
3. Konfiguration des Stubs
Jetzt ist ein guter Zeitpunkt, um darüber nachzudenken, wie der Stub konfiguriert werden soll. Diese Konfiguration muss durch Festlegen einiger Konstanten in der Datei avr-stub.h
erfolgen. Folgende Konstanten müssen gesetzt werden:
AVR8_BREAKPOINT_MODE
: Kann auf 1 oder 2 gesetzt werden. Modus 1 bedeutet, dass der Debugger den Code in Einzelschritten ausführt und den aktuellen Programmzähler mit einer Liste von Haltepunkten vergleicht. Dies ist schrecklich langsam und die Timer laufen nicht. Das Gute ist, dass man den Bootloader nicht ersetzen muss (d.h. man kann den unten beschriebenen Schritt 4 überspringen). Mode 2 bedeutet, dass die Haltepunkte im Flash-Speicher gesetzt werden, sodass das Programm mit normaler Geschwindigkeit ausgeführt werden kann. Dies könnte zu Flashspeicher-Verschleiß führen, da es für jede Flash-Seite eine Obergrenze von 10000 Schreibvorgängen gibt. Ich empfehle, den Haltepunktmodus 2 zu verwenden, aber für einen schnellen Test könnte es einfacher sein, nur Modus 1 zu nutzen.AVR8_SWINT_SOURCE
: Der Stub benötigt einen externen Interrupt-Pin zusammen mit dem zugehörigen Interrupt, um so etwas wie einen Software-Interrupt zu implementieren. Dies kann Arduino Pin 2 (INT0) – Wert 0 – oder Pin 3 (INT1) – Wert 1 – für einen ATmega328P sein. Wenn beide Pins anderweitig verwendet werden, kann man stattdessen den COMPA-Interrupt verwenden, der normalerweise nicht in Arduino-Programmen verwendet wird. In diesem Fall muss man den Wert -1 angeben. Da INT0 und INT1 eine höhere Priorität haben als der COMPA-Interrupt, empfehle ich, einen der ersteren zu verwenden. Nur wenn dies nicht möglich ist, weil das Anwenderprogramm diese Pins verwenden muss, sollte COMPA verwendet werden.AVR8_USE_TIMER0_INSTEAD_OF_WDT
: Wenn der Haltepunktmodus 2 verwendet wird, benötigt der Stub einen Timer-Interrupt, um zu überprüfen, ob ein Haltepunkt erreicht wurde. Die übliche Methode besteht darin, den Watchdog-Timer-Interrupt zu verwenden. Wenn dieser bereits vom Benutzerprogramm verwendet wird, kann man stattdessen denOCIE0A-Interrupt
verwenden, der jede Millisekunde vonTIMER0
ausgelöst wird, der vom Arduino-Kern verwendet wird, um Millisekunden zu zählen. Wenn man dies wünscht, legt man diese Konstante auf 1 fest. Andernfalls sollte es 0 sein.
Alles in allem könnte eine Konfiguration des Stubs in den ersten Zeilen der Datei avr-stub.h
wie folgt aussehen:
#define AVR8_BREAKPOINT_MODE 2 #define AVR8_SWINT_SOURCE 0 #define AVR8_USE_TIMER0_INSTEAD_OF_WDT 0
optiboot
-Bootloader
4. Brennen des Der optiboot
-Bootloader ist der Bootloader der Wahl für alle Nicht-USB-ATmega-Chips, da er sehr klein ist (mit nur 512 Bytes auf den kleineren ATmegas) und WDT-Neustarts bewältigen kann, was ältere Arduino-Bootloader nicht können. Darüber hinaus verfügen die neueren Versionen (>= 8.0) über eine API zur Neuprogrammierung des Flash-Speichers, die für das Setzen von Debugging-Haltepunkten unerlässlich ist.
Dazu lädt man sich den Bootloader aus dem optiboot
-GitHub-Repository herunter. Unter Umständen ist die gewünschte Version bereits als Hex-Datei vorhanden (siehe optiboot/optiboot/bootloaders/optiboot/
), oder man kann sie einfach mit dem Makefile generieren (z.B. für einen anderen CPU-Takt). Nachdem man den richtigen Ordner ausgewählt hat, kann man den Bootloader mit avrdude (siehe letzter Blogpost) wie folgt hochladen (vorausgesetzt, wir möchten optiboot
auf einen Uno, Nano oder Pro Mini mit 16 MHz Takt hochladen und wir haben einen STK500 Version2 kompatiblen Programmierer, und serialport ist der serielle Port des Programmers):
> avrdude -c stk500v2 -p m328p -P serialport -U flash:w:optiboot_atmega328.hex:a
Dann gibt der Programmer die folgenden Meldungen aus:
avrdude: AVR device initialized and ready to accept instructions Reading | ################################################## | 100% 0.00s avrdude: Device signature = 0x1e950f (probably m328p) avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed To disable this feature, specify the -D option. avrdude: erasing chip avrdude: reading input file "optiboot_atmega328.hex" avrdude: input file optiboot_atmega328.hex auto detected as Intel Hex avrdude: writing flash (32768 bytes): Writing | ################################################## | 100% 0.00s avrdude: 32768 bytes of flash written avrdude: verifying flash memory against optiboot_atmega328.hex: avrdude: load data flash data from input file optiboot_atmega328.hex: avrdude: input file optiboot_atmega328.hex auto detected as Intel Hex avrdude: input file optiboot_atmega328.hex contains 32768 bytes avrdude: reading on-chip flash data: Reading | ################################################## | 100% 0.00s avrdude: verifying … avrdude: 32768 bytes of flash verified avrdude: safemode: Fuses OK (E:FF, H:DE, L:FF) avrdude done. Thank you.
Je nachdem, ob der vorherige Bootloader größer war, muss man nun noch die Fuses ändern, die die Größe des Bootloaders beschreiben. Bei einem ATmega328 muss man die High Fuse auf 0xDE einstellen. Das Uno-Board benutzt übrigens bereits den Optiboot-Bootloader, allerdings in einer älteren Version, die noch nicht das Schreiben von Flash-Speicher unterstützt. Da die Größe des Bootloaders sich aber nicht geändert hat, braucht man bei den Fuses aber nichts zu ändern.
Wenn man den zusätzlichen Speicherplatz nutzen möchte, kann man auch die boards.txt
Datei bearbeiten. Ich schlage vor, eine neue Art von Board einzuführen und dann den Wert board.upload.maximum_size
anzupassen. Wie bereits oben bemerkt, braucht man im Falle des Uno-Boards an dieser Stelle nichts machen, da bereits der Wert für den Optiboot-Bootloader eingetragen ist.
Schließlich kann man überprüfen, ob alles nach Plan funktioniert hat, indem man die Arduino-IDE öffnet und das Blink
-Programm hochlädt.
5. Konfigurieren der Arduino-IDE, um debugbaren Code zu erzeugen
Dieser Schritt wurde bereits in meinem vorherigen Blogbeitrag beschrieben. Man muss eine Datei platform.local.txt
hinzufügen, Änderungen an boards.txt
vornehmen und man muss avr-gdb
installieren.
Um das Leben einfacher zu gestalten, sollte man dem Eintrag board.menu.debug.enabled.build.debug
die Zeichenfolge -DAVR8_DEBUG
hinzufügen. Mit anderen Worten, wenn das Debuggen aktiviert ist, wird das Symbol AVR8_DEBUG
definiert. Wir werden dies gleich verwenden, um den Debugcode zu deaktivieren, wenn kein Debuggen erforderlich ist.
6. Programmmodifikationen
Das erste Programm, das wir debuggen wollen, ist die folgende Variation des Blinkprogramms. Die bedingt zu kompilierenden Teile (eingeschlossen in #ifdef
und #endif
) sind diejenigen, die man zu jedem Programm hinzufügen muss, das man debuggen will.
#ifdef AVR8_DEBUG <avr8-stub.h>#include #endif int global = 0; void setup() { #ifdef AVR8_DEBUG debug_init(); Initialisieren des Debuggers #endif pinMode(13; AUSGABE); } void loop() { int local = 0; lokal++; digitalWrite(13, HOCH); Verspätung(100); digitalWrite(13, NIEDRIG); Verspätung(200); global++; lokal++; }</avr8-stub.h>
Nun muss dieses Programm, das wir blinky
nennen wollen, dort abgelegt werden, wo die Arduino-IDE es finden kann. Jetzt muss man die Arduino-IDE neu starten (damit sie das Beispielprogramm finden kann), das Programm öffnen und dann die folgenden Schritte ausführen:
- Das richtige Board im Menü
Werkzeuge
auswählen Debug Compiler Flag: "Debug Enabled"
im MenüWerkzeuge
wählen- Im Menü
Sketch
die OptionKompilierte Binärdatei exportieren
auswählen - Im Menü
Sketch
die OptionHochladen
auswählen.
7. Starten einer Debugsitzung
Schließlich muss man eine Shell starten und in das Verzeichnis wechseln, in dem sich das Programm befindet, das man debuggen will. In diesem Verzeichnis sollte man eine ELF-Datei finden. Wenn man das oben genannte Beispielprogramm verwendet hat, sollte es blinky.ino.elf
sein. Schauen wir uns an, wie eine Beispiel-Debug-Sitzung aussehen könnte. Alle Benutzereingaben sind blau. Alles nach # ist ein erklärender Kommentar, den man nicht eingeben muss.
> avr-gdb blinky.ino.elf # start the debugger with a particular ELF file GNU gdb (GDB) 10.2 ... For help, type "help". Type "apropos word" to search for commands related to "word"… Reading symbols from blinky.ino.elf… (gdb) set serial baud 115200 # set baudrate before connecting (gdb) target remote serialport # connect to stub, start program and stop it Remote debugging using serialport micros () # program stopped somewhere during delay at /.../1.8.3/cores/arduino/wiring.c:81 85 uint8_t oldSREG = SREG, t; (gdb) break loop # set breakpoint at beginning of loop function Breakpoint 1 at 0x8e2: file /.../blinky/blinky.ino, line 10. (gdb) list loop # list program around loop function 9 pinMode(13, OUTPUT); 10 } 11 void loop() { 12 int local = 0; 13 local++; 14 digitalWrite(13, HIGH); 15 delay(100); 16 digitalWrite(13, LOW); 17 delay(200); 18 global++; (gdb) break 18 # set breakpoint al line 18 in current file Breakpoint 2 at 0x90a: file /.../blinky/blinky.ino, line 18. (gdb) continue # continue execution Continuing. Breakpoint 2, loop () # stopped at brerakpoint 2: line 18 at /.../blinky/blinky.ino:18 18 global++; (gdb) continue # continue again Continuing. Breakpoint 1, loop () # stopped at breakpoint 1 at /.../blinky/blinky.ino:14 14 digitalWrite(13, HIGH); (gdb) print global # print value of variable global $1 = 2 (gdb) set variable local=122 # set value of variable local to 122 (gdb) print local # print value of variable local $2 = 122 (gdb) step # make a single step, perhaps into a function digitalWrite (pin=pin@entry=13 '\r', val=val@entry=1 '\001') at /.../1.8.3/cores/arduino/wiring_digital.c:144 144 uint8_t timer = digitalPinToTimer(pin); (gdb) step # make another step 145 uint8_t bit = digitalPinToBitMask(pin); (gdb) finish # return from function Run till exit from #0 digitalWrite (pin=pin@entry=13 '\r', val=val@entry=1 '\001') at /.../1.8.3/cores/arduino/wiring_digital.c:145 loop () at /.../blinky/blinky.ino:15 15 delay(100); (gdb) next # make a single step, overstepping functions 16 digitalWrite(13, LOW); (gdb) info breakpoints # list all breakpoints Num Type Disp Enb Address What 1 breakpoint keep y 0x000008e2 in loop at /.../blinky/blinky.ino:14 breakpoint already hit 2 times 2 breakpoint keep y 0x0000090a in loop at /.../blinky/blinky.ino:18 breakpoint already hit 2 times (gdb) delete 1 # delete breakpoint 1 (gdb) detach # detach from debugger Detaching from program: /.../blinky/blinky.ino.elf, Remote target Ending remote debugging. [Inferior 1 (Remote target) detached] (gdb) quit # exit from debugger >
Anstatt bei jedem Start des Debuggers eine Reihe von Befehlen erneut einzugeben, kann man diese Befehle in der Datei .gdbinit
in dem Verzeichnis, in dem man den Debugger startet, oder im Home-Verzeichnis ablegen.
Man könnte generell den Eindruck bekommen, dass das definitiv zu viel Tipparbeit ist und sich fragen, ob es eine GUI gibt. Nun, für Linux findet man eine Reihe von verschiedenen Debugger-GUIs, wie ddd, Insight und Nemiver. Für macOS konnte ich keine GUI finden, die funktioniert. Für Windows habe ich nicht gesucht. Es gibt einen gdb-internen GUI-Modus namens TUI, der es z.B. ermöglicht, den Programmtext und das Debugging-Fenster parallel anzuzeigen.
In jedem Fall kann man immer zu PlatformIO wechseln, das unter allen drei Betriebssystemen läuft. Und es ist relativ einfach zu installieren.
8. Einige weitere gdb-Befehle
In der obigen Beispielsitzung haben wir bereits eine Reihe relevanter Befehle gesehen. Wenn man wirklich mit gdb debuggen möchten, muss man jedoch ein paar weitere Befehle kennen. Hier ein kurzer Überblick über die wichtigsten Befehle (alles zwischen eckigen Klammern kann weggelassen werden, Argumente sind kursiv):
befehlen | Aktion |
h[elp] | Hilfe zu GDB-Befehlen |
h[elp] Befehl | Hilfe zu einem bestimmten GDB-Befehl |
s[tep] | Einzelschritt, absteigend in Funktionen (Step-in) |
n[ext] | einzelner Schritt ohne Abstieg in Funktionen (Step-over) |
fin[ish] | Aktuelle Funktion beenden und vom Aufruf zurückkehren (Step-out) |
c[ontinue] | Von der aktuellen Position aus fortfahren (oder beginnen) |
ba[cktrace] | Aufrufliste anzeigen |
up | Gehen Sie einen Stapelrahmen nach oben (um Variablen anzuzeigen) |
down | gehen Sie einen Stapelrahmen runter (nur nach dem Aufsteigen möglich) |
l[ist] | Quellcode um den aktuellen Punkt anzeigen |
l[list] Funktion | Quellcode um den Codeanfang für Funktion anzeigen |
Set var[iable] var=expr | Setzen Sie die Variable var auf den Wert expr |
p[rint] expr | Wert der expr anzeigen |
disp[lay] expr | Wert von expr jedes Mal nach einem Stopp anzeigen |
b[reak] Funktion | Haltepunkt am Anfang der Funktion festlegen |
b[reak] Nummer | Haltepunkt an der Quellcodezeilennummer in der aktuellen Datei festlegen |
i[nfo] b[reakpoints] | Alle Haltepunkte auflisten |
i[info] d[isplay] | Alle Anzeigebefehle auflisten |
dis[able] Number | Haltepunktnummer vorübergehend deaktivieren |
en[able] Nummer | Haltepunktnummer aktivieren |
d[elete] Nummer | Haltepunktnummer löschen |
d[elete] | Alle Haltepunkte löschen |
d[elete] d[isplay] Nummer | Befehlsnummer löschen |
cond[ition] Nummer expr | Stopp bei Haltepunktzahl nur, wenn expr true ist |
Cond[ition] Nummer | Haltepunktnummer unkonditional machen |
Strg-C | Programmausführung asynchron stoppen |
Zusätzlich zu den obigen Befehlen müssen Sie einige weitere Befehle kennen, die die Ausführung von avr-gdb steuern.
befehlen | Aktion |
set se[rial] b[aud] Nummer | Legen Sie die Baudrate der seriellen Leitung auf den gdb-stub fest |
tar[get] rem[ote] serialport | Geben Sie die serielle Leitung zum gdb-stub an (nur verwenden, nachdem die Baudrate eingestellt wurde) |
fil[e] name.elf | Laden Sie die Symboltabelle aus der angegebenen ELF-Datei |
tu[i] e[nable] | Textfenster-Benutzeroberfläche aktivieren (siehe Handbuch) |
tu[i] d[isable] | Deaktivieren der Benutzeroberfläche des Textfensters |
Schreibe einen Kommentar