# 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](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) ```sh 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: ```sh git clone --recursive # or, in an existing clone: git submodule update --init ``` Then: ```sh 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.img` and `boot/kernel7.img` - From the [Raspberry Pi firmware repo](https://github.com/raspberrypi/firmware) `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_BRIGHTNESS` and `MAX_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: ```sh make -C firmware EXTRADEFINE=-DSTRIP_REVERSED=1 # Circle cmake -B pico/build -S pico -DCMAKE_CXX_FLAGS=-DSTRIP_REVERSED=1 # Pico ``` > **Known limitation:** the note-to-LED mapping is linear in semitone index, > but a real keybed is not — 52 white keys span the same 1222mm, so one white > key is ~3.38 LEDs rather than 2. This drifts within each octave, worst at F, > by up to ~0.87 LEDs (~6mm) even after an optimal offset and scale. A > geometric map derived from white-key positions would remove it. Not yet > implemented. ## 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. ## Tests ```sh ./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.