diff --git a/releases/89_Lockstep/README.md b/releases/89_Lockstep/README.md new file mode 100644 index 00000000..cc423ddc --- /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 00000000..43412308 --- /dev/null +++ b/releases/89_Lockstep/info.yaml @@ -0,0 +1,95 @@ +draft: true +Name: Lockstep +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 +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 00000000..406c091a Binary files /dev/null and b/releases/89_Lockstep/lockstep.uf2 differ diff --git a/releases/89_Lockstep/sim/.gitignore b/releases/89_Lockstep/sim/.gitignore new file mode 100644 index 00000000..7070ed79 --- /dev/null +++ b/releases/89_Lockstep/sim/.gitignore @@ -0,0 +1,2 @@ +test_engine +build/ diff --git a/releases/89_Lockstep/sim/build.sh b/releases/89_Lockstep/sim/build.sh new file mode 100755 index 00000000..7b2f0f7e --- /dev/null +++ b/releases/89_Lockstep/sim/build.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -e +cd "$(dirname "$0")" +c++ -std=c++17 -I../src test_engine.cpp -o test_engine +./test_engine diff --git a/releases/89_Lockstep/sim/test_engine.cpp b/releases/89_Lockstep/sim/test_engine.cpp new file mode 100644 index 00000000..6fadc309 --- /dev/null +++ b/releases/89_Lockstep/sim/test_engine.cpp @@ -0,0 +1,63 @@ +// Host self-check for the Lockstep movement engine. No hardware, no Pico SDK. +// ./build.sh (compiles and runs) +#include +#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 00000000..567609b1 --- /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 00000000..ec78d79e --- /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 00000000..1da6de33 --- /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 00000000..002aa064 --- /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 00000000..ac7c5f12 --- /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 00000000..9c6193a5 --- /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 00000000..a0721d0d --- /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})