There is no console on a cartridge. Everything on this page exists so that a fault can be told apart from a different fault without one.
The counters
uvm2_stats (in uvm2_bus.h) is a plain global struct. Read it over SWD, or put it on screen. These are the fields that decide whether any other number is worth reading:
| field | read it as |
|---|---|
dropped | commands lost because the list was full. If it's not zero, nothing on screen is evidence of anything. |
ramps_clamped | ramps started while the zero clamp was on. Must be 0: such a ramp draws a line out of the centre instead of the stroke asked for. |
recals | frames completed, cumulative. If it doesn't climb, the frame loop isn't running: "it doesn't work" and "it never ran" are different investigations. |
commands, bus_cycles, exec_cycles | the frame's size, in commands and bus cycles (667 ns each; 30,000 is a 50 Hz frame). |
vectors, moves | lit strokes versus blanked repositions. The ratio tells you how well the drawing is chained. |
overrun | frames whose stream exceeded the 50 Hz budget. |
us_frame_last/min/max, slow_frames | the real frame period, end to end. |
us_exec, us_input, us_rest, us_wait | where core 1's time goes. |
vectors_last, moves_last, ramp_cycles_last | snapshots taken at the end of each frame. Read these, not the live ones. |
stack0_peak, stack1_peak, stack_overflow | the deepest each core's stack has been, and a sticky overflow flag. |
The live counters are reset at the start of each frame and fill up as it's built, so a debugger sampling at a random moment mostly catches them at zero. That's why the _last snapshots exist.
A hang with nothing in the counters to explain it? Read stack_overflow and stack0_peak first. The game runs on 4 KB of stack with core 1's stack right below it. An overflow doesn't fault: it overwrites core 1's stack and the console just stops. Keep big arrays static.
Solid red LED means a hard fault (bad pointer, stack overflow, unaligned access), not a game spinning.
The HUD: counters on the screen, no probe needed
In any .um2 game, hold buttons 1 and 4 (and only those) for two seconds. Two lines appear in the top-left corner; do it again to hide them.
F50 C12840 D0 Z0 fps, the game's bus cycles this frame, dropped, ramps_clamped
S2624 812 N3614 V322 core 0 / core 1 stack peaks, the game's commands and vectorsD and Z must be 0. If either isn't, !! is added; a stack overflow adds ! after the stacks.
The HUD isn't free. Text is expensive on this beam: the two lines cost roughly 600–1,000 commands and 4,000–7,000 bus cycles a frame, so a game close to its budget drops below 50 Hz while the HUD is on. It's skipped, and counted, rather than allowed to overflow a full list.
Debug Cart: the HUD isn't in the Debug Cart's BIOS; that cart has the probe and RTT instead.
Setting up the SWD probe from scratch
What you need: a Raspberry Pi Pico (RP2040) or Pico 2 (RP2350), which becomes the probe and nothing else; a USB cable; three jumper wires; and a 2.0 mm JST-PH 3-pin plug for the cartridge end. The official Raspberry Pi Debug Probe also works, but its cable ends in a 1.0 mm JST-SH plug and needs an adapter.
1. Flash the Debugprobe firmware. Download it from raspberrypi/debugprobe releases (verified with v2.3.1):
| board | file |
|---|---|
| Pico (RP2040) | debugprobe_on_pico.uf2 |
| Pico 2 (RP2350) | debugprobe_on_pico2.uf2 |
| Raspberry Pi Debug Probe | debugprobe.uf2 |
Hold the Pico's BOOTSEL button while plugging in USB, then copy the .uf2 onto the drive that appears. It reboots as a probe and the drive goes away.
2. Install probe-rs (verified with 0.31.0):
cargo install probe-rs-tools --lockedOn Linux, install probe-rs's udev rules (see its "Probe Setup" page) or the probe only works as root. macOS needs nothing.
3. Check the probe before touching the cartridge:
probe-rs --version
probe-rs list # reads USB descriptors only; should list "Debugprobe on Pico (CMSIS-DAP)"4. Wire it. The UVMC2 brings SWD out on a 3-pin JST-PH header labelled DEBUG:
DEBUG pin | signal | Debugprobe Pico |
|---|---|---|
| 1 | SWCLK | GP2 (header pin 4) |
| 2 | GND | any GND (e.g. header pin 3) |
| 3 | SWDIO | GP3 (header pin 5) |
Don't connect 3V3. The probe is powered by USB, and the Vectrex powers the cartridge, so the console must be on for the target to answer.
5. Read something harmless:
python3 sdk/uvm2-sdk/tools/stats.py build_uvm2/pico/<UVM2_NAME>.elfIt first checks that the ELF matches the image running, then prints the counters. "Target device did not respond" means the console is off, the cartridge isn't seated or a wire is wrong. If probe-rs list found the probe, the probe is fine.
Every command passes --chip RP235x.
What stops the core, and what doesn't
This is the rule that matters most:
probe-rs readandprobe-rs writedo not halt the RP2350. They go through the memory access port: you can read and write while a game keeps playing.- GDB halts it, and halting is the only way to read CPU registers. Halting the core that drives the Vectrex bus is a timing violation, and on this board a halted core doesn't come back: the only recovery is a power cycle.
- Even non-halting reads cost something. Every probe-rs call competes with the drawing core for the bus. A panel polling every 0.6 s pulled one game from 20 fps to 11. Read once, after the game has been running on its own for a while.
- Leave nothing attached. After a session, check that
pgrep -f probe-rsis empty.
The SWD tools
All in sdk/uvm2-sdk/tools/:
| tool | halts? | use it for |
|---|---|---|
stats.py <elf> | no | uvm2_stats in one read; --all for every field |
stats.py <elf> --fps [s] | no | the real frame rate, from recals over a quiet window |
swd_var.py <elf> <name> [value] | no | read any global by name, or write a 1-, 2- or 4-byte one: the live knobs |
list_from_ram.py <elf> out.json | no | the command list the console is replaying, from RAM (static screens only) |
smp_stats.py <elf> | no | the sample mixer's histograms (build with -DUVM2_SMP_TELEM=1) |
load.sh <elf> | briefly | put an image on the console over SWD instead of the SD card |
release.sh | — | kill a session load.sh or a stray GDB server left attached |
probe.sh [addr] [n] | yes | PC, LR, SP and a block of memory. Only on a console that has already hung: the PC of a hang is worth more than any counter. |
The address always comes from the ELF, and the ELF must be the running image. Adding one variable anywhere shifts everything after it, and an address from another build reads another variable that still looks like a number. stats.py resolves addresses with nm every run and refuses to read if the target's code differs from the ELF. For any other global, do the same by hand:
ELF=build_uvm2/pico/<UVM2_NAME>.elf
ADDR=$(arm-none-eabi-nm "$ELF" | awk '$3=="uvm2_sd_error"{print $1}')
probe-rs read --chip RP235x --speed 1000 b32 0x$ADDR 1Live knobs: A/B testing on the tube without rebuilding
Every runtime knob in the SDK is a volatile global (see Low-level API), so you can change it while a game runs and watch the tube: same console, same brightness, nothing else changed.
T=sdk/uvm2-sdk/tools
python3 $T/swd_var.py $ELF uvm2_zero_jump # read
python3 $T/swd_var.py $ELF uvm2_zero_jump 16 # write, and read back
python3 $T/swd_var.py $ELF uvm2_hud 1 # turn the HUD on
python3 $T/swd_var.py $ELF uvm2_pacer_cycles 0 # free refresh, no fillerA write lasts until the next power cycle. When a setting wins, make it the default in the source. When nothing existing isolates a fault, add a temporary volatile switch, flash once, and bisect with it.
The list the console is replaying
python3 $T/list_from_ram.py $ELF list.json
python3 $T/beam_sim.py list.json lit.svgThe probe reads the buffer core 1 replayed last. It reads twice and refuses if the two differ, so this works on a static screen: a menu, a paused game, a test card. For moving scenes, have the game dump the list to the SD card instead. That and the other host tools are covered in Host tools.