Here's how to debug a project for the Adafruit Metro M0 Express board using gdb, PlatformIO, and a Segger J-Link debug probe.

To my surprise, I spent quite some time figuring this out, which is why I'm creating this tutorial. I tried using Visual Studio Code (VS Code) for this purpose, but for some reason the debugger was stuck in a weird state. I'm not sure if it was a configuration problem or a PlatformIO/VS Code bug – I opened an issue on the Github project but I didn't get any help. Another thing that I tried was using the Arduino extension in VS Code, but debugging this particular board in the Arduino environment is currently not supported. In the end, I managed to debug the board with gdb and used PlatformIO only for building the project.

My laptop is running on Ubuntu 19.10. Since the Adafruit board doesn't have an on-board debug probe, I used a Segger J-Link EDU Mini debug probe. The debug probe comes with all the cables you need for the Adafruit board.

0. Prerequisites

  • Install the PlatformIO Core: check the instructions here.

I personally use Anaconda to manage my Python environments. You can create an environment file pio_env.yml:

dependencies:
- python
- pip:
  - platformio

And then create a Conda environment from it:

$ conda env create -n pio -f pio_env.yml

To activate the Conda environment:

$ conda activate pio

In practice, I'll probably just add PlatformIO to one of my already-existing Conda environments.

  • Download the Segger J-Link software from here. This package installed the J-Link tools in /opt/SEGGER/JLink_V670e for me, but it might be a different directory for someone else. I'll call this directory from now on $JLINK_DIR. The tools that we'll use from here are JLinkExe and JLinkGDBServerExe.

Make sure that you have the latest firmware on your debug probe. To install it, connect the debug probe alone to the computer and run:

./JLinkExe

This will install the latest firmware version on your board.

  • Install gdb-multiarch (from what I understood, the regular gdb might not work with this board's architecture):
$ sudo apt-get install gdb-multiarch

1. Create a project

First, let's create a simple project which blinks a LED (the "Hello world" of embedded programming). The root directory of the project is named adafruit_blink and has the following structure:

  • src directory:
    • Blink.cpp file with the following content:
/*
 * Blink
 * Turns on an LED on for one second,
 * then off for one second, repeatedly.
 */

#include <Arduino.h>

void setup()
{
  // initialize LED digital pin as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop()
{
  // turn the LED on (HIGH is the voltage level)
  digitalWrite(LED_BUILTIN, HIGH);
  // wait for a second
  delay(1000);
  // turn the LED off by making the voltage LOW
  digitalWrite(LED_BUILTIN, LOW);
   // wait for a second
  delay(1000);
}
  • platformio.ini file (in the root directory) with the following content:
[platformio]
default_envs = adafruit_metro_m0

[env:adafruit_metro_m0]
platform = atmelsam
board = adafruit_metro_m0
framework = arduino
build_type = debug
debug_tool = jlink
upload_protocol = jlink

You can probably also download it from somewhere but it's so simple that I would just go on and create it.

2. Build the project

Go to /path/to/adafruit_blink. If you installed PlatformIO in a Python environment, activate it now (if you installed it in the system, skip this step). If you use the Conda environment that we previously created:

$ conda activate pio

Build the project:

$ pio run -e adafruit_metro_m0

This step creates inside adafruit_blink the directories .pio/build/adafruit_metro_m0 inside which you'll find the files firmware.bin and firmware.elf that we'll further need.

3. Upload the project

Connect the J-Link debug probe to the Adafruit board and each of them to the PC like in the image below. The debug probe will not power the Adafruit board, this is why you need to also connect the board to a power supply.

Connecting the Adafruit board to the J-Link Edu Mini probe and (each of them) to the PC.

Go to $JLINK_DIR and start the J-Link Commander:

$ ./JLinkExe

You will be prompted to enter some commands, in the following order (below you'll find the entire terminal dialogue): connect (connects to the debug probe), ATSAMD21G18 (model of the Adafruit board, which is the target device), S (select SWD interface), 1000 (interface speed), loadbin /path/to/adafruit_blink/.pio/build/adafruit_metro_m0/firmware.bin 0x2000 (load the binary).

The entire sequence should look like this:

$ JLinkExe 
SEGGER J-Link Commander V6.70e (Compiled Apr 17 2020 17:52:55)
DLL version V6.70e, compiled Apr 17 2020 17:52:44

Connecting to J-Link via USB...O.K.
Firmware: J-Link EDU Mini V1 compiled Apr 16 2020 17:23:57
Hardware version: V1.00
S/N: 801018724
License(s): FlashBP, GDB
VTref=3.295V


Type "connect" to establish a target connection, '?' for help
J-Link>connect
Please specify device / core. <Default>: CORTEX-M0
Type '?' for selection dialog
Device>ATSAMD21G18
Please specify target interface:
  J) JTAG (Default)
  S) SWD
  T) cJTAG
