M5Stack Cardputer ADV · ESP32-S3 · BLE MIDI

Cardputer MIDI

Field manual · six modes · rev. 1

The keys have no velocity and no aftertouch. The BMI270 does. Every mode in this firmware falls out of that one trade: the keyboard gives you pitch, and your wrist gives you everything a keyboard can't.

01The instrument

Six sensors, one radio, and a 56-key grid. What makes this playable rather than fiddly is that each sensor does exactly one job, and none of them duplicates another.

240 × 135 ST7789V2 IPS BMI270 IMU tilt → expression shock → velocity ES8311 + MEMS mic breath → dynamics 56-key TCA8418 reports every held key → real chords Display chord names, meters, step grid, tilt crosshair BLE radio appears as a MIDI source IR / Grove / SD unused — room to grow SCALE N.T.S.
FIG. 1 Sensor assignments. Each input has exactly one musical job; nothing is doubled up, which is why modes can share the same fingering without fighting.

02Signal path

Three physical inputs converge on one MIDI stream. Pitch is discrete and comes from the keys; everything continuous — pressure, timbre, dynamics — comes from the body of the device.

KEYS 56-key grid IMU tilt / shock / shake MIC RMS envelope Note on / off pitch, discrete Velocity Channel pressure CC1 · CC74 continuous CC11 expression gates the note too RATE LIMIT 50 Hz, deduped expressionDue() BLE MIDI 7–10 ms jitter
FIG. 2 The rate limiter is load-bearing, not an optimisation. Tilt jitters constantly, and a BLE link busy carrying redundant CC updates is exactly what makes cheap wireless controllers feel late.

03The grid

Four rows of fourteen is a terrible piano and an excellent harmonic grid. All three layouts are isomorphic: the interval between two keys depends only on how far apart they are, never on where they sit. A chord is one shape, and you transpose it by moving your hand.

The explorer below runs the same arithmetic as the firmware — gridToNote() and chordName() ported verbatim from scales.cpp. Click keys to build a chord and it names it the way the device will. Learn the shapes here before the hardware arrives.

Chord
Held
click keys above
Range

Which layout, and when

LayoutRightUpReach for it when
4ths+1 semitone+5 Default. Same tuning logic as a bass or a Linnstrument — if your hands know a fretboard, they already know this.
Wicki+2 semitones+5 Up-right is a perfect fifth and rows run in whole tones. Melodies sit under one hand; wide voicings are easy.
Degree+1 degree+3 degrees The grid becomes the scale itself. Vertical neighbours are thirds, so stacking a triad is a straight line up. You cannot play a wrong note.

Scale lock snaps chromatic results down to the nearest scale tone, so it changes how 4ths and Wicki behave but does nothing in Degree — that layout is already built from scale steps.

04The six modes

Switch with Fn + a number. Pressing Fn also silences everything sounding, so nothing survives a switch and it doubles as a panic key.

01

NOTES

Fn + 1

The core playing mode. Isomorphic grid, scale-locked, with the chord under your fingers named on screen in real time.

While notes are held, tilt is live: leaning forward and back sends channel pressure, rolling left and right sends CC1. That is the whole thesis in one gesture — you fret a chord and lean into it, and the sound moves.

ENTER-less
every playable key is a note; no dead space to hunt for
Fn+L
cycle layout
Fn+K
scale lock on / off
Fn+A
tilt aftertouch on / off
Fn+V
velocity source: strike / tilt / fixed
Use case Writing on the couch with the laptop closed. Scale lock on, Degree layout, and you can noodle at a chord progression without once looking down — the screen names what you just found so you can write it back later.
02

XY

Fn + 2

The whole card becomes an XY pad. Roll sends CC1, pitch sends CC74 — the two most commonly mapped destinations in any synth, mod depth and filter cutoff. Any key latches the current position so you can set the device down without the values sliding away with gravity.

