The featured image is by Hebi B. on Pixabay
How can you use a stub in order to squash your software bugs? This blog post shows how to arrive in 7 easy steps at a working debugging solution using a gdb-stub for some 8-bit AVR MCUs. The only additional hardware you need is an ISP programmer in order to burn a new bootloader (well, if you are satisfied with a very slow running program, you do not even need this).
1. Download the stub
Download the gdb-stub from GitHub. You can either download the repository as a ZIP file and unzip it, or you clone the GitHub repository. The repository contains a quite extensive documentation in the folder doc
. It is a document with roughly 100 pages you probably do not want to study initially. But it may come in handy in the future.
Remember (from my description of the stub in my previous post) that the serial communication line cannot be used when you debug using the stub. So, if serial communication is needed by the program, on the ATmega328, you have to use the SoftwareSerial
library and use two other pins than the hardware serial pins 0 and 1. On other MCUs, you can use the alternate serial interfaces. If you want to interface this serial line to your desktop computer, you might need an FTDI adapter, though.
Another limitation is that the stub only works on the ATmega328(P), the ATmega1284(P), the ATmega1280 and the ATmega2560. Moreover, it uses roughly 5k byte flash and 500 bytes of RAM.
2. Install the Arduino library
You now have to copy or symlink the folder arduino/library/avr-debugger
of the downloaded GitHub repository to an Arduino library folder, which should probably be called avr-debugger
.
3. Configure the stub
Now is a good time to think about how to configure the stub. This configuration needs to be done by setting some compile time constants in the file avr-stub.h
. The following constants need to be set:
AVR8_BREAKPOINT_MODE
: Could be set to 1 or 2. 1 means that the debugger single-steps through the code and compares the current program counter with a list of breakpoints. This is awfully slow, and the timers will not run. The good thing is that you do not need to replace the bootloader (i.e., you can skip step 4). 2 means that the breakpoints are set in the flash memory so that the program can run with normal speed. This could lead to “flash wear”, because there is an upper limit of 10000 write operations for each flash page. I recommend to use breakpoint mode 2, but for a quick test it might be easier to just use 1.AVR8_SWINT_SOURCE
: The stub needs an external interrupt pin together with the associated interrupt in order to implement something like a software interrupt. This can be Arduino pin 2 (INT0) — use value 0 — or pin 3 (INT1) — use value 1 — for an ATmega328P. If both pins are in use, you could use instead the COMPA interrupt that usually is not used by an Arduino sketch. In this case, you have to specify the value -1. Since INT0 and INT1 have higher priority than the COMPA interrupt, it is recommended to use one of those. Only if this is impossible, because the user program has to use these pins, COMPA should be used.AVR8_USE_TIMER0_INSTEAD_OF_WDT
: When breakpoint mode 2 is used, the stub requires a timer interrupt to check whether a breakpoint has been hit. The usual method is to use the watchdog timer interrupt. If this is in use, one can use instead theOCIE0A
interrupt, which is raised every millisecond byTIMER0
, which is used by the Arduino core to count milliseconds. If you want this, set this constant to 1. Otherwise, it should be 0.
All in all, a configuration of the stub in the first lines in avr-stub.h
could look like as follows:
#define AVR8_BREAKPOINT_MODE 2 #define AVR8_SWINT_SOURCE 0 #define AVR8_USE_TIMER0_INSTEAD_OF_WDT 0
4. Burn the optiboot bootloader
The optiboot
bootloader is the bootloader of choice for all non-USB ATmega chips, because it is very light weight (using only 512 bytes on the smaller ATmegas) and it can cope with WDT restarts, which older Arduino bootloaders cannot. Further, the newer versions (>= 8.0) have an API for reprogramming flash memory, which is essential for setting debugging breakpoints. So, by all means, put an optiboot
bootloader on your Arduino.
It is simple: Just download from the optiboot
GitHub repository. Your version might already be there as a hex file (see optiboot/optiboot/bootloaders/optiboot/
), or it can be easily generated using the Makefile (e.g. for a different CPU clock). After selecting the right folder, you can upload the bootloader using avrdude (see last blog post) as follows (assuming we want to upload optiboot
to an Uno, Nano, or Pro Mini with 16 MHz clock, and we have a STK500 Version2 compatible programmer):
> avrdude -c stk500v2 -p m328p -P serialport -U flash:w:optiboot_atmega328.hex:a
Then the programmer will output the following messages:
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.
Depending on whether the previous bootloader was larger, you can now change the fuses that describe the size of the bootloader (on an Uno nothing changes). For an ATmega328, you have to set the high fuse to 0xDE. If you want to make use of the extra space, then you might also want to edit the boards.txt
file. I suggest introducing a new type of board and then adjust the value board.upload.maximum_size
.
You can check whether everything worked according to plan by opening the Arduino IDE and uploading the blink
sketch.
5. Setup the Arduino IDE for producing code that can be debugged
This step has been described already in my previous blog post. You have to add a file platform.local.txt
, you have to make changes to boards.txt
, and you have to get hold of a working copy of avr-gdb
.
Well, in order to make life easier, one should add to board.menu.debug.enabled.build.debug
the string -DAVR8_DEBUG
. In other words, when debugging is enabled, then the symbol AVR8_DEBUG
is defined. We will use this later in order to deactivate the debugging code when no debugging is needed.
6. Setup your sketch for debugging
The first sketch that we want to debug is the following variation of the blink sketch. The parts that are conditionally compiled are those that you have to add to every sketch that you want to debug.
#ifdef AVR8_DEBUG #include <avr8-stub.h> #endif int global = 0; void setup() { #ifdef AVR8_DEBUG debug_init(); // initialize the debugger #endif pinMode(13, OUTPUT); } void loop() { int local = 0; local++; digitalWrite(13, HIGH); delay(100); digitalWrite(13, LOW); delay(200); global++; local++; }
Store this sketch which we will call blinky
somewhere where the Arduino IDE can find it. Now you should restart the Arduino IDE (so that it can find your example sketch), open the sketch, and then do the following:
- Select the right board in the
Tools
menu - Select
Debug Compiler Flag: "Debug Enabled"
in theTools
menu - Select
Export Compiled Binary
in theSketch
menu - Select
Upload
in theSketch
menu
7. Start a debug session
Finally, you have to start a terminal window and change into the directory where the sketch resides that you want to debug. You should find an ELF file in this directory. If you used the above-mentioned example sketch, it should be blinky.ino.elf
. Let’s look at what an example debug session could like. All user inputs are in blue. Everything after # is an explanatory comment that you do not have to type in.
> 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 >
Instead of retyping a number of commands every time you start the debugger, you can put these commands in the file .gdbinit
in the directory where you start your debugger or in the home directory.
You might in general have the impression that this is definitely too much typing and ask whether there is a GUI. Well, for Linux you find a number of different debugger GUIs, such as ddd, Insight and Nemiver. For macOS, I was not able to find one that works. For Windows, I did not search. There is a GDB internal GUI mode called TUI, that, e.g., allows one to view the program text and the debugging window in parallel.
In any case, what one always can do is to move to PlatformIO, which runs under all three OSes. And it is easy to install. I will report on this in an upcoming blog post.
8. Some more gdb commands
In the example session above, we saw already a number of relevant commands. If you really want to debug using GDB, you need to know a few more commands, though. Let me just give a brief overview of the most important commands (anything between square brackets can be omitted, arguments are in italics).
command | action |
h[elp] | get help on gdb commands |
h[elp] command | get help on specific gdb command |
s[step] | single step, descending into functions (step in) |
n[ext] | single step without descending into functions (step over) |
fin[ish] | finish current function and return from call (step out) |
c[ontinue] | continue (or start) from current position |
ba[cktrace] | show call stack |
up | go one stack frame up (in order to display variables) |
down | go one stack frame (only possible after up) |
l[ist] | show source code around current point |
l[list] function | show source code around start of code for function |
set var[iable] var=expr | set variable var to value expr |
p[rint] expr | show value of expression |
disp[lay] expr | print value of expression every time after a stop |
b[reak] function | set breakpoint at beginning of function |
b[reak] number | set breakpoint at source code line number in the current file |
i[nfo] b[reakpoints] | list all breakpoints |
i[info] d[isplay] | list all display commands |
dis[able] number | disable breakpoint number temporarily |
en[able] number | enable breakpoint number |
d[elete] number | delete breakpoint number |
d[elete] | delete all breakpoints |
d[elete] d[display] number | delete display command number |
cond[ition] number expr | stop at breakpoint number only if expr is true |
cond[ition] number | make breakpoint number unconditional |
Ctrl-C | stop program execution asynchronously |
In addition to the commands above, you have to know a few more commands that control the execution of avr-gdb.
command | action |
set se[rial] b[aud] number | set baud rate of the serial line to the gdb-stub |
tar[get] rem[ote] serialport | specify the serial line to the gdb-stub (use only after baud rate has been set) |
fil[e] name.elf | load the symbol table from the specified ELF file |
tu[i] e[nable] | enable text window user interface (see manual) |
tu[i] d[isable] | disable text window user interface |
Leave a Reply