projects/ janelia/ Mouse Joystick/ Sources/ mouse-joystick-firm/ examples/ PyControlLinkQp/

Source files

mouse-joystick-firm/examples/PyControlLinkQp

Stage 4 again, structured as a QP/C++ (v7.3.4, QV kernel) application: the same ProtocolBoard and TmcMotion as examples/PyControlLink, unchanged, inside one active object, with a QS trace leaving the board on FW_TRIG1 so a logic analyser on the InstrumentPort can see what the firmware did, per request, without USB and without touching the pyControl link.

It exists because the only thing QP buys this board is observability, and that had to be measured before deciding anything about the delivery firmware. The measurement is in qp-arduino-example/evidence/2026-10-01-rp2040-joystick-link/ (the pilot this application is derived from) and in the firmware run recorded alongside it (see "Status"). examples/PyControlLink stays the non-QP delivery sketch; nothing here changes the protocol or the pyControl side.

What runs where

Piece Here Notes
ProtocolBoard, TrialWatch, TmcMotion, SimulatedMotion src/ shared with PyControlLink; protocol v1 (proto=2) since 2026-10-01, the 48-case corpus holds in both halves
LinkAo.cpp the active object 64-bit clock, lines in, lines out: what PyControlLink.ino's loop() did, as run-to-completion steps with trace records
Bsp.cpp board support SafeState first, boot signature and identity beacon, 1 kHz hardware-alarm tick (the sampler) with watchdog, non-blocking line transport, transmit-only QS on a PIO UART at FW_TRIG1, the rev 1 angle DAC
AppConfig.hpp configuration the mechanism and protocol numbers of PyControlLink.ino; has_angout follows MJ_BOARD_REV; pull_sign comes from src/BoardCalibration.hpp by serial once the board is known (board 4 is −1, measured 2026-10-02; others +1, unmeasured)
PyControlLinkQp.ino setup()/loop() only event pool (8 lines), queue (16 events), start

