Something fast is coming on July 30th.   Learn more

5 steps to set up and use a debugger with the Particle Photon

To see what’s going on inside your program while it’s running you need to bring out the debugger (also known as the Programmer Shield). Here’s what I did to set up and use the debugger on my project.

Julien Vanier article author avatarJulien VanierOctober 12, 2018
5 steps to set up and use a debugger with the Particle Photon

So you are developing on the excellent Photon from Particle and you’ve run into a dead end: your code crashes and print statements don’t help you solve the issue. To see what’s going on inside your program while it’s running you need to bring out the debugger (also known as the Programmer Shield). Here’s what I did to set up and use the debugger on my project.

NOTE: I use Linux. If you use Mac OSX or Windows and these instructions don’t work, post on the Particle community forum and I’ll update this article. See this follow-up article if you use Windows.

Editor’s note: this post was originally published on Medium by Particle community member Julien Vanier. The content remains accurate, and it is published here with permission of the author who has since become our Director of Engineering at Particle.

1. Buy the hardware

You need special hardware to debug the Photon. It’s for sale as the Programmer Shield on the Particle store.

2. Install the software

You first need to be able to compile the Particle firmware locally, so clone the firmware repository on GitHub and follow the Getting Started guide there.

Next install OpenOCD (a free and open On-Chip Debugger). It will be a bridge between the Photon and GDB, the GNU debugger.

  • Download the latest OpenOCD source code.
  • Download the Particle debugger config file particle-ftdi.cfg and copy it to tcl/interface/ftdi/ in the OpenOCD source code folder.
  • On Linux, install the libusb library
    sudo apt-get install libusb-1.0.0-dev
  • Open a terminal to the OpenOCD source folder and run
    ./configure --enable-ftdi
    make install
    (NOTE: on Linux you need sudo make install)
  • On Linux, copy UDEV rules files to avoid having to run openocd with sudo.
    sudo cp contrib/99-openocd.rules /etc/udev/rules.d/
  • On Mac OSX, you need to make changes to the USB programmer driver bundled with OSX. See the Programmer Shield repository on GitHub for more information.

3. Compile the firmware

Don’t use pins D3 to D7Since JTAG uses the pins D3 to D7, your firmware must not use these pins, including the blue user LED (D7) otherwise OpenOCD will not connect. (NOTE: There is an alternate debug mode called SWD that uses only D6 and D7.)

In order to enable the debugger, you must compile the firmware with USE_SWD_JTAG=y.

In the firmware folder, check out the develop branch if you want to debug with the latest and greatest features.

git checkout develop

Connect the USB cable to the Photon (not to the Programmer Shield), put the Photon in DFU mode, compile, and flash the firmware.

cd main
make clean all program-dfu PARTICLE_DEVELOP=1 PLATFORM=photon USE_SWD_JTAG=y MODULAR=n

Why use MODULAR=n? With this option, only one big executable will be created instead of 2 system parts and 1 user part. It’s a lot easier to debug. Just remember to reflash your device with the modular firmware after you are done debugging.

4. Start the debugger

Connect the USB cable to the Programmer Shield.

PRO TIP: you can connect both the device and the Programmer Shield to your computer at the same time to avoid unplugging cables all the time!
Start OpenOCD for the target. The command below will listen for GDB connections on port 3333. The last bit is to help with threads (type info threads in GDB).

openocd -f interface/ftdi/particle-ftdi.cfg -f target/stm32f2x.cfg -c "gdb_port 3333" -c "\$_TARGETNAME configure -rtos FreeRTOS"

Successfully connected to the Photon through the Programmer Shield

If OpenOCD can’t connect, make sure again that your code doesn’t use pin D7, the blue LED.

You can send OpenOCD commands by running telnet localhost 4444. A good command to try is reset. Try help or search online for more commands.

Reset the target using OpenOCD through telnet

Start GDB (GNU Project debugger) with an ELF file to load the symbols (mapping of memory addresses to names)

arm-none-eabi-gdb -ex "target remote localhost:3333" ../build/target/main/platform-6-m/main.elf

