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
150 lines
3.5 KiB
C++
150 lines
3.5 KiB
C++
//
|
|
// 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";
|
|
|
|
void CKernel::MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength,
|
|
unsigned nDevice, void *pParam)
|
|
{
|
|
CKernel *pThis = static_cast<CKernel *> (pParam);
|
|
assert (pThis != 0);
|
|
|
|
pThis->m_PianoLEDs.OnMIDIPacket (pPacket, nLength);
|
|
}
|
|
|
|
CKernel::CKernel (void)
|
|
: m_Timer (&m_Interrupt),
|
|
m_Logger (m_Options.GetLogLevel (), &m_Timer),
|
|
m_pUSB (new CUSBMIDIGadget (&m_Interrupt)),
|
|
m_pMIDIDevice (0),
|
|
m_PianoLEDs (m_LEDStrip)
|
|
{
|
|
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)
|
|
{
|
|
// The gadget builds a new device object on each enumeration, so
|
|
// clear anything held at the moment the host went away.
|
|
m_PianoLEDs.AllOff ();
|
|
|
|
m_pMIDIDevice->RegisterPacketHandler (MIDIPacketHandler, this);
|
|
|
|
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;
|
|
}
|