Debugging Arm-based microcontrollers in (Neo)vim with gdb

2020/07/18

Coming from the JavaScript world, I’m used to the amazing debugging capabilities that browsers offer these days. I wanted to find a way to do debugging in a sensible way, covering the basics like setting breakpoints, skipping through them and doing variable inspection at runtime. Ideally I wanted to use vim for that as this is where I do all of my coding! I ended up finding an elegant solution (at least in my opinion) that I would like to share with you.

Heads up! This is a guide targeted at macOS specifically! Build, installation and configuration instructions might differ for other UNIX based systems and especially for windows

Psst: you also might want to check out the blog post I have written on how to set up a (Neo)vim dev environment for C(++) with all bells and whistles: Modern C++ development in (Neo)vim

See it in action!

asciicast

In my examples I will sometimes refer to the STM32 libraries and the STM32F3DISCOVERY board as that’s what I’m using to test all this.

Installing gdb

We will need the GNU Arm Embedded Toolchain which includes the Arm version of gbd (arm-none-eabi-gdb). You can easily install it via homebrew:

brew cask install gcc-arm-embedded`

Note on macOS Catalina: Catalina is blocking the arm-none-eabi-gdb executable by default. Run it once in your console, then open Security & Privacy system settings, then, in the general tab allow gcc-arm-none-eabi to run.

Installing a gdb-server

The gdb-server is the tool that actually connects to your microcontroller programmer via USB. For STM32 MCUs there are two options: stlink (st-util) or OpenOCD. I will focus on OpenOCD here as this seems to be working much better for me and supports way more devices.

I highly recommend building openOCD from source as the homebrew formulas seem to be outdated. This is how I went about it:

brew install autoconf automake texinfo
git clone https://git.code.sf.net/p/openocd/code openocd-code
cd openocd-code
./bootstrap
./configure
make
sudo make install

See also OpenOCD - Open On-Chip Debugger / Code

Connect your board and run

openocd -f board/stm32f4discovery.cfg # replace with your corresponding board / interface

to see if it’s working. If it looks something like this, we’re in business!

Info : STLINK V2J27M15 (API v2) VID:PID 0483:374B
Info : Target voltage: 2.906461
Info : stm32f4x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : starting gdb server for stm32f4x.cpu on 3333
Info : Listening on port 3333 for gdb connections

You can also specify a custom interface and/or mcu target:

openocd -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg

See here for all supported devices and targets.

Configuring your project for gdb

To connect our project to gdb we need to configure a few things within our project directory. I am using a platformIO project as an example but if you know how to build your project in debug mode (-g) this shouldn’t be a problem.

Create an .gdbinit file in the project directory. This will be executed every time the gdb client connects to the server:

file .pio/build/disco_f303vc/firmware.elf # point this to your firmware file compiled with the --debug (platformIO) or -g flag
target remote localhost:3333 # points gdb to our OpenOCD server
load # loads all the debug symbols

To start debugging you’d compile the project with the debug flag, then start the OpenOCD server and keep it running in the background. I created a Makefile entry for this, for convenience:

OPENOCD := /usr/local/bin/openocd

# ...

debug:
	platformio -f -c vim run --target debug && $(OPENOCD) -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg

Using the (Neo)vim debugger

The debugger solution that we are going to use is actually baked into vim (from v8.1) and Neovim. It’s called termdebug. See here for a more in-depth introduction and here for the official manual.

The termdebug plugin is disabled by default, so we have to enable it in the .vimrc / init.vim:

packadd termdebug

We are making further adjustments to point the termdebug plugin to the arm gdb:

" Default is ARM debugging
" In the future, consider using https://github.com/embear/vim-localvimrc for
" project specific settings
let g:termdebugger = "arm-none-eabi-gdb"

Then, if you like, add some lines for a nice split window view and some keymappings:

" C(++) debugging
" See https://neovim.io/doc/user/nvim_terminal_emulator.html
" For a nice split window view
let g:termdebug_popup = 0
let g:termdebug_wide = 163
" Map ESC to exit terminal mode
tnoremap <Esc> <C-\><C-n>
nnoremap <silent> <leader>b :Break<CR>
nnoremap <silent> <leader>bc :Clear<CR>
nnoremap <silent> <leader>c :Continue<CR>

Usage

  1. Run make debug in another console window
  2. Open file to debug, then :Termdebug
  3. Set and remove breakpoints, see evaluated values, manage control flow with commands below

Shortcuts

See the manual for all the commands that are possible with the debugging terminal emulator

Supercharged termdebugger

When in the debugger, normally the window in the bottom left would show the program output. As this is not the case for remote debugging I thought I could try to make better use of that and maybe even show the uart output of the device, if there is any. For that I had to fork the termdebugger plugin and pour it into its own vim-plug compatible one.

To install add

Plug 'chmanie/termdebugx.nvim'

and remove the packadd line.

Then you are able to define the command that will be executed in the program window of the debugger (in my case the platformio device monitor command):

let g:termdebugger_program = "pio device monitor -b 38400" " only works with termdebugx.nvim

That’s it!

I hope I haven’t missed anything. If you’re having problems setting this up, feel free to reach out on one of the channels below.

References

Some links I stumbled upon during gathering the information in no particular order.