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:
+12
@@ -0,0 +1,12 @@
|
|||||||
|
# Build output
|
||||||
|
boot/
|
||||||
|
*.o
|
||||||
|
*.d
|
||||||
|
*.a
|
||||||
|
*.elf
|
||||||
|
*.lst
|
||||||
|
*.map
|
||||||
|
*.img
|
||||||
|
|
||||||
|
# Circle build configuration, generated by ./build.sh
|
||||||
|
circle/Config.mk
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "circle"]
|
||||||
|
path = circle
|
||||||
|
url = https://github.com/rsta2/circle.git
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Piano LED Visualizer — Circle firmware
|
||||||
|
|
||||||
|
Bare-metal firmware for a Raspberry Pi Zero that lights a WS2812B strip above
|
||||||
|
an 88-key keybed in response to MIDI.
|
||||||
|
|
||||||
|
The design rationale, bill of materials, electrical notes and project phases
|
||||||
|
live in [PIANO-LED-CIRCLE-PLAN.md](PIANO-LED-CIRCLE-PLAN.md). This file covers
|
||||||
|
only how to build and run the firmware (plan Phase 1).
|
||||||
|
|
||||||
|
## What this is
|
||||||
|
|
||||||
|
The Pi is a **USB MIDI gadget**. The PC is the host and owns everything else —
|
||||||
|
the piano connection, the learning software, the song library. From the PC's
|
||||||
|
side this firmware is just another ALSA MIDI output port:
|
||||||
|
|
||||||
|
```
|
||||||
|
Casio PX-S7000 --USB-B--> PC --USB--> Pi Zero (this firmware) --> WS2812B strip
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no network stack, no filesystem, no shell. The Pi boots into this
|
||||||
|
firmware in about a second and does one job.
|
||||||
|
|
||||||
|
Circle has no OTG support, so the USB controller is gadget-only here. The
|
||||||
|
piano **cannot** be plugged into the Pi directly; all MIDI arrives from the PC.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
| Path | |
|
||||||
|
|---|---|
|
||||||
|
| `firmware/config.h` | Every tunable. Start here. |
|
||||||
|
| `firmware/pianoleds.cpp` | Note-to-LED mapping, colour, brightness clamps. |
|
||||||
|
| `firmware/kernel.cpp` | USB gadget lifecycle and the main loop. |
|
||||||
|
| `tests/` | Host-side tests for the mapping and clamps. |
|
||||||
|
| `circle/` | Circle as a submodule, pinned to `Step51`. |
|
||||||
|
| `build.sh` | Builds both kernel images. |
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Circle is a submodule pinned to `Step51`, so clone recursively:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone --recursive <this repo>
|
||||||
|
# or, in an existing clone:
|
||||||
|
git submodule update --init
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt-get install gcc-arm-none-eabi # Debian/Ubuntu
|
||||||
|
./build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The pin is deliberate. A pinned Circle tree still builds in five years; nothing
|
||||||
|
here tracks a moving upstream.
|
||||||
|
|
||||||
|
This produces two images in `boot/`, which coexist on one card:
|
||||||
|
|
||||||
|
| Image | `RASPPI` | Model |
|
||||||
|
|---|---|---|
|
||||||
|
| `kernel.img` | 1 | Pi Zero / Zero W (ARM1176) |
|
||||||
|
| `kernel7.img` | 2 | Pi Zero 2 / Zero 2 W (Cortex-A7) |
|
||||||
|
|
||||||
|
The Pi picks the right one at boot, so the same SD card runs on either model.
|
||||||
|
|
||||||
|
## SD card
|
||||||
|
|
||||||
|
FAT32, single partition. Copy in:
|
||||||
|
|
||||||
|
- `boot/kernel.img` and `boot/kernel7.img`
|
||||||
|
- From the [Raspberry Pi firmware repo](https://github.com/raspberrypi/firmware)
|
||||||
|
`boot/` directory: `bootcode.bin`, `start.elf`, `fixup.dat`
|
||||||
|
- `cmdline.txt` (optional). Circle reads kernel options from it; useful for
|
||||||
|
raising the log level while bringing the board up.
|
||||||
|
|
||||||
|
Nothing is ever written to the card at runtime, so pulling the power mid-note
|
||||||
|
cannot corrupt it.
|
||||||
|
|
||||||
|
## Wiring
|
||||||
|
|
||||||
|
Verified against `circle/addon/WS28XX`: `CWS28XXStripe` clocks the WS2812B
|
||||||
|
waveform out over SPI at a fixed 6.4 MHz, encoding one LED bit per SPI byte.
|
||||||
|
On SPI master device 0 that puts the data line on:
|
||||||
|
|
||||||
|
**MOSI = GPIO10 (BCM) = physical pin 19.**
|
||||||
|
|
||||||
|
Three things from plan section 7 that are not optional:
|
||||||
|
|
||||||
|
- **Level shifter.** WS2812B wants logic high at 0.7 × VDD = 3.5V on a 5V rail;
|
||||||
|
the Pi's GPIO is 3.3V. Put a **74AHCT125** on the data line. Skipping this is
|
||||||
|
the single most common cause of "the strip flickers intermittently".
|
||||||
|
- **Common ground.** The Pi's ground and the LED supply's ground must be tied.
|
||||||
|
- **Power injection.** Feed 5V at both ends of the strip.
|
||||||
|
|
||||||
|
Connect the PC to the Zero's **USB** port, not **PWR**, with a data cable.
|
||||||
|
|
||||||
|
## Configuring
|
||||||
|
|
||||||
|
Everything is in `firmware/config.h`, and every value there is a Phase 0
|
||||||
|
question. Two are load-bearing:
|
||||||
|
|
||||||
|
- **`STRIP_REVERSED`** — whether pixel 0 sits at the bass or treble end. Decide
|
||||||
|
this *after* the strip is physically mounted; it is one flag to flip.
|
||||||
|
- **`GLOBAL_BRIGHTNESS` and `MAX_LIT_KEYS`** — the power clamps. 176 LEDs at
|
||||||
|
full white would draw ~10.5A against a 6A supply. Real playing never comes
|
||||||
|
close, but these two make a whited-out strip *unreachable* rather than merely
|
||||||
|
unlikely. Do not raise them without redoing the arithmetic in plan section 7.
|
||||||
|
|
||||||
|
Any of them can also be overridden at build time without editing the file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make -C firmware EXTRADEFINE=-DSTRIP_REVERSED=1
|
||||||
|
```
|
||||||
|
|
||||||
|
## MIDI behaviour
|
||||||
|
|
||||||
|
- Notes 21–108 (A0–C8) map to the strip; anything outside is dropped.
|
||||||
|
- Note On with velocity 0 is treated as Note Off.
|
||||||
|
- CC 120 (All Sound Off) and CC 123 (All Notes Off) clear the strip.
|
||||||
|
- Notes on `HINT_MIDI_CHANNEL` (default channel 16) light in a separate colour,
|
||||||
|
for the plan's Phase 3 "light the next key to play". A key actually being
|
||||||
|
played takes precedence over a hint on the same key.
|
||||||
|
- Notes held when the USB host suspends are cleared, so nothing stays lit.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./tests/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Compiles the real `firmware/pianoleds.cpp` against stubbed Circle headers and
|
||||||
|
exercises the mapping, the note-off paths, the range clamping and both power
|
||||||
|
clamps across nine configuration variants. This does not need the ARM
|
||||||
|
toolchain and does not replace bench-testing on real hardware — it checks the
|
||||||
|
arithmetic, not the wiring.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Builds the Piano LED Visualizer firmware for the Raspberry Pi Zero family.
|
||||||
|
#
|
||||||
|
# Produces both kernel images, which coexist on one SD card:
|
||||||
|
#
|
||||||
|
# kernel.img RASPPI=1 Pi Zero / Zero W (ARM1176)
|
||||||
|
# kernel7.img RASPPI=2 Pi Zero 2 / Zero 2 W (Cortex-A7)
|
||||||
|
#
|
||||||
|
# The Pi picks the right one at boot, so the same card runs on either model.
|
||||||
|
#
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
ROOT=$PWD
|
||||||
|
PREFIX=${PREFIX:-arm-none-eabi-}
|
||||||
|
|
||||||
|
if ! command -v "${PREFIX}gcc" >/dev/null 2>&1; then
|
||||||
|
echo "error: ${PREFIX}gcc not found." >&2
|
||||||
|
echo " Debian/Ubuntu: sudo apt-get install gcc-arm-none-eabi" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f circle/Rules.mk ]; then
|
||||||
|
echo "error: circle/ is empty. Run: git submodule update --init" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p boot
|
||||||
|
|
||||||
|
for SPEC in "1:kernel.img" "2:kernel7.img"; do
|
||||||
|
RASPPI=${SPEC%%:*}
|
||||||
|
IMAGE=${SPEC##*:}
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=============== RASPPI=$RASPPI -> $IMAGE ==============="
|
||||||
|
|
||||||
|
cd "$ROOT/circle"
|
||||||
|
./configure -r "$RASPPI" -p "$PREFIX" -f
|
||||||
|
./makeall clean >/dev/null
|
||||||
|
./makeall -j "$(nproc)"
|
||||||
|
|
||||||
|
# The WS28XX driver is an addon and is not built by makeall.
|
||||||
|
make -C addon/WS28XX clean >/dev/null 2>&1 || true
|
||||||
|
make -C addon/WS28XX -j "$(nproc)"
|
||||||
|
|
||||||
|
cd "$ROOT/firmware"
|
||||||
|
make clean >/dev/null 2>&1 || true
|
||||||
|
make -j "$(nproc)"
|
||||||
|
|
||||||
|
cp "$ROOT/firmware/$IMAGE" "$ROOT/boot/$IMAGE"
|
||||||
|
echo "wrote boot/$IMAGE"
|
||||||
|
done
|
||||||
|
|
||||||
|
cd "$ROOT/firmware" && make clean >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Built:"
|
||||||
|
ls -l "$ROOT/boot"
|
||||||
|
echo
|
||||||
|
echo "Copy boot/*.img plus the Raspberry Pi firmware files (bootcode.bin,"
|
||||||
|
echo "start.elf, fixup.dat) and cmdline.txt onto a FAT32 SD card."
|
||||||
|
echo "See README.md for the full card layout."
|
||||||
Submodule
+1
Submodule circle added at 6177984e30
@@ -0,0 +1,24 @@
|
|||||||
|
#
|
||||||
|
# Makefile
|
||||||
|
#
|
||||||
|
|
||||||
|
CIRCLEHOME = ../circle
|
||||||
|
|
||||||
|
OBJS = main.o kernel.o pianoleds.o
|
||||||
|
|
||||||
|
LIBS = $(CIRCLEHOME)/addon/WS28XX/libws28xx.a \
|
||||||
|
$(CIRCLEHOME)/lib/usb/gadget/libusbgadget.a \
|
||||||
|
$(CIRCLEHOME)/lib/usb/libusb.a \
|
||||||
|
$(CIRCLEHOME)/lib/input/libinput.a \
|
||||||
|
$(CIRCLEHOME)/lib/fs/libfs.a \
|
||||||
|
$(CIRCLEHOME)/lib/sched/libsched.a \
|
||||||
|
$(CIRCLEHOME)/lib/libcircle.a
|
||||||
|
|
||||||
|
# Build-time overrides for config.h, e.g.
|
||||||
|
# make EXTRADEFINE=-DSTRIP_REVERSED=1
|
||||||
|
# Appended, so Circle's own defines (-DRASPPI=... etc.) survive.
|
||||||
|
DEFINE += $(EXTRADEFINE)
|
||||||
|
|
||||||
|
include $(CIRCLEHOME)/sample/Rules.mk
|
||||||
|
|
||||||
|
-include $(DEPS)
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
//
|
||||||
|
// config.h
|
||||||
|
//
|
||||||
|
// Piano LED Visualizer on Circle - all tunable parameters.
|
||||||
|
//
|
||||||
|
// Every value in this file is a product decision that Phase 0 of
|
||||||
|
// PIANO-LED-CIRCLE-PLAN.md exists to answer. Bench-test on Raspberry Pi OS
|
||||||
|
// first, then transcribe the answers here and build once.
|
||||||
|
//
|
||||||
|
#ifndef _config_h
|
||||||
|
#define _config_h
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Keybed and strip geometry (plan section 6)
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// An 88-key keybed spans MIDI notes 21 (A0) through 108 (C8).
|
||||||
|
#define MIDI_NOTE_MIN 21
|
||||||
|
#define MIDI_NOTE_MAX 108
|
||||||
|
|
||||||
|
#define KEY_COUNT (MIDI_NOTE_MAX - MIDI_NOTE_MIN + 1) // 88
|
||||||
|
|
||||||
|
// LEDs per key. At 144 LEDs/m, 2 per key spans 1.222m, which lines up with a
|
||||||
|
// standard 88-key keybed almost exactly.
|
||||||
|
#ifndef LEDS_PER_KEY
|
||||||
|
#define LEDS_PER_KEY 2
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define LED_COUNT (KEY_COUNT * LEDS_PER_KEY) // 176
|
||||||
|
|
||||||
|
// Strip orientation. Pixel 0 of a WS2812B strip is at the end the data line
|
||||||
|
// enters. Decide this AFTER the strip is physically mounted, then flip this
|
||||||
|
// one flag.
|
||||||
|
//
|
||||||
|
// 0 = pixel 0 is at the bass end -> led = (note - 21) * 2
|
||||||
|
// 1 = pixel 0 is at the treble end -> led = (108 - note) * 2
|
||||||
|
#ifndef STRIP_REVERSED
|
||||||
|
#define STRIP_REVERSED 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Power safety (plan section 7) - NOT optional
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// 176 LEDs at full white draw ~60mA each = 10.56A theoretical maximum, against
|
||||||
|
// a 6A supply. Real playing never approaches that (a ten-finger chord lights 20
|
||||||
|
// LEDs, ~1.2A), but a firmware bug that whites out the strip would brown out
|
||||||
|
// the rail. These two clamps make that unreachable rather than unlikely.
|
||||||
|
|
||||||
|
// Global brightness ceiling, applied to every channel of every pixel.
|
||||||
|
// 0-255. At 96 a full-strip white would draw roughly 4A, still inside 6A.
|
||||||
|
#ifndef GLOBAL_BRIGHTNESS
|
||||||
|
#define GLOBAL_BRIGHTNESS 96
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Hard cap on simultaneously lit keys. Beyond this, further held notes are
|
||||||
|
// tracked but not lit, so current draw stays bounded no matter what arrives
|
||||||
|
// on the wire. 20 keys is a ten-finger chord; 30 leaves room for pedal-held
|
||||||
|
// passages without ever approaching the supply limit.
|
||||||
|
#ifndef MAX_LIT_KEYS
|
||||||
|
#define MAX_LIT_KEYS 30
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Colour (Phase 0 decides these against the actual diffuser)
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Colours look substantially different through a diffuser than on bare strip.
|
||||||
|
// Do not finalise these from a photo.
|
||||||
|
|
||||||
|
// Colour for a played key, before brightness scaling.
|
||||||
|
#ifndef NOTE_COLOR_R
|
||||||
|
#define NOTE_COLOR_R 0
|
||||||
|
#endif
|
||||||
|
#ifndef NOTE_COLOR_G
|
||||||
|
#define NOTE_COLOR_G 140
|
||||||
|
#endif
|
||||||
|
#ifndef NOTE_COLOR_B
|
||||||
|
#define NOTE_COLOR_B 255
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Distinct colour for a "next note to play" hint driven by learning software
|
||||||
|
// on the PC (plan Phase 3). Reached over MIDI channel HINT_MIDI_CHANNEL.
|
||||||
|
#ifndef HINT_COLOR_R
|
||||||
|
#define HINT_COLOR_R 255
|
||||||
|
#endif
|
||||||
|
#ifndef HINT_COLOR_G
|
||||||
|
#define HINT_COLOR_G 80
|
||||||
|
#endif
|
||||||
|
#ifndef HINT_COLOR_B
|
||||||
|
#define HINT_COLOR_B 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Velocity response
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 1 = velocity scales pixel brightness, 0 = every key lights at full
|
||||||
|
// GLOBAL_BRIGHTNESS regardless of how hard it was struck.
|
||||||
|
#ifndef VELOCITY_SENSITIVE
|
||||||
|
#define VELOCITY_SENSITIVE 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Floor for velocity scaling, as a percentage. A pianissimo note should still
|
||||||
|
// be clearly visible, so velocity maps onto [VELOCITY_FLOOR_PCT, 100] rather
|
||||||
|
// than onto [0, 100].
|
||||||
|
#ifndef VELOCITY_FLOOR_PCT
|
||||||
|
#define VELOCITY_FLOOR_PCT 35
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// MIDI routing
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Channel carrying notes actually played on the piano. 0-15 on the wire
|
||||||
|
// (channel 1 in a DAW), or MIDI_CHANNEL_ANY to accept every channel.
|
||||||
|
#define MIDI_CHANNEL_ANY 0xFF
|
||||||
|
#ifndef NOTE_MIDI_CHANNEL
|
||||||
|
#define NOTE_MIDI_CHANNEL MIDI_CHANNEL_ANY
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Channel reserved for Phase 3 "light the next key" hints from the PC. Kept
|
||||||
|
// separate from played notes so the two never overwrite each other. Set to
|
||||||
|
// MIDI_CHANNEL_NONE to ignore hints entirely.
|
||||||
|
#define MIDI_CHANNEL_NONE 0xFE
|
||||||
|
#ifndef HINT_MIDI_CHANNEL
|
||||||
|
#define HINT_MIDI_CHANNEL 15 // channel 16 in a DAW
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Hardware wiring (VERIFIED against circle/addon/WS28XX, do not guess)
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// CWS28XXStripe clocks the WS2812B waveform out over SPI at a fixed 6.4MHz,
|
||||||
|
// encoding each LED bit as one SPI byte. On SPI master device 0 that puts the
|
||||||
|
// data line on:
|
||||||
|
//
|
||||||
|
// MOSI = GPIO10 (BCM) = physical pin 19
|
||||||
|
//
|
||||||
|
// Feed that through a 74AHCT125 to get a 5V logic level at the strip, and tie
|
||||||
|
// the Pi's ground to the LED supply ground. See plan section 7.
|
||||||
|
#ifndef SPI_MASTER_DEVICE
|
||||||
|
#define SPI_MASTER_DEVICE 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
//
|
||||||
|
// kernel.h
|
||||||
|
//
|
||||||
|
#ifndef _kernel_h
|
||||||
|
#define _kernel_h
|
||||||
|
|
||||||
|
#include <circle/actled.h>
|
||||||
|
#include <circle/koptions.h>
|
||||||
|
#include <circle/devicenameservice.h>
|
||||||
|
#include <circle/serial.h>
|
||||||
|
#include <circle/exceptionhandler.h>
|
||||||
|
#include <circle/interrupt.h>
|
||||||
|
#include <circle/timer.h>
|
||||||
|
#include <circle/logger.h>
|
||||||
|
#include <circle/types.h>
|
||||||
|
#include <circle/usb/usbcontroller.h>
|
||||||
|
#include <circle/usb/usbmidi.h>
|
||||||
|
#include "pianoleds.h"
|
||||||
|
|
||||||
|
enum TShutdownMode
|
||||||
|
{
|
||||||
|
ShutdownNone,
|
||||||
|
ShutdownHalt,
|
||||||
|
ShutdownReboot
|
||||||
|
};
|
||||||
|
|
||||||
|
class CKernel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CKernel (void);
|
||||||
|
~CKernel (void);
|
||||||
|
|
||||||
|
boolean Initialize (void);
|
||||||
|
|
||||||
|
TShutdownMode Run (void);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Pick up the MIDI device after the host has enumerated us, and again
|
||||||
|
// after every re-enumeration.
|
||||||
|
void UpdateMIDIDevice (void);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// do not change this order
|
||||||
|
CActLED m_ActLED;
|
||||||
|
CKernelOptions m_Options;
|
||||||
|
CDeviceNameService m_DeviceNameService;
|
||||||
|
CSerialDevice m_Serial;
|
||||||
|
CExceptionHandler m_ExceptionHandler;
|
||||||
|
CInterruptSystem m_Interrupt;
|
||||||
|
CTimer m_Timer;
|
||||||
|
CLogger m_Logger;
|
||||||
|
|
||||||
|
CUSBController *m_pUSB;
|
||||||
|
CUSBMIDIDevice *m_pMIDIDevice;
|
||||||
|
|
||||||
|
CPianoLEDs m_PianoLEDs;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// main.cpp
|
||||||
|
//
|
||||||
|
#include "kernel.h"
|
||||||
|
#include <circle/startup.h>
|
||||||
|
|
||||||
|
int main (void)
|
||||||
|
{
|
||||||
|
// cannot return here because some destructors used in CKernel are not implemented
|
||||||
|
|
||||||
|
CKernel Kernel;
|
||||||
|
if (!Kernel.Initialize ())
|
||||||
|
{
|
||||||
|
halt ();
|
||||||
|
return EXIT_HALT;
|
||||||
|
}
|
||||||
|
|
||||||
|
TShutdownMode ShutdownMode = Kernel.Run ();
|
||||||
|
|
||||||
|
switch (ShutdownMode)
|
||||||
|
{
|
||||||
|
case ShutdownReboot:
|
||||||
|
reboot ();
|
||||||
|
return EXIT_REBOOT;
|
||||||
|
|
||||||
|
case ShutdownHalt:
|
||||||
|
default:
|
||||||
|
halt ();
|
||||||
|
return EXIT_HALT;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 ();
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
//
|
||||||
|
// pianoleds.h
|
||||||
|
//
|
||||||
|
// Maps incoming MIDI note events onto a WS2812B strip mounted above an
|
||||||
|
// 88-key keybed.
|
||||||
|
//
|
||||||
|
#ifndef _pianoleds_h
|
||||||
|
#define _pianoleds_h
|
||||||
|
|
||||||
|
#include <circle/usb/usbmidi.h>
|
||||||
|
#include <circle/types.h>
|
||||||
|
#include <WS28XX/ws28xxstripe.h>
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
class CPianoLEDs
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CPianoLEDs (void);
|
||||||
|
~CPianoLEDs (void);
|
||||||
|
|
||||||
|
boolean Initialize (void);
|
||||||
|
|
||||||
|
// Attach to a USB MIDI device. Safe to call again after the gadget has
|
||||||
|
// been re-enumerated, which destroys and recreates the device object.
|
||||||
|
void AttachMIDIDevice (CUSBMIDIDevice *pMIDIDevice);
|
||||||
|
|
||||||
|
// Push pending state to the strip. Call from the main loop only; this
|
||||||
|
// blocks for ~5.3ms of SPI traffic and must never run in IRQ context.
|
||||||
|
// Does nothing when no state has changed since the last call.
|
||||||
|
void Update (void);
|
||||||
|
|
||||||
|
// Extinguish every pixel and forget all held notes.
|
||||||
|
void AllOff (void);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Called in IRQ context by the USB MIDI driver.
|
||||||
|
static void MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength,
|
||||||
|
unsigned nDevice, void *pParam);
|
||||||
|
|
||||||
|
void OnMIDIPacket (const u8 *pPacket, unsigned nLength);
|
||||||
|
|
||||||
|
void SetKey (u8 ucNote, u8 ucVelocity, boolean bHint);
|
||||||
|
|
||||||
|
// Scale a colour channel by velocity and the global brightness ceiling.
|
||||||
|
static u8 Scale (u8 ucChannel, u8 ucVelocity);
|
||||||
|
|
||||||
|
static boolean ChannelMatches (u8 ucChannel, u8 ucWanted);
|
||||||
|
|
||||||
|
private:
|
||||||
|
CWS28XXStripe m_Stripe;
|
||||||
|
|
||||||
|
// Written in IRQ context, read by Update(). Index is
|
||||||
|
// note - MIDI_NOTE_MIN. Zero means the key is not lit.
|
||||||
|
volatile u8 m_KeyVelocity[KEY_COUNT];
|
||||||
|
volatile u8 m_HintVelocity[KEY_COUNT];
|
||||||
|
|
||||||
|
volatile boolean m_bDirty;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
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