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 den OCIE0A-Interrupt verwenden, der jede Millisekunde von TIMER0 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

4. Brennen des optiboot-Bootloader

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 Option Kompilierte Binärdatei exportieren auswählen
  • Im Menü Sketch die Option Hochladen 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):

befehlenAktion
h[elp]Hilfe zu GDB-Befehlen
h[elp] BefehlHilfe 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
upGehen Sie einen Stapelrahmen nach oben (um Variablen anzuzeigen)
downgehen Sie einen Stapelrahmen runter (nur nach dem Aufsteigen möglich)
l[ist]Quellcode um den aktuellen Punkt anzeigen
l[list] FunktionQuellcode um den Codeanfang für Funktion anzeigen
Set var[iable] var=exprSetzen Sie die Variable var auf den Wert expr
p[rint] exprWert der expr anzeigen
disp[lay] exprWert von expr jedes Mal nach einem Stopp anzeigen
b[reak] FunktionHaltepunkt am Anfang der Funktion festlegen
b[reak] NummerHaltepunkt an der Quellcodezeilennummer in der aktuellen Datei festlegen
i[nfo] b[reakpoints]Alle Haltepunkte auflisten
i[info] d[isplay]Alle Anzeigebefehle auflisten
dis[able] NumberHaltepunktnummer vorübergehend deaktivieren
en[able] NummerHaltepunktnummer aktivieren
d[elete] NummerHaltepunktnummer löschen
d[elete]Alle Haltepunkte löschen
d[elete] d[isplay] NummerBefehlsnummer löschen
cond[ition] Nummer exprStopp bei Haltepunktzahl nur, wenn expr true ist
Cond[ition] NummerHaltepunktnummer unkonditional machen
Strg-CProgrammausführung asynchron stoppen

Zusätzlich zu den obigen Befehlen müssen Sie einige weitere Befehle kennen, die die Ausführung von avr-gdb steuern.

befehlenAktion
set se[rial] b[aud] NummerLegen Sie die Baudrate der seriellen Leitung auf den gdb-stub fest
tar[get] rem[ote] serialportGeben Sie die serielle Leitung zum gdb-stub an (nur verwenden, nachdem die Baudrate eingestellt wurde)
fil[e] name.elfLaden 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

Views: 60