Using GDB with Nordic devices

GNU Debugger archer fish logoThe GNU Debugger (GDB) is a popular tool that provides familiar features like breakpoints along with a command syntax that allows creativity in the debugging process. Although its command line interface can be intimidating to some, tab completion makes many operations effortless. Foregoing a GUI also allows for a consistent user experience across multiple operating systems with minimal setup or configuration. In fact, GDB is supported on most platforms and can be used with almost any programming language so many engineers already have at least a little experience with it. This post will show you how to get started using GDB with Nordic devices regardless of your experience level.

Supported hardware
Nordic has included J-Link debug probe functionality on most of its development kits for years and Segger has excellent GDB support so this post will assume that you are using a J-Link. If you are working with custom hardware then a standalone J-Link is required. Note that many Nordic development kits include a "Debug out" port that allow programming and debugging external boards. It is possible to use other brands of debug probes but that would require modifying some of the steps presented here -- the main concepts remain the same.

The Arm Cortex-M series is supported by GDB so any contemporary Nordic device can be targeted. Version 6.40b of the J-Link drivers list support for the following ICs:

  • nRF51422_xxAA, nRF51422_xxAB, nRF51422_xxAC
  • nRF51801_xxAB
  • nRF51802_xxAA
  • nRF51822_xxAA, nRF51822_xxAB, nRF51822_xxAC
  • nRF52810_xxAA
  • nRF52832_xxAA, nRF52832_xxAB
  • nRF52840_xxAA
  • nRF9160

The list of devices that are supported by the J-Link drivers on your machine can be generated by using the "expDevList" command from inside J-Link Commander.


Installing GDB
For embedded development GDB consists of two discrete parts: a server that knows how to talk to your brand of debug probe and a client that is familiar with the processor architecture of your embedded device. These two pieces are usually run concurrently on the same PC and communicate with each other via a TCP socket.

Segger provides a server as part of the "J-Link Software and Documentation" download. Actually, two versions of the server are installed: JLinkGDBServer and JLinkGDBServerCL. The difference between the two executables is that the JLinkGDBServer has a GUI and runs in its own window whereas JLinkGDBServerCL uses a command line. Both executables accept the same arguments when you start them from a command line so for the purpose of this post they are interchangeable. Note that on Windows machines the executables have a ".exe" suffix but on Linux/MacOS the suffix is capitalized and appended without a dot: JLinkGDBServer is actually "JLinkGDBServer.exe" or "JLinkGDBServerExe" depending on your platform. The default installation directory appears to be "C:/Program Files (x86)/SEGGER/JLink_VXXX" on Windows (where XXX is the version of the J-Link drivers) and "/opt/SEGGER/JLink/" on Linux.

The client is provided as part of the standalone GCC toolchain for Cortex-M. With SDK15.2 the recommended version of this toolchain is "7 2017-q4-major". The executable itself is named "arm-none-eabi-gdb" (with an ".exe" suffix on Windows).

Launching the server
The server should be launched first so it has time to connect to your debug probe and listen for incoming TCP connections. If you pass the required arguments on the command line when you launch it then no further interaction is necessary. All of the supported Nordic devices use Arm's Serial Wire Debug interface so -if is used to specify the SWD interface. Port 2331 is used by default but this is configurable so it's a good idea to specify this with -port. Most importantly, -device specifies the target. With the nRF52-DK on Windows launching the server from a command shell looks like this:

> C:\Users\Daniel>JLinkGDBServerCL.exe -device nrf52832_xxaa -if swd -port 2331
SEGGER J-Link GDB Server V6.40 Command Line Version
…
J-Link is connected.
…
Target voltage: 3.30 V
Listening on TCP/IP port 2331
…
Waiting for GDB connection...


If no problems occur then you'll see some status information followed by "Waiting for GDB connection...". Note that you will need to use the absolute path to the executable if it's not in your path.

Launching the client
If your project uses a SoftDevice then you must ensure that it has been programmed onto your device before starting the GDB client. One way to accomplish this is to use the nrfjprog utility from the command line:
 

nrfjprog --program path_to_SD_hex_file --sectorerase

