First steps with PlatformIO’s Unified Debugger

>>> Watch on youtube

Many developers see debugging as an essential feature within the software development process. The ability to stop a program at a given point and look into all internals can be very helpful to track down bugs and understand inner workings of libraries and data states. Debugging embedded targets can be a bit tricky: Typically one needs hardware debuggers, and the combination of IDE, debugger and probe needs to be configured, swapping out one target for another typically means reconfiguration and so on.

PlatformIO launched its „PIO Unified Debugger“, so we're taking a look at it and showing you how to get started!

What is this for?

PIO Unified Debugger lets you start debugging sessions right out of an Atom or VSCode session where PlatformIO is installed as a plugin. This means you can set breakpoints in code lines, stop over conditions, look at variables (global, local), evaluate expressions, step through code, look at MCU registers and peripherals, and much more. And one of the big pros is the simply connectivity: USB is quite often sufficient, with little or no configuration necessary on the IDE side.

That’s a huge step forward compared to debugging support from before, because PlatformIO detaches the IDE handling tasks from the probe handling. We tested this using a developer board from STM, the DISCO 475g IOT 01a. This board has a on-board ST-LINK Debugger, which works over USB, and this method is supported by PIO.

How do i know if my board supports debugging?

So of course you’re working with your own boards and not necessarily the DISCO 475, so how can you know if your board is supported? The PIO documentation has a good overview of all boards that support debugging together with their debugging tools. Currently supported are Atmel-ICE, Black Magic, CMSIS-DAP, FTDI, J-LINK, Mini-Module, MSP Debug, Olimex ARM-USB OCD-H, -TINY-H, TI-ICDI, ST-LINK and custom configurations.

Head to PIO’s debugging page and the boards overview and search for your board. If it’s listed there, look at the „Debug“ column for supported probes. In our case, ST-LINK is the default probe.

Start a session

For this tutorial, we set up a small code sample using ARM Mbed threads. It should run on other Mbed-enabled boards as well:

$ git clone

The code defines a struct with an internal counter, and a string buffer which holds a string representation of the counter. Along with that comes a Mutex, to protect the resource from concurrent access by threads. Later on, three types of Threads are started:

  • a counter_thread locks the Mutex, increments the variable, sprintf’s it into the string buffer and unlocks the Mutex.
  • a blink_thread turns the on-board LED on and off, depending on the counter variable.
  • an output_thread writes the contents of the string buffer to USB serial.
struct counter_s {
    int  counter;
    char buf[32];
    Mutex _m;
void counter_init(counter_s *s);
void counter_update(counter_s *s);
void counter_dump(counter_s *s, Serial& out);

void counter_thread(void* arg);
void blink_thread(counter_s *s);
void output_thread(counter_s *s);

These threads run in parallel: 1x blink, 1x output with a scheduled frequency, e.g. 1/sec, and a variable number of counter_threads with random wait.

This code can be compiled and run using pio run, and there is a pio debug command as well which brings the board into a debugging session. platformio.ini allows for special debugging properties, in our case we added:

debug_tool = stlink

A new debugging session can be triggered from the IDE. Within VSCode, switch to the debug mode, and start the default debugging configuration:

Start debugging session

Screenshot 1: Start debugging session

As an example, we’d like to see what happens when the counter gets updated, and set a breakpoint on line #30. Terminal should show compilation progress (in debug configuration), then switching to the debugger console with a log of temporary output. The ST/LINK onboard LED keeps flashing red/green, to indicate the debugging session.

VSCode brings a new overlay button line in display, which allows for the typical pause/resume, step over, step into, step out-of debugging „rhythm“. The debugger halts the processor on the first line of the main function, so our board starts in a halted mode, at least from the applications perspective. A click to the first button „Continue“ continues the execution until the defined breakpoint is reached.

Debug actions button bar

Screenshot 2: Debug action button bar

At breakpoint

Screenshot 3: Sample breakpoint

Call Stack

Sometimes in larger Codebases, one wonders how the program code got to this point. The Call Stack window helps, it shows the current call trace:

Call Stack

Screenshot 4: Call stack at breakpoint

One nice thing about this is that all functions are shown together with the corresponding source code pointer, and a double click takes you there! Really good to examine the inner workings of Libraries etc.

Looking at variables

On the left side there is a panel named „Variables“, with different sections for global, local, and static variables Take some time to explore all of these, as they give many insights into the Mbed environment as well (especially the global variables :). In this example, the local counter_s *s variable can be observed. Now step over the next instruction, which increments the counter. It should show a blinking visual in the Variables UI, so you can see that something happened with this variable. A click to step over again renders the string buffer which can be observed as well.


Let’s say we want to examine this at a later stage when the counter reaches the value of 100. Easy, just right-click the breakpoint, „Edit Breakpoint“ and add a condition to it. Enter s->counter >= 100 into the input line and hit enter. Click the „Continue“ button to continue debugging. At the next stop, the counter should display „100“ within the variables list.


Variables and their structures can be fully observed in the Variables tab, but sometimes you need to evaluate certain expression or put watches on specific variables. In the „Watch“ tab, click the „+“ sign and add an expression such as s->buf[10], and it will be directly evaluated, updated after each step without the need to open trees or structs. To see what the mutex does, add s->_m->_count as a watch and debug over to the unlock function call: after unlock, the mutex’ count is reset to 0.

Assembly, anyone?

Now debugging and stepping over it takes place at the level of lines of C code, because by debugging info the debugger/IDE know what symbol is at what code line. VSCode Debugger allows to switch over to assembly, and step through assembler code, while looking at registers. Now that’s a great feature if you’re familiar with assembly language OR want to learn it - probably a good way to do so! You can switch back and forth between C and ASM, keeping all debugging functionality as-is with conditionals, watches, etc. Nice!


Screenshot 5: Switch between code and disassembly


As a peripheral example, we set a breakpoint at line #66, that’s within the blink_thread, when (potentially) LED state changes. led is a DigitalOut, and it is assigned a new boolean value depending on the counter:

 led = (s->counter % 2);

So this turns the LED either on or off. Execute/Debug into this point, open led from the Variables window (under globals). After clicking through the structure, we find a pin: PA_5 assigned as the pin. What does that mean? That’s from the GPIO Port „A“, the 5th I/O. Let’s look at this in the „Peripherals“ section. In the tree, open GPIOA, the the output registers ODR, and see the fifth bit (in my case, it’s 1).


Screenshot 6: Inspecting internal state of peripherals

Step over this instruction, and look at the Peripherals entry. When the LED state changes, this is reflected in the variable as well. Take some time to browse through the peripherals section, there’s many more to see such as SPI, UARTs, and more. This become really handy when there’s the need to debug sensor or other peripherals.

… And much more. For embedded enthusiasts and/or those interested in reversing: You can look at all processor registers as well as any point in memory :) PIO’s Unified Debugger is really fun to use. The feature we used most is the ability to look into variables and execute the firmware up to a certain point using conditionals. Together with a decent Unit Testing approach this makes up for great fun in firmware development. The Unified Debugger is free, but needs a (free) PIO Account to work. Give it a try!

If you would like to see debugging step-by-step, have a look at our youtube screencast!

Your ThingForward crew.

Follow ThingForward on Twitter, Facebook and Linkedin!