Move jog-mode stepping off the CPU loop onto a PIO state machine: one hardcoded instruction (out pins,4 [31] = 0x7F04, no adafruit_pioasm needed) shifts a 4-bit coil nibble to GP18..21 every 32 PIO cycles; one 32-bit word packs all 8 half-step phases; background_write(loop=) DMA-feeds it continuously. half-steps/s = clock/32, so speed + accel = setting sm.frequency. Pulses now run on dedicated hardware, so a display refresh or GC pause can't stall them - which was the ~1s "smooth then jump". Jog loop is now a light 100Hz CPU controller (joystick + accel ramp + frequency); the live step/rate readout is restored since the motor runs from PIO/DMA. Bit-bang Pendulum keeps a deinit() so jog can hand GP18..21 to PIO. Beat-pendulum still on bit-bang (PIO port next). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
PM_K‑1 "Kit" — CircuitPython edition (USB drive · push programming · MIDI audio · practice log)
The CircuitPython firmware for the 52Pi EP‑0172 Pico kit, set up as a self‑contained appliance.
It runs the same program‑string language as https://metronome.varasys.io. The simpler
MicroPython firmware (../pico/main.py) stays as a rock‑solid fallback — and the Pico can't be
bricked (BOOTSEL → drag a MicroPython .uf2 back).
What it does: drives the 3.5″ touchscreen with a lanes/pads display + anti‑aliased text, plays the
speaker + RGB beat light, logs your practice to /history.json, accepts new set lists pushed
from the web editor over USB‑MIDI, and plays through your computer's speakers over USB‑MIDI.
Two power‑on modes (set by boot.py)
- Appliance mode — default (just plug in / power up). The firmware owns the filesystem, so it saves your practice log and writes set lists the editor pushes over USB‑MIDI. The drive is then read‑only to the computer — which also protects the firmware from accidental deletion.
- Editor mode — hold BUTTON A alone while plugging in. The drive is writable by the computer, so you
can drag
programs.json/code.py/ fonts on from any OS or browser (the universal fallback). Reset afterwards to return to appliance mode. - Stepper jog/test mode — hold BUTTON A + B together while plugging in. A hidden screen where the joystick spins the stepper CW/CCW for bring-up (see Pendulum below). This chord stays in appliance mode (the drive is not flipped writable). Power-cycle with no buttons to return to normal.
Install
- Flash CircuitPython: hold BOOTSEL, plug in, drop the CircuitPython
.uf2ontoRPI‑RP2(https://circuitpython.org/board/raspberry_pi_pico/ — Pico 2 / W builds also fine). ACIRCUITPYdrive appears. - Copy the whole bundle onto
CIRCUITPY:boot.py,code.py(loader) +app.mpy(the application, precompiled),programs.json,font_s.bin/font_m.bin/font_l.bin,logo.bin/midi.bin/usb.bin(logo + MIDI/USB status icons),editor.html(offline editor), and the helper scripts. If an oldapp.pyis on the drive, delete it — the firmware ships as precompiledapp.mpy. (Why: CircuitPython compiling the ~57 KB source at boot fragments the heap and runs the RP2040 out of memory; a.mpyloads without compiling.code.pyis a tiny stable loader; the one-click updater pushes a newapp.mpy. The.binassets ride in the bundle — if one is missing the firmware just falls back to text and never fails to boot.) - Power‑cycle (so
boot.pytakes effect). It boots into appliance mode and runs.
Program it from the web (push over USB‑MIDI)
In the editor (Chrome / Edge / Firefox), build a set list → set‑list ⋯ menu → 📟 Save to device.
The editor sends it to the Pico over USB‑MIDI (SysEx); the firmware writes /programs.json, reloads, and
acknowledges — the editor shows Saved ✓. 📥 Load from device reads it back.
Universal fallback (any browser / OS, even Safari): Save to device downloads programs.json when no
device answers — boot the Pico in editor mode (hold A) and drag the file onto the CIRCUITPY drive.
Firmware updates (one‑click, A/B with auto‑rollback)
code.py is a small stable loader; the application is the precompiled app.mpy (it carries
APP_VERSION). To update: the editor's ⋯ menu → ⬆ Update firmware… queries the device's version, fetches
the latest app.mpy from the site, shows device vs latest, and on confirm pushes it over USB‑MIDI
(base64, in flow‑controlled chunks; the device decodes each chunk to disk and verifies the .mpy header
before installing). It goes to a trial slot (old build kept as app.bak) and reboots; if the new build
doesn't boot, the loader automatically rolls back to app.bak. A build that runs cleanly for ~5 s is
confirmed. No BOOTSEL, no dragging. (Updating CircuitPython itself still uses BOOTSEL + a .uf2, but that's
rare. And the Pico is unbrickable as the ultimate backstop.)
Play through the computer's speakers
The Pico is a USB‑MIDI device and sends a note per click (GM drum note per lane, velocity by accent). In the editor click 🎹 Device audio, grant MIDI access, and press play on the device — the editor voices the groove through its full synth, out your speakers, locked to the device's clock. While a host is listening the screen shows a green MIDI badge and the speaker auto‑mutes (the computer plays instead). The editor also syncs the device clock, so the practice log gets real wall‑clock timestamps.
Playlists, editing & Continue
- Built-in playlists (Styles / Practice / Song) are baked into the firmware — read-only, updated with
firmware. Your own playlists live in
programs.json(synced from the editor's Save to device). - Switch playlist: tap the set-list tab (above the title; grey = built-in, cyan = yours). Item: joystick left/right.
- Edit on the device: tap a beat to cycle it (off → normal → accent → ghost); tap the instrument name for the lane editor (sound · beats · subdivision · swing · mute, plus + Lane / Remove). The title turns red (unsaved); tap the title to Save or Revert. Editing a built-in saves a copy into a My edits playlist (built-ins never change). Editing your own updates it in place.
- Continue (auto-advance): tap CONT (top-right of the tab line) — when on, a playlist auto-advances
to the next item at the end of each item's
b<n>segment (turn it on for the Song playlist).
Controls & the practice log
- Joystick: up/down = tempo, left/right = previous/next groove.
- Button A (GP15): play / stop. Button B (GP14): tap tempo.
- Screen: VARASYS logo + firmware version + MIDI/USB status icons up top. Running time and bar count
show of the segment total when the track has a bar length (
b<n>), e.g.1:23 of 2:00andbar 3 of 16. A track with a tempo ramp (rmp) shows a ramp arrow + amount/every-bars (e.g.+4/2b); a gap-trainer track (tr) shows a play|rest symbol + bars (e.g.2/2b). Main beats are squares, subdivisions are circles, with vertical gridlines lining the beats up across lanes. - RGB LED = run state: dim green when stopped ("on"), dim red while playing, with the beat pulsing brighter on top. (The screen background stays black — recoloring it forces a full-screen repaint.)
- The firmware performs ramps (tempo steps every N bars) and gap-trainer cycles (silent rest bars).
- Touchscreen: the bottom shows the practice log for the current track (time · BPM · duration · bars) — newest first. Plays under 5 s aren't logged. Tap a row to arm it (turns amber), tap again to delete.
- RGB LED flashes the beat (amber accent / cyan normal / violet ghost); the speaker clicks to match.
- The log is saved to
/history.json(next toprograms.json) in appliance mode and survives power‑cycles.
Pendulum (stepper motion) — optional
The Kit can drive a physical metronome pendulum: a 4-input unipolar stepper (e.g. a ULN2003 board + 28BYJ-48) swung in time with the beat, plus a matching pendulum drawn on the screen.
- Wiring: controller IN1..IN4 → GP18, GP19, GP20, GP21; controller GND → a Pico GND (shared ground). Power the motor from the controller's own supply, not the Pico. (These four pins are free on the EP‑0172 kit. On the custom PM_K‑1 board GP19/20/21 are already taken by SIG/CLIP LED + ground‑lift, so the production pendulum will need a pin reassignment.)
- Motion: the arm reaches an extreme exactly on each beat, then reverses; it reads the live beat clock, so it follows tempo ramps. Coils de‑energize when stopped. The on‑screen pendulum (shown over the practice log while playing) mirrors the arm exactly — and animates even with no motor wired.
- Config (top of
code.py):STEPPER_ENABLED— off leaves the four pins free.PEND_SWING_DEG— total swing arc end‑to‑end, in degrees (default 120). Single source of truth: drives the screen graphic exactly and the motor.STEPPER_STEPS_PER_REV— your motor's half‑steps per full turn (28BYJ‑48 half‑step ≈ 4096); maps degrees → steps.STEPPER_MAX_RATE— top half‑steps/sec the motor sustains smoothly. Jog mode spins at this rate, and the pendulum auto‑shrinks its arc (rather than desync) when a beat is too short to sweep the full angle.STEPPER_ACCEL— ramp (half‑steps/sec²) used to reach top speed without stalling; lower it if the motor stalls/buzzes when starting.STEPPER_JOG_START— jog kickoff rate from rest (keep at or below the motor's pull‑in rate).- Tune without recompiling: these five are also read from
/settings.json(keysstepper_max_rate,stepper_accel,stepper_jog_start,pend_swing_deg,stepper_steps_per_rev) — edit in editor mode, power‑cycle.
- Jog / test mode (hold A + B at boot): the joystick sets direction only — L = CCW, R = CW — and
the motor accelerates to
STEPPER_MAX_RATE(reversing decelerates through zero first), with an on‑screen needle + RGB LED and a live step count + rate readout. The step pulses are generated by PIO + DMA (hardware‑timed on a state machine), so the motor stays smooth even while the screen redraws — there's no CPU step loop to stall. Tuning: hold to spin; raiseSTEPPER_MAX_RATEuntil the motor skips, then back off; if it stalls starting, lowerSTEPPER_ACCEL/STEPPER_JOG_START. Power‑cycle (no buttons) to exit. (The beat‑pendulum during play still uses the simple step loop for now; it moves to the PIO driver next.)
programs.json
{ "title": "PolyMeter",
"programs": [ { "name": "Four on the floor", "prog": "t120;kick:4;snare:4=.x.x;hatClosed:4/2" } ] }
Each prog is a program string from the editor (tempo, lanes, patterns, /2 subdivision, /2s swing,
(3,8) Euclid, ~ polymeter, @-3 dB). The push above is the easy way to update it.
Calibration (flags at the top of code.py)
- Red/blue swapped: flip
MADCTLbetween0x48(default) and0x40. - Colours look negative: toggle
INVERT_COLORS. - Taps land wrong: set
TOUCH_DEBUG = True, read the raw coords over USB serial, then setTOUCH_SWAP_XY/TOUCH_INVERT_X/TOUCH_INVERT_Y. - Joystick reversed: toggle
JOY_INVERT_X/JOY_INVERT_Y. - Computer audio:
MIDI_ENABLED(default on);MUTE_SPEAKERforces the speaker off even standalone. - LED too bright/dim:
LED_BRIGHTNESS(0..1, default 0.15). - Screen tearing: SPI panels have no tearing‑effect sync;
SPI_BAUD(default 62.5 MHz) is pushed fast to minimise it — lower only if unstable. - Blank / garbled: the panel lot may differ; drop
SPI_BAUD, and if it's a 240×320 ILI9341 rather than the 320×480 ST7796, the init/size need changing (this targets the 320×480 you have). - RGB LED uses the core
neopixel_write(no library to install).
If code.py ever errors, CircuitPython prints the traceback on the screen and over USB serial — send
me that. The fonts are the baked anti‑aliased blobs from ../pico/gen_font.py. protect-firmware.sh (hide
the firmware files) is mainly for editor mode — appliance mode already keeps the drive read‑only.