Doing this after the GDB server has been started should not cause any problems.

Next you'll need to locate the debug version of the ELF file that your toolchain produces when you build your project. With the Segger Embedded Studio projects inside the Nordic SDK its location is usually predictable: "{$PROJECT}\{$BOARD}\{$SOFTDEVICE}\ses\Output\Debug\Exe\{$PROJECT}_{$BOARD}_{$SOFTDEVICE}.elf". The GDB client will need to open this file so for the sake of convenience it might be easiest to copy it to a convenient location like your Desktop.

Now it's time to launch the GDB client itself from a shell. On a Linux machine that might look like this:

$ /usr/local/gcc-arm-none-eabi-6-2017-q2-update/bin/arm-none-eabi-gdb
GNU gdb (GNU Tools for ARM Embedded Processors 6-2017-q2-update) 7.12.1.20170417-git
...
This GDB was configured as "--host=x86_64-linux-gnu --target=arm-none-eabi".
...
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb) 

Which should leave you with a blinking cursor sitting at the GDB command prompt. A good first step is to have GDB load your ELF file so it can parse the symbols (e.g. file names, function names, variables) from your project. This is done via the file command:
(gdb) file path_to_ELF_file

GDB is good at tab completion so entering file paths can be expedited by tapping the TAB key. There's not much else to do without connecting the GDB client to the server so do that next:
(gdb) target remote localhost:2331

If a different port was specified when you launched the server then make sure to substitute 2331 for the actual port.

The server is responsible for downloading your project to the device. This functionality can be enabled by sending a few mon ("monitor") commands to the server.
(gdb) mon speed 10000
Target interface speed set to 1000 kHz

(gdb) mon flash download=1
Flash download enabled

Then the load command is used to program the contents of the ELF file to the device:
(gdb) load
Loading section .vectors, size 0x200 lma 0x26000
...
Start address 0x26378, load size 29380
Transfer rate: 5738 KB/sec, 2260 bytes/write.
(gdb)

At this point you should be back at the GDB prompt and are ready to set a breakpoint. Many developers want to start with a breakpoint at the top of main:
(gdb) break main
Breakpoint 1 at 0x2ef28: file
C:\Nordic_Semiconductor\nRF5_SDK_15.2.0_9412b96\examples\ble_peripheral\ble_app_uart\main.c, line 700.

Breakpoints can be created by specifying a function name, line number, or a "file name:line number" combination. GDB assigns an index to breakpoints when they are created that can be used to identify them later. Asking the server to perform a soft reset will ensure that execution starts from a known location and following that with the continue command should lead to the processor running until the breakpoint is reached:
(gdb) mon reset 0
Resets core & peripherals via SYSRESETREQ & VECTRESET bit.
 
(gdb) c
Continuing.
 
Breakpoint 1, main ()
  at C:\Nordic_Semiconductor\nRF5_SDK_15.2.0_9412b96\examples\ble_peripheral\ble_app_uart\main.c:700
700         uart_init();
(gdb)

Here GDB has reached the breakpoint at line 700 of the file main.c and is waiting at the command prompt.

There are shortcuts to make this process easier. When starting the client it's common to specify the path to the ELF file as a command line argument instead of using the file command. Furthermore, any commands that you want to execute at startup -- including all of the ones mentioned above that lead to hitting the breakpoint in main -- can be saved as a text file that GDB will execute automatically. Simply create a text file with the following contents and give it a name like "gdb_cmds.txt":
target remote localhost:2331
mon speed 10000
mon flash download=1
load
break main
mon reset 0
continue

Then invoke the client like this:
$ /usr/local/gcc-arm-none-eabi-6-2017-q2-update/bin/arm-none-eabi-gdb path_to_elf_file -x gdb_cmds.txt

This could be automated using a shell script or batch file but the best way might be integrating everything above into a Makefile like this one so the whole thing can be kicked off with a simple "make gdb".

Learning the ropes
You can always use the help command from the GDB prompt to get information about supported commands. Of course, the best way to learn them is to read the documentation. But the fastest way to get started is to look at a good "GDB cheatsheet" like this one.