Add Phase 1 Circle firmware

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
This commit is contained in:
prosolis
2026-08-27 22:09:47 -07:00
parent e5b92de3e6
commit 3904703de1
18 changed files with 1176 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
//
// config.h
//
// Piano LED Visualizer on Circle - all tunable parameters.
//
// Every value in this file is a product decision that Phase 0 of
// PIANO-LED-CIRCLE-PLAN.md exists to answer. Bench-test on Raspberry Pi OS
// first, then transcribe the answers here and build once.
//
#ifndef _config_h
#define _config_h
// --------------------------------------------------------------------------
// Keybed and strip geometry (plan section 6)
// --------------------------------------------------------------------------
// An 88-key keybed spans MIDI notes 21 (A0) through 108 (C8).
#define MIDI_NOTE_MIN 21
#define MIDI_NOTE_MAX 108
#define KEY_COUNT (MIDI_NOTE_MAX - MIDI_NOTE_MIN + 1) // 88
// LEDs per key. At 144 LEDs/m, 2 per key spans 1.222m, which lines up with a
// standard 88-key keybed almost exactly.
#ifndef LEDS_PER_KEY
#define LEDS_PER_KEY 2
#endif
#define LED_COUNT (KEY_COUNT * LEDS_PER_KEY) // 176
// Strip orientation. Pixel 0 of a WS2812B strip is at the end the data line
// enters. Decide this AFTER the strip is physically mounted, then flip this
// one flag.
//
// 0 = pixel 0 is at the bass end -> led = (note - 21) * 2
// 1 = pixel 0 is at the treble end -> led = (108 - note) * 2
#ifndef STRIP_REVERSED
#define STRIP_REVERSED 0
#endif
// --------------------------------------------------------------------------
// Power safety (plan section 7) - NOT optional
// --------------------------------------------------------------------------
//
// 176 LEDs at full white draw ~60mA each = 10.56A theoretical maximum, against
// a 6A supply. Real playing never approaches that (a ten-finger chord lights 20
// LEDs, ~1.2A), but a firmware bug that whites out the strip would brown out
// the rail. These two clamps make that unreachable rather than unlikely.
// Global brightness ceiling, applied to every channel of every pixel.
// 0-255. At 96 a full-strip white would draw roughly 4A, still inside 6A.
#ifndef GLOBAL_BRIGHTNESS
#define GLOBAL_BRIGHTNESS 96
#endif
// Hard cap on simultaneously lit keys. Beyond this, further held notes are
// tracked but not lit, so current draw stays bounded no matter what arrives
// on the wire. 20 keys is a ten-finger chord; 30 leaves room for pedal-held
// passages without ever approaching the supply limit.
#ifndef MAX_LIT_KEYS
#define MAX_LIT_KEYS 30
#endif
// --------------------------------------------------------------------------
// Colour (Phase 0 decides these against the actual diffuser)
// --------------------------------------------------------------------------
//
// Colours look substantially different through a diffuser than on bare strip.
// Do not finalise these from a photo.
// Colour for a played key, before brightness scaling.
#ifndef NOTE_COLOR_R
#define NOTE_COLOR_R 0
#endif
#ifndef NOTE_COLOR_G
#define NOTE_COLOR_G 140
#endif
#ifndef NOTE_COLOR_B
#define NOTE_COLOR_B 255
#endif
// Distinct colour for a "next note to play" hint driven by learning software
// on the PC (plan Phase 3). Reached over MIDI channel HINT_MIDI_CHANNEL.
#ifndef HINT_COLOR_R
#define HINT_COLOR_R 255
#endif
#ifndef HINT_COLOR_G
#define HINT_COLOR_G 80
#endif
#ifndef HINT_COLOR_B
#define HINT_COLOR_B 0
#endif
// --------------------------------------------------------------------------
// Velocity response
// --------------------------------------------------------------------------
// 1 = velocity scales pixel brightness, 0 = every key lights at full
// GLOBAL_BRIGHTNESS regardless of how hard it was struck.
#ifndef VELOCITY_SENSITIVE
#define VELOCITY_SENSITIVE 1
#endif
// Floor for velocity scaling, as a percentage. A pianissimo note should still
// be clearly visible, so velocity maps onto [VELOCITY_FLOOR_PCT, 100] rather
// than onto [0, 100].
#ifndef VELOCITY_FLOOR_PCT
#define VELOCITY_FLOOR_PCT 35
#endif
// --------------------------------------------------------------------------
// MIDI routing
// --------------------------------------------------------------------------
// Channel carrying notes actually played on the piano. 0-15 on the wire
// (channel 1 in a DAW), or MIDI_CHANNEL_ANY to accept every channel.
#define MIDI_CHANNEL_ANY 0xFF
#ifndef NOTE_MIDI_CHANNEL
#define NOTE_MIDI_CHANNEL MIDI_CHANNEL_ANY
#endif
// Channel reserved for Phase 3 "light the next key" hints from the PC. Kept
// separate from played notes so the two never overwrite each other. Set to
// MIDI_CHANNEL_NONE to ignore hints entirely.
#define MIDI_CHANNEL_NONE 0xFE
#ifndef HINT_MIDI_CHANNEL
#define HINT_MIDI_CHANNEL 15 // channel 16 in a DAW
#endif
// --------------------------------------------------------------------------
// Hardware wiring (VERIFIED against circle/addon/WS28XX, do not guess)
// --------------------------------------------------------------------------
//
// CWS28XXStripe clocks the WS2812B waveform out over SPI at a fixed 6.4MHz,
// encoding each LED bit as one SPI byte. On SPI master device 0 that puts the
// data line on:
//
// MOSI = GPIO10 (BCM) = physical pin 19
//
// Feed that through a 74AHCT125 to get a 5V logic level at the strip, and tie
// the Pi's ground to the LED supply ground. See plan section 7.
#ifndef SPI_MASTER_DEVICE
#define SPI_MASTER_DEVICE 0
#endif
#endif