Featured picture: OpenClipart-Vectors on Pixabay.

One has to add to the title (quoted from a tweet by Filipe Fortes) that the detective suffers from memory loss. Otherwise, the case could be solved easily. Similarly, with debugging: If I only knew what nasty things I have hidden in the source code, I could just remove them – but I simply do not know. In this blog post, we will have a look at what kind of tools one could use to find the skeletons hidden in the closet.

Debugging with print statements

If you work with the Arduino IDE, you probably have noticed that it is very accessible, but also very limited. In particular, it does not support any kind of debugging tools (at least in the stable version 1.X). So the only way to find out, what is happening in your code, is to throw in some print statements, perhaps showing variable values. This can help to show whether the flow of control and the values of variables are what you expect.

So what do you do if your MCU does not have a serial interface or this interface is used by the very program you want to debug? One option is to signal the state of the program using one (otherwise unused) output pin and switch an LED on or off. This gives you only very limited insights into the inner workings of your program, though.

I recommend instead using one pin as an additional serial output. You can use my TXOnlySerial library. It just needs one pin and uses less flash and RAM than the SoftwareSerial library. However, you need an FTDI adapter in order to be able to interface the serial line to your computer.

Another trick, I use, is to define debugging print statements. When I am done with debugging, I simply define these statements to be empty, which means I do not have to delete all the debug print statements in the end. The definitions go all into a header file (debug.h), which looks as follows:

/* Debug macros */
#ifdef DEBUG
#define DEBDECLARE() TXOnlySerial deb(DEBTX)
#define DEBINIT() deb.begin(19200)
#define DEBPR(str) deb.print(str)
#define DEBPRF(str,frm) deb.print(str,frm)
#define DEBLN(str) deb.println(str)
#define DEBLNF(str,frm) deb.println(str,frm)
#define DEBDECLARE()
#define DEBINIT()
#define DEBPR(str)
#define DEBPRF(str,frm)
#define DEBLN(str)
#define DEBLNF(str,frm)

In the Arduino sketch, you have to include TXOnlySerial.h, you have to define the constant symbol DEBTX (the pin you will use as the serial output line), have a line with DEBDECLARE(), have DEBINIT() in the setup, and then you can use all the statements above. This could look like as follows:

#define DEBUG // comment out if not needed anymore
#define DEBTX 8 // used as debugging output
#include <TXOnlySerial.h>
#include "debug.h"

