14.1.1.4. Debugging the firmware

On-hardware debugging means halting the processor, setting breakpoints in the C source, single-stepping, and inspecting variables, memory, registers, and peripherals – from inside VS Code. This needs three things: a debug build, a SWD debug probe (a Segger J-Link), and the Cortex-Debug extension driving arm-none-eabi-gdb against a J-Link GDB server.

14.1.1.4.1. Build for debugging

Always rebuild the target with DEBUG=1:

make -j$(nproc) TARGET=<TARGET> DEBUG=1

A release (DEBUG=0) image is compiled -O2; in the debugger you will see <optimized out> for many variables, inlined functions collapse into their callers, and stepping jumps around unpredictably. DEBUG=1 builds -Og -ggdb3, which is debuggable while still booting on the camera. The ELF you point the debugger at is:

build/<TARGET>/bin/firmware.elf

(For the Alif AE3, debug build/OPENMV_AE3/bin/firmware_M55_HP.elf – the high-performance core.)

14.1.1.4.3. VS Code Cortex-Debug setup

Create .vscode/launch.json in the repository. The simplest case – VS Code, the J-Link, and the build are all on the same Linux / macOS machine – uses servertype: "jlink", which makes Cortex-Debug start a J-Link GDB server itself:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "OpenMV J-Link",
      "type": "cortex-debug",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "executable": "${workspaceFolder}/build/OPENMV4/bin/firmware.elf",
      "servertype": "jlink",
      "device": "STM32H743VI",
      "interface": "swd",
      "runToEntryPoint": "main",
      "armToolchainPath": "${env:HOME}/openmv-sdk-1.6.0/gcc/bin",
      "gdbPath": "${env:HOME}/openmv-sdk-1.6.0/gcc/bin/arm-none-eabi-gdb"
    }
  ]
}

Change executable and device for your board (see the table above). Press F5 to build-flash-and-run to main and stop there.

Tip

To rebuild automatically every time you start debugging, add a build task to .vscode/tasks.json and reference it from the launch config with "preLaunchTask". For example a task running make -j$(nproc) TARGET=OPENMV4 DEBUG=1, named "build-firmware", plus "preLaunchTask": "build-firmware" in the configuration above, so that F5 rebuilds, flashes, and starts the debugger in a single step.

Warning

Cortex-Debug needs arm-none-eabi-gdb. It ships in the SDK at ~/openmv-sdk-<version>/gcc/bin but is not on PATH by default, so debugging fails with “GDB executable ‘arm-none-eabi-gdb’ was not found”. Fix it either by setting armToolchainPath / gdbPath as shown above, or by adding ~/openmv-sdk-<version>/gcc/bin to your PATH (printenv PATH should then list it).

14.1.1.4.4. Peripheral register view (SVD)

Point Cortex-Debug at a CMSIS SVD file to get a decoded peripheral-register view (timers, DMA, the camera interface, etc.) by name and bitfield:

"svdFile": "/path/to/STM32H743.svd"

For STM32 and MIMXRT, get the SVD from the ST / NXP CMSIS packs or the Cortex-Debug SVD registry. The Alif SVDs are vendored in the firmware repo at lib/micropython/lib/alif_ensemble-cmsis-dfp/Debug/SVD/ (use the ..._CM55_HP_View.svd for the AE3 HP core).

14.1.1.4.6. Command-line debugging with gdbrunner

Setting up a GDB session against an embedded target by hand is a five-step dance: start the J-Link / ST-Link GDB server in one window with the right device, port, and interface flags; wait for it to print Waiting for GDB connection; run arm-none-eabi-gdb in a second window; type target remote localhost:<port>; point gdb at the ELF. When the gdb session ends, remember to kill the server window. gdbrunner is a small CLI that collapses all of that into one foreground command. It ships in the OpenMV SDK’s Python environment, so there is nothing to install; the usual entry point is the firmware repository’s make debug target:

make -j$(nproc) TARGET=<TARGET> DEBUG=1 debug

This runs gdbrunner with the debugger arguments from the board’s configuration – the J-Link device name and, where needed, the ST-Link external flash loader – with the SDK’s arm-none-eabi-gdb already on PATH. The default backend is J-Link; make DEBUGGER=STLINK debug works with an ST-Link probe instead.

gdbrunner can also be invoked directly (outside the SDK, pip install gdbrunner):

gdbrunner jlink --device STM32H743VI build/OPENMV4/bin/firmware.elf
gdbrunner stlink build/OPENMV4/bin/firmware.elf
gdbrunner qemu --machine mps2-an500 build/MPS2_AN500/bin/firmware.elf

