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)

Link

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

Link

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

Link

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

Link

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.

Link

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.

Link

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.

Link

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:

  1. Compile and upload the code above so that the avr-stub library is uploaded to the board and is activated
  2. Start GDB yourself: /home/<user>/.platformio/packages/toolchain-atmelavr/bin/avr-gdb -b 115200
  3. Connect to the board: target remote /dev/ttyACM0 (change ttyACM0 to port for your board - likely ttyUSB0)
  4. If the connection is successful run show architecture and write the output down
  5. 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.

  1. 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",
  1. 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",
  1. 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.
  • -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.
  1. (Optional) If you have overseer you can use tasks.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!