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:
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/sh
|
||||
# Host-side tests for the mapping and power-clamp logic.
|
||||
# Compiles the real firmware sources against stubbed Circle headers, across
|
||||
# every configuration variant that changes the rendering path.
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=$(mktemp -d)
|
||||
trap 'rm -rf "$OUT"' EXIT
|
||||
|
||||
FAIL=0
|
||||
for CFG in \
|
||||
"default:" \
|
||||
"reversed strip:-DSTRIP_REVERSED=1" \
|
||||
"velocity insensitive:-DVELOCITY_SENSITIVE=0" \
|
||||
"reversed + velocity insensitive:-DSTRIP_REVERSED=1 -DVELOCITY_SENSITIVE=0" \
|
||||
"full brightness:-DGLOBAL_BRIGHTNESS=255" \
|
||||
"tight key cap:-DMAX_LIT_KEYS=5" \
|
||||
"single LED per key:-DLEDS_PER_KEY=1" \
|
||||
"three LEDs per key:-DLEDS_PER_KEY=3" \
|
||||
"hints disabled:-DHINT_MIDI_CHANNEL=MIDI_CHANNEL_NONE" \
|
||||
; do
|
||||
NAME=${CFG%%:*}
|
||||
FLAGS=${CFG#*:}
|
||||
printf '\n=== %s ===\n' "$NAME"
|
||||
g++ -std=c++17 -Wall -Wextra -Wno-unused-parameter $FLAGS \
|
||||
-o "$OUT/t" -Itests/stubs -Ifirmware \
|
||||
tests/test_pianoleds.cpp firmware/pianoleds.cpp
|
||||
"$OUT/t" || FAIL=1
|
||||
done
|
||||
|
||||
printf '\n'
|
||||
if [ $FAIL -ne 0 ]; then echo "SOME CONFIGURATIONS FAILED"; exit 1; fi
|
||||
echo "all configurations passed"
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef _stub_ws28xx_h
|
||||
#define _stub_ws28xx_h
|
||||
#include <circle/types.h>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
enum TWS28XXType { WS2801, WS2812, WS2812B, SK6812 = WS2812B };
|
||||
|
||||
// Captures what the firmware pushed to the strip, so tests can inspect pixels.
|
||||
class CWS28XXStripe
|
||||
{
|
||||
public:
|
||||
std::vector<std::array<u8,3>> m_Pixels;
|
||||
unsigned m_nUpdates = 0;
|
||||
CWS28XXStripe (TWS28XXType, unsigned nLEDCount, unsigned = 4000000, unsigned = 0)
|
||||
: m_Pixels (nLEDCount, {0,0,0}) {}
|
||||
boolean Initialize (void) { return TRUE; }
|
||||
void SetLED (unsigned n, u8 r, u8 g, u8 b) { m_Pixels.at (n) = {r,g,b}; }
|
||||
boolean Update (void) { m_nUpdates++; return TRUE; }
|
||||
boolean Blackout (void)
|
||||
{
|
||||
for (auto &p : m_Pixels) p = {0,0,0};
|
||||
return TRUE;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef _stub_types_h
|
||||
#define _stub_types_h
|
||||
#include <cstdint>
|
||||
typedef uint8_t u8;
|
||||
typedef uint16_t u16;
|
||||
typedef uint32_t u32;
|
||||
typedef int boolean;
|
||||
#define TRUE 1
|
||||
#define FALSE 0
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef _stub_usbmidi_h
|
||||
#define _stub_usbmidi_h
|
||||
#include <circle/types.h>
|
||||
typedef void TMIDIPacketHandlerEx (unsigned nCable, u8 *pPacket, unsigned nLength,
|
||||
unsigned nDevice, void *pParam);
|
||||
class CUSBMIDIDevice
|
||||
{
|
||||
public:
|
||||
TMIDIPacketHandlerEx *m_pHandler = nullptr;
|
||||
void *m_pParam = nullptr;
|
||||
void RegisterPacketHandler (TMIDIPacketHandlerEx *p, void *pParam)
|
||||
{
|
||||
m_pHandler = p; m_pParam = pParam;
|
||||
}
|
||||
// Deliver a plain MIDI message the way Circle's driver would.
|
||||
void Inject (u8 a, u8 b, u8 c)
|
||||
{
|
||||
u8 packet[3] = {a, b, c};
|
||||
if (m_pHandler) m_pHandler (0, packet, 3, 1, m_pParam);
|
||||
}
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,4 @@
|
||||
#ifndef _stub_util_h
|
||||
#define _stub_util_h
|
||||
#include <cstring>
|
||||
#endif
|
||||
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// 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 <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
static int g_nFail = 0;
|
||||
|
||||
static void Check (const char *pName, bool bCond)
|
||||
{
|
||||
printf ("%-58s %s\n", pName, bCond ? "ok" : "FAIL");
|
||||
if (!bCond) g_nFail++;
|
||||
}
|
||||
|
||||
static unsigned CountLit (CPianoLEDs &L)
|
||||
{
|
||||
unsigned n = 0;
|
||||
for (auto &p : L.m_Stripe.m_Pixels)
|
||||
if (p[0] || p[1] || p[2]) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
static bool Dark (CPianoLEDs &L, unsigned i)
|
||||
{
|
||||
auto &p = L.m_Stripe.m_Pixels.at (i);
|
||||
return !p[0] && !p[1] && !p[2];
|
||||
}
|
||||
|
||||
// every pixel of one key's span is lit
|
||||
static bool Span (CPianoLEDs &L, unsigned nBase)
|
||||
{
|
||||
for (unsigned i = 0; i < LEDS_PER_KEY; i++)
|
||||
if (Dark (L, nBase + i)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static unsigned LedFor (u8 ucNote)
|
||||
{
|
||||
unsigned nKey = ucNote - MIDI_NOTE_MIN;
|
||||
#if STRIP_REVERSED
|
||||
return (KEY_COUNT - 1 - nKey) * LEDS_PER_KEY;
|
||||
#else
|
||||
return nKey * LEDS_PER_KEY;
|
||||
#endif
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
// --- highest key, C8 = note 108 -----------------------------------
|
||||
MIDI.Inject (0x80, 21, 0);
|
||||
MIDI.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 ("the two extremes are at opposite ends", nLow != nHigh);
|
||||
|
||||
// --- note off ------------------------------------------------------
|
||||
MIDI.Inject (0x80, 108, 0);
|
||||
LEDs.Update ();
|
||||
Check ("note off extinguishes the key", CountLit (LEDs) == 0);
|
||||
|
||||
// --- note on with velocity 0 is a note off -------------------------
|
||||
MIDI.Inject (0x90, 60, 100);
|
||||
LEDs.Update ();
|
||||
Check ("note on lights middle C", CountLit (LEDs) == LEDS_PER_KEY);
|
||||
MIDI.Inject (0x90, 60, 0);
|
||||
LEDs.Update ();
|
||||
Check ("note on velocity 0 acts as note off", CountLit (LEDs) == 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);
|
||||
LEDs.Update ();
|
||||
Check ("notes outside 21-108 are ignored", CountLit (LEDs) == 0);
|
||||
|
||||
// --- brightness ceiling --------------------------------------------
|
||||
for (u8 n = MIDI_NOTE_MIN; n <= MIDI_NOTE_MAX; n++)
|
||||
MIDI.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 (int c = 0; c < 3; c++)
|
||||
if (p[c] > GLOBAL_BRIGHTNESS) bWithinCeiling = false;
|
||||
#endif
|
||||
Check ("no channel ever exceeds GLOBAL_BRIGHTNESS", bWithinCeiling);
|
||||
|
||||
// --- simultaneous-key cap ------------------------------------------
|
||||
Check ("all 88 keys held stays within MAX_LIT_KEYS",
|
||||
CountLit (LEDs) <= MAX_LIT_KEYS * LEDS_PER_KEY);
|
||||
|
||||
// --- all notes off --------------------------------------------------
|
||||
MIDI.Inject (0xB0, 123, 0);
|
||||
LEDs.Update ();
|
||||
Check ("CC 123 (all notes off) clears the strip", CountLit (LEDs) == 0);
|
||||
|
||||
for (u8 n = MIDI_NOTE_MIN; n <= MIDI_NOTE_MAX; n++)
|
||||
MIDI.Inject (0x90, n, 127);
|
||||
MIDI.Inject (0xB0, 120, 0);
|
||||
LEDs.Update ();
|
||||
Check ("CC 120 (all sound off) clears the strip", CountLit (LEDs) == 0);
|
||||
|
||||
// --- velocity sensitivity -------------------------------------------
|
||||
MIDI.Inject (0x90, 60, 127);
|
||||
LEDs.Update ();
|
||||
auto Loud = LEDs.m_Stripe.m_Pixels.at (LedFor (60));
|
||||
MIDI.Inject (0x90, 60, 1);
|
||||
LEDs.Update ();
|
||||
auto Soft = LEDs.m_Stripe.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);
|
||||
|
||||
// --- hint channel ----------------------------------------------------
|
||||
#if HINT_MIDI_CHANNEL != MIDI_CHANNEL_NONE
|
||||
MIDI.Inject (0x90 | HINT_MIDI_CHANNEL, 64, 127);
|
||||
LEDs.Update ();
|
||||
auto Hint = LEDs.m_Stripe.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);
|
||||
LEDs.Update ();
|
||||
auto Both = LEDs.m_Stripe.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);
|
||||
LEDs.Update ();
|
||||
auto Back = LEDs.m_Stripe.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);
|
||||
LEDs.Update ();
|
||||
Check ("re-enumeration clears notes held at suspend", CountLit (LEDs) == 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);
|
||||
LEDs.Update ();
|
||||
Check ("a 1-byte realtime message lights nothing", CountLit (LEDs) == 0);
|
||||
|
||||
printf ("\n%s\n", g_nFail ? "FAILURES" : "all tests passed");
|
||||
return g_nFail != 0;
|
||||
}
|
||||
Reference in New Issue
Block a user