From 87bd88f8db711cdbca7c1582e5e2af6f7ae8e4d1 Mon Sep 17 00:00:00 2001 From: Jason Moore Date: Mon, 20 Jul 2026 11:09:52 -0400 Subject: [PATCH 1/2] Add new module --- releases/89_Lockstep/README.md | 164 +++ releases/89_Lockstep/info.yaml | 95 ++ releases/89_Lockstep/lockstep.uf2 | Bin 0 -> 176128 bytes releases/89_Lockstep/sim/.gitignore | 2 + releases/89_Lockstep/sim/build.sh | 5 + releases/89_Lockstep/sim/test_engine.cpp | 63 + releases/89_Lockstep/src/.gitignore | 1 + releases/89_Lockstep/src/CMakeLists.txt | 23 + releases/89_Lockstep/src/ComputerCard.h | 1182 +++++++++++++++++ releases/89_Lockstep/src/lockstep.cpp | 160 +++ releases/89_Lockstep/src/lockstep_engine.h | 60 + releases/89_Lockstep/src/markov_scales.h | 54 + .../89_Lockstep/src/pico_sdk_import.cmake | 84 ++ 13 files changed, 1893 insertions(+) create mode 100644 releases/89_Lockstep/README.md create mode 100644 releases/89_Lockstep/info.yaml create mode 100644 releases/89_Lockstep/lockstep.uf2 create mode 100644 releases/89_Lockstep/sim/.gitignore create mode 100755 releases/89_Lockstep/sim/build.sh create mode 100644 releases/89_Lockstep/sim/test_engine.cpp create mode 100644 releases/89_Lockstep/src/.gitignore create mode 100644 releases/89_Lockstep/src/CMakeLists.txt create mode 100644 releases/89_Lockstep/src/ComputerCard.h create mode 100644 releases/89_Lockstep/src/lockstep.cpp create mode 100644 releases/89_Lockstep/src/lockstep_engine.h create mode 100644 releases/89_Lockstep/src/markov_scales.h create mode 100644 releases/89_Lockstep/src/pico_sdk_import.cmake diff --git a/releases/89_Lockstep/README.md b/releases/89_Lockstep/README.md new file mode 100644 index 000000000..cc423ddc1 --- /dev/null +++ b/releases/89_Lockstep/README.md @@ -0,0 +1,164 @@ +# Lockstep + +**Dual quantized pitch-mover for the Music Thing Modular Workshop Computer.** + +Two CV outputs wander together through a scale. You tune two oscillators by hand to +set the interval; Lockstep walks them in **parallel**, so the harmony you dialled in +moves as one — clocked, quantized, and always in key. One knob sets how *often* it +moves, another how *far*. The Main knob is a resonant low-pass filter that processes +the two oscillators returned through the audio inputs, so the card is a complete +generative two-voice instrument on its own. + +--- + +## Why this card exists + +It came out of a long dead-end: driving oscillators from another card's bassline that +output pitch CV in *negative* territory, which some oscillator inputs (a SineSquare's +Pitch jack, for one) simply won't track. Lockstep is built the opposite way — its walk +is **centered high (around MIDI 72) and floored so the CV never drops below ~0 V**. It +drives positive-only pitch inputs cleanly, and you tune the oscillators down to taste. + +--- + +## I/O + +| Jack | Direction | Function | +|------|-----------|----------| +| **Pulse In 1** | Input | Clock. Each rising edge can advance the walk (see X knob for division). Disconnected → internal free-running clock. | +| **Pulse In 2** | Input | Re-seed. A rising edge jumps the walk to a fresh pattern (and recenters it). | +| **CV Out 1** | Output | Voice 1 pitch CV. V/Oct, hardware-calibrated, quantized to the scale. | +| **CV Out 2** | Output | Voice 2 pitch CV. Follows CV Out 1 (parallel motion), except when it takes a mid-beat passing move (see **Doubling** below). | +| **Pulse Out 1** | Output | Gate (~10 ms) on every step where voice 1's quantized note changes. Drive an external envelope for voice 1. | +| **Pulse Out 2** | Output | Same gate for voice 2 — fires on the main step *and* on any mid-beat double. | +| **Audio In 1 / 2** | Input | The two oscillators, patched back in for processing. | +| **Audio Out 1 / 2** | Output | The oscillators through the resonant low-pass filter. | +| **CV In 1** | Input | Doubling probability (bipolar). Unpatched → the ~25% default. Patched: full negative = never, 0 V ≈ 50%, full positive = nearly every interval. Patch a slow LFO or random CV to make the ornamentation itself drift. | +| **CV In 2** | Input | Scale offset (bipolar). Shifts the knob-selected scale up or down the 12-scale table; a full swing reaches any scale. 0 V = no shift. Sweep it slowly to morph dark→bright, or step it to change key/mode per phrase. | + +--- + +## Controls + +### X — Movement rate + +How often the pitch moves. + +- **Clocked** (Pulse In 1 patched): X is a **clock divider** — fully CW = move on every clock pulse, fully CCW = hold for up to 60 pulses. +- **Free** (Pulse In 1 unpatched): X is the **internal rate** — fully CW ≈ fast (many moves/sec), fully CCW ≈ slow (~2 sec per move). + +### Y — Movement range + +How far it wanders from the center. + +- Fully **CCW** = frozen on a single note (no movement). +- Fully **CW** = up to ±1 octave of quantized wander. + +Everything in between scales the span; small settings drift gently around the center, large settings roam. + +### Doubling — voice B passing tones + +Both voices normally step together on every beat. But *sometimes* **voice B (CV Out 2) +takes one extra step at the midpoint of the interval** — a passing tone of ±1–2 +scale steps — then re-locks with voice A on the next beat. Voice A holds the pulse; B +adds a little melodic flourish over it. + +- Probability is set by **CV In 1** (unpatched ≈ 25% of intervals). See the I/O table. +- It only happens when there's room: the interval must be at least 2 clocks long (so + **not** at X fully CW / every-clock) and **Y** must be above zero. +- **Pulse Out 2** gates on the double too, so an external envelope re-articulates the + passing note. + +### Main knob — Filter cutoff *(switch-dependent)* + +- **Switch UP or MIDDLE:** resonant low-pass cutoff for the two oscillators returned on Audio In 1/2. Left = dark, right = bright and open. Resonance is fixed and moderately singing. +- **Switch DOWN (held):** selects the scale instead (see below). The cutoff is held at its last value while you do this. + +### Switch + +| Position | Function | +|----------|----------| +| **MIDDLE** | **Run** — normal generative movement. | +| **UP** | **Freeze** — hold the current pitches. The walk stops; CV outs stay put. Flip back to MIDDLE to resume. | +| **DOWN** *(momentary)* | **Scale select** — while held, the Main knob picks the scale. LEDs show the scale index in 6-bit binary. Release to return to Run/Freeze. | + +--- + +## LEDs + +| State | Display | +|-------|---------| +| **Run** | A single bright LED shows the walk's current position within its range (a bouncing bar). Dim flashes mark note changes. | +| **Freeze** | All six LEDs dim and static. | +| **Scale select** (switch DOWN) | Scale index (0–11) in 6-bit binary at half brightness. | + +--- + +## Scales + +Twelve scales, arranged CCW (dark/minor) → CW (bright/ambiguous), reused from the Markov +card: Phrygian, Hirajōshi, Harmonic Minor, Natural Minor *(default)*, Minor Pentatonic, +m7 Arpeggio, Dorian, Major Pentatonic, Ionian (Major), Maj7 Arpeggio, Whole Tone, +Chromatic. All scales are rooted at C — you set the actual key by tuning the oscillators. + +Hold **switch DOWN** and turn the Main knob to pick the base scale. **CV In 2** then +offsets that choice up or down the table (see I/O), so you can modulate the scale under +CV while the knob sets home. Scale changes re-quantize the current notes immediately — a +slowly swept LFO on CV In 2 makes the held voices drift through modes and reharmonize. + +--- + +## How to patch it + +1. **CV Out 1 → Oscillator A Pitch (1V/Oct).** **CV Out 2 → Oscillator B Pitch.** +2. **Tune the two oscillators by ear** to the interval you want (a fifth, an octave, a + third — whatever). Both now move in parallel, preserving that interval. +3. Optional articulation: **Pulse Out 1 → envelope A**, **Pulse Out 2 → envelope B**, so + the notes re-trigger on each change instead of gliding continuously. +4. Optional processing: **Osc A → Audio In 1**, **Osc B → Audio In 2**, then + **Audio Out 1/2 → your mixer**. The Main knob now filters both voices. +5. **Clock:** patch your system clock to **Pulse In 1**, or leave it unpatched to + free-run at the rate set by X. +6. Set **Y** for how much the line should move, **X** for how fast, hold **switch DOWN** + and sweep **Main** to pick a scale. + +**Freeze a phrase:** let it run until a nice shape appears, flip the switch **UP** to lock +the pitches, then keep playing over it. Flip back to **MIDDLE** to set it moving again. + +--- + +## Technical notes + +- Sample rate 48 kHz, system clock 192 MHz. Integer-only DSP in the audio callback. +- **Movement:** a reflected random walk in semitones, bounded by Y, quantized to the + selected scale. The output is octave-folded to stay ≥ MIDI 60 (~0 V) so it drives + positive-only pitch inputs. +- **Parallel motion:** CV Out 1 and CV Out 2 carry the same walk, so the interval between + your two voices is whatever you tuned into the oscillators. The exception is doubling — + voice B occasionally departs for a mid-beat passing tone, then re-locks (see above). +- **Filter:** a Chamberlin state-variable low-pass per channel, fixed-point Q12, with a + fixed moderate resonance. +- The pure movement engine (`src/lockstep_engine.h`) is unit-tested on the host — + see `sim/test_engine.cpp` (`./sim/build.sh`), which checks that every output note is a + scale tone, stays positive-CV, and respects the range. + +--- + +## Build + +Requires the Pico SDK at `PICO_SDK_PATH`. + +```bash +cd src +cmake -S . -B build +cmake --build build -j$(sysctl -n hw.logicalcpu) +# Output: build/lockstep.uf2 +``` + +Flash by holding BOOTSEL while connecting USB, then copy the `.uf2` to the mounted drive. + +--- + +## Status + +**Built and validated in offline simulation; not yet tested on hardware.** diff --git a/releases/89_Lockstep/info.yaml b/releases/89_Lockstep/info.yaml new file mode 100644 index 000000000..e76399e3f --- /dev/null +++ b/releases/89_Lockstep/info.yaml @@ -0,0 +1,95 @@ +draft: true +Name: Lockstep +Description: Dual quantized pitch-mover — two CV outs walk together through a scale to drive two oscillators in parallel +Language: C++ (Pico SDK) +Creator: Jason Moore +Version: 1.0 +Status: Draft +License: MIT +date: 2026-07-16 + +summary: | + Two CV outputs wander together through a scale (parallel motion): you tune two + oscillators by hand to set the interval and the card moves them in lockstep, clocked + and quantized. X sets how often it moves (clock division when clocked, internal rate + when free), Y sets how far (frozen single note up to +/- one octave). Switch DOWN + (momentary) selects the scale via the Main knob; UP freezes the current pitches; + MIDDLE runs. Main knob is a resonant low-pass filter processing the two oscillators + returned through Audio In 1/2, out Audio Out 1/2. Pulse In 1 clocks it, Pulse In 2 + re-seeds. The walk is centered high and floored so the pitch CV never goes negative. + +panel: + inputs: + - id: PulseIn1 + name: Clock + description: Rising edge advances the walk (divided by X); internal clock when unpatched + - id: PulseIn2 + name: Re-seed + description: Rising edge jumps the walk to a fresh pattern + - id: AudioIn1 + name: Oscillator A Return + description: Oscillator A fed back in for filtering + - id: AudioIn2 + name: Oscillator B Return + description: Oscillator B fed back in for filtering + - id: CVIn1 + name: Unused + description: Not used in this version + - id: CVIn2 + name: Unused + description: Not used in this version + outputs: + - id: CVOut1 + name: Voice A Pitch + description: Quantized V/Oct pitch CV for oscillator A + - id: CVOut2 + name: Voice B Pitch + description: Quantized V/Oct pitch CV for oscillator B (parallel, identical walk) + - id: PulseOut1 + name: Voice A Gate + description: 10 ms gate on each note change for voice A + - id: PulseOut2 + name: Voice B Gate + description: 10 ms gate on each note change for voice B + - id: AudioOut1 + name: Voice A Filtered + description: Oscillator A through the resonant low-pass + - id: AudioOut2 + name: Voice B Filtered + description: Oscillator B through the resonant low-pass + +controls: + knobs: + - when: { z: any } + main: + name: Filter Cutoff / Scale Select + description: Resonant low-pass cutoff for the returned oscillators; selects the scale while switch is held DOWN + x: + name: Movement Rate + description: Clock division when clocked, internal rate when free-running + y: + name: Movement Range + description: How far the walk wanders, from a single held note to plus/minus one octave + + leds: + - when: { z: any } + display: list + items: + - id: LED0 + name: Position / Scale bit 0 + description: Walk position bar in Run; scale-index bit while selecting + - id: LED1 + name: Position / Scale bit 1 + description: Walk position bar in Run; scale-index bit while selecting + - id: LED2 + name: Position / Scale bit 2 + description: Walk position bar in Run; scale-index bit while selecting + - id: LED3 + name: Position / Scale bit 3 + description: Walk position bar in Run; scale-index bit while selecting + - id: LED4 + name: Position / Scale bit 4 + description: Walk position bar in Run; scale-index bit while selecting + - id: LED5 + name: Position / Scale bit 5 + description: Walk position bar in Run; scale-index bit while selecting diff --git a/releases/89_Lockstep/lockstep.uf2 b/releases/89_Lockstep/lockstep.uf2 new file mode 100644 index 0000000000000000000000000000000000000000..406c091aa4e2a3dd36de4ca5856b07a050df3fc9 GIT binary patch literal 176128 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

