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 the OCIE0A interrupt, which is raised every millisecond by TIMER0, 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:


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 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>
int global = 0;
void setup() {
#ifdef AVR8_DEBUG
  debug_init();    // initialize the debugger
  pinMode(13, OUTPUT);    
void loop() {
  int local = 0;
  digitalWrite(13, HIGH); 
  digitalWrite(13, LOW); 

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 the Tools menu
  • Select Export Compiled Binary in the Sketch menu
  • Select Upload in the Sketch 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
Breakpoint 2, loop ()                # stopped at brerakpoint 2: line 18
     at /.../blinky/blinky.ino:18
18      global++; 
(gdb) continue                       # continue again
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).

h[elp]get help on gdb commands
h[elp] commandget 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
upgo one stack frame up (in order to display variables)
downgo one stack frame (only possible after up)
l[ist]show source code around current point
l[list] functionshow source code around start of code for function
set var[iable] var=exprset variable var to value expr
p[rint] exprshow value of expression
disp[lay] exprprint value of expression every time after a stop
b[reak] functionset breakpoint at beginning of function
b[reak] numberset 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] numberdisable breakpoint number temporarily
en[able] numberenable breakpoint number
d[elete] numberdelete breakpoint number
d[elete]delete all breakpoints
d[elete] d[display] numberdelete display command number
cond[ition] number exprstop at breakpoint number only if expr is true
cond[ition] numbermake breakpoint number unconditional
Ctrl-Cstop program execution asynchronously

In addition to the commands above, you have to know a few more commands that control the execution of avr-gdb.

set se[rial] b[aud] numberset baud rate of the serial line to the gdb-stub
tar[get] rem[ote] serialportspecify the serial line to the gdb-stub (use only after baud rate has been set)
fil[e] name.elfload 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