VSCode ESP32 debugging

It’s been a long time since I had anything else than an ARM Cortex-M on my table but ever since Espressif came out with its ESP32 two years ago I’ve been very eager to play with it. Well, it looks like the time has finally come because last week at work we decided to look into the ESP32 for its Bluetooth capabilities in order to get some kind of wireless data acquisition for remote debugging going. So I ordered a couple of ESP32-PICO-D4 boards to play around.

My first goal was to get Bluetooth Classic Pairing working. Digging into the matter I discovered three things:

  1. A vast majority of material found online uses Arduino and not Espressifs own ESP-IDF framework. (Just a statement, not an opinion)
  2. ESP-IDF is still not what I would call mature.
  3. Debugging an ESP32 is not quite as easy as debugging an ARM. For some reason people seem to fall back to “printf” debugging but that might be due to the nature (IoT?) of most projects.

Anyhow. I like debugging so lets take a look at how to get a working connection with VSCode. For starters I’d recommend to read the JTAG debugging part in Espressifs documentation. Apparently they have decided to go the OpenOCD way. OpenOCD (short for Open On-Chip Debugger) can interface various hardware debuggers JTAG port. It basically sits between GDB and the debugger lying on your desk. The thing is though… while I really admire all the work that went into OpenOCD I’m not really fond of all the forks out there. Below is a screen of all the hits I get for OpenOCD in the AUR repository and there are still many more forks out there in the wild…

Anyone wanna take a guess what would happen if you’d install all of those?

Usually most of my software runs directly from either the official or the AUR repository but in this case please go ahead and download the binary directly at Espressif.

In case you’ve never used OpenOCD before you should know that the workflow to setup a connection involves two steps. Prior to running GDB you’ve to start the OpenOCD server with the right configuration for your adapter. Since such a configuration can contain a large number of commands they are usually put into .cfg files (the esp32.cfg file for example has 167 lines). It’s also common to pass multiple configuration files to OpenOCD, e.g. one for specifying the interface and one with further details about the used board or chip. Thankfully most prebuilt binaries of OpenOCD are already packed with a lot of different .cfg files you can use or at least get some inspiration from. So to summarize we need to get VSCode to do two things for us:

  1. Start the OpenOCD server with the right configuration
  2. Launch GDB and connect to OpenOCD

VSCode uses what they call tasks to run external tools. We can easily setup such a task to start OpenOCD for us by creating a tasks.json file in the projects .vscode folder. The JSON to do that looks something like this:

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "openocd",
      "type": "shell",
      "isBackground": true,
      "options": {
        "cwd": "/opt/esp/openocd-esp32"
      },
      "command": "bin/openocd -s share/openocd/scripts -f interface/jlink.cfg -f board/esp-wroom-32.cfg",
    }
  ]
}

The lauch command is pretty straight forward. It invokes OpenOCD with the -s flag specifying a script folder (to shorten the upcoming paths) and then passes two configuration files, one which tells it to use my J-Link adapter and another one which says I’m using an ESP32. esp-wroom-32.cfg contains only 3 commands which select JTAG, the adapter speed and load another esp32.cfg file afterwards. Although I’m not using an ESP32-WROOM myself the configuration file still fits my needs (and most likely those of most ESP32 boards out there). If you’re experiencing any kind of connection troubles try to lower the adapter speed. The default of 20kHz might be a little too high if you’re using long cables.

Lets move on to part 2 and create the actual debug configuration. Since Microsoft’s own GDB extension does not support specifying a GDB executable path I’d suggest to use Native-Debug (an extension I’ve already mentioned here) instead. Explicitly specifying which GDB to use allows you to prevent polluting your path with different GDB executables…

My launch.json file looks like this:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "gdb",
      "request": "launch",
      "name": "Launch Program",
      "target": "./build/${workspaceFolderBasename}.elf",
      "cwd": "${workspaceFolder}",
      "gdbpath": "/opt/esp/xtensa-esp32-elf/bin/xtensa-esp32-elf-gdb",
      "autorun": [
        "target remote :3333",
        "mon reset halt",
        "flushregs",
        "thb app_main",
        "c"
      ],
      "preLaunchTask": "openocd"
    }
  ]
}

It specifies a target, the GDB path to the xtensa-esp32 toolchain, a couple of GDB commands I simply copied from the Espressif documentation and the “openocd” task which starts the OpenOCD server.

Launching the debug configuration should now connect you to the target, reset it and run till app_main (which is Espressifs version of main after initialization). Well… almost. Sadly there is one minor drawback. Apparently VSCode has some built in timeout for tasks which to my knowledge can not be adjusted. Running into this timeout results in an ugly error message:

The specified task cannot be tracked

Don’t be too alarmed when this message pops up. The terminal should still tell you that OpenOCD is running as planned and a simple click on “Debug Anyway” lets you continue.

Debug Anyway

If you’re getting an error message from GDB itself make sure you’ve compiled your binary with -Og. This option can easily be changed by running Espressif project configuration with “make menuconfig” inside your project folder. I haven’t quite figured out why but compiling with -Os results in GDB failing with an assertion…