Debugging classic AVRs in the Arduino IDE 2 is finally possible! It took a while to implement this feature, but now it is just a piece of cake to enable debugging and start using the debugger.
Debugging in the Arduino IDE 2
Since its first beta release in February 2021, Arduino IDE 2 has offered debugging features, but only for a limited range of Arduino boards. And there was no way to configure the system to interact with other boards. Meanwhile, this has changed, and after a bit of trial and error, I was able to come up with a solution that makes it relatively easy for a user to debug a classic AVR chip.
Some effort is necessary for the initial setup. You must prepare a UNO to act as the debugging probe (the hardware interface to the target chip). And you have to enter two additional board manager URLs and install the respective packages:
- Install the dw-link firmware on a UNO, which will be used as a hardware debugger probe.
- Set up the hardware to connect the UNO to the target board, i.e., build a special ICSP cable, assemble an Arduino shield, or prepare yourself to mess around with jumper wires on a bread board.
- Enter two
additional boards manager URLs
(in thePreference
dialog), one for the ATTinyCore and one for the MiniCore: - Install the debug-enabled version of the respective cores, i.e., ATTinyCore and MiniCore, in the
Boards Manager
dialog.
Then, each time you want to run the debugger, the following needs to be done:
- Connect the hardware debugger with the target board using jumper wires on a breadboard, using a modified ICSP cable, or employing a special Arduino shield.
- Connect the host to the harware-debugger using a USB cable.
- Load the sketch you want to debug.
- Choose the board or chip on which the sketch will be executed in the
Tools
menu. - Compile the program by clicking on the
Verify
button (after enablingOptimize for Debugging
in theSketch
menu). - Start debugging by clicking the debug button in the top row.
A quick-start guide provides the details, and the Arduino website has a short tutorial on working with the Arduino IDE 2 debugger. So, I wish you happy debugging!
For those curious about how dw-link has been integrated with the Arduino IDE 2 and other frontends, read on.
Integrating a hardware debugger
When integrating a hardware debugger, the IDE or GUI needs to know at least the following facts:
- Where to find the binary, usually an ELF file, of the program that needs to be debugged. An IDE usually knows that already.
- Where to find the source files. This is necessary so that the GUI or IDE can show the source code. IDEs are usually aware of it.
- Where to find the GDB debugger, in our case avr-gdb.
- Where to find the debug server, a program that communicates with the hardware debugger using USB or a serial line and with the GDB debugger using a TCP/IP port. Such a server is dependent on the hardware debugger.
- The communication connection to the hardware debugger. This could be a serial device, in which case we do not need a debug server. Alternatively, it is a TCP/IP port on the local or on a remote machine.
- A way to know that the debug server is ready to accept connections from GDB. Often, that is given as a regular expression, and the IDE waits for it to appear in the output of the debug server. Sometimes, IDEs just use a timeout.
- Finally, some initial GDB commands.
This is all that is needed in order to get the ball rolling. For ARM processors, one usually has a few other things to specify. However, AVRs are so simple that the above is all that is needed. Let us have a look at three different examples.
Integrating dw-link into the PlatformIO IDE
Using the dw-link debugger in the PlatformIO IDE proved relatively straightforward. You simply have to specify everything in the platformio.ini
configuration file. PlatformIO already knows where to find the source and executable files and brings it own (very dated) version of avr-gdb. So, the relevant part of this file could look like this:
debug_tool = custom
debug_server = dw-server.py ;; <-- needs to be callable (e.g., in /usr/local/bin)
-p 3333
;;debug_port = /dev/cu.usbmodem211101 ;; <-- specify instead of dw-server
debug_init_cmds =
define pio_reset_halt_target
monitor reset
end
define pio_reset_run_target
monitor reset
continue
end
set serial baud 115200 ;; <-- only needed for serial line
target remote $DEBUG_PORT
monitor version
monitor mcu
$LOAD_CMDS
$INIT_BREAK
debug_build_flags =
-Og
-g3
-fno-lto
It states in the first line that the debug_tool
is a custom
tool. In the following line, the debug_server
is specified, which in our case is called dw-server.py
and listens at TCP/IP port 3333. This server is simply a serial line to TCP/IP bridge with the additional capability of being able to discover a dw-link hardware debugger. Instead of the server, we could specify the serial line to which the dw-link debugger is connected using the debug_port
attribute.
After having specified how the hardware debugger is connected to the GDB debug program (here over port 3333), one needs to configure how GDB should be launched, which is described under debug_init_cmds. First, two new GDB commands are defined, pio_reset_halt_target
and pio_reset_run_taget
, which PlatformIO will call when the program is started first and restarted, respectively. Then, the serial baud rate is set to 115200, which is only necessary when using the serial line. After that, a connection to the debug server or hardware debugger is established using the target remote
command. The argument $DEBUG_PORT
will at this point either a serial line or a TCP/IP port.
The following two monitor
commands provide information for the user. Finally, the executable is uploaded to the MCU, and an initial breakpoint is set. After that, the execution is started.
As a final part of the configuration, compiler flags for creating the executable for a debug session are specified. Usually, the compiler would optimize the source code minimizing program space, which will move the generated machine code around. In addition, the usual compiler optimizations will try to do away with variables as much as possible. This implies that the generated code is only loosely related to the source code. Requesting a breakpoint at some point often leads to breaking several lines after the requested point, and local variables might not be accessible. With the compiler option -Og
, the compiler tries to be less intrusive. With -O0
, one would abandon all compiler optimizations, which sometimes might be necessary. However, it blows the code up significantly. The option -g3
attempts to put as much symbolic information about variables, functions, etc., into the executable file as possible . Finally, -fno-lto
requests not to apply link -time optimizations. Again, this will blow up the code, but it may be necessary to debug object-oriented programs.
Integrating dw-link into Gede
Gede is not an IDE but simply a graphical user interface to GDB. In contrast to other GUI frontends, I could make dw-link work with Gede (after some tweaking). When one starts up Gede, the following window is presented.
In contrast to PlatformIO, in the case of Gede, the project directory, the GDB executable, and the ELF file of the program to be debugged must be specified. In the following block, one needs to specify the debugging mode, i.e., launching a program on the host, connecting to a debug server for remote debugging, using a serial port for remote debugging, opening a core dump, or connecting to a running process. For us, only the second and third options are relevant. In both cases, one should check the Download
box, which ensures the code will be loaded into the MCU before debugging starts.
Gede has a standard launch sequence for remote debugging, where an initial breakpoint can be specified, and breakpoints from earlier runs could be re-instantiated. In addition, in the Commands section (see the top of the window), one can add additional initialization commands, which I did not use. When clicking OK, the Gede GUI starts up, which looks as follows.
One final note: One has to use a recent avr-gdb version because Gede relies on the target extended-remote
command, which apparently does not work correctly in older avr-gdb versions (previous to 10.2).
Making dw-link work in the Arduino IDE 2
While the above all sounds straightforward, it needed a bit of trial and error and also some tweaking of the source code. In contrast, my initial trials of getting dw-link to work in the Arduino IDE 2 were utterly unsuccessful. And initially, there was no documentation around to aid in how to interface a new hardware debugger to the IDE.
Deqing Sun was the first one to come up with an Arduino package that allowed the debugging of an Arduino UNO in the Arduino IDE 2. He figured out that Arduino IDE 2 debugging is based on the cortex-debug extension for VSCode and that the debug server should be an OpenOCD server. So, his package provided two executables called arm-none-eabi-gdb and dwdebug. The former is a renamed avr-gdb, and the latter is a slightly hacked version of dwire-debug acting as an OpenOCD server. In addition, there were a few modifications in the platform.txt
file:
debug.executable={build.path}/{build.project_name}.elf
debug.toolchain=gcc
debug.toolchain.path={runtime.tools.DWDebugTools.path}/linux
debug.toolchain.path.windows={runtime.tools.DWDebugTools.path}/win
debug.toolchain.path.macosx={runtime.tools.DWDebugTools.path}/macosx
debug.server=openocd
debug.server.openocd.path={debug.toolchain.path}/dwdebug
#doesn't matter
debug.server.openocd.scripts_dir={debug.toolchain.path}/dwdebug
#doesn't matter
debug.server.openocd.script={debug.toolchain.path}/dwdebug
Of course, I tried it out, but it did not work for me. The main thing I missed was that the hacked version of dwire-debug prints a line that signals to the Arduino IDE 2 the start of the OpenOCD server, so that the IDE can fire up the GDB debugger. This was the following line:
Info : Listening on port 50000 for gdb connection
When I retried to interface dw-link to the Arduino IDE 2 this time, I discovered it and could get everything working. Even better, I noticed that meanwhile there exists documentation about how to specify the debug interface. And with that, the platform.txt
additions in the ATTinyCore and MiniCore packages look now as follows.
# optimization flags for debugging
compiler.optimization_flags=-Os
compiler.optimization_flags.release=-Os
compiler.optimization_flags.debug=-Og -g3
...
# Debugger configuration (general options)
# ----------------------------------------
debug.executable={build.path}/{build.project_name}.elf
debug.toolchain=gcc
debug.toolchain.path={runtime.tools.dw-link-tools.path}
debug.server=openocd
debug.server.openocd.path={debug.toolchain.path}/dw-server
#doesn't matter, but should be specified so that cortex-debug is happy
debug.server.openocd.script=doesnotmatter
debug.cortex-debug.custom.gdbPath={debug.toolchain.path}/bin/avr-gdb
debug.cortex-debug.custom.objdumpPath={runtime.tools.avr-gcc.path}/avr-objdump
debug.cortex-debug.custom.serverArgs.0=-s
debug.cortex-debug.custom.serverArgs.1=noop
debug.cortex-debug.custom.serverArgs.2=-p
debug.cortex-debug.custom.serverArgs.3=50000
debug.cortex-debug.custom.postLaunchCommands.0=monitor mcu {build.mcu}
debug.cortex-debug.custom.postLaunchCommands.1=tbreak setup
debug.cortex-debug.custom.preRestartCommands.0=tbreak setup
The first block of three lines handles the compiler options to make the compiler optimizations more debug-friendly (as described above). This can be triggered by choosing the entry Optimize for Debugging
in the Sketch menu.
The debug.executable
attribute provides the absolute path of where to find the ELF file of the sketch we want to debug. If this attribute is not specified, then the debug button in the IDE is grayed out. The attribute debug.toolchain
always has to have the value gcc
. And the debug.toolchain.path
provides the path where to find the tools, which in our case are the programs dw-server and avr-gdb.
Then, as above, we pretend to run an OpenOCD server and specify its location. The following line, debug.server.openocd.script=doesnotmatter
, is necessary to make cortex-debug happy, but it does precisely nothing in our case. After that, we specify the paths where to find avr-gdb and avr-objdump. I still do not know the purpose of calling nm and objdump in our context, and everything works without it.
Finally, we add some custom arguments to the cortex-debug extension. The serverArgs
signal to dw-server that no external program should be started and that the TCP/IP port to listen to is 50000. This means that I did not have to hack the original dw-server Python script. I only had to ignore unknown options, provide a port with the -p
option, and block the -s
option by overriding the earlier value with a keyword signalling not to start any program. The call to dw-server issued by Arduino IDE 2 looks now as follows:
/Users/nebel/Library/Arduino15/packages/MiniCore/tools/dw-link-tools/1.3.0/dw-server -c "gdb_port 50000" -c "tcl_port 50001" -c "telnet_port 50002" -s /Users/nebel/GitHub/dw-link/examples/blinkmodes -f "/Applications/Arduino IDE.app/Contents/Resources/app/plugins/cortex-debug/extension/support/openocd-helpers.tcl" -f doesnotmatter -s noop -p 50000
Everything up to -f doesnotmatter
is generated by the cortex-debug extension and ignored by dw-server. Only the last two options are important.
As mentioned above, the cortex-debug extension looks for the output of the debug server, trying to find a match with a specified regular expression in order to decide when the server is ready to accept a TCP/IP connection. While I simply modified the output of dw-sever, one could also adapt the regular expression using the cortex-debug attribute overrideGDBServerStartedRegex
.
The final three lines concern GDB startup and restart commands. The first one of the postLaunchCommands
makes sure that the MCU type chosen in the Arduino IDE is the same as the connected MCU type. This eliminates cases when you debug code produced for one MCU on an MCU that is incompatible with that MCU. The second command sets up a temporary breakpoint at the start of the setup
function. And this temporary breakpoint is also set up when a restart is performed as the final line instructs.
In addition to the modifications in the platform.txt
file, I also provide some modifications to the boards.txt
file to allow for more fine-grained control of the debug optimization option and maintain compatibility with earlier versions.
Finally, the executables have to be provided as tools. I shopped around and found reasonably dated versions of avr-gdb for Windows, 64-Bit Mac, and AARCH64/X86_64 Linux. Since I also have access to such machines, I was able to create self-contained binaries of the dw-server Python script using Pyinstaller. However, I was unsuccessful in creating 32-bit ARM or i686 Linux versions. However, interestingly, you cannot install the Arduino IDE 2 for 32-bit architectures. So, it is probably not a big loss.
As a final note: After I had installed everything and run a test, it turned out that the Windows Defender believed that the compiled Python script was a virus and deleted it. After using a different form of compilation (generating a whole directory instead of one binary), everything worked out, however.
Summary
The hardware debugger dw-link can now be used with the PlatformIO IDE, with Gede, and, finally, with the Arduino IDE 2. In addition to making it pleasant for people to use the debugger, I also hope to have provided some insights for others on how to integrate a hardware debugger into one of these IDEs or GUIs.
Leave a Reply