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:
@@ -0,0 +1,230 @@
|
||||
//
|
||||
// pianoleds.cpp
|
||||
//
|
||||
#include "pianoleds.h"
|
||||
#include <circle/util.h>
|
||||
#include <assert.h>
|
||||
|
||||
// MIDI status nibbles
|
||||
#define MIDI_NOTE_OFF 0x80
|
||||
#define MIDI_NOTE_ON 0x90
|
||||
#define MIDI_CONTROL_CHANGE 0xB0
|
||||
|
||||
// Control numbers that mean "stop everything"
|
||||
#define MIDI_CC_ALL_SOUND_OFF 120
|
||||
#define MIDI_CC_ALL_NOTES_OFF 123
|
||||
|
||||
CPianoLEDs::CPianoLEDs (void)
|
||||
: m_Stripe (WS2812B, LED_COUNT, 4000000, SPI_MASTER_DEVICE),
|
||||
m_bDirty (TRUE)
|
||||
{
|
||||
memset ((void *) m_KeyVelocity, 0, sizeof m_KeyVelocity);
|
||||
memset ((void *) m_HintVelocity, 0, sizeof m_HintVelocity);
|
||||
}
|
||||
|
||||
CPianoLEDs::~CPianoLEDs (void)
|
||||
{
|
||||
}
|
||||
|
||||
boolean CPianoLEDs::Initialize (void)
|
||||
{
|
||||
if (!m_Stripe.Initialize ())
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Start from a known-dark strip rather than whatever the pixels held
|
||||
// when power came up.
|
||||
return m_Stripe.Blackout ();
|
||||
}
|
||||
|
||||
void CPianoLEDs::AttachMIDIDevice (CUSBMIDIDevice *pMIDIDevice)
|
||||
{
|
||||
assert (pMIDIDevice != 0);
|
||||
|
||||
// The gadget destroys and recreates its CUSBMIDIDevice across a suspend,
|
||||
// so any notes held at that moment would otherwise stay lit forever.
|
||||
AllOff ();
|
||||
|
||||
pMIDIDevice->RegisterPacketHandler (MIDIPacketHandler, this);
|
||||
}
|
||||
|
||||
void CPianoLEDs::MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength,
|
||||
unsigned nDevice, void *pParam)
|
||||
{
|
||||
CPianoLEDs *pThis = static_cast<CPianoLEDs *> (pParam);
|
||||
assert (pThis != 0);
|
||||
|
||||
pThis->OnMIDIPacket (pPacket, nLength);
|
||||
}
|
||||
|
||||
void CPianoLEDs::OnMIDIPacket (const u8 *pPacket, unsigned nLength)
|
||||
{
|
||||
// Circle hands us one already-framed MIDI message of 1-3 bytes. Anything
|
||||
// shorter than a channel message cannot be a note event.
|
||||
if (nLength < 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
u8 ucStatus = pPacket[0] & 0xF0;
|
||||
u8 ucChannel = pPacket[0] & 0x0F;
|
||||
|
||||
switch (ucStatus)
|
||||
{
|
||||
case MIDI_NOTE_ON:
|
||||
// Note On with velocity 0 is the conventional Note Off.
|
||||
SetKey (pPacket[1], pPacket[2], ChannelMatches (ucChannel, HINT_MIDI_CHANNEL));
|
||||
break;
|
||||
|
||||
case MIDI_NOTE_OFF:
|
||||
SetKey (pPacket[1], 0, ChannelMatches (ucChannel, HINT_MIDI_CHANNEL));
|
||||
break;
|
||||
|
||||
case MIDI_CONTROL_CHANGE:
|
||||
if ( pPacket[1] == MIDI_CC_ALL_SOUND_OFF
|
||||
|| pPacket[1] == MIDI_CC_ALL_NOTES_OFF)
|
||||
{
|
||||
AllOff ();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CPianoLEDs::SetKey (u8 ucNote, u8 ucVelocity, boolean bHint)
|
||||
{
|
||||
// Drop anything off the ends of the keybed rather than trusting the
|
||||
// input; an out-of-range note would index past the strip.
|
||||
if ( ucNote < MIDI_NOTE_MIN
|
||||
|| ucNote > MIDI_NOTE_MAX)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned nKey = ucNote - MIDI_NOTE_MIN;
|
||||
|
||||
if (bHint)
|
||||
{
|
||||
m_HintVelocity[nKey] = ucVelocity;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_KeyVelocity[nKey] = ucVelocity;
|
||||
}
|
||||
|
||||
m_bDirty = TRUE;
|
||||
}
|
||||
|
||||
void CPianoLEDs::AllOff (void)
|
||||
{
|
||||
memset ((void *) m_KeyVelocity, 0, sizeof m_KeyVelocity);
|
||||
memset ((void *) m_HintVelocity, 0, sizeof m_HintVelocity);
|
||||
|
||||
m_bDirty = TRUE;
|
||||
}
|
||||
|
||||
boolean CPianoLEDs::ChannelMatches (u8 ucChannel, u8 ucWanted)
|
||||
{
|
||||
if (ucWanted == MIDI_CHANNEL_NONE)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (ucWanted == MIDI_CHANNEL_ANY)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return ucChannel == ucWanted;
|
||||
}
|
||||
|
||||
u8 CPianoLEDs::Scale (u8 ucChannel, u8 ucVelocity)
|
||||
{
|
||||
unsigned nValue = ucChannel;
|
||||
|
||||
// Global brightness ceiling. This is the clamp that keeps a whited-out
|
||||
// strip inside the supply's current budget; see config.h.
|
||||
nValue = nValue * GLOBAL_BRIGHTNESS / 255;
|
||||
|
||||
#if VELOCITY_SENSITIVE
|
||||
// Map velocity 1-127 onto [VELOCITY_FLOOR_PCT, 100] percent, so even the
|
||||
// softest note stays visible.
|
||||
unsigned nPercent = VELOCITY_FLOOR_PCT
|
||||
+ (100 - VELOCITY_FLOOR_PCT) * ucVelocity / 127;
|
||||
nValue = nValue * nPercent / 100;
|
||||
#endif
|
||||
|
||||
return (u8) nValue;
|
||||
}
|
||||
|
||||
void CPianoLEDs::Update (void)
|
||||
{
|
||||
if (!m_bDirty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the flag before reading state, not after. An event arriving
|
||||
// mid-render then leaves the flag set and we render again next pass,
|
||||
// rather than being dropped.
|
||||
m_bDirty = FALSE;
|
||||
|
||||
unsigned nLit = 0;
|
||||
|
||||
for (unsigned nKey = 0; nKey < KEY_COUNT; nKey++)
|
||||
{
|
||||
u8 ucVelocity = m_KeyVelocity[nKey];
|
||||
boolean bHint = FALSE;
|
||||
|
||||
if (ucVelocity == 0)
|
||||
{
|
||||
// A key being played wins over a "next note" hint on it.
|
||||
ucVelocity = m_HintVelocity[nKey];
|
||||
bHint = TRUE;
|
||||
}
|
||||
|
||||
u8 ucRed = 0;
|
||||
u8 ucGreen = 0;
|
||||
u8 ucBlue = 0;
|
||||
|
||||
// Bound the number of simultaneously lit keys, so no sequence of
|
||||
// MIDI events can drive the strip past the supply's budget.
|
||||
if ( ucVelocity != 0
|
||||
&& nLit < MAX_LIT_KEYS)
|
||||
{
|
||||
nLit++;
|
||||
|
||||
if (bHint)
|
||||
{
|
||||
ucRed = Scale (HINT_COLOR_R, ucVelocity);
|
||||
ucGreen = Scale (HINT_COLOR_G, ucVelocity);
|
||||
ucBlue = Scale (HINT_COLOR_B, ucVelocity);
|
||||
}
|
||||
else
|
||||
{
|
||||
ucRed = Scale (NOTE_COLOR_R, ucVelocity);
|
||||
ucGreen = Scale (NOTE_COLOR_G, ucVelocity);
|
||||
ucBlue = Scale (NOTE_COLOR_B, ucVelocity);
|
||||
}
|
||||
}
|
||||
|
||||
#if STRIP_REVERSED
|
||||
unsigned nBase = (KEY_COUNT - 1 - nKey) * LEDS_PER_KEY;
|
||||
#else
|
||||
unsigned nBase = nKey * LEDS_PER_KEY;
|
||||
#endif
|
||||
|
||||
for (unsigned i = 0; i < LEDS_PER_KEY; i++)
|
||||
{
|
||||
unsigned nLED = nBase + i;
|
||||
assert (nLED < LED_COUNT);
|
||||
|
||||
m_Stripe.SetLED (nLED, ucRed, ucGreen, ucBlue);
|
||||
}
|
||||
}
|
||||
|
||||
m_Stripe.Update ();
|
||||
}
|
||||
Reference in New Issue
Block a user