GDB ready to debug!

5. Using the debugger

Now comes the hard part: figuring out why your program crashes.

A lot has been written about debugging with GDB, so here’s my little summary.

  • Set breakpoints in your code with break
  • List breakpoints with info breakpoints and remove them with delete
  • Resume execution with c (continue) and interrupt execution with Ctrl-C (this can be very useful to find an infinite loop).
  • Go the next line with s (step) or n (next). Next skips over function calls. Run until the current function returns with fin (finish).
  • Show the value of a variable with p and all locals with info locals.
  • Print a stack trace with bt
  • Send OpenOCD commands using monitor
  • Reset the Photon and pause immediately at start with monitor reset halt. You can then set breakpoints and start the program with continue.

I also find it useful to use the GDB split screen mode using layout split since it shows the source code, the assembly and the command line at the same time. Change which window gets arrow keys with Ctrl-X O.

Break at setup and get ready to trace the program


It’s possible to flash through the Programmer Shield. In fact, it’s the only way to flash the bootloader. However since the goal of this article was to give a step-by-step goal to be able to trace through a program I didn’t focus on flashing with the Programmer Shield. One important note is that to flash with the Programmer Shield the JTAG debugging protocol must be enabled in the microcontroller. JTAG is enabled when the firmware is compiled with USE_SWD_JTAG=y or when the device is in DFU mode.


In order to debug a problem that only happens in the modular firmware, you can either load user-part.elf, system-part1.elf, or system-part2.elf. What if you need to load all 3 in the debugger?

Here’s a method to be able to debug all parts of the modular firmware together.

Load all symbols in the same GDB session:

  • Extract the address of each module .text section.
  • Load each ELF into GDB with add-symbol-file, passing the address above (for some reason GDB doesn’t use the address in the ELF file as a default and forces you to find that address on your own).

Here’s my script to load GDB with this configuration.



function elf {
  echo $COMMON_BUILD/target/$1/platform-$PLATFORM_ID-m$LTO/$1.elf

SYSTEM_PART1_ELF=$(elf system-part1)
SYSTEM_PART2_ELF=$(elf system-part2)
USER_PART_ELF=$(elf user-part)

function text_section_address {
  $READELF $1 --headers | grep .text | head -n 1 | sed "s/.*PROGBITS *\\([^ ]*\\).*/0x\\1/"

SYSTEM_PART1_ADDRESS=$(text_section_address $SYSTEM_PART1_ELF)
SYSTEM_PART2_ADDRESS=$(text_section_address $SYSTEM_PART2_ELF)
USER_PART_ADDRESS=$(text_section_address $USER_PART_ELF)

$GDB \
  -ex "target remote localhost:3333" \
  -ex "set confirm off" \
  -ex "add-symbol-file $SYSTEM_PART1_ELF $SYSTEM_PART1_ADDRESS" \
  -ex "add-symbol-file $SYSTEM_PART2_ELF $SYSTEM_PART2_ADDRESS" \
  -ex "add-symbol-file $USER_PART_ELF $USER_PART_ADDRESS" \
  -ex "set confirm on"

Set breakpoints:

When setting breakpoints, multiple symbols will often match because of the dynalib structure. Set a breakpoint as normal then disable the ones you don’t want.

For example, break setup

(gdb) break setup
Breakpoint 1 at 0x806ed2c: setup. (3 locations)

Find out the one you need with info break

(gdb) info break
Num     Type           Disp Enb Address    What
1       breakpoint     keep y    
1.1                         y     0x0806ed2c in system_part2_init at src/module_system_part2.c:96
1.2                         y     0x0806ed2d <setup+4294967295>
1.3                         y     0x080a02f0 in setup() at src/application.cpp:40

Disable all except the one in application.cpp

(gdb) disable 1.1 1.2


(gdb) continue
Program received signal SIGINT, Interrupt.
setup () at src/application.cpp:40
40	{
(gdb) list
38	/* This function is called once at start up ----------------------------------*/