Debugging Arduino in NeoVim using PlatformIO
Table of Contents
So you’ve probably tried debugging in something like Visual Studio Code.
I did and I do miss the ability to debug my Arduino code in NeoVim as I could with PlatformIO and VSCode.
Well I finally figured out how to properly setup nvim-dap
so that it connects to my Arduino every single time and everything,
including breakpoints, works without any problems.
The setup isn’t as plug and play as with VSCode but it’s still quite simple
and should take only few minutes if you have things like PlatformIO and nvim-dap
already setup.
What you need to have already set up⌗
PlatformIO Core (CLI)⌗
You’ll need to setup PlatformIO so that we can use the bundled GDB.
Remember the install location as it will be needed later
(you can just let it install itself to $HOME/.platformio/
).
avr-stub
Arduino library⌗
This is the library we need to debug our Arduino. It doesn’t need any special hardware and can debug (to my knowledge) all the basic Arduino models.
From the PlatformIO documentation:
avr-stub
is a source level debugger based on GDB stub mechanism. It works with ATmega328 and Arduino Mega microcontrollers without an external programmer.
nvim-dap
⌗
You already need to have nvim-dap
setup. You also need to have a way to load the
launch.json
file as nvim-dap
doesn’t do that automatically. I use this in my
config
(it’s Astronvim so modify it for your config)
(Optional) overseer
⌗
If you want to make useof launch.json
(for eg. automatically uploading the code
to your Arduino if you start debugging) you’ll need to set this up. It’s not
necessary but it’ll make your life a little bit easier.
(Recommended) nvim-dap-ui
⌗
What use is a debugger in NeoVim without a proper UI? This sets up UI quite simmilar to VSCode so it will be familiar to most people.
(Recommended) vim-pio
⌗
This will let you easily setup and manage (not fully) your project.
It generates all the needed files (through PlatformIO Core) as well
as the files needed for code completion (like compile_commands.json
).
It also creates a Makefile
that I make use of in my tasks.json
.
(Recommended) Optiboot⌗
Having Optiboot on your Arduino (Uno) will allow you to use some functions of
the avr-stub
debugging library that allow some extra debugging features (regarding breakpoints).
It also allows for larger projects to be uploaded as it’s smaller than the default bootloader.
The PlatformIO project setup⌗
Makefile⌗
I use the Makefile generated by vim-pio
:
# CREATED BY VIM-PIO
all:
platformio -f -c vim run
upload:
platformio -f -c vim run --target upload
clean:
platformio -f -c vim run --target clean
program:
platformio -f -c vim run --target program
uploadfs:
platformio -f -c vim run --target uploadfs
It’s very simple but it makes the basic command quicker to use and you don’t have to remember the (sometimes) weir syntax.
Setting up the arv-stub
library⌗
To set this library up we need to first add it to our platformio.ini
:
[env:uno]
platform = atmelavr
board = uno
framework = arduino
lib_deps = jdolinay/avr-debugger@^1.5.0
Then in our code (main.cpp
) we need to include the library and initialize it:
#include <Arduino.h>
#include <avr8-stub.h>
void setup() {
// Initialize the debug (GDB stub) library
debug_init();
}
void loop() {
// We can't use `Serial` as it's used by the library
debug_message("Hello World");
delay(1000);
}
Getting the debugger path⌗
Arduino has it’s own debugger that comes with the Atmel AVR toolchain.
If you didn’t change the install path for PlatformIO Core it’s in your
home directory, so the path looks like this (yes you need to have the absolute path):
/home/<user>/.platformio/packages/toolchain-atmelavr/bin/avr-gdb
.
Getting the board architecture⌗
I can save you some trouble for Arduino Uno - it’s avr:5
If you want to be sure or you use something other than an Uno you’ll need to do these steps:
- Compile and upload the code above so that the
avr-stub
library is uploaded to the board and is activated - Start GDB yourself:
/home/<user>/.platformio/packages/toolchain-atmelavr/bin/avr-gdb -b 115200
- Connect to the board:
target remote /dev/ttyACM0
(changettyACM0
to port for your board - likelyttyUSB0
) - If the connection is successful run
show architecture
and write the output down quit
Why is this needed?⌗
Honestly, I don’t know. If I connect GDB manually it detects the architecture
just fine but if I configure nvim-dap
to launch and connect GDB it desn’t
recognize it and incorrectly assumes that it’s x86_64
. So this way you’ll
get the architecture and we’ll set it manually later.
launch.json
for nvim-dap
⌗
Here’s where the magic happens.
There’s multiple things we need to do here.
The whole file will be at the end of this section.
- First we need to set the debugger path (we found it earlier).
(you have to use absolute path):
"miDebuggerPath": "/home/<user>/.platformio/packages/toolchain-atmelavr/bin/avr-gdb",
- Now we have to find the firmware file.
The location depends on the PlatformIO environment
in use so it should look like this for a simple project for Arduino Uno
(the
platformio.ini
I posted earlier):
"program": "${workspaceFolder}/.pio/build/debug/firmware.elf",
- Getting and setting the Arduino board architecture and port.
We already figured the architeture out and here’s where we use it. There are more arguments so I’ll break them appart so it’s easier to understand.
The whole line looks like this:
-b 115200 -ex 'target remote /dev/ttyACM0' -ex 'set architecture avr:5'
-b 115200
- We don’t need to change this. It’s the baud rate used by the library.
-ex 'target remote /dev/ttyACM0
- This connects GDB to the Arduino. As mentioned before,
change the
ttyACM0
to the port used by your Arduino.
- This connects GDB to the Arduino. As mentioned before,
change the
-ex 'set architecture avr:5'
- This tells GDB the architecture of the board we’re debugging.
If your’s differrent, just change
avr:5
to what you wrote down.
- This tells GDB the architecture of the board we’re debugging.
If your’s differrent, just change
- (Optional) If you have
overseer
you can usetasks.json
to automatically build and upload the code to the board when you start debugging:"preLaunchTask": "upload"
So the whole file looks like this:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug",
"type": "cppdbg",
"request": "launch",
"args": [],
"stopAtEntry": true,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"linux": {
"MIMode": "gdb",
"miDebuggerPath": "/home/jirka/.platformio/packages/toolchain-atmelavr/bin/avr-gdb",
"program": "${workspaceFolder}/.pio/build/debug/firmware.elf",
"miDebuggerArgs": "-ex 'set serial baud 115200' -ex 'target remote /dev/ttyACM0' -ex 'set architecture avr:5'",
"miDebuggerArgs": "-b 115200 -ex 'target remote /dev/ttyACM0' -ex 'set architecture avr:5'"
},
"preLaunchTask": "upload"
}
]
}
tasks.json
for overseer
(Optional)⌗
If you have overseer
set up and you have the Makefile I provided
you can just copy-paste it.
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"group": {
"kind": "build",
"isDefault": true
},
"linux": {
"command": "bash",
"args": [
"-c",
"make"
]
}
},
{
"label": "upload",
"type": "shell",
"group": {
"kind": "build",
"isDefault": true
},
"linux": {
"command": "bash",
"args": [
"-c",
"make upload"
]
}
},
{
"label": "clean",
"type": "shell",
"linux": {
"command": "bash",
"args": [
"-c",
"'make clean'"
]
}
}
]
}
And you’re done!⌗
Now it should be a matter of launching the debugger (F5
in my case) and
everything should start up!