any key
latch / unlatch
Fn+C
re-zero tilt to how you're holding it
Fn+X / Fn+Y
invert an axis
Use case Recording a filter sweep that a mouse can't do convincingly. Two hands on the card, arm the track, and perform the automation as a physical gesture. Latch it at the top of the sweep and it stays there.
03

BREATH

Fn + 3

Fingers on the grid pick pitch; the mic decides whether and how loudly it sounds. This is the EWI workflow — you articulate by blowing, not by pressing. Hold a note and it stays silent until you breathe into the device.

Envelope is fast-attack and slow-release so the front of a phrase lands on time and the tail doesn't chatter the CC, with a noise gate underneath so room tone doesn't open it.

CC11
expression, continuously
velocity
taken from breath at the moment the note opens
caveat
mic and speaker share one I2S bus, so this mode takes it exclusively
Use case Making a sampled string or brass patch phrase like a player instead of a keyboard. Swells, crescendos into a downbeat, and dynamics that follow your actual lungs. A dedicated breath controller costs a few hundred dollars.
04

STRUM

Fn + 4

Hold a chord — the notes stay silent. Then shake the device and it strums them. Harder shake gives a tighter spread and a louder stroke; the direction you rotate picks an up- or down-stroke, exactly like hitting strings.

Notes are scheduled with a real inter-note delay rather than fired together, which is what makes it sound strummed rather than blocked.

ENTER
manual downstroke
SPACE
manual upstroke
spread
8–46 ms, from shake strength
Use case Guitar and harp parts that a grid-drawn chord never sells. Also the most fun mode to hand to somebody who doesn't play — the gesture is obvious and the chord is already correct because it's scale-locked.
05

GRAVITY

Fn + 5

A 16-step sequencer where tilt is the transport. Hold it level and it stops dead. Tilt right and it runs forward; tilt left and it runs backwards. How far you tilt is the tempo, up to twelve steps a second.

You scrub a riff the way you'd tip a music box, and reversing mid-phrase is a wrist movement rather than a parameter.

tap top row
toggle that step on / off
hold step + note
set that step's pitch
level
stop, holding position
Use case Generating source material rather than performing a part. Record a few minutes of tilting a sequence around, then cut the good bars out. The rate is deliberately free-running and untethered from your DAW clock — that's what makes the timing feel human instead of quantised.
06

MACRO

Fn + 6

The whole keyboard as a 56-pad control surface, no notes involved. Two rows of latching CC toggles, a row of momentary notes for clip launching, and a transport row.

rows 1–2
26 latching toggles, CC20 upward, 0 / 127
row 3
momentary notes from 36 up — clip launch in most DAWs
row 4
MMC play / stop / record, panic, program changes
Use case The pocket remote for a session — arm tracks, punch in, toggle plugin bypasses from across the room. This is the mode that ties into the studio HUD work: a physical surface for things that currently need the mouse.

05Tilt & expression

Tilt is measured against gravity, which means it has no absolute zero — it only knows where "level" is because you told it. Fn+C sets the current position as centre, and you'll reach for it constantly: sitting down, standing up, or handing the device to somebody else all move where neutral is.

CC1  roll 64
CC74 pitch 64
Range ±45° = full scale
Drag the pad. This is the mapping XY mode uses; NOTES mode uses the same roll axis for CC1 and the pitch axis for channel pressure.

Three velocity sources

SourceDerived fromCharacter
strikechassis shock Experimental. A key bottoming out sends a transient through the case and the IMU sees it. Real velocity sensing on a keyboard that has none — if it survives contact with hardware.
tiltpitch angle Reliable and genuinely playable. How far you're leaning sets how hard the next notes land, so dynamics become a posture rather than an attack.
fixednothing Constant 100. Correct choice for organ, clav, and anything where you want the DAW's own velocity curve out of the picture.

06Command layer

Everything global lives under Fn. Holding it mutes the instrument, so you can change key or octave mid-take without a stuck note.