q9vYVOwrck_7h5SGAEX4H0q$; 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 +#include +#include "lockstep_engine.h" + +static bool chromaInScale(uint8_t note, uint8_t scaleIdx) { + int c = note % 12; + const Scale& s = generative_scales[scaleIdx]; + for (int i = 0; i < s.num_notes; i++) if (s.notes[i] == c) return true; + return false; +} + +int main() { + // 1) Every output note is a scale tone AND positive-CV safe (>= MINNOTE), + // and the walk never escapes its range, across all scales and Y settings. + for (uint8_t sc = 0; sc < NUM_SCALES; sc++) { + Walk w; w.reseed(0xC0FFEEu + sc); + for (int i = 0; i < 20000; i++) { + int32_t range = i % (LOCKSTEP_HALFMAX + 1); // sweep Y: 0..12 + w.step(range); + assert(w.pos >= -LOCKSTEP_HALFMAX && w.pos <= LOCKSTEP_HALFMAX); + uint8_t n = LockstepNote(w.pos, sc); + assert(n >= LOCKSTEP_MINNOTE); // never negative CV + assert(chromaInScale(n, sc)); // always on the scale + } + } + + // 2) range == 0 pins to the center note (Y fully CCW = single held note). + { + Walk w; w.reseed(1); + for (int i = 0; i < 100; i++) { w.step(0); assert(w.pos == 0); } + } + + // 3) range > 0 actually moves (not frozen). + { + Walk w; w.reseed(42); + int moved = 0; + for (int i = 0; i < 200; i++) { w.step(7); if (w.pos != 0) moved++; } + assert(moved > 0); + } + + // 4) Ornament (voice B passing tone) stays in range, on-scale, and actually + // differs from the base at least sometimes (it's a *passing* tone). + for (uint8_t sc = 0; sc < NUM_SCALES; sc++) { + Walk w; w.reseed(0xBEEF + sc); + int differed = 0; + for (int i = 0; i < 5000; i++) { + int32_t range = 1 + (i % LOCKSTEP_HALFMAX); // 1..12 (range 0 is the frozen case) + w.step(range); + int32_t pb = LockstepOrnament(w.pos, range, w.next()); + assert(pb >= -range && pb <= range); + uint8_t nb = LockstepNote(pb, sc); + assert(nb >= LOCKSTEP_MINNOTE); + assert(chromaInScale(nb, sc)); + if (pb != w.pos) differed++; + } + assert(differed > 0); + } + + printf("lockstep engine: all checks passed\n"); + return 0; +} diff --git a/releases/89_Lockstep/src/.gitignore b/releases/89_Lockstep/src/.gitignore new file mode 100644 index 000000000..567609b12 --- /dev/null +++ b/releases/89_Lockstep/src/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/releases/89_Lockstep/src/CMakeLists.txt b/releases/89_Lockstep/src/CMakeLists.txt new file mode 100644 index 000000000..ec78d79e7 --- /dev/null +++ b/releases/89_Lockstep/src/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required (VERSION 3.13) +include(pico_sdk_import.cmake) +project(computercard C CXX ASM) +set(CMAKE_CXX_STANDARD 17) +pico_sdk_init() + + +macro (add_program _name) + add_executable(${ARGV}) + if (TARGET ${_name}) + target_compile_definitions(${_name} PRIVATE PICO_XOSC_STARTUP_DELAY_MULTIPLIER=64) + target_include_directories(${_name} PUBLIC ${CMAKE_CURRENT_LIST_DIR}) + target_link_libraries(${_name} pico_unique_id pico_stdlib hardware_dma hardware_i2c hardware_pwm hardware_adc hardware_spi) + pico_add_extra_outputs(${_name}) + target_sources(${_name} PUBLIC ${_name}.cpp) + target_compile_definitions(${_name} PRIVATE PICO_XOSC_STARTUP_DELAY_MULTIPLIER=64) + + pico_enable_stdio_usb(${_name} 0) + endif() + endmacro() + + +add_program(lockstep) diff --git a/releases/89_Lockstep/src/ComputerCard.h b/releases/89_Lockstep/src/ComputerCard.h new file mode 100644 index 000000000..1da6de33e --- /dev/null +++ b/releases/89_Lockstep/src/ComputerCard.h @@ -0,0 +1,1182 @@ +/* +ComputerCard - by Chris Johnson + +version 0.3.0 - 12 May 2026 + +ComputerCard is a header-only C++ library, providing a class that +manages the hardware aspects of the Music Thing Modular Workshop +System Computer. + +It aims to present a very simple C++ interface for card programmers +to use the jacks, knobs, switch and LEDs, for programs running at +a fixed 48kHz audio sample rate. + +See examples/ directory +*/ + + +#ifndef COMPUTERCARD_H +#define COMPUTERCARD_H + +#include "hardware/gpio.h" +#include "hardware/pwm.h" + +#define PULSE_1_RAW_OUT 8 +#define PULSE_2_RAW_OUT 9 + +#define CV_OUT_1 23 +#define CV_OUT_2 22 + +// USB host status pin +#define USB_HOST_STATUS 20 + +class ComputerCard +{ + constexpr static int numLeds = 6; + constexpr static uint8_t leds[numLeds] = { 10, 11, 12, 13, 14, 15 }; +public: + + /// Knob index, used by KnobVal + enum Knob {Main, X, Y}; + /// Switch position, used by SwitchVal + enum Switch {Down, Middle, Up}; + /// Input jack socket, used by Connected and Disconnected + enum Input {Audio1, Audio2, CV1, CV2, Pulse1, Pulse2}; + /// Hardware version + enum HardwareVersion_t {Proto1=0x2a, Proto2_Rev1=0x30, Rev1_1=0x0C, Unknown=0xFF}; + /// USB Power state + enum USBPowerState_t {DFP, UFP, Unsupported}; + + ComputerCard(); + + /** \brief Start audio processing. + + The Run method starts audio processing, calling ProcessSample using an interrupt. + Run is a blocking function (it never returns) + */ + void Run() + { + ComputerCard::thisptr = this; + AudioWorker(); + } + + /// Use before Run() to enable Connected/Disconnected detection + void EnableNormalisationProbe() {useNormProbe = true;} + + static ComputerCard *ThisPtr() {return thisptr;} + +protected: + + class NotchFilter + { + public: + NotchFilter() + { + mix1 = mix2 = mixf1 = mixf2 = 0; + } + int32_t operator()(int32_t val) + { + int32_t mixf = (ooa0 * (val + mix2) - a2oa0 * mixf2) >> 14; + mix2 = mix1; + mix1 = val; + mixf2 = mixf1; + mixf1 = mixf; + return mixf; + } + private: + // 12kHz notch filter, to remove interference from mux lines + int32_t mix1, mix2, mixf1, mixf2; + static constexpr int32_t ooa0 = 16302, a2oa0 = 16221; // Q = 100, very narrow notch + + }; + + NotchFilter notchLeft, notchRight; + + /// Callback, called once per sample at 48kHz + virtual void ProcessSample() = 0; + + + + + /// Read knob position (returns 0-4095) + int32_t __not_in_flash_func(KnobVal)(Knob ind) {return knobs[ind];} + + /// Read switch position + Switch __not_in_flash_func(SwitchVal)() {return switchVal;} + + /// Read switch position + bool __not_in_flash_func(SwitchChanged)() {return switchVal != lastSwitchVal;} + + + /// Set Audio output (values -2048 to 2047) + void __not_in_flash_func(AudioOut)(int i, int16_t val) + { + dacOut[i] = val; + } + + /// Set Audio 1 output (values -2048 to 2047) + void __not_in_flash_func(AudioOut1)(int16_t val) + { + dacOut[0] = val; + } + + /// Set Audio 2 output (values -2048 to 2047) + void __not_in_flash_func(AudioOut2)(int16_t val) + { + dacOut[1] = val; + } + + + /// Set CV output (values -2048 to 2047) + void __not_in_flash_func(CVOut)(int i, int16_t val) + { + if (val<-2048) val = -2048; + if (val > 2047) val = 2047; + cvValue[i] = (2047-val)*125; + } + + /// Set CV 1 output (values -2048 to 2047) + void __not_in_flash_func(CVOut1)(int16_t val) + { + if (val<-2048) val = -2048; + if (val > 2047) val = 2047; + cvValue[0] = (2047-val)*125; + } + + /// Set CV 2 output (values -2048 to 2047) + void __not_in_flash_func(CVOut2)(int16_t val) + { + if (val<-2048) val = -2048; + if (val > 2047) val = 2047; + cvValue[1] = (2047-val)*125; + } + + + /// Set CV output (values -262144 to 262143) + void __not_in_flash_func(CVOutPrecise)(int i, int32_t val) + { + if (val<-262144) val = -262144; + if (val > 262143) val = 262143; + cvValue[i] = ((262143-val)*125)>>7; + } + + /// Set CV 1 output (values -262144 to 262143) + void __not_in_flash_func(CVOut1Precise)(int32_t val) + { + if (val<-262144) val = -262144; + if (val > 262143) val = 262143; + cvValue[0] = ((262143-val)*125)>>7; + } + + /// Set CV 2 output (values -262144 to 262143) + void __not_in_flash_func(CVOut2Precise)(int32_t val) + { + if (val<-262144) val = -262144; + if (val > 262143) val = 262143; + cvValue[1] = ((262143-val)*125)>>7; + } + + /// Set CV 1 output from calibrated MIDI note number (values 0 to 127) + void __not_in_flash_func(CVOutMIDINote)(int i, uint8_t noteNum) + { + cvValue[i] = MIDIToDAC(noteNum, i); + } + + /// Set CV 1 output from calibrated MIDI note number (values 0 to 127) + void __not_in_flash_func(CVOut1MIDINote)(uint8_t noteNum) + { + cvValue[0] = MIDIToDAC(noteNum, 0); + } + + /// Set CV 2 output from calibrated MIDI note number (values 0 to 127) + void __not_in_flash_func(CVOut2MIDINote)(uint8_t noteNum) + { + cvValue[1] = MIDIToDAC(noteNum, 1); + } + + + /// Set CV 1 output from calibrated MIDI note number (values 0 to 127) + bool __not_in_flash_func(CVOutMillivolts)(int i, int32_t millivolts) + { + bool limited = false; + cvValue[i] = MillivoltsToDAC(millivolts, i, limited); + return limited; + } + + /// Set CV 1 output from calibrated MIDI note number (values 0 to 127) + bool __not_in_flash_func(CVOut1Millivolts)(int32_t millivolts) + { + bool limited = false; + cvValue[0] = MillivoltsToDAC(millivolts, 0, limited); + return limited; + } + + /// Set CV 2 output from calibrated MIDI note number (values 0 to 127) + bool __not_in_flash_func(CVOut2Millivolts)(int32_t millivolts) + { + bool limited = false; + cvValue[1] = MillivoltsToDAC(millivolts, 1, limited); + return limited; + } + + + /// Set Pulse output (true = on) + void __not_in_flash_func(PulseOut)(int i, bool val) + { + gpio_put(PULSE_1_RAW_OUT + i, !val); + } + + /// Set Pulse 1 output (true = on) + void __not_in_flash_func(PulseOut1)(bool val) + { + gpio_put(PULSE_1_RAW_OUT, !val); + } + + /// Set Pulse 2 output (true = on) + void __not_in_flash_func(PulseOut2)(bool val) + { + gpio_put(PULSE_2_RAW_OUT, !val); + } + + /// Return audio in (-2048 to 2047) + int16_t __not_in_flash_func(AudioIn)(int i){return i?adcInR:adcInL;} + + /// Return audio in 1 (-2048 to 2047) + int16_t __not_in_flash_func(AudioIn1)(){return adcInL;} + + /// Return audio in 1 (-2048 to 2047) + int16_t __not_in_flash_func(AudioIn2)(){return adcInR;} + + /// Return CV in (-2048 to 2047) + int16_t __not_in_flash_func(CVIn)(int i){return cv[i];} + + /// Return CV in 1 (-2048 to 2047) + int16_t __not_in_flash_func(CVIn1)(){return cv[0];} + + /// Return CV in 2 (-2048 to 2047) + int16_t __not_in_flash_func(CVIn2)(){return cv[1];} + + /// Read pulse in + bool __not_in_flash_func(PulseIn)(int i){return pulse[i];} + /// Return true for one sample on pulse rising edge + bool __not_in_flash_func(PulseInRisingEdge)(int i){return pulse[i] && !last_pulse[i];} + /// Return true for one sample on pulse falling edge + bool __not_in_flash_func(PulseInFallingEdge)(int i){return !pulse[i] && last_pulse[i];} + + /// Read pulse in 1 + bool __not_in_flash_func(PulseIn1)(){return pulse[0];} + /// Return true for one sample on pulse 1 rising edge + bool __not_in_flash_func(PulseIn1RisingEdge)(){return pulse[0] && !last_pulse[0];} + /// Return true for one sample on pulse 1 falling edge + bool __not_in_flash_func(PulseIn1FallingEdge)(){return !pulse[0] && last_pulse[0];} + + /// Read pulse in 2 + bool __not_in_flash_func(PulseIn2)(){return pulse[1];} + /// Return true for one sample on pulse 2 falling edge + bool __not_in_flash_func(PulseIn2FallingEdge)(){return !pulse[1] && last_pulse[1];} + /// Return true for one sample on pulse 2 rising edge + bool __not_in_flash_func(PulseIn2RisingEdge)(){return pulse[1] && !last_pulse[1];} + + + /// Return true if jack connected to input + bool __not_in_flash_func(Connected)(Input i){return connected[i];} + /// Return true if no jack connected to input + bool __not_in_flash_func(Disconnected)(Input i){return !connected[i];} + + + /// Set LED brightness, values 0-4095 + // Led numbers are: + // 0 1 + // 2 3 + // 4 5 + void __not_in_flash_func(LedBrightness)(uint32_t index, uint16_t value) + { + pwm_set_gpio_level(leds[index], (value*value)>>8); + } + + /// Turn LED on/off + void __not_in_flash_func(LedOn)(uint32_t index, bool value = true) + { + pwm_set_gpio_level(leds[index], value?65535:0); + } + + /// Turn LED off + void __not_in_flash_func(LedOff)(uint32_t index) + { + pwm_set_gpio_level(leds[index], 0); + } + + // Return power state of USB port + USBPowerState_t USBPowerState() + { + if (HardwareVersion() != Rev1_1) + return Unsupported; + else if (gpio_get(USB_HOST_STATUS)) + return UFP; + else + return DFP; + } + + /// Return hardware version + HardwareVersion_t HardwareVersion() const + { + return hw; + } + + /// Return ID number unique to flash card + uint64_t UniqueCardID() const + { + return uniqueID; + } + + /// Return true iff CV outputs are calibrated. + /// Returns false if using default calibration values. + bool CVOutsCalibrated() const + { + return cvOutsCalibrated; + } + + + void Abort(); + + uint16_t CRCencode(const uint8_t *data, int length); + +private: + + typedef struct + { + float m, b; + int32_t mi, bi; + } CalCoeffs; + + typedef struct + { + int32_t dacSetting; + int8_t voltage; + } CalPoint; + + static constexpr int calMaxChannels = 2; + static constexpr int calMaxPoints = 10; + + static volatile uint32_t cvValue[2]; + + uint8_t numCalibrationPoints[calMaxChannels]; + CalPoint calibrationTable[calMaxChannels][calMaxPoints]; + CalCoeffs calCoeffs[calMaxChannels]; + + uint64_t uniqueID; + + uint8_t ReadByteFromEEPROM(unsigned int eeAddress, bool &failed); + int ReadIntFromEEPROM(unsigned int eeAddress, bool &failed); + void CalcCalCoeffs(int channel); + int ReadEEPROM(); + uint32_t MIDIToDAC(int midiNote, int channel); + uint32_t MillivoltsToDAC(int millivolts, int channel, bool &limited); + + HardwareVersion_t hw; + HardwareVersion_t ProbeHardwareVersion(); + + int16_t dacOut[2]; + + volatile int32_t knobs[4] = { 0, 0, 0, 0 }; // 0-4095 + volatile bool pulse[2] = { 0, 0 }; + volatile bool last_pulse[2] = { 0, 0 }; + volatile int32_t cv[2] = { 0, 0 }; // -2047 - 2048 + volatile int16_t adcInL = 0x800, adcInR = 0x800; + + volatile uint8_t mxPos = 0; // external multiplexer value + + volatile int32_t plug_state[6] = {0,0,0,0,0,0}; + volatile bool connected[6] = {0,0,0,0,0,0}; + bool useNormProbe; + + Switch switchVal, lastSwitchVal; + + volatile uint8_t runADCMode; + + bool cvOutsCalibrated; + +// Buffers that DMA reads into / out of + uint16_t ADC_Buffer[2][8]; + uint16_t SPI_Buffer[2][2]; + + uint8_t adc_dma, spi_dma; // DMA ids + + + + uint8_t dmaPhase = 0; + + // Convert signed int16 value into data string for DAC output + uint16_t __not_in_flash_func(dacval)(int16_t value, uint16_t dacChannel) + { + if (value<-2048) value = -2048; + if (value > 2047) value = 2047; + return (dacChannel | 0x3000) | (((uint16_t)((value & 0x0FFF) + 0x800)) & 0x0FFF); + } + uint32_t next_norm_probe(); + + + void CorrectADCDNL(uint16_t &value) const; + + void BufferFull(); + + void AudioWorker(); + + static void AudioCallback() + { + thisptr->BufferFull(); + } + static ComputerCard *thisptr; + + // 19-bit CV outputs + static void OnCVPWMWrap() + { + static int32_t error1 = 0, error2 = 0; + + pwm_clear_irq(pwm_gpio_to_slice_num(CV_OUT_1)); // clear the interrupt flag + uint32_t truncated_cv1_val = (cvValue[0]-error1) & 0xFFFFFF00; + error1 += truncated_cv1_val - cvValue[0]; + pwm_set_gpio_level(CV_OUT_1, (truncated_cv1_val>>8)); + uint32_t truncated_cv2_val = (cvValue[1]-error2) & 0xFFFFFF00; + error2 += truncated_cv2_val - cvValue[1]; + pwm_set_gpio_level(CV_OUT_2, (truncated_cv2_val>>8)); + } + +}; + + +#ifndef COMPUTERCARD_NOIMPL + + +#include "hardware/adc.h" +#include "hardware/clocks.h" +#include "hardware/dma.h" +#include "hardware/flash.h" +#include "hardware/i2c.h" +#include "hardware/irq.h" +#include "hardware/spi.h" + +// Input normalisation probe pin +#define NORMALISATION_PROBE 4 + +// Mux pins +#define MX_A 24 +#define MX_B 25 + +// ADC input pins +#define AUDIO_L_IN_1 27 +#define AUDIO_R_IN_1 26 +#define MUX_IO_1 28 +#define MUX_IO_2 29 + +#define DAC_CHANNEL_A 0x0000 +#define DAC_CHANNEL_B 0x8000 + +#define DAC_CS 21 +#define DAC_SCK 18 +#define DAC_TX 19 + +#define EEPROM_SDA 16 +#define EEPROM_SCL 17 + +#define PULSE_1_INPUT 2 +#define PULSE_2_INPUT 3 + +#define DEBUG_1 0 +#define DEBUG_2 1 + +#define SPI_PORT spi0 +#define SPI_DREQ DREQ_SPI0_TX + + +#define BOARD_ID_0 7 +#define BOARD_ID_1 6 +#define BOARD_ID_2 5 + +// The ADC (/DMA) run mode, used to stop DMA in a known state before writing to flash +#define RUN_ADC_MODE_RUNNING 0 +#define RUN_ADC_MODE_REQUEST_ADC_STOP 1 +#define RUN_ADC_MODE_ADC_STOPPED 2 +#define RUN_ADC_MODE_REQUEST_ADC_RESTART 3 + + +#define EEPROM_ADDR_ID 0 +#define EEPROM_ADDR_VERSION 2 +#define EEPROM_ADDR_CRC_L 87 +#define EEPROM_ADDR_CRC_H 86 +#define EEPROM_VAL_ID 2001 +#define EEPROM_NUM_BYTES 88 + +#define EEPROM_PAGE_ADDRESS 0x50 + + +// Initialise CV output delta-sigma target to half-way (near 0V) +volatile uint32_t ComputerCard::cvValue[2] = {262144,262144}; + + +ComputerCard *ComputerCard::thisptr; + +// Return pseudo-random bit for normalisation probe +uint32_t __not_in_flash_func(ComputerCard::next_norm_probe)() +{ + static uint32_t lcg_seed = 1; + lcg_seed = 1664525 * lcg_seed + 1013904223; + return lcg_seed >> 31; +} + +// Main audio core function +void __not_in_flash_func(ComputerCard::AudioWorker)() +{ + + adc_select_input(0); + adc_set_round_robin(0b0001111U); + + // enabled, with DMA request when FIFO contains data, no erro flag, no byte shift + adc_fifo_setup(true, true, 1, false, false); + + + // ADC clock runs at 48MHz + // 48MHz ÷ (124+1) = 384kHz ADC sample rate + // = 8×48kHz audio sample rate + adc_set_clkdiv(124); + + // claim and setup DMAs for reading to ADC, and writing to SPI DAC + adc_dma = dma_claim_unused_channel(true); + spi_dma = dma_claim_unused_channel(true); + + dma_channel_config adc_dmacfg, spi_dmacfg; + adc_dmacfg = dma_channel_get_default_config(adc_dma); + spi_dmacfg = dma_channel_get_default_config(spi_dma); + + // Reading from ADC into memory buffer, so increment on write, but no increment on read + channel_config_set_transfer_data_size(&adc_dmacfg, DMA_SIZE_16); + channel_config_set_read_increment(&adc_dmacfg, false); + channel_config_set_write_increment(&adc_dmacfg, true); + + // Synchronise ADC DMA the ADC samples + channel_config_set_dreq(&adc_dmacfg, DREQ_ADC); + + // Setup DMA for 8 ADC samples + dma_channel_configure(adc_dma, &adc_dmacfg, ADC_Buffer[dmaPhase], &adc_hw->fifo, 8, true); + + // Turn on IRQ for ADC DMA + dma_channel_set_irq0_enabled(adc_dma, true); + + // Call buffer_full ISR when ADC DMA finished + irq_set_enabled(DMA_IRQ_0, true); + irq_set_exclusive_handler(DMA_IRQ_0, ComputerCard::AudioCallback); + + + // Turn on IRQ for CV output PWM + uint slice_num = pwm_gpio_to_slice_num(CV_OUT_1); + pwm_clear_irq(slice_num); + pwm_set_irq_enabled(slice_num, true); + + irq_set_exclusive_handler(PWM_IRQ_WRAP, ComputerCard::OnCVPWMWrap); + irq_set_priority(PWM_IRQ_WRAP, 255); + irq_set_enabled(PWM_IRQ_WRAP, true); + + + // Set up DMA for SPI + spi_dmacfg = dma_channel_get_default_config(spi_dma); + channel_config_set_transfer_data_size(&spi_dmacfg, DMA_SIZE_16); + + // SPI DMA timed to SPI TX + channel_config_set_dreq(&spi_dmacfg, SPI_DREQ); + + // Set up DMA to transmit 2 samples to SPI + dma_channel_configure(spi_dma, &spi_dmacfg, &spi_get_hw(SPI_PORT)->dr, NULL, 2, false); + + adc_run(true); + + while (1) + { + // If ready to restart + if (runADCMode == RUN_ADC_MODE_REQUEST_ADC_RESTART) + { + runADCMode = RUN_ADC_MODE_RUNNING; + + dma_hw->ints0 = 1u << adc_dma; // reset adc interrupt flag + dma_channel_set_write_addr(adc_dma, ADC_Buffer[dmaPhase], true); // start writing into new buffer + dma_channel_set_read_addr(spi_dma, SPI_Buffer[dmaPhase], true); // start reading from new buffer + + adc_set_round_robin(0); + adc_select_input(0); + adc_set_round_robin(0b0001111U); + adc_run(true); + } + else if (runADCMode == RUN_ADC_MODE_ADC_STOPPED) + { + // We can't remove the PWM IRQ from within the ADC IRQ callback, so we do it here instead. + irq_set_enabled(PWM_IRQ_WRAP, false); + pwm_clear_irq(pwm_gpio_to_slice_num(CV_OUT_1)); // reset CV PWM interrupt flag + irq_remove_handler(PWM_IRQ_WRAP, ComputerCard::OnCVPWMWrap); + break; + } + + + } +} + +void ComputerCard::Abort() +{ + runADCMode = RUN_ADC_MODE_REQUEST_ADC_STOP; +} + +void __not_in_flash_func(ComputerCard::CorrectADCDNL)(uint16_t &value) const +{ + uint16_t adc512 = value + 512; + value += ((value & 0x3FF) == 0x1FF) << 2; + value += (adc512 >> 10) << 3; + value = uint32_t(value * 520349) >> 19; // Multiply by factor that maps 0-4095 input into 0-4095 output +} + +// Per-audio-sample ISR, called when two sets of ADC samples have been collected from all four inputs +void __not_in_flash_func(ComputerCard::BufferFull)() +{ + static int startupCounter = 8; // Decreases by 1 each sample, can do startup things when nonzero. + static int mux_state = 0; + static int norm_probe_count = 0; + + // Internal variables for IIR filters on knobs/cv + static volatile int32_t knobssm[4] = { 0, 0, 0, 0 }; + static volatile int32_t cvsm[2] = { 0, 0 }; + __attribute__((unused)) static int np = 0, np1 = 0, np2 = 0; + + adc_select_input(0); + + // Advance external mux to next state + int next_mux_state = (mux_state + 1) & 0x3; + gpio_put(MX_A, next_mux_state & 1); + gpio_put(MX_B, next_mux_state & 2); + + // Set up new writes into next buffer + uint8_t cpuPhase = dmaPhase; + dmaPhase = 1 - dmaPhase; + + dma_hw->ints0 = 1u << adc_dma; // reset adc interrupt flag + dma_channel_set_write_addr(adc_dma, ADC_Buffer[dmaPhase], true); // start writing into new buffer + dma_channel_set_read_addr(spi_dma, SPI_Buffer[dmaPhase], true); // start reading from new buffer + + //////////////////////////////////////// + // Collect various inputs and put them in variables for the DSP + + // Set CV inputs, with ~240Hz LPF on CV input + int cvi = mux_state % 2; + + // Compensation of ADC DNL errors. + CorrectADCDNL(ADC_Buffer[cpuPhase][7]); // CV inputs + CorrectADCDNL(ADC_Buffer[cpuPhase][0]); // Audio inputs + CorrectADCDNL(ADC_Buffer[cpuPhase][4]); + CorrectADCDNL(ADC_Buffer[cpuPhase][1]); + CorrectADCDNL(ADC_Buffer[cpuPhase][5]); + + cvsm[cvi] = (15 * (cvsm[cvi]) + 16 * ADC_Buffer[cpuPhase][7]) >> 4; + cv[cvi] = 2048 - (cvsm[cvi] >> 4); + + + // Set audio inputs, by averaging the two samples collected. + // Invert to counteract inverting op-amp input configuration + adcInR = -(((ADC_Buffer[cpuPhase][0] + ADC_Buffer[cpuPhase][4]) - 0x1000) >> 1); + adcInL = -(((ADC_Buffer[cpuPhase][1] + ADC_Buffer[cpuPhase][5]) - 0x1000) >> 1); + + // 12kHz notch filters + adcInR = notchRight(adcInR); + adcInL = notchLeft(adcInL); + + // Set pulse inputs + last_pulse[0] = pulse[0]; + last_pulse[1] = pulse[1]; + pulse[0] = !gpio_get(PULSE_1_INPUT); + pulse[1] = !gpio_get(PULSE_2_INPUT); + + // Set knobs, with ~60Hz LPF + int knob = mux_state; + knobssm[knob] = (127 * (knobssm[knob]) + 16 * ADC_Buffer[cpuPhase][6]) >> 7; + knobs[knob] = knobssm[knob] >> 4; + + // Set switch value + switchVal = static_cast((knobs[3]>1000) + (knobs[3]>3000)); + if (startupCounter) + { + // Don't detect switch changes in first few cycles + lastSwitchVal = switchVal; + // Should initialise knob and CV smoothing filters here too + } + + //////////////////////////// + // Normalisation probe + + if (useNormProbe) + { + // Set normalisation probe output value + // and update np to the expected history string + if (norm_probe_count == 0) + { + int32_t normprobe = next_norm_probe(); + gpio_put(NORMALISATION_PROBE, normprobe); + np = (np<<1)+(normprobe&0x1); + } + + // CV sampled at 24kHz comes in over two successive samples + if (norm_probe_count == 14 || norm_probe_count == 15) + { + plug_state[2+cvi] = (plug_state[2+cvi]<<1)+(ADC_Buffer[cpuPhase][7]<1800); + } + + // Audio and pulse measured every sample at 48kHz + if (norm_probe_count == 15) + { + plug_state[Input::Audio1] = (plug_state[Input::Audio1]<<1)+(ADC_Buffer[cpuPhase][5]<1800); + plug_state[Input::Audio2] = (plug_state[Input::Audio2]<<1)+(ADC_Buffer[cpuPhase][4]<1800); + plug_state[Input::Pulse1] = (plug_state[Input::Pulse1]<<1)+(pulse[0]); + plug_state[Input::Pulse2] = (plug_state[Input::Pulse2]<<1)+(pulse[1]); + + for (int i=0; i<6; i++) + { + connected[i] = (np != plug_state[i]); + } + } + + // Force disconnected values to zero, rather than the normalisation probe garbage + if (Disconnected(Input::Audio1)) adcInL = 0; + if (Disconnected(Input::Audio2)) adcInR = 0; + if (Disconnected(Input::CV1)) cv[0] = 0; + if (Disconnected(Input::CV2)) cv[1] = 0; + if (Disconnected(Input::Pulse1)) pulse[0] = 0; + if (Disconnected(Input::Pulse2)) pulse[1] = 0; + } + + //////////////////////////////////////// + // Run the DSP + ProcessSample(); + + //////////////////////////////////////// + // Collect DSP outputs and put them in the DAC SPI buffer + // CV/Pulse outputs are done immediately in ProcessSample + + // Invert dacout to counteract inverting output configuration + SPI_Buffer[cpuPhase][0] = dacval(-dacOut[0], DAC_CHANNEL_A); + SPI_Buffer[cpuPhase][1] = dacval(-dacOut[1], DAC_CHANNEL_B); + + mux_state = next_mux_state; + + // If Abort called, stop ADC and DMA + if (runADCMode == RUN_ADC_MODE_REQUEST_ADC_STOP) + { + adc_run(false); + adc_set_round_robin(0); + adc_select_input(0); + + dma_hw->ints0 = 1u << adc_dma; // reset adc interrupt flag + dma_channel_cleanup(adc_dma); + dma_channel_cleanup(spi_dma); + irq_set_enabled(DMA_IRQ_0, false); + irq_remove_handler(DMA_IRQ_0, ComputerCard::AudioCallback); + + + + runADCMode = RUN_ADC_MODE_ADC_STOPPED; + } + + norm_probe_count = (norm_probe_count + 1) & 0xF; + + lastSwitchVal = switchVal; + + if (startupCounter) startupCounter--; +} + +ComputerCard::HardwareVersion_t ComputerCard::ProbeHardwareVersion() +{ + // Enable pull-downs, and measure + gpio_set_pulls(BOARD_ID_0, false, true); + gpio_set_pulls(BOARD_ID_1, false, true); + gpio_set_pulls(BOARD_ID_2, false, true); + sleep_us(1); + + // Pull-down state in bits 0, 2, 4 + uint8_t pd = gpio_get(BOARD_ID_0) | (gpio_get(BOARD_ID_1) << 2) | (gpio_get(BOARD_ID_2) << 4); + + // Enable pull-ups, and measure + gpio_set_pulls(BOARD_ID_0, true, false); + gpio_set_pulls(BOARD_ID_1, true, false); + gpio_set_pulls(BOARD_ID_2, true, false); + sleep_us(1); + + // Pull-up state in bits 1, 3, 5 + uint8_t pu = (gpio_get(BOARD_ID_0) << 1) | (gpio_get(BOARD_ID_1) << 3) | (gpio_get(BOARD_ID_2) << 5); + + // Combine to give 6-bit ID + uint8_t id = pd | pu; + + // Set pull-downs + gpio_set_pulls(BOARD_ID_0, false, true); + gpio_set_pulls(BOARD_ID_1, false, true); + gpio_set_pulls(BOARD_ID_2, false, true); + + switch (id) + { + case Proto1: + case Proto2_Rev1: + case Rev1_1: + return static_cast(id); + default: + return Unknown; + } +} + +ComputerCard::ComputerCard() +{ + runADCMode = RUN_ADC_MODE_RUNNING; + + adc_run(false); + adc_select_input(0); + + + useNormProbe = false; + for (int i=0; i<6; i++) + { + connected[i] = false; + } + + + //////////////////////////////////////// + // Initialise LEDs (PWM, set up in pairs due pinout and PWM hardware) + for (int i = 0; i < numLeds; i+=2) + { + gpio_set_function(leds[i], GPIO_FUNC_PWM); + gpio_set_function(leds[i]+1, GPIO_FUNC_PWM); + + // now create PWM config struct + pwm_config config = pwm_get_default_config(); + pwm_config_set_wrap(&config, 65535); // 16-bit PWM + + + // now set this PWM config to apply to the two outputs + pwm_init(pwm_gpio_to_slice_num(leds[i]), &config, true); + pwm_init(pwm_gpio_to_slice_num(leds[i]+1), &config, true); + + // set initial level + pwm_set_gpio_level(leds[i], 0); + pwm_set_gpio_level(leds[i]+1, 0); + } + + + //////////////////////////////////////// + // Initialise knobs / audio in / CV in (ADC + Mux) + + adc_init(); // Initialize the ADC + + // Set ADC pins + adc_gpio_init(AUDIO_L_IN_1); + adc_gpio_init(AUDIO_R_IN_1); + adc_gpio_init(MUX_IO_1); + adc_gpio_init(MUX_IO_2); + + // Initialize Mux Control pins + gpio_init(MX_A); + gpio_init(MX_B); + gpio_set_dir(MX_A, GPIO_OUT); + gpio_set_dir(MX_B, GPIO_OUT); + + + //////////////////////////////////////// + + gpio_init(PULSE_1_RAW_OUT); + gpio_set_dir(PULSE_1_RAW_OUT, GPIO_OUT); + gpio_put(PULSE_1_RAW_OUT, true); // set raw value high (output low) + + + gpio_init(PULSE_2_RAW_OUT); + gpio_set_dir(PULSE_2_RAW_OUT, GPIO_OUT); + gpio_put(PULSE_2_RAW_OUT, true); // set raw value high (output low) + + + //////////////////////////////////////// + // Initialise pulse inputs + gpio_init(PULSE_1_INPUT); + gpio_set_dir(PULSE_1_INPUT, GPIO_IN); + gpio_pull_up(PULSE_1_INPUT); // NB Needs pullup to activate transistor on inputs + + gpio_init(PULSE_2_INPUT); + gpio_set_dir(PULSE_2_INPUT, GPIO_IN); + gpio_pull_up(PULSE_2_INPUT); // NB: Needs pullup to activate transistor on inputs + + + //////////////////////////////////////// + // Initialise audio outputs (SPI for external DAC) + spi_init(SPI_PORT, 15625000); + spi_set_format(SPI_PORT, 16, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST); + gpio_set_function(DAC_SCK, GPIO_FUNC_SPI); + gpio_set_function(DAC_TX, GPIO_FUNC_SPI); + gpio_set_function(DAC_CS, GPIO_FUNC_SPI); + + + //////////////////////////////////////// + // Initialise CV outputs + // We set up the PWM here, and add the IRQ for sigma-delta later one Run() is called + + // First, tell the CV pins that the PWM is in charge of the value. + gpio_set_function(CV_OUT_1, GPIO_FUNC_PWM); + gpio_set_function(CV_OUT_2, GPIO_FUNC_PWM); + + // now create PWM config struct + { + pwm_config config = pwm_get_default_config(); + pwm_config_set_wrap(&config, 1999); // less than 11-bit PWM + // now set this PWM config to apply to the two outputs + // NB: CV_A and CV_B share the same PWM slice, which means that they share a PWM config + // They have separate 'gpio_level's (output compare unit) though, so they can have different PWM on-times + pwm_init(pwm_gpio_to_slice_num(CV_OUT_1), &config, true); // Slice 1, channel A + pwm_init(pwm_gpio_to_slice_num(CV_OUT_2), &config, true); // slice 1 channel B (redundant to set up again) + + } + // set initial level to half way (0V) + pwm_set_gpio_level(CV_OUT_1, 1000); + pwm_set_gpio_level(CV_OUT_2, 1000); + + + //////////////////////////////////////// + // Miscellaneous pins + + // Initialise board version ID pins + gpio_init(BOARD_ID_0); + gpio_init(BOARD_ID_1); + gpio_init(BOARD_ID_2); + gpio_set_dir(BOARD_ID_0, GPIO_IN); + gpio_set_dir(BOARD_ID_1, GPIO_IN); + gpio_set_dir(BOARD_ID_2, GPIO_IN); + + // Initialise USB host status pin + gpio_init(USB_HOST_STATUS); + gpio_disable_pulls(USB_HOST_STATUS); + + // Initialise normalisation probe pin + gpio_init(NORMALISATION_PROBE); + gpio_set_dir(NORMALISATION_PROBE, GPIO_OUT); + gpio_put(NORMALISATION_PROBE, false); + + // Initialise EEPROM (I2C) + i2c_init(i2c0, 100 * 1000); + gpio_set_function(EEPROM_SDA, GPIO_FUNC_I2C); + gpio_set_function(EEPROM_SCL, GPIO_FUNC_I2C); + + + // If not using UART pins for UART, instead use as debug lines +#ifndef ENABLE_UART_DEBUGGING + // Debug pins + gpio_init(DEBUG_1); + gpio_set_dir(DEBUG_1, GPIO_OUT); + + gpio_init(DEBUG_2); + gpio_set_dir(DEBUG_2, GPIO_OUT); +#endif + + // Read hardware version + hw = ProbeHardwareVersion(); + + // Read EEPROM calibration values + cvOutsCalibrated = (ReadEEPROM() == 0); + + // Read unique card ID + flash_get_unique_id((uint8_t *) &uniqueID); + // Do some mixing up of the bits using full-cycle 64-bit LCG + // Should help ensure most bytes change even if many bits of + // the original flash unique ID are the same between flash chips. + for (int i=0; i<20; i++) + { + uniqueID = uniqueID * 6364136223846793005ULL + 1442695040888963407ULL; + } +} + + + +// Read a byte from EEPROM +uint8_t ComputerCard::ReadByteFromEEPROM(unsigned int eeAddress, bool &failed) +{ + uint8_t deviceAddress = EEPROM_PAGE_ADDRESS | ((eeAddress >> 8) & 0x0F); + uint8_t data = 0xFF; + + uint8_t addr_low_byte = eeAddress & 0xFF; + + if (i2c_write_timeout_us(i2c0, deviceAddress, &addr_low_byte, 1, false, 10000) <= 0) + { + failed = true; + return 0; + } + + if (i2c_read_timeout_us(i2c0, deviceAddress, &data, 1, false, 10000) <= 0) + { + failed = true; + return 0; + } + + return data; +} + +// Read a 16-bit integer from EEPROM +int ComputerCard::ReadIntFromEEPROM(unsigned int eeAddress, bool &failed) +{ + uint8_t highByte = ReadByteFromEEPROM(eeAddress, failed); + uint8_t lowByte = ReadByteFromEEPROM(eeAddress + 1, failed); + + return (highByte << 8) | lowByte; +} + +uint16_t ComputerCard::CRCencode(const uint8_t *data, int length) +{ + uint16_t crc = 0xFFFF; // Initial CRC value + for (int i = 0; i < length; i++) + { + crc ^= ((uint16_t)data[i]) << 8; // Bring in the next byte + for (uint8_t bit = 0; bit < 8; bit++) + { + if (crc & 0x8000) + { + crc = (crc << 1) ^ 0x1021; // CRC-CCITT polynomial + } + else + { + crc = crc << 1; + } + } + } + return crc; +} + + +int ComputerCard::ReadEEPROM() +{ + // Set up default values in the calibration table, + // to be used if we can't read valid calibration from EEPROM + for (unsigned channel = 0; channel < calMaxChannels; channel++) + { + numCalibrationPoints[channel] = 3; + calibrationTable[channel][0].voltage = -20; // -2V + calibrationTable[channel][0].dacSetting = 347700; + calibrationTable[channel][1].voltage = 0; // 0V + calibrationTable[channel][1].dacSetting = 261200; + calibrationTable[channel][2].voltage = 20; // +2V + calibrationTable[channel][2].dacSetting = 174400; + CalcCalCoeffs(channel); // calculate the coefficients + } + + // Read magic number + // Failure here could occur if I2C failed, or if incorrect/no magic number stored in EEPROM + bool i2cFailed = false; + if (ReadIntFromEEPROM(EEPROM_ADDR_ID, i2cFailed) != EEPROM_VAL_ID) + { + return 1; + } + + // Read the EEPROM into RAM + uint8_t buf[EEPROM_NUM_BYTES]; + for (int i = 0; i < EEPROM_NUM_BYTES; i++) + { + buf[i] = ReadByteFromEEPROM(i, i2cFailed); + } + + // Check CRC and fail if incorrect + uint16_t calculatedCRC = CRCencode(buf, 86); + uint16_t foundCRC = ((uint16_t)buf[EEPROM_ADDR_CRC_H] << 8) | buf[EEPROM_ADDR_CRC_L]; + if (calculatedCRC != foundCRC) + { + return 1; + } + + // CRC passed, so now read the calibration information + for (uint8_t channel = 0; channel < calMaxChannels; channel++) + { + int channelOffset = 4 + (41 * channel); // channel 0 = 4, channel 1 = 45 + numCalibrationPoints[channel] = buf[channelOffset++]; + for (uint8_t point = 0; point < numCalibrationPoints[channel]; point++) + { + // Unpack Pack targetVoltage (int8_t) from buf + int8_t targetVoltage = (int8_t)buf[channelOffset++]; + + // Unpack dacSetting (uint32_t) from buf (4 bytes) + uint32_t dacSetting = 0; + dacSetting |= ((uint32_t)buf[channelOffset++]) << 24; // MSB + dacSetting |= ((uint32_t)buf[channelOffset++]) << 16; + dacSetting |= ((uint32_t)buf[channelOffset++]) << 8; + dacSetting |= ((uint32_t)buf[channelOffset++]); // LSB + + // Write settings into calibration table + calibrationTable[channel][point].voltage = targetVoltage; + calibrationTable[channel][point].dacSetting = dacSetting; + } + + // Now calculate the calibration coeffs that are actually used + // by the calibrated CVOut functions + CalcCalCoeffs(channel); + } + + return 0; +} + +void ComputerCard::CalcCalCoeffs(int channel) +{ + float sumV = 0.0; + float sumDAC = 0.0; + float sumV2 = 0.0; + float sumVDAC = 0.0; + int N = numCalibrationPoints[channel]; + + for (int i = 0; i < N; i++) + { + float v = calibrationTable[channel][i].voltage * 0.1f; + float dac = calibrationTable[channel][i].dacSetting; + sumV += v; + sumDAC += dac; + sumV2 += v * v; + sumVDAC += v * dac; + } + + float denominator = N * sumV2 - sumV * sumV; + if (denominator != 0) + { + calCoeffs[channel].m = (N * sumVDAC - sumV * sumDAC) / denominator; + } + else + { + calCoeffs[channel].m = 0.0; + } + calCoeffs[channel].b = (sumDAC - calCoeffs[channel].m * sumV) / N; + + calCoeffs[channel].mi = int32_t(calCoeffs[channel].m * 1.333333333333333f + 0.5f); + calCoeffs[channel].bi = int32_t(calCoeffs[channel].b + 0.5f); +} + + +uint32_t ComputerCard::MIDIToDAC(int midiNote, int channel) +{ + int32_t dacValue = ((calCoeffs[channel].mi * (midiNote - 60)) >> 4) + calCoeffs[channel].bi; + if (dacValue > 524287) dacValue = 524287; + if (dacValue < 0) dacValue = 0; + return (dacValue*125)>>7; +} + +/// Converts voltage in millivolts to corresponding 19-bit sigma-delta PWM DAC value +/// Returns true if requested voltage is outside of full range of DAC values +/// millivolts should be in range -6000 to 6000. +/// Accuracy is dependent, of course, on the calibration coefficients +uint32_t ComputerCard::MillivoltsToDAC(int millivolts, int channel, bool &limited) +{ + limited = false; + int32_t dacValue = ((((calCoeffs[channel].mi * millivolts) >> 9) * 1573) >> 12) + calCoeffs[channel].bi; + if (dacValue > 524287) + { + dacValue = 524287; + limited = true; + } + if (dacValue < 0) + { + dacValue = 0; + limited = true; + } + return (dacValue*125)>>7; +} + +#endif + +#endif diff --git a/releases/89_Lockstep/src/lockstep.cpp b/releases/89_Lockstep/src/lockstep.cpp new file mode 100644 index 000000000..002aa064e --- /dev/null +++ b/releases/89_Lockstep/src/lockstep.cpp @@ -0,0 +1,160 @@ +#include "ComputerCard.h" +#include "lockstep_engine.h" + +// Lockstep -- dual quantized pitch-mover for the Music Thing Workshop Computer. +// +// Two CV outs walk together through a scale; you tune the two oscillators by hand +// to set the interval and the card moves them in parallel. X = how often it moves, +// Y = how far. Switch DOWN (hold) = scale select, UP = freeze, MIDDLE = run. +// Main knob = resonant low-pass cutoff on the two oscillators returned via Audio In. +// See README.md. + +class Lockstep : public ComputerCard { +public: + Lockstep() {} + + virtual void __not_in_flash_func(ProcessSample)() { + Switch sw = SwitchVal(); + int32_t kx = KnobVal(X); + int32_t ky = KnobVal(Y); + int32_t km = KnobVal(Main); + + // ---- scale select while switch held DOWN (Main knob picks the scale) ---- + if (sw == Down) { + uint8_t s = (uint8_t)((km * NUM_SCALES) >> 12); // 0..11 + if (s >= NUM_SCALES) s = NUM_SCALES - 1; + scaleIdx = s; + } + + // ---- re-seed / jump to a fresh pattern on Pulse In 2 ---- + if (PulseIn2RisingEdge()) walk.reseed(walk.rng ^ 0x9e3779b9u); + + // ---- when to move: X sets rate; detect the main beat and the mid-beat (for the double) ---- + int32_t range = (ky * LOCKSTEP_HALFMAX) >> 12; // 0..12 semis (Y) + bool frozen = (sw == Up); + bool mainMove = false; // both voices step together + bool midMove = false; // voice B takes an extra step + + if (Connected(Pulse1)) { + int32_t div = 1 + (((4095 - kx) * 60) >> 12); // 1 (CW, fast) .. 60 (CCW, slow) + if (PulseIn1RisingEdge()) { + if (++divCount >= div) { divCount = 0; mainMove = true; } + else if (doubleArmed && div >= 2 && divCount == div / 2) { midMove = true; } + } + } else { + intPeriod = 2000 + (((4095 - kx) * 94000) >> 12); // 2000 (fast) .. ~96000 (slow, ~2s) samples + if (++intCount >= intPeriod) { intCount = 0; mainMove = true; } + else if (doubleArmed && intCount == intPeriod / 2) { midMove = true; } + } + + if (mainMove && !frozen) { + walk.step(range); // both voices move together... + posB = walk.pos; // ...B re-locked to the base walk + doubleArmed = (range > 0) && ((walk.next() & 0xFF) < DoubleThreshold()); // arm a double, "sometimes" + } + if (midMove && !frozen) { + posB = LockstepOrnament(walk.pos, range, walk.next()); // B's mid-beat passing move + doubleArmed = false; // consumed + } + + // ---- effective scale: knob-selected base, offset by CV In 2 (bipolar) ---- + uint8_t effScale = scaleIdx; + if (Connected(CV2)) { + int32_t e = (int32_t)scaleIdx + ((CVIn(1) * 12) >> 11); // ~ -12..+11 scale steps + if (e < 0) e = 0; + if (e >= NUM_SCALES) e = NUM_SCALES - 1; + effScale = (uint8_t)e; + } + + // ---- pitch out: voice A = base walk, voice B = base or its passing move; gate per voice ---- + uint8_t noteA = LockstepNote(walk.pos, effScale); + uint8_t noteB = LockstepNote(posB, effScale); + if (noteA != lastNoteA) { lastNoteA = noteA; gateCountA = GATE_SAMPLES; } + if (noteB != lastNoteB) { lastNoteB = noteB; gateCountB = GATE_SAMPLES; } + CVOut1MIDINote(noteA); + CVOut2MIDINote(noteB); + bool gateA = gateCountA > 0; if (gateA) gateCountA--; + bool gateB = gateCountB > 0; if (gateB) gateCountB--; + PulseOut1(gateA); + PulseOut2(gateB); + + // ---- audio: resonant low-pass on the two returned oscillators (Main = cutoff) ---- + if (sw != Down) cutoffCoef = FCoef(km); // hold cutoff during scale-select + int32_t in0 = Connected(Audio1) ? AudioIn(0) : 0; + int32_t in1 = Connected(Audio2) ? AudioIn(1) : 0; + AudioOut(0, Svf(0, in0, cutoffCoef)); + AudioOut(1, Svf(1, in1, cutoffCoef)); + + UpdateLeds(sw, range, gateA || gateB); + } + +private: + Walk walk; + uint8_t scaleIdx = 3; // Natural Minor default + int32_t posB = 0; // voice B position (== walk.pos except mid-beat doubles) + bool doubleArmed = false; // this interval will take a mid-beat double + uint8_t lastNoteA = 0, lastNoteB = 0; + int32_t gateCountA = 0, gateCountB = 0; + int32_t intCount = 0, intPeriod = 12000, divCount = 0; + int32_t cutoffCoef = 1200; + int32_t svfLow[2] = {0, 0}, svfBand[2] = {0, 0}; + + static constexpr int32_t GATE_SAMPLES = 480; // ~10 ms @ 48 kHz + + // Probability (0..255) that voice B takes an extra mid-beat move. Default ~25%. + // CV In 1 is bipolar (-2048..2047): full negative = never, 0 V ~ 50%, full positive + // = always. Unpatched CVIn reads 0, so the ~25% default is used instead. + int32_t DoubleThreshold() { + if (!Connected(CV1)) return 64; // ~25% of 256 + int32_t t = (CVIn(0) + 2048) >> 4; // -2048..2047 -> 0..255 + return t < 0 ? 0 : (t > 255 ? 255 : t); + } + + static constexpr int32_t SVF_Q = 1000; // Q12 damping; lower = more resonant. + // ponytail: fixed resonance; expose on a knob if you want it playable. + + // Knob (0..4095) -> SVF frequency coefficient (~40..3600, Q12). Squared for an exponential feel. + int32_t FCoef(int32_t k) { + int32_t kk = (k * k) >> 12; // 0..4094, exp curve + int32_t f = 40 + ((kk * 3560) >> 12); + return f > 3600 ? 3600 : f; + } + + // Chamberlin state-variable low-pass, fixed point Q12, one per channel. + int16_t Svf(int ch, int32_t in, int32_t f) { + int32_t low = svfLow[ch], band = svfBand[ch]; + low += (f * band) >> 12; + int32_t high = in - low - ((SVF_Q * band) >> 12); + band += (f * high) >> 12; + if (band > 8192) band = 8192; // keep state bounded if it rings hard + if (band < -8192) band = -8192; + svfLow[ch] = low; svfBand[ch] = band; + if (low > 2047) low = 2047; + if (low < -2048) low = -2048; + return (int16_t)low; + } + + void UpdateLeds(Switch sw, int32_t range, bool gate) { + if (sw == Down) { // scale index, 6-bit binary, half brightness + for (int i = 0; i < 6; i++) LedBrightness(i, ((scaleIdx >> i) & 1) ? 1500 : 0); + return; + } + if (sw == Up) { // frozen: all dim + for (int i = 0; i < 6; i++) LedBrightness(i, 300); + return; + } + int32_t idx = range > 0 ? ((walk.pos + range) * 5) / (2 * range) : 2; // position bar 0..5 + if (idx < 0) idx = 0; + if (idx > 5) idx = 5; + for (int i = 0; i < 6; i++) LedBrightness(i, i == idx ? 4095 : (gate ? 400 : 0)); + } +}; + +#ifndef COMPUTERCARD_HOST_SIM +int main() { + set_sys_clock_khz(192000, true); + static Lockstep card; + card.EnableNormalisationProbe(); + card.Run(); +} +#endif diff --git a/releases/89_Lockstep/src/lockstep_engine.h b/releases/89_Lockstep/src/lockstep_engine.h new file mode 100644 index 000000000..ac7c5f121 --- /dev/null +++ b/releases/89_Lockstep/src/lockstep_engine.h @@ -0,0 +1,60 @@ +#pragma once +#include +#include "markov_scales.h" + +// Pure, hardware-free movement engine so it can be unit-tested on the host +// (see ../sim/test_engine.cpp). A bounded, reflected random walk in semitones +// around a center note, quantized to a scale. + +static constexpr int32_t LOCKSTEP_CENTER = 72; // walk center (MIDI). High, so the CV stays positive. +static constexpr int32_t LOCKSTEP_HALFMAX = 12; // max half-range (Y fully CW) = +/- one octave +static constexpr int32_t LOCKSTEP_MINNOTE = 60; // floor: keep output >= ~0V for positive-only osc inputs + +struct Walk { + int32_t pos = 0; // semitones from center + uint32_t rng = 0x1a2b3c4du; // xorshift32 state + + uint32_t next() { + rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5; + return rng; + } + + // One step of a reflected random walk bounded to +/- range semitones. + void step(int32_t range) { + if (range <= 0) { pos = 0; return; } + int32_t stepMax = 1 + range / 3; // gentle drift when range is small + int32_t s = (int32_t)(next() % (uint32_t)(2 * stepMax + 1)) - stepMax; + pos += s; + if (pos > range) pos = 2 * range - pos; // reflect at top + if (pos < -range) pos = -2 * range - pos; // reflect at bottom + if (pos > range) pos = range; // clamp (safety after reflect) + if (pos < -range) pos = -range; + } + + void reseed(uint32_t seed) { rng = seed ? seed : 1u; pos = 0; } +}; + +// Passing-tone offset for the doubling voice: base +/- 1..2 semitones, bounded to +// the walk range. `r` is any random 32-bit value. Result stays within [-range, range]. +static inline int32_t LockstepOrnament(int32_t base, int32_t range, uint32_t r) { + if (range <= 0) return 0; + int32_t d = 1 + (int32_t)(r % 2u); // 1 or 2 semitones + if (r & 0x10000u) d = -d; // direction from a different bit + int32_t p = base + d; + if (p > range) p = range; + if (p < -range) p = -range; + return p; +} + +// Quantize center+pos to a scale tone, octave-folded to stay at/above LOCKSTEP_MINNOTE +// so the CV never goes negative (some oscillator pitch inputs won't track sub-0V -- +// this card exists partly because a SineSquare wouldn't). +static inline uint8_t LockstepNote(int32_t pos, uint8_t scaleIdx) { + int32_t raw = LOCKSTEP_CENTER + pos; + if (raw < 0) raw = 0; + if (raw > 127) raw = 127; + uint8_t n = QuantizeToScale((uint8_t)raw, scaleIdx); + while (n < LOCKSTEP_MINNOTE) n += 12; // fold up, same scale degree + while (n > 120) n -= 12; + return n; +} diff --git a/releases/89_Lockstep/src/markov_scales.h b/releases/89_Lockstep/src/markov_scales.h new file mode 100644 index 000000000..9c6193a50 --- /dev/null +++ b/releases/89_Lockstep/src/markov_scales.h @@ -0,0 +1,54 @@ +#pragma once +#include + +struct Scale { + const char* name; + uint8_t num_notes; + uint8_t notes[12]; // semitone offsets from root (0) +}; + +// Sorted CCW→CW: Dark/Minor → Bright/Major → Ambiguous +static constexpr uint8_t NUM_SCALES = 12; +static const Scale generative_scales[NUM_SCALES] = { + {"Phrygian", 7, {0, 1, 3, 5, 7, 8, 10, 0, 0, 0, 0, 0}}, // 0: Fully CCW + {"Hirajoshi", 5, {0, 2, 3, 7, 8, 0, 0, 0, 0, 0, 0, 0}}, // 1 + {"Harmonic Minor", 7, {0, 2, 3, 5, 7, 8, 11, 0, 0, 0, 0, 0}}, // 2 + {"Natural Minor", 7, {0, 2, 3, 5, 7, 8, 10, 0, 0, 0, 0, 0}}, // 3 + {"Minor Pentatonic", 5, {0, 3, 5, 7, 10, 0, 0, 0, 0, 0, 0, 0}}, // 4 + {"m7 Arpeggio", 4, {0, 3, 7, 10, 0, 0, 0, 0, 0, 0, 0, 0}}, // 5: Centre-Left + {"Dorian", 7, {0, 2, 3, 5, 7, 9, 10, 0, 0, 0, 0, 0}}, // 6: Centre-Right + {"Major Pentatonic", 5, {0, 2, 4, 7, 9, 0, 0, 0, 0, 0, 0, 0}}, // 7 + {"Ionian (Major)", 7, {0, 2, 4, 5, 7, 9, 11, 0, 0, 0, 0, 0}}, // 8 + {"Maj7 Arpeggio", 4, {0, 4, 7, 11, 0, 0, 0, 0, 0, 0, 0, 0}}, // 9 + {"Whole Tone", 6, {0, 2, 4, 6, 8, 10, 0, 0, 0, 0, 0, 0}}, // 10 + {"Chromatic", 12, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}}, // 11: Fully CW +}; + +// Quantize a MIDI note to the nearest degree of the given scale. +// Wraps correctly across octave boundaries. +// Returns the quantized MIDI note (same octave or adjacent). +static inline uint8_t QuantizeToScale(uint8_t midi_note, uint8_t scale_idx) { + if (scale_idx >= NUM_SCALES) scale_idx = NUM_SCALES - 1; + const Scale& s = generative_scales[scale_idx]; + + int32_t octave = midi_note / 12; + int32_t chroma = midi_note % 12; + + int32_t best_note = s.notes[0]; + int32_t best_dist = 13; + + for (int i = 0; i < s.num_notes; i++) { + int32_t dist = chroma - s.notes[i]; + if (dist < 0) dist = -dist; + if (dist > 6) dist = 12 - dist; // wrap around octave + if (dist < best_dist) { + best_dist = dist; + best_note = s.notes[i]; + } + } + + int32_t result = octave * 12 + best_note; + if (result < 0) result = 0; + if (result > 127) result = 127; + return (uint8_t)result; +} diff --git a/releases/89_Lockstep/src/pico_sdk_import.cmake b/releases/89_Lockstep/src/pico_sdk_import.cmake new file mode 100644 index 000000000..a0721d0d1 --- /dev/null +++ b/releases/89_Lockstep/src/pico_sdk_import.cmake @@ -0,0 +1,84 @@ +# This is a copy of /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() + +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 () + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + GIT_SUBMODULES_RECURSE FALSE + ) + else () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + ) + endif () + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + FetchContent_Populate(pico_sdk) + 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}) From 9ea44b0d32ea0c31cd0a6856a53513a83acd3006 Mon Sep 17 00:00:00 2001 From: Dune Desormeaux Date: Wed, 22 Jul 2026 06:41:19 -0700 Subject: [PATCH 2/2] Change 'Description' to 'short-description' in info.yaml --- releases/89_Lockstep/info.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releases/89_Lockstep/info.yaml b/releases/89_Lockstep/info.yaml index e76399e3f..434123082 100644 --- a/releases/89_Lockstep/info.yaml +++ b/releases/89_Lockstep/info.yaml @@ -1,6 +1,6 @@ draft: true Name: Lockstep -Description: Dual quantized pitch-mover — two CV outs walk together through a scale to drive two oscillators in parallel +short-description: Dual quantized pitch-mover — two CV outs walk together through a scale to drive two oscillators in parallel Language: C++ (Pico SDK) Creator: Jason Moore Version: 1.0