void setup() {

void loop() {

The disadvantage of debugging with print statements is that when you want to see the value of another variable, you have to change your sketch, compile, upload, and then start the program again. It would be much nicer if you were able to stop at a particular point in the program and then inspect the program state. Perhaps even changing variable values before continuing with the execution. And this is what real debuggers are for.

Preparing the Arduino IDE 1.X for debugging

If you want to use avr-gdb, the GNU debugger for AVR MCUs, for debugging Arduino sketches, you need first to install it, because it is no any longer part of the Arduino software. On a Mac, it is available as a homebrew package, but the current version (July 2021) does not work. I have figured out a way to install a working version of avr-gdb. Under Linux, you can simply load the avr-gdb package (which is called gdb-avr) using your favorite package manager. Under Windows, you have to download the entire AVR toolchain from somewhere, which then contains the debugger. WinAVR is probably a good choice, either in the “official” version, which is more than 10 years old, or a more recent version from Zak’s Electronics Blog ~*.

Then (on all platforms) you need to change some configuration files in order to be able to get access to the ELF files (which contain debugging information) and to change the GCC optimization level from -Os (meaning: optimize for space) to -Og (meaning: optimize in a debug-friendly manner). Well, doing this is not strictly necessary, but very advisable. Otherwise, you may get confused because the compiler can rearrange code quite a lot, and then you might not be able to stop at a place in the program where you want to stop.

EDIT: Meanwhile, I have modified the board definition files of ATTinyCore and Minicore so that no modifications of the configuration files are necessary anymore.

First, you have to add a file platform.local.txt to the directory where the relevant platform.txt resides. If you have installed the board information using the Board Manager, then you find the relevant folder depending on your OS as follows:

  • macOS: ~/Library/Arduino15/packages/corename/hardware/avr/version/
  • Linux: ~/.arduino15/packages/corename/hardware/avr/version/
  • Windows: C:\Users\user\AppData\Local\Arduino15\packages\corename\hardware\avr\version\

Here, you should add a platform.local.txt file with the following contents:

recipe.hooks.savehex.postsavehex.1.pattern.macosx=cp "{build.path}/{build.project_name}.elf" "{sketch_path}" recipe.hooks.savehex.postsavehex.1.pattern.linux=cp "{build.path}/{build.project_name}.elf" "{sketch_path}" recipe.hooks.savehex.postsavehex.1.pattern.windows=cmd /C copy "{build.path}\{build.project_name}.elf" "{sketch_path}"

After restarting the IDE, you now get ELF files in your sketch folder when you select Export compiled Binary in the menu Sketch.

Now, we want to add an option to your board selection that allows us to switch between -Os and -Og. In order to do so, you add the line

menu.debug=Debug Compiler Flag

to the beginning of the file boards.txt in the same directory where you placed platform.local.txt. Further, for the boards, you want to be able to generate debug information, you add the following lines:

board.menu.debug.disabled=Debug Disabled
board.menu.debug.enabled=Debug Enabled

board.build.extra_flags = {build.debug}

If there is already a line with the extra_flags option for this board, then just append {build.debug}. You have to do that for all boards that you want to debug. When you now restart the IDE, there will be an option Debug Compiler Flag in the Tools menu, which is disabled by default.

Instead of modifying the board.txt file, you can use the Arduino CLI and simply specify an additional built option:

arduino-cli compile --build-property "build.extra_flags=-Og" ...

Now you can generate ELF files with the right optimization level by first selecting Debug Compiler Flag: "Debug Enabled" in the Tools menu and then selecting Export compiled Binary in the menu Sketch. So what can you do with this file? Well, you could load it in the AVR debugger, but then how do you connect to the MCU?

Debugging your MCU

There are basically three different methods when it comes to embedded debugging:

  1. You can use an MCU simulator, connect to the debugger remotely and interact with the simulator.
  2. You can use a debug stub, i.e., a library that is linked to the code of the embedded system and that is able to communicate with the remote debugger.
  3. Finally, you can use a hardware debug probe that connects to the on-chip debugging (OCD) hardware of the MCU and to a debugger on your computer.


For AVR MCUs, professional simulator solutions can be found in Microchip’s IDEs: Microchip Studio 7 (formerly Atmel Studio 7) and MPLAB X, the former only for Windows, the latter for Windows, Linux and macOS (but only Intel chips). The interesting thing is that they do not cost anything! Well, you could pay some money for Microchip’s compilers (called XC), but it is no problem at all to install the avr-gcc toolchain instead (I will come back to that in a future blog post).

The two open-source alternatives are simulavr and simavr. The first seems less complete and extendable, the latter could have better documentation. I have only played around with the latter, but I have never tried to create new external devices. What seems like a great idea is that you can provide simavr with a stream of input events, and you can record states of all outputs and visualize them, similar to what you get from a logic analyzer. That could be a great tool for designing unit tests. By the way, the fork of simavr by gatk555 contains a “Getting started guide to simavr” that might be helpful when you first want to use simavr. Based on simavr, there are two extensions that also provide very extensive GUIs: PICSimLab and simutron.

In general, I am not a great fan of simulator solutions because it is not clear, how much of the real MCU the simulator covers. In other words, something working on the simulator might not work on the real MCU and vice versa. So, why should I bother to invest time to understand how you work with the simulator?

If you are keen to try out a simulator, Lars Kellogg-Stedman has written a blog post on how to debug a program that runs on simavr.

Stub solutions

Debug stubs are the poor men’s hardware debugger. They allow you to debug the software running on the MCU, but you do not have to invest in expensive hardware. However, there is, of course, a price to pay.

A debug stub is a piece of software, in the case of the Arduino framework a library, that interacts with a debugger using, e.g., the serial line, which implies that the program you are debugging cannot use the serial line. Furthermore, such stub usually needs a few more resources, such as one of the interrupts of the MCU and/or the watchdog timer. And last, but not least, the stub takes up the scarcest resources of MCUs, namely, RAM and flash. So, there is a substantial price to pay. You can compensate for it by debugging the software on an MCU with more resources, e.g., if you develop for an ATmega328 you can use an ATmega1284 for debugging. However, then you have a problem similar to the one with simulators.

The only debug stub for AVR8 processors that are supported by the Arduino framework I am aware of is avr_debug by Jan Dolinay. It supports ATmega328, ATmega1284, and ATmega2560. Further, in order to run smoothly, it needs a modern optiboot bootloader, which is an excellent idea in any way. I will describe the setup and how to use the stub in the next blog post of this series.

Hardware Debugger

Hardware debuggers are the premier class in the area of debugging tools. They communicate on one side with a software debugger on your desktop computer, and on the other side, they control the on-chip debugging (OCD) hardware on the chip. If you want to buy a hardware debugger for the classic ATtinys and the small ATmegas (e.g. 328), then there are only a few candidates:

  • AVR Dragon (discontinued),
  • Atmel-ICE (90€ incl. postage),
  • MPLAB Snap (35€ + postage).

The reason that there are only a few alternatives is that this particular family of MCUs uses the debugWIRE interface for debugging. This interface uses only the RESET line as a communication line, which means that you do not have to sacrifice some other precious pin. Other debugging interfaces, e.g., JTAG, use much more. Here you have to reserve 5 data lines. In contrast to debugWIRE, which is a proprietary undisclosed protocol, JTAG is standardized and for this reason, there exist a number of different and cheap hardware debuggers and there are also a lot of software solutions, including open-source ones. So, if you want to debug larger ATmegas or ARM processors, you have a lot of choices, and it appears to be the case that there are also many different alternatives concerning software. There is in particular openOCD, which provides you with a gdbserver interface, i.e., an interface to the GDB debugger, and to many different hardware debuggers. I have not tried it out (yet) and therefore will not write about it; maybe later. Instead, I will write about how you can use the Atmel-ICE debugger on MCUs supporting debugWIRE in one of the upcoming blog posts.

EDIT: Meanwhile, I have implemented an Arduino sketch, called dw-link, that turns an Arduino Uno or Nano into a hardware debugger.