Velo: velocity from the FM-1's keys
An experiment firmware for the M-VAVE FM-1. Its keys are rubber domes with carbon pills and a single contact each, scanned as a diode matrix: on or off, no velocity. But the chip's ADC can read two of the four key rows (PA7 and PA8 are SARADC channels 0 and 1), and a carbon pill's resistance falls steeply with force. So this firmware samples those rows between two ticks of the key scan and reads each key's contact resistance: 1023 open, falling as the key is pressed harder. A press crosses a TOUCH threshold and then a PRESS threshold, and the time between them is the velocity. Twenty of the 27 keys (F3 to C5) are on those rows; the top seven are not and play at a fixed velocity. No hardware changes.
Status: working; parked 2026-10-14 with per-key calibration built but not yet right (the
first session with it played more extremes, not fewer: notes/01-lab.md, "Calibration"). Untested
so far: CHORD, aftertouch in drive mode by feel, release velocity (not sent). Parked on 2026-10-08 after the first session:
read through the row's 10k pull-up, every played key seats at the same reading (the matrix
diode's drop) and only slow presses read soft. Revived by DRIVE: a held key's every other
sample is taken with its row driven high by one of the pin's weaker drivers (DRIVE 1 is the
datasheet's 2.4 mA level; DRIVE 2, the default, repeats firm presses within +-5 counts), which
then reads the carbon pill's resistance above the diode's drop. Soft and firm presses separate by
100..150 counts where the pull-up gave 5; on a log scale of that resistance, free playing gives
velocities with quartiles 40 / 59 / 74, and the held force is polyphonic aftertouch. The top
seven keys (no converter) are read through the LED line PA10.
notes/01-lab.md has the measurements (section 02), the converter settings, and the traps.
The lab
- The screen shows the last press as a curve (8 samples before the touch, 48 in all, 2.2 ms apart), the three thresholds as lines, the velocity and the touch-to-press time, every key's live reading as a bar, and the converter's conversion time in the header.
- Keys play a small velocity-sensitive FM voice and send USB MIDI notes (channel 1) with the velocity, so a DAW's velocity display is the second instrument.
tools/velo_log.pyprints one line per press and appends each curve to a CSV. The FM-1 sends each capture as SysEx (F0 7D 56 4C 01 key vel dt dt n samples F7); FX toggles it.- Pages on SELECT (or EDIT): VELOCITY has MAX, AT RANGE and LOW / HIGH (the landing's height
above the key's floor that means 127 and 1; with DRIVE on, CURVE and LIGHTEST for the driven
log scale); CONVERTER has the ADC clock divider and
start delay (divider 7, delay 2: anything faster returns rails), immediate reads (IMMED), and
the key lights; EXPERIMENT has DRIVE (a held key sampled with its row driven high, to see the
pill behind the matrix diode), CHORD (a chord's notes share its first velocity) and TOP / TOP
RANGE (the top seven keys' velocity: fixed, from context, or a probe through the LED line PA10;
notes/01-lab.md, section 02). SAVE keeps the settings; OCT-/OCT+ the octave; ALGORITHM the fixed velocity of the top keys.
Build, test, flash
As the other firmwares (see ../CLAUDE.md): ./build.sh makes build/velo.fwsc,
sh tests/run_tests.sh runs the host tests (the sensing state machine on synthetic curves, the
voice, Felucca's platform tests), python3 tools/fm1_install.py build/velo.fwsc --yes flashes.
Package identity FM-1_5 (dev builds FM-1_500), USB name "Velo FM-1".
Where things are
firmware/hal/fm1_adc.h | the converter: non-blocking start / done / take, the CON word (clock divider, start delay) |
firmware/hal/fm1_input.h | the scan tick samples one analog row of the column it has just latched; FM1_INPUT_ANALOG hook |
firmware/src/app/sense.c | the state machine per key (open, touch, down, lift), the velocity map, the capture, the events; no HAL, host-tested |
firmware/src/app/ui.c | the lab screen, the pages, MIDI out, the SysEx dump |
firmware/src/dsp/velo.c | the voice: 8 voices of 2-op FM, velocity sets level and brightness |
tools/velo_log.py | the host log: one line and a CSV row per press (with its curve) |
tools/velo_notes.py | every note-on's velocity to a CSV, fast playing included |
tools/velo_compare.py | probe_all: each press read directly and through the PA10 probe; summary compares |
tools/velo_ctl.py | status, a live watch of the readings, and the settings over MIDI CC (channel 16) |
Built on the platform layer of Felucca (Hügelton Instruments, GPL-3.0-only), by way of FoMni. GPL-3.0-only.