FIG. 3 The command layer. Orange keys change what you're playing; blue keys change how the sensors behave; the red key panics.
CombinationAction
Fn + 1…6Select mode
Fn + ,  .Octave down / up
Fn + [  ]Root note down / up
Fn + ;  'Previous / next scale
Fn + -  =MIDI channel down / up
Fn + LCycle grid layout
Fn + KScale lock on / off
Fn + ATilt aftertouch on / off
Fn + VVelocity source: strike / tilt / fixed
Fn + CZero the tilt to your current grip
Fn + X  YInvert a tilt axis
Fn + Z  \Panic — all notes off

07Flashing

Pair over Bluetooth from Audio MIDI Setup → MIDI Studio → Bluetooth. It appears as an ordinary MIDI source; no IP, no session, no dongle.

Build and stage both firmwares

/Users/zsahs/uvr-env-311/build-cardputer.sh

Build straight onto the SD card

/Users/zsahs/uvr-env-311/build-cardputer.sh /Volumes/YOURCARD

Flash over USB, skipping the card

cd /Users/zsahs/uvr-env-311/cardputer-midi && pio run -t upload

Run the musical logic tests

cd /Users/zsahs/uvr-env-311/cardputer-midi && \
  c++ -std=c++17 -I test/shim -I src \
  test/test_scales.cpp src/scales.cpp -o /tmp/t && /tmp/t
Use the app-only image

Stage firmware.bin. Do not merge it with bootloader.bin and partitions.bin using esptool — the launcher writes your image into an app partition and the bootloader already on the device stays put. A merged image on the SD card simply won't boot, and it's the most common way this goes wrong.

08Multi-boot layout

M5Launcher installs rather than boots — it writes the selected image into an app partition and reboots into it. With the stock table that means one resident app and a reflash every time you switch. Carving custom partitions with PMan is what turns it into real multi-boot.

FIG. 4 8 MB of flash, drawn to scale. UniGeek alone is larger than Launcher and both of my firmwares combined, and its own layout claims 5 MB of filesystem — which is the real constraint, not the code size.
FirmwareBinaryNote
UniGeek2,866 KWants a 3072 K slot plus 5000 K of SPIFFS
Launcher 2.8.01,368 KNo ADV-specific asset in the release — check which build you run
HA Remote1,039 KWill grow as you add menu items
Cardputer MIDI729 KLots of headroom

The three-partition plan is the better trade: UniGeek stays resident with a workable filesystem, and the two small firmwares share a swap slot you reinstall from SD in a few seconds. Cramming all four leaves every slot at 93–97% with nowhere to grow.

09Tuning strike velocity

The experimental one. Three constants in config.h govern it, and they interact — change one at a time.

ConstantDefaultSymptom it fixes
STRIKE_THRESH_G0.18 Everything reads loud → raise it. Soft presses land on the fallback velocity of 64 → lower it.
STRIKE_FULL_G1.10 Can't reach 127 without hurting your hand → lower it. Everything pins at 127 → raise it.
STRIKE_WINDOW25 ms Neighbouring keys stealing each other's transients → shorten it. Hits reading as 64 at random → lengthen it.

If it stays noisy, Fn+V to tilt is not a consolation prize — leaning to set dynamics is expressive in its own right and perfectly stable.

10Known unknowns

Everything here is compile-verified, and the musical logic is covered by host-side tests that already caught one real bug — Csus4 being named Fsus2/C, because the bass note has to win over table order. None of it has run on hardware yet. Three things to check first:

  • Tilt axis direction. I couldn't confirm which physical direction the BMI270's roll and pitch map to in the hand. Fn+X and Fn+Y invert either axis if they feel backwards.
  • Strike velocity. Adjacent-key crosstalk could sink it entirely. Section 09 is the tuning path; tilt velocity is the fallback.
  • Chord rollover depth. The TCA8418 reports every held key, but how many simultaneous notes actually make it through is worth measuring before writing parts that assume six-note voicings.

Latency is a known quantity rather than an unknown: BLE MIDI jitters 7–10 ms. Inaudible for tilt, breath and expression; marginal for tight drum triggering. The fix if it bites is RTP-MIDI over WiFi, which is a transport swap — everything in midi_out.cpp stays as it is.