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
librarysudo 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"
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.
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
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.
Notes
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.
Bonus
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.
#!/bin/bash READELF=arm-none-eabi-readelf GDB=arm-none-eabi-gdb COMMON_BUILD=~/Programming/Photon/build PLATFORM_ID=6 LTO= 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
Go!
(gdb) continue Continuing. Program received signal SIGINT, Interrupt. setup () at src/application.cpp:40 40 { (gdb) list 35 36 SYSTEM_MODE(AUTOMATIC); 37 38 /* This function is called once at start up ----------------------------------*/