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
+135
View File
@@ -0,0 +1,135 @@
//
// kernel.cpp
//
// Piano LED Visualizer for Circle.
//
// The Pi is a USB MIDI *gadget*: the PC is the host, and this firmware appears
// on the PC as an ordinary ALSA MIDI output port. Circle has no OTG support, so
// the USB controller is gadget-only in this build and the piano can never be
// plugged in here directly - all MIDI arrives from the PC. See section 2 of
// PIANO-LED-CIRCLE-PLAN.md.
//
#include "kernel.h"
#include <circle/usb/gadget/usbmidigadget.h>
#include <circle/devicenameservice.h>
#include <assert.h>
static const char FromKernel[] = "kernel";
CKernel::CKernel (void)
: m_Timer (&m_Interrupt),
m_Logger (m_Options.GetLogLevel (), &m_Timer),
m_pUSB (new CUSBMIDIGadget (&m_Interrupt)),
m_pMIDIDevice (0)
{
m_ActLED.Blink (5); // show we are alive
}
CKernel::~CKernel (void)
{
}
boolean CKernel::Initialize (void)
{
boolean bOK = TRUE;
if (bOK)
{
bOK = m_Serial.Initialize (115200);
}
if (bOK)
{
// Headless appliance: there is no screen, so the log goes to the
// serial port and nowhere else.
bOK = m_Logger.Initialize (&m_Serial);
}
if (bOK)
{
bOK = m_Interrupt.Initialize ();
}
if (bOK)
{
bOK = m_Timer.Initialize ();
}
if (bOK)
{
// Bring the strip up before USB, so the LEDs are known-dark by the
// time the host can start sending us notes.
bOK = m_PianoLEDs.Initialize ();
}
if (bOK)
{
assert (m_pUSB != 0);
bOK = m_pUSB->Initialize ();
}
return bOK;
}
void CKernel::UpdateMIDIDevice (void)
{
assert (m_pUSB != 0);
if (!m_pUSB->UpdatePlugAndPlay ())
{
return;
}
// The gadget deletes its CUSBMIDIDevice when the host suspends the bus
// and builds a new one on the next enumeration, so the pointer we hold
// is only valid until the next status change. Re-fetch it every time.
CUSBMIDIDevice *pMIDIDevice =
(CUSBMIDIDevice *) m_DeviceNameService.GetDevice ("umidi1", FALSE);
if (pMIDIDevice == m_pMIDIDevice)
{
return;
}
m_pMIDIDevice = pMIDIDevice;
if (m_pMIDIDevice != 0)
{
m_PianoLEDs.AttachMIDIDevice (m_pMIDIDevice);
m_Logger.Write (FromKernel, LogNotice, "USB MIDI gadget connected");
}
else
{
// Host went away mid-chord. Do not leave keys lit.
m_PianoLEDs.AllOff ();
m_Logger.Write (FromKernel, LogNotice, "USB MIDI gadget disconnected");
}
}
TShutdownMode CKernel::Run (void)
{
m_Logger.Write (FromKernel, LogNotice, "Compile time: " __DATE__ " " __TIME__);
m_Logger.Write (FromKernel, LogNotice,
"%u LEDs, %u keys, notes %u-%u, %s orientation",
(unsigned) LED_COUNT, (unsigned) KEY_COUNT,
(unsigned) MIDI_NOTE_MIN, (unsigned) MIDI_NOTE_MAX,
STRIP_REVERSED ? "reversed" : "normal");
m_Logger.Write (FromKernel, LogNotice,
"Brightness ceiling %u/255, at most %u keys lit at once",
(unsigned) GLOBAL_BRIGHTNESS, (unsigned) MAX_LIT_KEYS);
m_Logger.Write (FromKernel, LogNotice, "Waiting for USB host");
for (;;)
{
UpdateMIDIDevice ();
// Rendering blocks for ~5.3ms of SPI traffic, which is why it runs
// here and not in the MIDI packet handler's IRQ context. Update()
// returns immediately when nothing has changed.
m_PianoLEDs.Update ();
}
return ShutdownHalt;
}