The debugger

Stop a board on a line of your own code, look at what it was doing, and step through it while the circuit stays exactly where the halt found it.

What it is for

A blinking LED tells you the board is alive. It does not tell you what count was when the light went out, or whether the branch you just wrote was taken. The debugger does: press Run, stop the board on a line, and read the registers and your own variables at that instant.

While the board is stopped, so is everything else. The LED keeps whatever brightness it had, the servo stays at its angle, the chip's timers stop where they were. So when you press Continue, millis() carries on from the instruction you were looking at rather than jumping forward by however long you spent reading.

Setting a breakpoint

Click in the narrow gutter to the left of the line numbers. A red dot appears, and the board will stop before that line runs: what you see on screen has not happened yet.

Press Run. A board with a breakpoint on it is always built from the files in front of you, even if the Firmware panel names a built-in program. A breakpoint is on a line of this code, and the built-in programs ship without the information that says which instruction a line is.

Click the dot again to take it off. Clear all in the Debug tab takes off every breakpoint on that board at once.

A hollow dot

A hollow dot means the breakpoint is where you put it and is doing nothing: the compiler generated no instruction for that line. Blank lines, comments, declarations with no code in them and lines the optimizer folded away all look like this. Move it to the next line that does something.

The bar beside Run

Pause stops the board wherever it is. It becomes Continue, which lets it go again.

Step over runs one line of source, calls and all: a line that calls delay(500) takes 500 ms of simulated time and the rest of the circuit lives through all of it, because that is what really happens.

Step into runs to the next line the board reaches, which is the first line of a function the current line calls.

A step can be cut short by a breakpoint further on, and it stops there and says so. The one breakpoint that never cuts a step short is the one the step started from: an interrupt taken at that instant (a timer, a pin, the tick that millis() counts) runs its handler and hands the board back to the very line you were on, and stopping there would mean the press did nothing at all.

Step out is grayed out, and its tooltip says why: it would have to work out where the current function returns to, and the debugger does not read the call stack. Put a breakpoint on the line after the call instead.

On a phone the bar moves into the Debug tab, where there is room for it.

The Debug tab

Beside the serial monitor at the bottom of the window (its own tab in the sheet on a phone), with four things in it.

Where it stopped, in one line: the file and line, the address, and why it stopped: a breakpoint, a step, or Pause. When the line table does not cover the address it shows the address and says so, rather than naming a line it is not sure about. "Inside sketch.ino:14" means the board is partway through that line, not at the start of it, and the highlight in the code pane is drawn more faintly to match.

Registers, under the names the core itself uses: a0 and sp on an ESP32-C3, r24 and sreg on an Uno. They are read from the core at the halt, so they are what it holds now and not what it held last time. The pc is first, and it is the instruction that will run next.

Watch takes the name of a global variable. Start typing and it offers the ones your program has. Each row shows the type as the compiler recorded it and how many bytes that type takes, then the value, read out of the board's memory at every halt. A name the program does not have says so instead of showing a zero. A struct or an array shows its bytes, low address first, because those are bytes and not one number.

Breakpoints lists every one on this board with the address it resolved to. Click one to jump to it in the code.

What the serial monitor does meanwhile

Nothing changes. Everything a board printed before the halt is still there, and anything it prints after you continue lands under it. Stopping a board is not stopping the run.

Two boards

Each board has its own breakpoints, its own watch list and its own halt. The debugger acts on the board the code pane is showing, which is the one the picker above the tabs names.

Which boards it works on

All of them. Every board Mokxi has answers the debugger: the ESP32-C3 and the ESP32-C6, the ESP32 and the ESP8266, the Arduino Uno, Nano, Mega, Leonardo and Uno R4 Minima, the ATtiny85, the Raspberry Pi Pico and Pico W, the STM32 Black Pill and Blue Pill, the BBC micro:bit and the Seeed XIAO SAMD21.

The registers are the ones the board's own core has, under the names its own documentation uses: a0 and sp on an ESP32-C3, r24 and sreg on an AVR, r0, lr and xpsr on anything with an ARM in it, a0 to a15 on the Xtensa boards (the ESP32, the ESP32-S3 and the ESP8266). So what you read in the panel is what you would read on the real part.

Step out is grayed out on every board, for the reason its tooltip gives.

The bar can still gray itself out, for a reason that has nothing to do with which board it is: nothing is running yet, or the part the code pane is showing runs no program at all (a resistor cannot be stopped). A board the debugger genuinely could not reach would name itself ("The debugger does not yet support mega1."), and no board does that any more. A board it does reach is never grayed out for want of a breakpoint: Pause works the moment the board is running, breakpoint or none.