Greenfield bare-metal firmware for a Pi Zero that lights a WS2812B strip above an 88-key keybed. The Pi is a USB MIDI gadget; the PC is the host and owns the piano connection and everything else. No network stack, no filesystem, no shell. Circle is a submodule pinned to Step51. Builds kernel.img (RASPPI=1, Zero / Zero W) and kernel7.img (RASPPI=2, Zero 2 / Zero 2 W); both coexist on one card, so either model runs from the same SD. Resolves two open assumptions from the plan against the Circle sources rather than by guessing: - CWS28XXStripe clocks the waveform out over SPI at a fixed 6.4MHz, one SPI byte per LED bit. On device 0 that puts data on MOSI = GPIO10 = physical pin 19. The implied 5.28ms frame time matches the plan's arithmetic. - The USB gadget lifecycle follows sample/29-miniorgan, which already carries a USB_GADGET_MODE path. Three details the hardware forces: - The gadget destroys and recreates its CUSBMIDIDevice across a USB suspend, so the kernel re-fetches it and clears notes held at that moment. Otherwise a chord would stay lit forever when the PC sleeps. - Rendering blocks for ~5.3ms of SPI traffic, so the MIDI packet handler only records state and the main loop draws. - MAX_LIT_KEYS complements the global brightness ceiling. 176 LEDs at full white would draw ~10.5A against a 6A supply; together the two clamps make that unreachable rather than merely unlikely. Every Phase 0 product decision has a named slot in firmware/config.h, all overridable at build time via EXTRADEFINE. tests/run.sh compiles the real pianoleds.cpp against stubbed Circle headers and checks the mapping, note-off paths, range clamping and both power clamps across nine configuration variants. It verifies arithmetic, not wiring, and does not replace bench-testing on real hardware. Claude-Session: https://claude.ai/code/session_01TVCB25LBsmeteWvaSMz4Ne
136 lines
4.7 KiB
Markdown
136 lines
4.7 KiB
Markdown
# Piano LED Visualizer — Circle firmware
|
||
|
||
Bare-metal firmware for a Raspberry Pi Zero that lights a WS2812B strip above
|
||
an 88-key keybed in response to MIDI.
|
||
|
||
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 Pi is a **USB MIDI gadget**. 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--> Pi Zero (this firmware) --> WS2812B strip
|
||
```
|
||
|
||
There is no network stack, no filesystem, no shell. The Pi boots into this
|
||
firmware in about a second and does one job.
|
||
|
||
Circle has no OTG support, so the USB controller is gadget-only here. The
|
||
piano **cannot** be plugged into the Pi directly; all MIDI arrives from the PC.
|
||
|
||
## Layout
|
||
|
||
| Path | |
|
||
|---|---|
|
||
| `firmware/config.h` | Every tunable. Start here. |
|
||
| `firmware/pianoleds.cpp` | Note-to-LED mapping, colour, brightness clamps. |
|
||
| `firmware/kernel.cpp` | USB gadget lifecycle and the main loop. |
|
||
| `tests/` | Host-side tests for the mapping and clamps. |
|
||
| `circle/` | Circle as a submodule, pinned to `Step51`. |
|
||
| `build.sh` | Builds both kernel images. |
|
||
|
||
## Building
|
||
|
||
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
|
||
|
||
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
|
||
|
||
Verified against `circle/addon/WS28XX`: `CWS28XXStripe` clocks the WS2812B
|
||
waveform out over SPI at a fixed 6.4 MHz, encoding one LED bit per SPI byte.
|
||
On SPI master device 0 that puts the data line on:
|
||
|
||
**MOSI = GPIO10 (BCM) = physical pin 19.**
|
||
|
||
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.
|
||
|
||
Connect the PC to the Zero's **USB** port, not **PWR**, with a data cable.
|
||
|
||
## 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
|
||
```
|
||
|
||
## 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 `firmware/pianoleds.cpp` against stubbed Circle headers and
|
||
exercises the mapping, the note-off paths, the range clamping and both power
|
||
clamps across nine configuration variants. This does not need the ARM
|
||
toolchain and does not replace bench-testing on real hardware — it checks the
|
||
arithmetic, not the wiring.
|