The qp-arduino library comes from its released tag on GitHub (https://github.com/QuantumLeaps/qp-arduino.git#v7.3.4) through platformio.ini's [qp_env_data], since 2026-10-04; PlatformIO clones it once into .pio/libdeps and refreshes it only when the pin changes. Before the release it was the sibling checkout ../qp-arduino, linked with symlink://.

Signals and ports

Signal Pin (from BoardPins.hpp) Behaviour
pyControl protocol UART0 kPycUartTx/kPycUartRx, 115200 8N1 as before; MJ_LINK_USB=1 moves it to USB CDC for a bench with no host
QS trace kFwTrig1 as a PIO UART TX, 115200 8N1, transmit only starts after the boot signature and the identity beacon, which still use the pin as GPIO; nothing on this line can command the board
operation marker kFwTrig0 high from the start of a homing or move to its completion (OP_START/OP_DONE)
USB CDC Serial a banner at boot; in the bench build, the protocol

FW_TRIG1 therefore no longer carries the RELEASED level of PyControlLink: that state is a LINK_STATE record. Which marker roles the v1.0 board's InstrumentPort overlay finally assigns is an OBS-IF decision still owed (mouse-joystick-pcb/docs/observability-plan.md); the two uses here are one-line changes.

Trace records

Record Fields Meaning
LINK_RX seq, verb, length a protocol line was dispatched to the AO
LINK_TX sent flag, ready time, first 20 characters a reply or event was queued for the port (0 = ring full, dropped)
LINK_STATE name ProtocolBoard's state changed
OP_START / OP_DONE homing flag, angle an operation started / completed; FW_TRIG0 follows
LINK_WORK advance µs, feed µs, output µs where a dispatched line's time went
DAC_STATE phase (0 down, 1 angle DAC reset, 2 angle DAC up; pcb v1.0: 3 brake DAC reset, 4 brake DAC up, 5 torque ADC up), begin failures the switched-domain peripherals' bring-up after hv=1, one phase per tick, and their release when the rail drops
BRAKE_STATE engaging flag, commanded level N·cm, torque N·cm the brake's commanded state changed (pcb v1.0): BRAKE accepted (engaging), the drive released (engaging clears, the level holds), the brake off
BOOT_INFO watchdog timeout?, forced reboot?, last breadcrumb once after boot: why the chip booted and where the previous run was when the hardware watchdog bit
SAMPLE_STAT ticks, handled, coalesced, dropped, max µs, last µs, skipped (idle gap), skipped (decimated), decimated flag the 1 kHz sampler: ticks the alarm posted, ticks the AO ran, samples lost to a tick still in flight or to a full queue, and the cost of the tick handler (poll, classifier, capture, DAC write), every second
TRACE_STAT high-water, buffer-full count, size, ticks QS buffer use, every second
LINK_STAT lines, drops (pool, queue, ring), overlong, queue min free, pool min free, ring high-water transport and budget use, every second
TICK_LOST ticks, re-arms, catch-ups the tick watchdog fired (never expected)

Plus QP's own state transitions, entry/exit and assertions. The per-event dispatch/post/get records are filtered out by default (LINK_QS_FULL=1 keeps them, and saturates the line at a 1 kHz tick).

The sampler under the rail (2026-10-02)

Board 4's second attempt reached breadcrumb 9, hv on complete in 272 ms, and then never idled again: with the bus up every 1 kHz tick polled the TMC5130A, and the handler ran back to back until the watchdog bit. Four things changed:

Watchdog and breadcrumbs

Since 2026-10-02 the BSP enables the RP2040 hardware watchdog, 2 s, after SafeState and the USB host wait, and feeds it in QV::onIdle() only: a run-to-completion step that never returns reboots the chip, and the reset drops HV_EN. Q_onError keeps its two-second FW_TRIG0 signature by feeding the watchdog inside it. The reason for a boot and the last breadcrumb are read from the watchdog block and its scratch registers before anything else runs, printed in the banner (Boot: WATCHDOG TIMEOUT, last breadcrumb N) and traced as BOOT_INFO.

BSP::crumb(n) is the breadcrumb: a train of n pulses, 25 µs high and 25 µs low, on FW_TRIG0, and n into watchdog scratch 0. An operation marker pulse is 500 µs or longer, so the two are told apart on the analyzer. A train during a running operation briefly lowers the marker; none of the checkpoints below runs during a move.

Count Where
1 TmcMotion::setHvEnabled(true) entered, before powerUp
2 powerUp returned
3 connectBus done
4 beginSpi done (after its 10 ms delays)
5 TMC version 0x11 and link verified
6 SD_MODE check passed
7 configure done, bus up
8 position cache seeded, setHvEnabled returning true
9 the hv reply queued in the port ring (LINE handler)
11 DAC: claiming the bus (first tick after hv=1, rev 1)
12 DAC: bus up (SPI1 or Wire begun)
13 DAC: soft reset frame sent
14 DAC: writing CONFIG, GAIN, SYNC (2 ms later)
15 DAC: configured
16 DAC: releasing the bus (rail going down)
17 DAC: bus released

The hook into TmcMotion is setCheckpointHook(); PyControlLink does not install one and pulses nothing.

Builds

Env For
mouse_joystick_qp rev 0 (pcb v0.1/v0.2): TmcMotion, UART0, trace on FW_TRIG1
mouse_joystick_qp_rev1 rev 1 (pcb v0.3 and later): angle DAC on SPI1
mouse_joystick_qp_rev2 pcb v1.0 (MJ_BOARD_REV=2): TmcMotion, UART0, trace on FW_TRIG1 (Digital-50 DIG1), the angle DAC on I2C0 GP16/GP17 at 0x48. The brake DAC (0x49) and the ADS8689 are not driven yet, so caps lacks brake and torque. Compiles; no v1.0 board had arrived when it was written (2026-10-04). Replaces mouse_joystick_qp_rev1_i2c, which paired rev 1 pins with an I2C DAC and matched no board
mouse_joystick_qp_rev2_usb the same for the bench: protocol over USB CDC, tick probe on the UART0 TX pad, TICK handler level on FW_TRIG0; the first rail-up runs on a v1.0 board
mouse_joystick_qp_rev1_usb board 4's rail-up bench build: TmcMotion, rev 1, DAC on SPI1, protocol over USB CDC, trace on FW_TRIG1, tick probe on the UART0 TX pad with the RJ-45 unplugged; driven by evidence/bench-scripts/board4_session.sh. Hung on the first hv on on board 4, 2026-10-02 (see Status)
mouse_joystick_qp_rev0_usb the bisect build for that hang: the same, with the rev 0 pad map and therefore no DAC path at all; safe on a v0.3 board, whose rail-up pins are the same
mouse_joystick_qp_sim_usb a board with no 36 V and no host: SimulatedMotion, protocol over USB CDC, tick probe on the unused UART0 TX pad (InstrumentPort DIG8). Accepts the bench stimulus sim;<seq>;angle;<deg> (sets the simulated encoder, as the corpus's ANGLE step does, so a WATCH outcome can be driven by hand; ack;<seq>), which exists in no other build and is not a protocol verb
pixi run build examples/PyControlLinkQp mouse_joystick_qp
pixi run upload examples/PyControlLinkQp mouse_joystick_qp_sim_usb /dev/ttyACM0

Drive the bench build with qp-arduino-example/tools/link_pilot.py and read the trace as evidence/bench-scripts/link_run.sh does (sigrok uart decoder on the FW_TRIG1 channel, 17 through the rev 0/1 adapter the script was written for and 1 on a v1.0 board, bytes to qspy -k -f).

Status


Files

NameSize
../
AppConfig.hpp4 kB
Bsp.cpp36 kB
Bsp.hpp4 kB
LinkAo.cpp17 kB
LinkAo.hpp2 kB
PyControlLinkQp.ino793 B
README.md (rendered above; raw)17 kB