Port to RP2040/RP2350, extract the shared logic
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
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
# Board: pico, pico2, pico_w, pico2_w. All run this firmware unchanged; the
|
||||
# radio variants simply leave their radio unused.
|
||||
set(PICO_BOARD pico2 CACHE STRING "Target board")
|
||||
|
||||
include(pico_sdk_import.cmake)
|
||||
|
||||
project(pianoled C CXX ASM)
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
pico_sdk_init()
|
||||
|
||||
add_executable(pianoled
|
||||
main.cpp
|
||||
picostrip.cpp
|
||||
../src/pianoleds.cpp
|
||||
usb_descriptors.c
|
||||
)
|
||||
|
||||
# PLATFORM_PICO selects the Pico wiring block in config.h.
|
||||
target_compile_definitions(pianoled PRIVATE PLATFORM_PICO)
|
||||
|
||||
target_include_directories(pianoled PRIVATE
|
||||
${CMAKE_CURRENT_LIST_DIR}
|
||||
${CMAKE_CURRENT_LIST_DIR}/../src
|
||||
)
|
||||
|
||||
pico_generate_pio_header(pianoled ${CMAKE_CURRENT_LIST_DIR}/ws2812.pio)
|
||||
|
||||
target_link_libraries(pianoled
|
||||
pico_stdlib
|
||||
hardware_pio
|
||||
tinyusb_device
|
||||
tinyusb_board
|
||||
)
|
||||
|
||||
# Debug log on UART only. USB is the MIDI device and must not be shared.
|
||||
pico_enable_stdio_uart(pianoled 1)
|
||||
pico_enable_stdio_usb(pianoled 0)
|
||||
|
||||
pico_add_extra_outputs(pianoled)
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Builds the Piano LED Visualizer firmware for RP2040 / RP2350.
|
||||
#
|
||||
# Produces pico/build/pianoled.uf2. Hold BOOTSEL while plugging the board in
|
||||
# and copy the .uf2 onto the mass-storage device that appears.
|
||||
#
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Board: pico, pico2, pico_w, pico2_w. All run this firmware unchanged; the
|
||||
# radio variants simply leave their radio unused.
|
||||
BOARD=${BOARD:-pico2}
|
||||
|
||||
export PICO_SDK_PATH=${PICO_SDK_PATH:-$HOME/git/pico-sdk}
|
||||
|
||||
if [ ! -f "$PICO_SDK_PATH/pico_sdk_init.cmake" ]; then
|
||||
echo "error: pico-sdk not found at $PICO_SDK_PATH" >&2
|
||||
echo " git clone --recursive https://github.com/raspberrypi/pico-sdk" >&2
|
||||
echo " or set PICO_SDK_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v arm-none-eabi-gcc >/dev/null 2>&1; then
|
||||
echo "error: arm-none-eabi-gcc not found." >&2
|
||||
echo " Debian/Ubuntu: sudo apt-get install gcc-arm-none-eabi" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cmake -B build -S . -DPICO_BOARD="$BOARD"
|
||||
cmake --build build -j "$(nproc)"
|
||||
|
||||
echo
|
||||
echo "Built for $BOARD:"
|
||||
ls -l build/pianoled.uf2
|
||||
arm-none-eabi-size build/pianoled.elf
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// main.cpp
|
||||
//
|
||||
// Piano LED Visualizer on RP2040 / RP2350.
|
||||
//
|
||||
// The board is a USB MIDI device: the PC is the host and owns the piano
|
||||
// connection and everything else. From the PC's side this enumerates as an
|
||||
// ordinary ALSA MIDI port. See PIANO-LED-CIRCLE-PLAN.md section 2.
|
||||
//
|
||||
#include "pianoleds.h"
|
||||
#include "picostrip.h"
|
||||
#include "pico/stdlib.h"
|
||||
#include "tusb.h"
|
||||
|
||||
static CPicoLEDStrip s_Strip (LED_COUNT, WS2812_PIN);
|
||||
static CPianoLEDs s_PianoLEDs (s_Strip);
|
||||
|
||||
// Number of MIDI bytes carried by a USB MIDI event packet, indexed by its
|
||||
// Code Index Number (USB MIDI 1.0, table 4-1). 0 means "not a message we
|
||||
// forward".
|
||||
static const uint8_t s_CINLength[16] =
|
||||
{
|
||||
0, 0, 2, 3, 3, 1, 2, 3,
|
||||
3, 3, 3, 3, 2, 2, 3, 1
|
||||
};
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// TinyUSB device callbacks
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
extern "C" void tud_mount_cb (void)
|
||||
{
|
||||
// Fresh session: nothing should still be lit from the last one.
|
||||
s_PianoLEDs.AllOff ();
|
||||
}
|
||||
|
||||
extern "C" void tud_umount_cb (void)
|
||||
{
|
||||
// Host went away, possibly mid-chord. Do not leave keys lit.
|
||||
s_PianoLEDs.AllOff ();
|
||||
}
|
||||
|
||||
extern "C" void tud_suspend_cb (bool remote_wakeup_en)
|
||||
{
|
||||
(void) remote_wakeup_en;
|
||||
s_PianoLEDs.AllOff ();
|
||||
}
|
||||
|
||||
extern "C" void tud_resume_cb (void)
|
||||
{
|
||||
s_PianoLEDs.AllOff ();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
static void PollMIDI (void)
|
||||
{
|
||||
uint8_t Packet[4];
|
||||
|
||||
while (tud_midi_available ())
|
||||
{
|
||||
if (!tud_midi_packet_read (Packet))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Packet[0] is cable number (high nibble) and Code Index Number
|
||||
// (low nibble); the message itself is in Packet[1..3].
|
||||
unsigned nLength = s_CINLength[Packet[0] & 0x0F];
|
||||
if (nLength == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
s_PianoLEDs.OnMIDIPacket (&Packet[1], nLength);
|
||||
}
|
||||
}
|
||||
|
||||
int main (void)
|
||||
{
|
||||
stdio_init_all ();
|
||||
|
||||
if (!s_Strip.Initialize ())
|
||||
{
|
||||
// Nothing sensible left to do; make the failure visible rather
|
||||
// than sitting dark and looking like a power problem.
|
||||
while (true)
|
||||
{
|
||||
tight_loop_contents ();
|
||||
}
|
||||
}
|
||||
|
||||
s_PianoLEDs.Initialize ();
|
||||
|
||||
tusb_init ();
|
||||
|
||||
while (true)
|
||||
{
|
||||
tud_task ();
|
||||
|
||||
PollMIDI ();
|
||||
|
||||
// Rendering blocks for the strip's frame time, so it runs here
|
||||
// rather than in the USB callback. Returns immediately when
|
||||
// nothing has changed.
|
||||
s_PianoLEDs.Update ();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
|
||||
|
||||
# This can be dropped into an external project to help locate this SDK
|
||||
# It should be include()ed prior to project()
|
||||
|
||||
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
||||
# following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
||||
# disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
|
||||
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
|
||||
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
|
||||
endif ()
|
||||
|
||||
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
|
||||
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
|
||||
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
|
||||
endif ()
|
||||
|
||||
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
|
||||
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
|
||||
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
|
||||
endif ()
|
||||
|
||||
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
|
||||
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
|
||||
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
|
||||
endif ()
|
||||
|
||||
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
|
||||
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
|
||||
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
|
||||
endif()
|
||||
|
||||
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
|
||||
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
|
||||
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
|
||||
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
|
||||
|
||||
if (NOT PICO_SDK_PATH)
|
||||
if (PICO_SDK_FETCH_FROM_GIT)
|
||||
include(FetchContent)
|
||||
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
|
||||
if (PICO_SDK_FETCH_FROM_GIT_PATH)
|
||||
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
|
||||
endif ()
|
||||
FetchContent_Declare(
|
||||
pico_sdk
|
||||
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||
)
|
||||
|
||||
if (NOT pico_sdk)
|
||||
message("Downloading Raspberry Pi Pico SDK")
|
||||
# GIT_SUBMODULES_RECURSE was added in 3.17
|
||||
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
|
||||
FetchContent_Populate(
|
||||
pico_sdk
|
||||
QUIET
|
||||
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||
GIT_SUBMODULES_RECURSE FALSE
|
||||
|
||||
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
|
||||
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
|
||||
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
|
||||
)
|
||||
else ()
|
||||
FetchContent_Populate(
|
||||
pico_sdk
|
||||
QUIET
|
||||
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||
|
||||
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
|
||||
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
|
||||
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
|
||||
)
|
||||
endif ()
|
||||
|
||||
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
|
||||
endif ()
|
||||
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
|
||||
else ()
|
||||
message(FATAL_ERROR
|
||||
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
|
||||
)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
|
||||
if (NOT EXISTS ${PICO_SDK_PATH})
|
||||
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
|
||||
endif ()
|
||||
|
||||
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
|
||||
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
|
||||
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
|
||||
endif ()
|
||||
|
||||
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
|
||||
|
||||
include(${PICO_SDK_INIT_CMAKE_FILE})
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// picostrip.cpp
|
||||
//
|
||||
#include "picostrip.h"
|
||||
#include "ws2812.pio.h"
|
||||
#include "pico/stdlib.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// WS2812B bit cell is 1.25us; the PIO program spends 10 cycles per bit.
|
||||
static const float WS2812_FREQ = 800000.0f;
|
||||
|
||||
// Datasheet reset is >50us of low. 300us is the commonly used safe value and
|
||||
// costs nothing at this frame rate.
|
||||
static const uint64_t WS2812_RESET_US = 300;
|
||||
|
||||
CPicoLEDStrip::CPicoLEDStrip (unsigned nLEDCount, unsigned nPin)
|
||||
: m_nLEDCount (nLEDCount),
|
||||
m_nPin (nPin),
|
||||
m_pBuffer (nullptr),
|
||||
m_PIO (nullptr),
|
||||
m_nSM (0),
|
||||
m_nOffset (0),
|
||||
m_bInitialized (false),
|
||||
m_nLastFrameUs (0)
|
||||
{
|
||||
}
|
||||
|
||||
CPicoLEDStrip::~CPicoLEDStrip (void)
|
||||
{
|
||||
free (m_pBuffer);
|
||||
m_pBuffer = nullptr;
|
||||
}
|
||||
|
||||
bool CPicoLEDStrip::Initialize (void)
|
||||
{
|
||||
m_pBuffer = (uint32_t *) calloc (m_nLEDCount, sizeof (uint32_t));
|
||||
if (m_pBuffer == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Let the SDK place the program on whichever PIO block has room, so
|
||||
// this cannot collide with anything else added later.
|
||||
if (!pio_claim_free_sm_and_add_program_for_gpio_range (
|
||||
&ws2812_program, &m_PIO, &m_nSM, &m_nOffset, m_nPin, 1, true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ws2812_program_init (m_PIO, m_nSM, m_nOffset, m_nPin, WS2812_FREQ);
|
||||
|
||||
m_bInitialized = true;
|
||||
m_nLastFrameUs = time_us_64 ();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CPicoLEDStrip::SetLED (unsigned nIndex, uint8_t nRed, uint8_t nGreen, uint8_t nBlue)
|
||||
{
|
||||
if (nIndex >= m_nLEDCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// WS2812B wants GRB. The PIO shifts out MSB first with a 24-bit
|
||||
// threshold, so the triple is left-justified in the word.
|
||||
m_pBuffer[nIndex] = ((uint32_t) nGreen << 24)
|
||||
| ((uint32_t) nRed << 16)
|
||||
| ((uint32_t) nBlue << 8);
|
||||
}
|
||||
|
||||
void CPicoLEDStrip::WaitForLatch (void)
|
||||
{
|
||||
uint64_t nElapsed = time_us_64 () - m_nLastFrameUs;
|
||||
if (nElapsed < WS2812_RESET_US)
|
||||
{
|
||||
busy_wait_us (WS2812_RESET_US - nElapsed);
|
||||
}
|
||||
}
|
||||
|
||||
bool CPicoLEDStrip::Update (void)
|
||||
{
|
||||
if (!m_bInitialized)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
WaitForLatch ();
|
||||
|
||||
for (unsigned i = 0; i < m_nLEDCount; i++)
|
||||
{
|
||||
// Blocking push. The FIFO drains at 800kbit/s, so this paces the
|
||||
// loop naturally and needs no DMA at this strip length.
|
||||
pio_sm_put_blocking (m_PIO, m_nSM, m_pBuffer[i]);
|
||||
}
|
||||
|
||||
m_nLastFrameUs = time_us_64 ();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CPicoLEDStrip::Blackout (void)
|
||||
{
|
||||
memset (m_pBuffer, 0, m_nLEDCount * sizeof (uint32_t));
|
||||
|
||||
return Update ();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// picostrip.h
|
||||
//
|
||||
// ILEDStrip on RP2040 / RP2350, driving WS2812B from a PIO state machine.
|
||||
//
|
||||
#ifndef _picostrip_h
|
||||
#define _picostrip_h
|
||||
|
||||
#include "ledstrip.h"
|
||||
#include "hardware/pio.h"
|
||||
|
||||
class CPicoLEDStrip : public ILEDStrip
|
||||
{
|
||||
public:
|
||||
CPicoLEDStrip (unsigned nLEDCount, unsigned nPin);
|
||||
~CPicoLEDStrip (void);
|
||||
|
||||
bool Initialize (void) override;
|
||||
|
||||
unsigned GetLEDCount (void) const override { return m_nLEDCount; }
|
||||
|
||||
void SetLED (unsigned nIndex, uint8_t nRed, uint8_t nGreen, uint8_t nBlue) override;
|
||||
|
||||
bool Update (void) override;
|
||||
|
||||
bool Blackout (void) override;
|
||||
|
||||
private:
|
||||
// Blocks until the strip has latched, so a caller cannot start a new
|
||||
// frame inside the reset window.
|
||||
void WaitForLatch (void);
|
||||
|
||||
private:
|
||||
unsigned m_nLEDCount;
|
||||
unsigned m_nPin;
|
||||
uint32_t *m_pBuffer; // one GRB word per LED, left-justified
|
||||
|
||||
PIO m_PIO;
|
||||
unsigned m_nSM;
|
||||
unsigned m_nOffset;
|
||||
bool m_bInitialized;
|
||||
|
||||
uint64_t m_nLastFrameUs;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// tusb_config.h
|
||||
//
|
||||
#ifndef _TUSB_CONFIG_H_
|
||||
#define _TUSB_CONFIG_H_
|
||||
|
||||
#ifndef CFG_TUSB_MCU
|
||||
#error CFG_TUSB_MCU must be defined by the SDK
|
||||
#endif
|
||||
|
||||
#define CFG_TUSB_OS OPT_OS_PICO
|
||||
#define CFG_TUSB_RHPORT0_MODE OPT_MODE_DEVICE
|
||||
|
||||
#ifndef CFG_TUSB_MEM_SECTION
|
||||
#define CFG_TUSB_MEM_SECTION
|
||||
#endif
|
||||
#ifndef CFG_TUSB_MEM_ALIGN
|
||||
#define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4)))
|
||||
#endif
|
||||
|
||||
#define CFG_TUD_ENDPOINT0_SIZE 64
|
||||
|
||||
// Device classes: MIDI only. No CDC, no HID, nothing else to enumerate.
|
||||
#define CFG_TUD_MIDI 1
|
||||
#define CFG_TUD_CDC 0
|
||||
#define CFG_TUD_MSC 0
|
||||
#define CFG_TUD_HID 0
|
||||
#define CFG_TUD_VENDOR 0
|
||||
|
||||
#define CFG_TUD_MIDI_RX_BUFSIZE 64
|
||||
#define CFG_TUD_MIDI_TX_BUFSIZE 64
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// usb_descriptors.c
|
||||
//
|
||||
// A single USB MIDI 1.0 streaming interface. From the PC this enumerates as
|
||||
// an ordinary ALSA MIDI port, exactly as the Circle gadget build does.
|
||||
//
|
||||
#include "tusb.h"
|
||||
|
||||
#define USB_VID 0xCafe
|
||||
#define USB_PID 0x4001
|
||||
#define USB_BCD 0x0200
|
||||
|
||||
static const tusb_desc_device_t desc_device =
|
||||
{
|
||||
.bLength = sizeof (tusb_desc_device_t),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE,
|
||||
.bcdUSB = USB_BCD,
|
||||
.bDeviceClass = 0x00,
|
||||
.bDeviceSubClass = 0x00,
|
||||
.bDeviceProtocol = 0x00,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.idVendor = USB_VID,
|
||||
.idProduct = USB_PID,
|
||||
.bcdDevice = 0x0100,
|
||||
.iManufacturer = 0x01,
|
||||
.iProduct = 0x02,
|
||||
.iSerialNumber = 0x03,
|
||||
.bNumConfigurations = 0x01
|
||||
};
|
||||
|
||||
const uint8_t *tud_descriptor_device_cb (void)
|
||||
{
|
||||
return (const uint8_t *) &desc_device;
|
||||
}
|
||||
|
||||
enum { ITF_NUM_MIDI = 0, ITF_NUM_MIDI_STREAMING, ITF_NUM_TOTAL };
|
||||
|
||||
#define EPNUM_MIDI_OUT 0x01
|
||||
#define EPNUM_MIDI_IN 0x81
|
||||
|
||||
#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_MIDI_DESC_LEN)
|
||||
|
||||
static const uint8_t desc_configuration[] =
|
||||
{
|
||||
TUD_CONFIG_DESCRIPTOR (1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100),
|
||||
TUD_MIDI_DESCRIPTOR (ITF_NUM_MIDI, 0, EPNUM_MIDI_OUT, EPNUM_MIDI_IN, 64)
|
||||
};
|
||||
|
||||
const uint8_t *tud_descriptor_configuration_cb (uint8_t index)
|
||||
{
|
||||
(void) index;
|
||||
return desc_configuration;
|
||||
}
|
||||
|
||||
static const char *const string_desc_arr[] =
|
||||
{
|
||||
(const char[]) { 0x09, 0x04 }, // 0: English (0x0409)
|
||||
"PianoLED", // 1: Manufacturer
|
||||
"Piano LED Visualizer", // 2: Product
|
||||
"000000000001", // 3: Serial
|
||||
"Piano LED MIDI", // 4: MIDI interface
|
||||
};
|
||||
|
||||
static uint16_t _desc_str[32];
|
||||
|
||||
const uint16_t *tud_descriptor_string_cb (uint8_t index, uint16_t langid)
|
||||
{
|
||||
(void) langid;
|
||||
|
||||
uint8_t chr_count;
|
||||
|
||||
if (index == 0)
|
||||
{
|
||||
memcpy (&_desc_str[1], string_desc_arr[0], 2);
|
||||
chr_count = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (index >= sizeof (string_desc_arr) / sizeof (string_desc_arr[0]))
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *str = string_desc_arr[index];
|
||||
|
||||
chr_count = (uint8_t) strlen (str);
|
||||
if (chr_count > 31)
|
||||
{
|
||||
chr_count = 31;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < chr_count; i++)
|
||||
{
|
||||
_desc_str[1 + i] = str[i];
|
||||
}
|
||||
}
|
||||
|
||||
_desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * chr_count + 2));
|
||||
|
||||
return _desc_str;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
;
|
||||
; ws2812.pio
|
||||
;
|
||||
; WS2812B waveform generator. One PIO state machine clocks the 1.25us bit
|
||||
; cell directly, which is what PIO is for - unlike the Circle build, which
|
||||
; has to fake the waveform by streaming 8 SPI bytes per data bit.
|
||||
;
|
||||
; Timing per bit (T1+T2+T3 = 10 cycles = 1.25us, so the SM runs at 8MHz):
|
||||
; '0' = 0.4us high, 0.85us low
|
||||
; '1' = 0.8us high, 0.45us low
|
||||
;
|
||||
.program ws2812
|
||||
.side_set 1
|
||||
|
||||
.define public T1 3
|
||||
.define public T2 3
|
||||
.define public T3 4
|
||||
|
||||
.wrap_target
|
||||
bitloop:
|
||||
out x, 1 side 0 [T3 - 1]
|
||||
jmp !x do_zero side 1 [T1 - 1]
|
||||
do_one:
|
||||
jmp bitloop side 1 [T2 - 1]
|
||||
do_zero:
|
||||
nop side 0 [T2 - 1]
|
||||
.wrap
|
||||
|
||||
% c-sdk {
|
||||
#include "hardware/clocks.h"
|
||||
|
||||
static inline void ws2812_program_init (PIO pio, uint sm, uint offset, uint pin, float freq)
|
||||
{
|
||||
pio_gpio_init (pio, pin);
|
||||
pio_sm_set_consecutive_pindirs (pio, sm, pin, 1, true);
|
||||
|
||||
pio_sm_config c = ws2812_program_get_default_config (offset);
|
||||
sm_config_set_sideset_pins (&c, pin);
|
||||
|
||||
// 24 bits per LED, shifted out MSB first, auto-pulled a byte-triple at a time.
|
||||
sm_config_set_out_shift (&c, false, true, 24);
|
||||
sm_config_set_fifo_join (&c, PIO_FIFO_JOIN_TX);
|
||||
|
||||
int cycles_per_bit = ws2812_T1 + ws2812_T2 + ws2812_T3;
|
||||
float div = clock_get_hz (clk_sys) / (freq * cycles_per_bit);
|
||||
sm_config_set_clkdiv (&c, div);
|
||||
|
||||
pio_sm_init (pio, sm, offset, &c);
|
||||
pio_sm_set_enabled (pio, sm, true);
|
||||
}
|
||||
%}
|
||||
Reference in New Issue
Block a user