Debugging ESP8266 with GDB and VSCode + Platformio

VSCode with the Platformio IDE plugin makes for an excellent platform for developing ESP8266 code, especially using the Arduino Framework for ESP8266 but the Platformio debugging features do not work. It is, however, possible to use GDB together with VSCode and Plaformio to be able to single-step through code, inspecting flow of control and variable values as you go, in an experience that is comparable to VisualGDB but costs nothing.

There are some limitations, “gotchas”, etc, but once these are understood it really does work!

Setting Up Debug Compilation

The program must be compiled differently for debugging to work because additional information must be included, along with the GDBStub (see below). Once debugging is over, however, the firmware uploaded to the ESP8266 should not have this overhead.

Allowing for both options to be compiled and uploaded is easy in Platformio if you use platformio.ini environments. For example:

[env]
platform = espressif8266
framework = arduino
upload_port = COM15
upload_speed = 115200
monitor_port = ${env.upload_port}
monitor_speed = ${env.upload_speed}

[env:d1_mini_pro]
board = d1_mini_pro

[env:debug_d1_mini_pro]
board = d1_mini_pro
build_flags = -Og -ggdb3 -g3 -D WITH_GDB
Note that I have created two targets – “d1_mini_pro” and “debug_d1_mini_pro” – with the latter defining some non-default build_flags. The “-g” flags make the compiler include debug information, while -Og prevents the compiler optimising code (which breaks the nice relationship between lines of source code and compiled MCU code). I have also included “-D WITH_GDB”; the -D directive is equivalent to a “#define” in the source code and means we can use “#ifdef” to selectively include chunks of code depending on whether we’re using the debug_d1_mini_pro target or not.
Remember to recompile and upload before debugging any changes you make.

Adding GDBStub to Your Code

The GDBStub is code which executes on the ESP8266 to communicate with GDB running on your PC via the serial port. Serial.print() will still work so long as you avoid using a rapidly-repeating loop() which contains little more then Serial.print().

The Arduino Framework already contains the GDBStub code and their documentation explains the basics, although in the context of using the Arduino IDE.

Following on from the use of “WITH_GDB” in the previous section, add the following chunks of code.

At the top:

#ifdef WITH_GDB
#include <GDBStub.h>
#endif
At the start of setup():
  Serial.begin(115200);  // or whatever baud rate you prefer
  #ifdef WITH_GDB
  gdbstub_init();
  #endif

Command Line GDB

An essential truth is that you must use the correct gdb for the core processor of the ESP8266: xtensa-lx106-elf-gdb.exe. For my Platformio install, this can be found at (“~” denotes my home directory, i.e. C:\Users\Adam): ~/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-gdb.exe

A second essential truth is that GDB needs to be set up correctly before you can actually debug. This is most easily done with a set of commands stored in a file, which I call “gdbcmds” and which should be created in the home folder of the Platformio project (i.e. in the same folder as platformio.ini). My gdbcmds file contains:

# ESP8266 HW limits the number of breakpoints/watchpoints
set remote hardware-breakpoint-limit 1
set remote hardware-watchpoint-limit 1
# Some GDBstub settings
set remote interrupt-on-connect on
set remote kill-packet off
set remote symbol-lookup-packet off
set remote verbose-resume-packet off
# The memory map, so GDB knows where it can install SW breakpoints
mem 0x20000000 0x3fefffff ro cache
mem 0x3ff00000 0x3fffffff rw
mem 0x40000000 0x400fffff ro cache
mem 0x40100000 0x4013ffff rw cache
mem 0x40140000 0x5fffffff ro cache
mem 0x60000000 0x60001fff rw
# Locate the ELF file
file .pio/build/debug_d1_mini_pro/firmware.elf
# Change the following to your serial port and baud
set serial baud 115200
target remote \\.\COM15

The ELF file location follows the pattern .pio/build/{target}/firmware.elf, where “{target}” matches the relevant entry in platformio.ini.

Obviously: change the serial port and baud rate!

Now open a new terminal in VSCode. It should open with the project home as the working directory (this is necessary). You can open a terminal window outside VSCode too (I like ConEMU) but should make sure the working directory is right.

You need to run xtensa-lx106-elf-gdb.exe, passing the commands file by adding “-x gdbcmds”. I tend to use a bash shell, so the command is:

~/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-gdb.exe -x gdbcmds

If you prefer a normal Windows command prompt just replace the “~” with C:\Users\You.

If you get an error like “\\.\COM15: No such file or directory.” then you will have the Serial port open somewhere such as the “Monitor”. Close it.

I suggest you read the Arduino Framework notes for “what next”. The GDB documentation is available online but is rather long and has much which is not applicable. Generally useful commands are: thb (see below), next, step, finish, print, and info locals. Interesting to explore are: disassemble and frame.

When single-stepping (step or next), the execution will sometimes step over several lines of code.

Avoid resetting the device while a GDB session is connected; it gets rather confused!

ESP8266 Limitation – One Breakpoint

This causes things to get ugly if you don’t work with care. The first thing to do is to use temporary breakpoints. Rather than the normal “break” GDB command, use “thb”. This means that once the breakpoint has been reached it is deleted. Otherwise you need to use the “delete” command to remove the breakpoint.

