Raspberry Pi supply is unreliable, and Circle is Broadcom-only - there is no Allwinner or Rockchip support anywhere in its tree, so an Orange Pi is not a board swap but a restart on an unproven base. RP2040/RP2350 is the better answer: available, ~$4, and a better fit for this job than the Zero ever was. Structure. All the visualizer logic moves to src/ and is now platform-independent, depending only on ILEDStrip (four methods) with MIDI pushed in via OnMIDIPacket(). Each platform supplies a backend and a main loop. The Circle build is unchanged in behaviour and still produces both kernel images. Pico backend: - WS2812B from a PIO state machine, which clocks the 1.25us bit cell directly rather than faking it with 8 SPI bytes per data bit as the Circle build must. - TinyUSB MIDI 1.0 device. Enumerates as an ordinary ALSA port, as the Circle gadget does. Packet framing comes from the USB MIDI Code Index Number rather than being re-derived. - Mount, unmount, suspend and resume all clear held notes, so a chord held when the host goes away cannot stay lit. - Latch spacing is enforced against a timestamp, so a caller cannot start a frame inside the WS2812B reset window. Verified: builds clean for both pico (RP2040, 30052 bytes) and pico2 (RP2350, 28284 bytes), no warnings from project sources, and the Circle build still produces kernel.img and kernel7.img. Tests pass across nine configurations. Incidental findings. PIO frees both hardware SPI blocks; on a Pi Zero Circle exposes only one SPI master (DEVICES=1 for RASPPI<4) and the WS2812B driver monopolises it, so a display and the strip could not coexist there. RP2040/ RP2350 also support USB host and, on the W variants, BLE via btstack - both of which section 3a records as impossible on Circle. Also documents a known limitation found while looking at calibration: the note-to-LED map is linear in semitone index, but a keybed is not. 52 white keys span the same 1222mm, making one white key ~3.38 LEDs rather than 2. The error drifts within each octave, worst at F, by up to ~0.87 LEDs (~6mm) even after an optimal offset and scale. A geometric map would remove it. Not yet implemented. Claude-Session: https://claude.ai/code/session_01TVCB25LBsmeteWvaSMz4Ne
182 lines
7.1 KiB
Markdown
182 lines
7.1 KiB
Markdown
# 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 <this repo>
|
||
# 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.
|