Port to RP2040/RP2350, extract the shared logic

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
This commit is contained in:
prosolis
2026-08-27 22:53:00 -07:00
parent 138efc28ca
commit 469b321a40
27 changed files with 1046 additions and 267 deletions
+63 -57
View File
@@ -2,9 +2,8 @@
// Host-side tests for the note-to-LED mapping and the power clamps.
// Compiles the real firmware/pianoleds.cpp against stubbed Circle headers.
//
#define private public // inspect the captured strip state
#include "pianoleds.h"
#undef private
#include "capturestrip.h"
#include <cstdio>
#include <cstring>
@@ -17,29 +16,39 @@ static void Check (const char *pName, bool bCond)
if (!bCond) g_nFail++;
}
static unsigned CountLit (CPianoLEDs &L)
static CCaptureLEDStrip Strip (LED_COUNT);
static CPianoLEDs LEDs (Strip);
static unsigned CountLit (void)
{
unsigned n = 0;
for (auto &p : L.m_Stripe.m_Pixels)
for (auto &p : Strip.m_Pixels)
if (p[0] || p[1] || p[2]) n++;
return n;
}
static bool Dark (CPianoLEDs &L, unsigned i)
static bool Dark (unsigned i)
{
auto &p = L.m_Stripe.m_Pixels.at (i);
auto &p = Strip.m_Pixels.at (i);
return !p[0] && !p[1] && !p[2];
}
// Deliver a plain MIDI message the way a platform backend would.
static void Inject (uint8_t a, uint8_t b, uint8_t c)
{
uint8_t packet[3] = {a, b, c};
LEDs.OnMIDIPacket (packet, 3);
}
// every pixel of one key's span is lit
static bool Span (CPianoLEDs &L, unsigned nBase)
static bool Span (unsigned nBase)
{
for (unsigned i = 0; i < LEDS_PER_KEY; i++)
if (Dark (L, nBase + i)) return false;
if (Dark (nBase + i)) return false;
return true;
}
static unsigned LedFor (u8 ucNote)
static unsigned LedFor (uint8_t ucNote)
{
unsigned nKey = ucNote - MIDI_NOTE_MIN;
#if STRIP_REVERSED
@@ -54,63 +63,60 @@ int main (void)
printf ("STRIP_REVERSED=%d LED_COUNT=%d MAX_LIT_KEYS=%d GLOBAL_BRIGHTNESS=%d\n\n",
STRIP_REVERSED, LED_COUNT, MAX_LIT_KEYS, GLOBAL_BRIGHTNESS);
CUSBMIDIDevice MIDI;
CPianoLEDs LEDs;
LEDs.Initialize ();
LEDs.AttachMIDIDevice (&MIDI);
// --- lowest key, A0 = note 21 -------------------------------------
MIDI.Inject (0x90, 21, 127);
Inject (0x90, 21, 127);
LEDs.Update ();
#if STRIP_REVERSED
unsigned nLow = (KEY_COUNT - 1) * LEDS_PER_KEY; // 174
#else
unsigned nLow = 0;
#endif
Check ("note 21 lights its whole key span", Span (LEDs, nLow));
Check ("note 21 lights exactly LEDS_PER_KEY LEDs", CountLit (LEDs) == LEDS_PER_KEY);
Check ("note 21 lights its whole key span", Span (nLow));
Check ("note 21 lights exactly LEDS_PER_KEY LEDs", CountLit () == LEDS_PER_KEY);
// --- highest key, C8 = note 108 -----------------------------------
MIDI.Inject (0x80, 21, 0);
MIDI.Inject (0x90, 108, 127);
Inject (0x80, 21, 0);
Inject (0x90, 108, 127);
LEDs.Update ();
#if STRIP_REVERSED
unsigned nHigh = 0;
#else
unsigned nHigh = (KEY_COUNT - 1) * LEDS_PER_KEY; // 174
#endif
Check ("note 108 lights its whole key span", Span (LEDs, nHigh));
Check ("note 108 lights exactly LEDS_PER_KEY LEDs", CountLit (LEDs) == LEDS_PER_KEY);
Check ("note 108 lights its whole key span", Span (nHigh));
Check ("note 108 lights exactly LEDS_PER_KEY LEDs", CountLit () == LEDS_PER_KEY);
Check ("the two extremes are at opposite ends", nLow != nHigh);
// --- note off ------------------------------------------------------
MIDI.Inject (0x80, 108, 0);
Inject (0x80, 108, 0);
LEDs.Update ();
Check ("note off extinguishes the key", CountLit (LEDs) == 0);
Check ("note off extinguishes the key", CountLit () == 0);
// --- note on with velocity 0 is a note off -------------------------
MIDI.Inject (0x90, 60, 100);
Inject (0x90, 60, 100);
LEDs.Update ();
Check ("note on lights middle C", CountLit (LEDs) == LEDS_PER_KEY);
MIDI.Inject (0x90, 60, 0);
Check ("note on lights middle C", CountLit () == LEDS_PER_KEY);
Inject (0x90, 60, 0);
LEDs.Update ();
Check ("note on velocity 0 acts as note off", CountLit (LEDs) == 0);
Check ("note on velocity 0 acts as note off", CountLit () == 0);
// --- out-of-range notes are dropped, not clamped into the strip ----
MIDI.Inject (0x90, 20, 127);
MIDI.Inject (0x90, 109, 127);
MIDI.Inject (0x90, 0, 127);
MIDI.Inject (0x90, 127, 127);
Inject (0x90, 20, 127);
Inject (0x90, 109, 127);
Inject (0x90, 0, 127);
Inject (0x90, 127, 127);
LEDs.Update ();
Check ("notes outside 21-108 are ignored", CountLit (LEDs) == 0);
Check ("notes outside 21-108 are ignored", CountLit () == 0);
// --- brightness ceiling --------------------------------------------
for (u8 n = MIDI_NOTE_MIN; n <= MIDI_NOTE_MAX; n++)
MIDI.Inject (0x90, n, 127);
for (uint8_t n = MIDI_NOTE_MIN; n <= MIDI_NOTE_MAX; n++)
Inject (0x90, n, 127);
LEDs.Update ();
bool bWithinCeiling = true;
#if GLOBAL_BRIGHTNESS < 255 // at 255 a u8 channel cannot exceed the ceiling by construction
for (auto &p : LEDs.m_Stripe.m_Pixels)
for (auto &p : Strip.m_Pixels)
for (int c = 0; c < 3; c++)
if (p[c] > GLOBAL_BRIGHTNESS) bWithinCeiling = false;
#endif
@@ -118,65 +124,65 @@ int main (void)
// --- simultaneous-key cap ------------------------------------------
Check ("all 88 keys held stays within MAX_LIT_KEYS",
CountLit (LEDs) <= MAX_LIT_KEYS * LEDS_PER_KEY);
CountLit () <= MAX_LIT_KEYS * LEDS_PER_KEY);
// --- all notes off --------------------------------------------------
MIDI.Inject (0xB0, 123, 0);
Inject (0xB0, 123, 0);
LEDs.Update ();
Check ("CC 123 (all notes off) clears the strip", CountLit (LEDs) == 0);
Check ("CC 123 (all notes off) clears the strip", CountLit () == 0);
for (u8 n = MIDI_NOTE_MIN; n <= MIDI_NOTE_MAX; n++)
MIDI.Inject (0x90, n, 127);
MIDI.Inject (0xB0, 120, 0);
for (uint8_t n = MIDI_NOTE_MIN; n <= MIDI_NOTE_MAX; n++)
Inject (0x90, n, 127);
Inject (0xB0, 120, 0);
LEDs.Update ();
Check ("CC 120 (all sound off) clears the strip", CountLit (LEDs) == 0);
Check ("CC 120 (all sound off) clears the strip", CountLit () == 0);
// --- velocity sensitivity -------------------------------------------
MIDI.Inject (0x90, 60, 127);
Inject (0x90, 60, 127);
LEDs.Update ();
auto Loud = LEDs.m_Stripe.m_Pixels.at (LedFor (60));
MIDI.Inject (0x90, 60, 1);
auto Loud = Strip.m_Pixels.at (LedFor (60));
Inject (0x90, 60, 1);
LEDs.Update ();
auto Soft = LEDs.m_Stripe.m_Pixels.at (LedFor (60));
auto Soft = Strip.m_Pixels.at (LedFor (60));
#if VELOCITY_SENSITIVE
Check ("a soft note is dimmer than a loud one", Soft[2] < Loud[2]);
Check ("a soft note is still visible", Soft[2] > 0);
#else
Check ("velocity does not change brightness", Soft[2] == Loud[2]);
#endif
MIDI.Inject (0x80, 60, 0);
Inject (0x80, 60, 0);
// --- hint channel ----------------------------------------------------
#if HINT_MIDI_CHANNEL != MIDI_CHANNEL_NONE
MIDI.Inject (0x90 | HINT_MIDI_CHANNEL, 64, 127);
Inject (0x90 | HINT_MIDI_CHANNEL, 64, 127);
LEDs.Update ();
auto Hint = LEDs.m_Stripe.m_Pixels.at (LedFor (64));
auto Hint = Strip.m_Pixels.at (LedFor (64));
Check ("a hint note lights in the hint colour", Hint != Loud && (Hint[0] || Hint[1] || Hint[2]));
// a key actually played wins over a hint on the same key
MIDI.Inject (0x90, 64, 127);
Inject (0x90, 64, 127);
LEDs.Update ();
auto Both = LEDs.m_Stripe.m_Pixels.at (LedFor (64));
auto Both = Strip.m_Pixels.at (LedFor (64));
Check ("a played note overrides a hint on the same key", Both == Loud);
// releasing the played note falls back to the still-pending hint
MIDI.Inject (0x80, 64, 0);
Inject (0x80, 64, 0);
LEDs.Update ();
auto Back = LEDs.m_Stripe.m_Pixels.at (LedFor (64));
auto Back = Strip.m_Pixels.at (LedFor (64));
Check ("releasing a played note reveals the hint again", Back == Hint);
#endif
// --- reconnect clears held notes -------------------------------------
MIDI.Inject (0x90, 60, 127);
LEDs.AttachMIDIDevice (&MIDI);
Inject (0x90, 60, 127);
LEDs.AllOff ();
LEDs.Update ();
Check ("re-enumeration clears notes held at suspend", CountLit (LEDs) == 0);
Check ("re-enumeration clears notes held at suspend", CountLit () == 0);
// --- short packets are not parsed as notes ----------------------------
u8 Short[1] = {0xF8}; // clock, 1 byte
MIDI.m_pHandler (0, Short, 1, 1, MIDI.m_pParam);
uint8_t Short[1] = {0xF8}; // clock, 1 byte
LEDs.OnMIDIPacket (Short, 1);
LEDs.Update ();
Check ("a 1-byte realtime message lights nothing", CountLit (LEDs) == 0);
Check ("a 1-byte realtime message lights nothing", CountLit () == 0);
printf ("\n%s\n", g_nFail ? "FAILURES" : "all tests passed");
return g_nFail != 0;