TIF>S
Specify target interface speed [kHz]. <Default>: 4000 kHz
Speed>1000
Device "ATSAMD21G18" selected.


Connecting to target via SWD
InitTarget() start
InitTarget()
InitTarget() end
Found SW-DP with ID 0x0BC11477
DPIDR: 0x0BC11477
Scanning AP map to find all available APs
AP[1]: Stopped AP scan as end of AP map has been reached
AP[0]: AHB-AP (IDR: 0x04770031)
Iterating through AP map to find AHB-AP to use
AP[0]: Core found
AP[0]: AHB-AP ROM base: 0x41003000
CPUID register: 0x410CC601. Implementer code: 0x41 (ARM)
Found Cortex-M0 r0p1, Little endian.
FPUnit: 4 code (BP) slots and 0 literal slots
CoreSight components:
ROMTbl[0] @ 41003000
ROMTbl[0][0]: E00FF000, CID: B105100D, PID: 000BB4C0 ROM Table
ROMTbl[1] @ E00FF000
ROMTbl[1][0]: E000E000, CID: B105E00D, PID: 000BB008 SCS
ROMTbl[1][1]: E0001000, CID: B105E00D, PID: 000BB00A DWT
ROMTbl[1][2]: E0002000, CID: B105E00D, PID: 000BB00B FPB
ROMTbl[0][1]: 41006000, CID: B105900D, PID: 001BB932 MTB-M0+
Cortex-M0 identified.
J-Link>loadbin /path/to/adafruit_blink/.pio/build/adafruit_metro_m0/firmware.bin 0x2000
Halting CPU for downloading file.
Downloading file [/path/to/adafruit_blink/.pio/build/adafruit_metro_m0/firmware.bin]...
Comparing flash   [100%] Done.
Erasing flash     [100%] Done.
Programming flash [100%] Done.
Verifying flash   [100%] Done.
J-Link: Flash download: Bank 0 @ 0x00000000: 1 range affected (51712 bytes)
J-Link: Flash download: Total time needed: 0.986s (Prepare: 0.058s, Compare: 0.053s, Erase: 0.043s, Program: 0.819s, Verify: 0.001s, Restore: 0.009s)
O.K.
J-Link>

You can exit the program (Ctrl-C).

One tip: I've noticed that sometimes, even if I make changes in the code, rebuild the firmware, and try to upload it, the J-Link Commander skips the uploading because it says that the content already matches the binary:

J-Link: Flash download: Bank 0 @ 0x00000000: Skipped. Contents already match

Depending on your settings, this might happen because the old CRC already matches the new CRC. To completely bypass this check, following this recommendation, enter the follwing commands while you are in the J-Link Commander:

exec SetSkipProgOnCRCMatch=0

and

exec SetVerifyDownload=0

4. Debug the project

In your home directory, create the ~/.gdbinit file with the following content:

target remote localhost:2331                                        
monitor device Cortex-M0                                                
monitor speed auto                                                          
file /path/to/adafruit_blink/.pio/build/adafruit_metro_m0/firmware.elf
load                                                                        
monitor reset   

In the $JLINK_DIR directory, start the gdb server with the following configuration:

$ ./JLinkGDBServer -if SWD -device ATSAMD21G18 -log /home/user/logdbg -speed 1000

While this is running, in another terminal tab, start gdb-multiarch:

$ gdb-multiarch
GNU gdb (Ubuntu 8.3-0ubuntu1) 8.3
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word".

warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x0000212c in ?? ()
Selecting device: Cortex-M0
Select auto target interface speed (2000 kHz)
Loading section .text, size 0x2efc lma 0x2000
Loading section .ramfunc, size 0x5c lma 0x4efc
Loading section .data, size 0x100 lma 0x4f58
Start address 0x2144, load size 12376
Transfer rate: 128 KB/sec, 4125 bytes/write.
Resetting target

And you should be good to debug! Here's a quick guide to gdb debugging if you're new to it.

To check that everythong works, you can set a breakpoint at the first line of the loop function in Blink.cpp and continue:

(gdb) b Blink.cpp:18
Breakpoint 1 at 0x210a: file src/Blink.cpp, line 18.
(gdb) c
Continuing.

Breakpoint 1, loop () at src/Blink.cpp:18
18        digitalWrite(LED_BUILTIN, HIGH);

Once you are here, you can step over the functions (n) and verify that the LED is indeed blinking. Happy debugging!