Files
PianoLED-Circle-Edition/README.md
T
prosolis 59bd3b4cea Add boot self-test and idle indicator; target the Waveshare RP2350-Plus
The board arriving is a Waveshare RP2350-Plus (4MB, USB-C), which is now the
default BOARD. Its pico-sdk header defines neither PICO_DEFAULT_LED_PIN nor
PICO_DEFAULT_WS2812_PIN - it has no onboard indicator at all - so a bare board
gives no sign of life. The status-LED support added for boards that do have one
(zero, one, tiny, usb_a on GPIO16; eth on GPIO25) is kept and compiles out here.

That makes feedback on the strip itself the useful path, and it turns out to be
the better one anyway:

- Boot self-test sweeps one pixel from index 0 to the far end once at startup.
  It answers in a single glance whether the firmware runs, PIO drives the line,
  the strip is the length LED_COUNT claims, the far end holds voltage, and -
  because you see which end it starts from - whether STRIP_REVERSED is right.
  It runs before USB, so the first test needs nothing but 5V.
- Idle indicator holds one dim pixel lit while no host is connected, separating
  "powered and waiting" from "no power" and from "crashed".

Both are platform-independent, so the Circle build gets them too.

Verified: tests pass across seventeen configurations, now including the
self-test and idle paths on and off. Both platforms build clean with no
warnings from project sources.

Note on the previous commit's verification: a filtered build log hid a real
compile error in the Pico target (sleep_ms takes uint32_t, which is unsigned
long here, and did not match the portable void(*)(unsigned) delay callback).
The build script now gets an explicit success check rather than a grep.

Claude-Session: https://claude.ai/code/session_01TVCB25LBsmeteWvaSMz4Ne
2026-08-27 23:22:56 -07:00

270 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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; the default is `waveshare_rp2350_plus_4mb`
(Waveshare RP2350-Plus, 4MB, USB-C). pico-sdk 2.3.0 also ships definitions for
eleven other Waveshare RP2350 boards and the stock `pico`/`pico2`:
```sh
ls "$PICO_SDK_PATH"/src/boards/include/boards/ | grep rp2350
BOARD=waveshare_rp2350_zero ./pico/build.sh
```
Note the RP2350-Plus has **no onboard LED** — its board header defines neither
`PICO_DEFAULT_LED_PIN` nor `PICO_DEFAULT_WS2812_PIN`, so the status-LED code
compiles out. Boards that do have one (`zero`, `one`, `tiny`, `usb_a` on
GPIO16; `eth` on GPIO25) get it automatically. On boards without, the boot
self-test and idle indicator on the strip itself are the feedback.
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
```
### 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.
## First power-up
Two things run before any PC is involved, so a bare board on a bench still
tells you something:
- **Boot self-test** — one pixel sweeps from index 0 to the far end, once, then
clears. Watching it answers, in a single glance: the firmware runs, PIO
drives the data line, the strip is the length `LED_COUNT` claims, the far end
still has voltage, and — because you see which end it starts from — whether
`STRIP_REVERSED` is the right way round.
- **Idle indicator** — while no USB host is connected, one dim pixel stays lit
at the strip's start. This distinguishes "powered and waiting for the PC"
from "no power" and from "crashed".
Both are on by default (`BOOT_SELF_TEST`, `IDLE_INDICATOR` in `src/config.h`).
So the very first test needs nothing but 5V: power the strip and the board, and
watch for the sweep.
## 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:
```sh
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.
1. **`ends`** — one pixel lights at each end of the strip. If red is at the
treble end, set `STRIP_REVERSED 1` and rebuild. If either end is dark, the
strip is not the length `LED_COUNT` assumes.
2. **`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 roughly
`LED_COUNT × 3 × GLOBAL_BRIGHTNESS/255 × 20mA` — about 4A at the defaults,
inside a 6A supply but well beyond normal play, which caps at
`MAX_LIT_KEYS`.
3. **`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.
4. **`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.
5. **`sweep`** or **`walk <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 your `LED_OFFSET`. Set it and rebuild.
6. **`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 21108 (A0C8) 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
```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.