Typical uses are to break on entry to a function. So long as the function name is unique, something like “thb loop” works. Since the compiled code contains source from many files, line numbers must identify the file they relate to, so the command should be something like “thb src/main.cpp:25”.

“Visual” Debugging

This is how to get the debugging experience more like what the Platformio and VisualGDB screenshots suggest.

The launch.json file (within the .vscode folder for the Platformio project) should be edited. Ignore the warning about this being an automatically-generated file and replace the platformio debug entries with the following:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "GDB",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/.pio/build/debug_d1_mini_pro/firmware.elf",
            "args": [],
            "stopAtEntry": false,
            "hardwareBreakpoints": {"require": true, "limit": 1},
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": true,
            "MIMode": "gdb",
            "miDebuggerPath": "${userHome}/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-gdb.exe",
            "miDebuggerServerAddress": "\\\\.\\COM15",
            "setupCommands": [
                {"text": "set remote hardware-breakpoint-limit 1"},
                {"text": "set remote hardware-watchpoint-limit 1"},
                {"text": "set remote interrupt-on-connect on"},
                {"text": "set remote kill-packet off"},
                {"text": "set remote symbol-lookup-packet off"},
                {"text": "set remote verbose-resume-packet off"},
                {"text": "mem 0x20000000 0x3fefffff ro cache"},
                {"text": "mem 0x3ff00000 0x3fffffff rw"},
                {"text": "mem 0x40000000 0x400fffff ro cache"},
                {"text": "mem 0x40100000 0x4013ffff rw cache"},
                {"text": "mem 0x40140000 0x5fffffff ro cache"},
                {"text": "mem 0x60000000 0x60001fff rw"},
                {"text": "set serial baud 115200"}
            ]
        }
    ]
}

Note that: the entry for “program” will need changing to match your debug compilation target; the serial port appears as “miDebuggerServerAddress” and has additional back-slashes compared to the previous configuration.

This uses the “machine interface” to GDB, which you can see by adding “–interface=mi” after the “-x gdbcmds”. There is a Python package which can interpret the gdb/mi responses: pygdbmi.

You should now be able to use the “Run and Debug” feature of VSCode; there should be an entry called “GDB” (using the “Quick Access” Plaformio Debug > Start Debugging also seems to work but I avoid that route as I don’t know what else it might do). I won’t repeat information on using the VSCode debugging interface here; there is plenty on the web! You should be able to see variables and to inspect the call stack while stepping through the code. What I will provide are some caveats and gotcha-avoidance measures.

Remember there is only one breakpoint

Unfortunately, VSCode does not let you set temporary breakpoints. So you must adopt a working practice of setting a breakpoint and then deleting it as soon as you get there (the config line ‘”hardwareBreakpoints”: {“require”: true, “limit”: 1}’  prevents you from adding more than one, but it isn’t that simple! It appears that the VSCode gdb/mi implementation uses breakpoints for “step over” operations, so you really must delete the breakpoint before stepping. If you get horrible red boxes popping up reporting exceptions etc, the cause is likely to be because you have left a breakpoint.

Where is my Serial.print() output?

In spite of the debugger, I still find Serial.print() very useful for reporting sensor readings etc.

These appear in the Debug Console.

Not all variables are locals

Although the VARIABLES panel shows local variables (and changes these as you select different levels in the call stack), global variables will need you to issue the GDB print command. Locate the Debug Console and note that it has a command entry line at the bottom. GDB commands may be entered here, but must be prefixed by “-exec” (e.g. -exec print foo). You can also add these as WATCH entries.

Adding a breakpoint while things are running

Adding a breakpoint while the ESP8266 is running causes it to stop execution where it is, rather than where the breakpoint has been added. Simply use Continue (F5) to make it continue until the breakpoint is met.

Some things don’t work

Although VSCode suggests otherwise, some things are not supported: PERIPHERALS, MEMORY, DISASSEMBLY.

The Debug Console command line entry comes to the rescue, e.g. try “-exec disassemble”.

Debugging Inside Setup

The problem with attempting to debug inside setup() is that the ESP8266 will normally have exited setup() before you connect GDB.

The neatest way of handling this is to add the following code into setup() just before you want to start debugging:

  #ifdef WITH_GDB
  gdb_do_break();
  #endif

When you reset the ESP8266, this triggers entry into the debugger on the ESP8266, which looks like a breakpoint hit, and means that when you connect GDB you find you are stopped inside gdb_do_break(). Send the “finish” command to GDB twice and you’ll be inside setup().

This works nicely with the command line use of GDB but I haven’t been able to find how to make the VSCode debugger play nicely with it. In this case, the quickest option is to use something like delay(3000) instead of gdb_do_break(). This gives you enough time to start the debugger after resetting the device.

Make sure you quit any GDB session before resetting; things get all messed up otherwise.

Setting Variables

GDB permits variable setting through commands such as “set var my_var = 5”.

Unfortunately, this does not always work. Various comments on the web suggest this comes down to where the variable data is to be found when the program is running, because GDBStub cannot access all possible places.

A work-around is to declare variables with the “volatile” keyword.

One thought on “Debugging ESP8266 with GDB and VSCode + Platformio

Leave a Reply

Your email address will not be published.