Hardware arrives tomorrow, which makes this the blocking work: the appliance has no console, so without it there is no way to answer "is pixel 0 at the end I think it is" except by guessing. Geometric mapping. NOTE_MAP_GEOMETRIC (default on) derives each key from white-key geometry rather than semitone index: 52 white keys span the strip, so a white key is LED_COUNT/52 pixels - about 3.38 at 176 LEDs, not 2 - with black keys on the boundaries. The old linear map drifts within each octave, worst at F, by up to ~0.87 LEDs (~6mm) even after an optimal offset and scale. Set NOTE_MAP_GEOMETRIC=0 to restore it. This exposed a real bug. Under the geometric map adjacent key spans overlap, because the semitone pitch (~1.7 LEDs) is narrower than LEDS_PER_KEY. The renderer painted unlit keys black, so a key erased its lit neighbour's pixels. It now clears once and paints only lit keys. The linear map never overlapped, so this could not have been found without the geometry change. LED_OFFSET shifts every key, absorbing where the strip was actually cut and where the profile ended up. Off-strip pixels are clipped, never wrapped. Calibration patterns, selected by CC 20, with CC 21/22 setting the pixel for the walk: ends (orientation and length), octaves (mapping drift), keys (whole mapping at once), walk (finding LED_OFFSET), all (voltage droop at the far end). Patterns run at the same brightness ceiling as normal operation, so none can exceed the current budget the design already allows. tools/calibrate.sh drives all of it from the PC over ALSA MIDI, and README carries the six-step procedure in dependency order. Verified: tests pass across fourteen configurations, now including both mapping modes and positive, negative and reversed offsets. Both platforms build clean - pianoled.uf2 for RP2350 and both Circle kernel images - with no warnings from project sources. Claude-Session: https://claude.ai/code/session_01TVCB25LBsmeteWvaSMz4Ne
9.7 KiB
Piano LED Visualizer — firmware
Bare-metal firmware that lights a WS2812B strip above an 88-key keybed in response to MIDI. Two platforms, one shared implementation:
| Platform | Board | USB MIDI | Strip |
|---|---|---|---|
| Pico | RP2040 / RP2350 | TinyUSB device | PIO |
| Circle | Pi Zero / Zero 2 | CUSBMIDIGadget |
SPI |
All the visualizer logic lives in src/ and is shared verbatim. Each platform
supplies two small things: an ILEDStrip implementation, and a main loop that
feeds MIDI packets to CPianoLEDs::OnMIDIPacket().
The design rationale, bill of materials, electrical notes and project phases live in PIANO-LED-CIRCLE-PLAN.md. This file covers only how to build and run the firmware (plan Phase 1).
What this is
The board is a USB MIDI device. The PC is the host and owns everything else — the piano connection, the learning software, the song library. From the PC's side this firmware is just another ALSA MIDI output port:
Casio PX-S7000 --USB-B--> PC --USB--> board (this firmware) --> WS2812B strip
There is no network stack, no shell, and nothing writable at runtime. The board boots into this firmware in about a second and does one job.
On the Circle build the piano cannot be plugged into the board directly: Circle has no OTG support, so its USB controller is gadget-only and all MIDI must arrive from the PC. RP2040/RP2350 can do USB host, so that restriction is a Circle property rather than an architectural one — but nothing here uses it.
Layout
| Path | |
|---|---|
src/config.h |
Every tunable. Start here. |
src/pianoleds.cpp |
Note-to-LED mapping, colour, brightness clamps. Platform-independent. |
src/ledstrip.h |
ILEDStrip — the entire hardware surface the logic depends on. |
pico/ |
RP2040 / RP2350 backend: TinyUSB MIDI + PIO WS2812. |
firmware/ |
Circle backend: CUSBMIDIGadget + CWS28XXStripe. |
tests/ |
Host-side tests. No toolchain or SDK needed. |
circle/ |
Circle as a submodule, pinned to Step51. |
Building — Pico (RP2040 / RP2350)
sudo apt-get install gcc-arm-none-eabi cmake
git clone --recursive https://github.com/raspberrypi/pico-sdk # if you lack one
PICO_SDK_PATH=/path/to/pico-sdk ./pico/build.sh
Produces pico/build/pianoled.uf2. Hold BOOTSEL while plugging the board in
and copy the .uf2 onto the drive that appears.
BOARD selects the target (pico, pico2, pico_w, pico2_w; default
pico2). All four run this firmware unchanged — the radio variants simply
leave their radio unused.
The strip data pin is WS2812_PIN in src/config.h, default GPIO2. PIO can
drive it from any GPIO, so this is a free choice.
Building — Circle (Raspberry Pi Zero)
Circle is a submodule pinned to Step51, so clone recursively:
git clone --recursive <this repo>
# or, in an existing clone:
git submodule update --init
Then:
sudo apt-get install gcc-arm-none-eabi # Debian/Ubuntu
./build.sh
The pin is deliberate. A pinned Circle tree still builds in five years; nothing here tracks a moving upstream.
This produces two images in boot/, which coexist on one card:
| Image | RASPPI |
Model |
|---|---|---|
kernel.img |
1 | Pi Zero / Zero W (ARM1176) |
kernel7.img |
2 | Pi Zero 2 / Zero 2 W (Cortex-A7) |
The Pi picks the right one at boot, so the same SD card runs on either model.
SD card (Circle only)
The Pico boots from internal flash and needs none of this.
FAT32, single partition. Copy in:
boot/kernel.imgandboot/kernel7.img- From the Raspberry Pi firmware repo
boot/directory:bootcode.bin,start.elf,fixup.dat cmdline.txt(optional). Circle reads kernel options from it; useful for raising the log level while bringing the board up.
Nothing is ever written to the card at runtime, so pulling the power mid-note cannot corrupt it.
Wiring
On Pico, the WS2812B waveform comes from a PIO state machine, so the data
line is any GPIO you like — WS2812_PIN in src/config.h, default GPIO2.
Both hardware SPI blocks stay free.
On Circle, verified against circle/addon/WS28XX: CWS28XXStripe clocks
the waveform out over SPI at a fixed 6.4 MHz, encoding one LED bit per SPI
byte. On SPI master device 0 that fixes the data line at MOSI = GPIO10 (BCM)
= physical pin 19, and it occupies the only SPI master a Pi Zero exposes.
Three things from plan section 7 that are not optional:
- Level shifter. WS2812B wants logic high at 0.7 × VDD = 3.5V on a 5V rail; the Pi's GPIO is 3.3V. Put a 74AHCT125 on the data line. Skipping this is the single most common cause of "the strip flickers intermittently".
- Common ground. The Pi's ground and the LED supply's ground must be tied.
- Power injection. Feed 5V at both ends of the strip.
On a Pi Zero, connect the PC to the USB port, not PWR, with a data cable. On a Pico, the single USB connector is the one.
Configuring
Everything is in firmware/config.h, and every value there is a Phase 0
question. Two are load-bearing:
STRIP_REVERSED— whether pixel 0 sits at the bass or treble end. Decide this after the strip is physically mounted; it is one flag to flip.GLOBAL_BRIGHTNESSandMAX_LIT_KEYS— the power clamps. 176 LEDs at full white would draw ~10.5A against a 6A supply. Real playing never comes close, but these two make a whited-out strip unreachable rather than merely unlikely. Do not raise them without redoing the arithmetic in plan section 7.
Any of them can also be overridden at build time without editing the file:
make -C firmware EXTRADEFINE=-DSTRIP_REVERSED=1 # Circle
cmake -B pico/build -S pico -DCMAKE_CXX_FLAGS=-DSTRIP_REVERSED=1 # Pico
Note-to-LED mapping
NOTE_MAP_GEOMETRIC (default 1) derives each key's position from white-key
geometry: 52 white keys span the strip, so a white key is LED_COUNT / 52
pixels — ~3.38 at 176 LEDs, not 2 — with black keys on the boundaries.
Setting it to 0 restores the plan's original (note - 21) * 2. That map is
linear in semitone index, but a keybed is not: it drifts within each octave,
worst at F, by up to ~0.87 LEDs (~6mm) even after an optimal offset and scale.
Keep it only to reproduce the original behaviour.
One consequence of the geometric map: adjacent key spans overlap, because
the semitone pitch (~1.7 LEDs) is narrower than LEDS_PER_KEY. That is
expected, and the renderer paints only lit keys so a neighbour cannot erase
them.
Calibration
This is a headless appliance, so calibration runs over MIDI — the one channel
that already exists. tools/calibrate.sh drives it from the PC:
tools/calibrate.sh list # find the port
tools/calibrate.sh ends # pixel 0 (red), last pixel (green)
tools/calibrate.sh octaves # every C, middle C in red
tools/calibrate.sh keys # every key: white green, black blue
tools/calibrate.sh walk 37 # one pixel only
tools/calibrate.sh sweep # walk every pixel in turn
tools/calibrate.sh all # every pixel — voltage droop test
tools/calibrate.sh off # back to normal
A pattern replaces the note display entirely while it is active; off
restores it.
Procedure
Work in this order — each step depends on the one before.
ends— one pixel lights at each end of the strip. If red is at the treble end, setSTRIP_REVERSED 1and rebuild. If either end is dark, the strip is not the lengthLED_COUNTassumes.all— every pixel white. Watch the far end: if it drifts dim or warm, the strip needs 5V injected at that end too. This draws roughlyLED_COUNT × 3 × GLOBAL_BRIGHTNESS/255 × 20mA— about 4A at the defaults, inside a 6A supply but well beyond normal play, which caps atMAX_LIT_KEYS.keys— every key lit, whites and blacks in different colours. Check the colours line up with the actual keys across the whole span. This is the fastest way to see a mapping or length error.octaves— every C, middle C in red. Drift shows up as the marks walking off the keys as you move up the keyboard. With the geometric map they should stay put.sweeporwalk <n>— step one pixel at a time until you find the pixel sitting over A0. If that is not the pixel the firmware expects, the difference is yourLED_OFFSET. Set it and rebuild.chromatic— plays every key in turn. Watch for the lit span leading or lagging the key as it climbs.
Then decide the product questions the firmware cannot: colours through the
diffuser (not bare), brightness, and whether velocity should modulate
anything. All of them live in src/config.h.
MIDI behaviour
- Notes 21–108 (A0–C8) map to the strip; anything outside is dropped.
- Note On with velocity 0 is treated as Note Off.
- CC 120 (All Sound Off) and CC 123 (All Notes Off) clear the strip.
- Notes on
HINT_MIDI_CHANNEL(default channel 16) light in a separate colour, for the plan's Phase 3 "light the next key to play". A key actually being played takes precedence over a hint on the same key. - Notes held when the USB host suspends are cleared, so nothing stays lit.
- CC 20 selects a calibration pattern; CC 21/22 set the pixel for the walk pattern. See Calibration above.
Tests
./tests/run.sh
Compiles the real src/pianoleds.cpp against a capture backend that records
pixels in memory, and exercises the mapping, the note-off paths, the range
clamping and both power clamps across nine configuration variants.
Because the logic depends only on ILEDStrip, this needs no ARM toolchain, no
Circle, and no pico-sdk — just g++. It checks the arithmetic, not the
wiring, and does not replace bench-testing on real hardware.