The first positional argument picks the server backend (jlink, stlink, qemu); the rest are forwarded to that backend, with defaults that work for the OpenMV cams. gdbrunner --help lists the full per-backend flag list; each backend’s argument table is JSON-driven (src/gdbrunner/backends.json), so adding a new server is a config edit rather than code.

What gdbrunner does for command-line work:

  • One process, clean lifecycle. Server starts, gdb attaches when the port is open, server is terminated cleanly when gdb exits. No orphan JLinkGDBServer surviving the session, no two terminals to manage.

  • STM32CubeProgrammer auto-discovery. The stlink backend searches the usual install locations (~/STM32CubeProgrammer/, /opt/st/, the STM32CubeIDE plugin tree) for the STM32CubeProgrammer tools, so the long --cube-prog path doesn’t have to be typed every time. The SDK bundles its own copy at ~/openmv-sdk-<version>/stcubeprog/bin – point --cube-prog there if no system install exists.

  • Per-project gdbinit honoured. A .gdbinit in the current directory is loaded with -ix – overriding the user-wide ~/.gdbinit – so per-project gdb scripting (pretty-printers, board-specific macros, breakpoint sets) drops in by being present in the working directory. make debug runs from the repository root, so a .gdbinit there applies.

  • Dry run. --dryrun prints the server command without running it, useful for adapting the invocation to a wrapper script, copying it into an IDE launcher config, or just checking what arguments gdbrunner is composing.

  • Server output visible. --show-output keeps the server’s stdout / stderr visible. The default suppresses it (so gdb’s UI stays clean); flip the flag when the server itself is what’s misbehaving.

  • QEMU backend. qemu-system-arm debugs a firmware build with no board plugged in. The MPS2_AN500 target selects this backend in its board configuration, so make TARGET=MPS2_AN500 DEBUG=1 debug builds for QEMU’s mps2-an500 machine and steps the platform-independent code – everything that doesn’t touch cam-specific peripherals – on a flight. (qemu-system-arm is a host install, not part of the SDK.)

For source-level stepping with breakpoint gutters and a peripheral register view, the VS Code Cortex-Debug setup above is the better tool; gdbrunner is the right one for everything that lives at the command line.

14.1.1.4.7. Using the debugger

Once a session is running (the processor halted at main):

  • Breakpoints – click the gutter next to a C line, or in the Debug Console break <file>:<line> / break <function>. Cortex-M cores have a small number of hardware breakpoint comparators (typically 6–8 on M7 / H7, 8 on M55). Exceeding that on code in flash silently fails – keep the active breakpoint count modest.

  • SteppingF10 step over (next), F11 step into (step), Shift+F11 step out (finish), F5 continue. Instruction-level stepping is stepi / nexti in the Debug Console.

  • Variables / watch / call stack – the Variables and Call Stack panes show locals and the backtrace; add expressions to Watch. Hover a variable in the source to see its value. Anything showing <optimized out> means you are not on a DEBUG=1 build.

  • Watchpoints (data breakpoints)watch <expr> halts when a variable is written, rwatch on read, awatch on either. The Cortex-M DWT unit supports ~4 hardware watchpoints – invaluable for catching who corrupted a variable.

  • Registers and peripherals – the Cortex Registers view shows core registers; with svdFile set, the Peripherals view decodes every peripheral register and bitfield (DMA, timers, the camera / CSI interface, XSPI, etc.) – the fastest way to see why a driver is misbehaving.

  • Memory – use the Cortex-Debug memory viewer or gdb x/ to inspect framebuffers, DMA buffers, and structures directly.

  • printf without halting (SWO/RTT) – for timing-sensitive issues, Segger RTT or SWO gives near-zero-overhead printf while the target runs. Build with DEBUG_PRINTF=1 and add Cortex-Debug’s rttConfig (RTT) or swoConfig (SWO, needs the core clock). This is the right tool when a breakpoint would change the timing you are trying to observe.

  • DisconnectingStop on a launch session halts the target; Disconnect on an attach session leaves the camera running. Power-cycle the camera to return it to normal operation afterwards.

14.1.1.4.8. Debugging pitfalls

  • Optimized-out variables. Everything shows <optimized out> – you built DEBUG=0. Rebuild with DEBUG=1.

  • “GDB executable not found” – the SDK gcc/bin is not on PATH; set armToolchainPath / gdbPath.

  • “Cannot connect” / wrong memory map – wrong or missing device name; use the exact string from the table.

  • Breakpoints silently not hit – too many hardware breakpoints on flash-resident code; reduce them.

  • Source paths don’t match (Docker-built ELF) – build with the Docker build-firmware-dev target (same absolute path inside and outside the container) or set gdb set substitute-path.