nimphea_patch_sm
Nim wrapper for Electro-Smith Daisy Patch SM (Surface Mount) development board.
The Daisy Patch SM is a compact Eurorack module DSP engine featuring:
- 8 CV inputs (bipolar, +/-5V Eurorack standard)
- 4 auxiliary ADC inputs (0-3.3V)
- 2 CV outputs (12-bit DAC, 0-5V)
- 2 gate inputs (digital, Eurorack compatible)
- 2 gate outputs (GPIO, Eurorack compatible)
- Audio I/O (Eurorack level, 24-bit/48kHz)
- USB for programming/power
- SDRAM (64MB)
- QSPI Flash (8MB)
- Pin bank system (A/B/C/D headers, 10 pins each)
Hardware Overview:
- Based on Daisy Seed (STM32H750, 480MHz ARM Cortex-M7)
- Compact Eurorack form factor (8HP)
- Perfect for CV processors, quantizers, utilities
- PCM3060 codec for high-quality audio
Example - Simple CV processor:
import nimphea/boards/daisy_patch_sm var patchsm: DaisyPatchSM patchsm.init() patchsm.startAdc() patchsm.startDac() while true: patchsm.processAllControls() # Read CV inputs let cv1 = patchsm.getAdcValue(CV_1) let cv2 = patchsm.getAdcValue(CV_2) # Process and output patchsm.writeCvOut(CV_OUT_1, (cv1 + cv2) * 2.5) patchsm.writeCvOut(CV_OUT_2, cv1 * 5.0) patchsm.delay(1)
Example - With audio processing:
var patchsm: DaisyPatchSM var gain: float32 = 1.0 proc audioCallback(input, output: AudioBuffer, size: int) {.cdecl.} = for i in 0..<size: output[0][i] = input[0][i] * gain output[1][i] = input[1][i] * gain proc main() = patchsm.init() patchsm.startAdc() patchsm.startAudio(audioCallback) while true: patchsm.processAllControls() gain = patchsm.getAdcValue(CV_1) * 2.0 # 0-10V control range patchsm.delay(1)
Types
AnalogControl {.importcpp: "daisy::AnalogControl", header: "hid/ctrl.h".} = object
- Analog control (knob/CV input) wrapper Source Edit
DaisyPatchSM {.importcpp: "daisy::patch_sm::DaisyPatchSM".} = object system*: System ## System utilities sdram*: SdramHandle ## 64MB SDRAM qspi*: QSPIHandle ## 8MB QSPI Flash audio*: AudioHandle ## Audio I/O (PCM3060 codec) adc*: nimphea.AdcHandle ## 12-channel ADC (8 CV + 4 aux) usb*: UsbHandle ## USB peripheral codec*: Pcm3060 ## PCM3060 audio codec dac*: nimphea.DacHandle ## 2-channel DAC for CV outputs user_led*: GPIO ## Onboard user LED controls*: array[12, AnalogControl] ## Array of 12 analog controls gate_in_1*: GateIn ## Gate input 1 gate_in_2*: GateIn ## Gate input 2 gate_out_1*: GPIO ## Gate output 1 gate_out_2*: GPIO ## Gate output 2
-
Daisy Patch SM board handle
Contains all hardware peripherals pre-configured for the Patch SM platform. Access individual components as public members.
Source Edit PatchSmCv {.importcpp: "daisy::patch_sm::CV_1", size: 4.} = enum CV_1 = 0, ## CV input 1 (bipolar +/-5V) CV_2 = 1, ## CV input 2 (bipolar +/-5V) CV_3 = 2, ## CV input 3 (bipolar +/-5V) CV_4 = 3, ## CV input 4 (bipolar +/-5V) CV_5 = 4, ## CV input 5 (bipolar +/-5V) CV_6 = 5, ## CV input 6 (bipolar +/-5V) CV_7 = 6, ## CV input 7 (bipolar +/-5V) CV_8 = 7, ## CV input 8 (bipolar +/-5V) ADC_9 = 8, ## Auxiliary ADC input (0-3.3V) ADC_10 = 9, ## Auxiliary ADC input (0-3.3V) ADC_11 = 10, ## Auxiliary ADC input (0-3.3V) ADC_12 = 11, ## Auxiliary ADC input (0-3.3V) ADC_LAST = 12 ## Sentinel value
-
CV input identifiers (8 bipolar CV + 4 auxiliary ADC)
CV Inputs (bipolar, +/-5V):
- CV_1 through CV_8: Eurorack CV inputs with protection
Auxiliary ADC (0-3.3V):
- ADC_9 through ADC_12: Direct ADC inputs (non-Eurorack)
PatchSmCvOut {.importcpp: "daisy::patch_sm::CV_OUT_BOTH", size: 4.} = enum CV_OUT_BOTH = 0, ## Both CV outputs CV_OUT_1 = 1, ## CV output 1 (0-5V) CV_OUT_2 = 2 ## CV output 2 (0-5V)
-
CV output channel identifiers
Channels:
- CV_OUT_1, CV_OUT_2: Individual outputs (0-5V)
- CV_OUT_BOTH: Write to both outputs simultaneously
PatchSmPinBank {.importcpp: "daisy::patch_sm::DaisyPatchSM::PinBank", size: 4.} = enum PIN_BANK_A = 0, ## Bank A (header A) PIN_BANK_B = 1, ## Bank B (header B) PIN_BANK_C = 2, ## Bank C (header C) PIN_BANK_D = 3 ## Bank D (header D)
-
Pin bank identifiers for GetPin function
Each bank has 10 pins (indices 1-10)
Source Edit
Procs
proc audioBlockSize(this: var DaisyPatchSM): csize_t {. importcpp: "#.AudioBlockSize()", ...raises: [], tags: [], forbids: [].}
-
Returns the number of samples processed in an audio callback
Returns: Samples per channel per callback
Source Edit proc audioCallbackRate(this: var DaisyPatchSM): cfloat {. importcpp: "#.AudioCallbackRate()", ...raises: [], tags: [], forbids: [].}
-
Returns the rate at which the audio callback will be called in Hz
Returns: Callbacks per second (Hz)
Formula: sample_rate / block_size Example: 48000 / 48 = 1000 Hz (1ms per callback)
Source Edit proc audioSampleRate(this: var DaisyPatchSM): cfloat {. importcpp: "#.AudioSampleRate()", ...raises: [], tags: [], forbids: [].}
-
Returns the audio engine's samplerate in Hz
Returns: Sample rate in Hz (e.g., 48000.0)
Source Edit proc changeAudioCallback(patchsm: var DaisyPatchSM; callback: InterleavingAudioCallback) {....raises: [], tags: [], forbids: [].}
-
Change the interleaved audio callback while audio is running
Note: May cause clicks if done while audio is processing.
Source Edit proc changeAudioCallback(patchsm: var DaisyPatchSM; callback: AudioCallback) {. ...raises: [], tags: [], forbids: [].}
-
Change the audio callback while audio is running
Note: May cause clicks if done while audio is processing.
Source Edit proc delay(this: var DaisyPatchSM; milliseconds: uint32) {. importcpp: "#.Delay(#)", ...raises: [], tags: [], forbids: [].}
-
Delay execution for specified milliseconds
Parameters:
- milliseconds - Delay time in milliseconds
Note: Blocking delay. For audio applications, prefer timing based on audio callback rate or use timers.
Source Edit proc getAdcValue(this: var DaisyPatchSM; idx: cint): cfloat {. importcpp: "#.GetAdcValue(#)", ...raises: [], tags: [], forbids: [].}
-
Returns the current value for one of the ADCs
Parameters:
- idx - ADC channel index (cast PatchSmCv to cint: CV_1.cint)
Returns: Normalized value from 0.0 to 1.0
Voltage Mapping:
- CV inputs (CV_1 to CV_8): 0.0 = -5V, 0.5 = 0V, 1.0 = +5V (bipolar)
- ADC inputs (ADC_9 to ADC_12): 0.0 = 0V, 1.0 = 3.3V (unipolar)
Example:
let cv1Value = patchsm.getAdcValue(CV_1.cint)
Note: Call processAnalogControls() regularly for accurate readings.
Source Edit proc getPin(this: var DaisyPatchSM; bank: PatchSmPinBank; idx: cint): Pin {. importcpp: "#.GetPin(#, #)", ...raises: [], tags: [], forbids: [].}
-
Returns the STM32 port/pin combo for the desired pin
Parameters:
- bank - Pin bank (PIN_BANK_A, PIN_BANK_B, PIN_BANK_C, or PIN_BANK_D)
- idx - Pin number between 1 and 10 for each bank
Returns: Pin object (or invalid pin for hardware-only pins)
Note: Some pins are hardware-only (power, audio, etc.) and will return invalid Pin objects.
Deprecated: Please use the Pin definitions in daisy::patch_sm namespace instead.
Source Edit proc getRandomFloat(this: var DaisyPatchSM; min: cfloat = 0.0; max: cfloat = 1.0): cfloat {. importcpp: "#.GetRandomFloat(#, #)", ...raises: [], tags: [], forbids: [].}
-
Gets a random floating point value between the specified minimum and maximum
Parameters:
- min - Minimum value (default: 0.0)
- max - Maximum value (default: 1.0)
Returns: Random float in range min, max
Example:
let noise = patchsm.getRandomFloat(-0.1, 0.1) # ±0.1 noise let cv = patchsm.getRandomFloat(0.0, 5.0) # 0-5V random CV
Source Edit proc getRandomValue(this: var DaisyPatchSM): uint32 {. importcpp: "#.GetRandomValue()", ...raises: [], tags: [], forbids: [].}
-
Gets a random 32-bit value
Returns: Random uint32 value
Uses STM32 hardware RNG for true random numbers.
Source Edit proc init(this: var DaisyPatchSM) {.importcpp: "#.Init()", ...raises: [], tags: [], forbids: [].}
-
Initialize the Daisy Patch SM board
Configures all hardware peripherals:
- Audio codec (PCM3060, 24-bit/48kHz)
- ADC for CV inputs (8 CV + 4 aux)
- DAC for CV outputs (started automatically)
- Gate inputs (GateIn class)
- SDRAM (64MB)
- QSPI Flash (8MB)
Note: Audio and ADC are started automatically during Init(). DAC for CV outputs is also started with default 48kHz callback.
Source Edit proc processAllControls(this: var DaisyPatchSM) {. importcpp: "#.ProcessAllControls()", ...raises: [], tags: [], forbids: [].}
-
Does both analog and digital control processing
Convenience method. Equivalent to:
patchsm.processAnalogControls() patchsm.processDigitalControls()
Call regularly in main loop (e.g., every 1ms).
Source Edit proc processAnalogControls(this: var DaisyPatchSM) {. importcpp: "#.ProcessAnalogControls()", ...raises: [], tags: [], forbids: [].}
-
Reads and filters all of the analog control inputs
Call regularly in main loop (e.g., every 1-10ms) for smooth readings. Applies filtering to ADC values for stability.
Updates all 12 channels (8 CV + 4 aux ADC).
Source Edit proc processDigitalControls(this: var DaisyPatchSM) {. importcpp: "#.ProcessDigitalControls()", ...raises: [], tags: [], forbids: [].}
-
Reads and debounces any of the digital control inputs
Note: This does nothing on Patch SM (no digital controls besides gates). Provided for API compatibility with other boards.
Source Edit proc setAudioBlockSize(this: var DaisyPatchSM; size: csize_t) {. importcpp: "#.SetAudioBlockSize(#)", ...raises: [], tags: [], forbids: [].}
-
Sets the number of samples processed in an audio callback
Parameters:
- size - Number of samples per channel (default: 48)
Note: This will only take effect on the next invocation of startAudio().
Smaller blocks = lower latency, higher CPU load. Larger blocks = higher latency, lower CPU load.
Source Edit proc setAudioSampleRate(this: var DaisyPatchSM; sample_rate: SampleRate) {. importcpp: "#.SetAudioSampleRate(#)", ...raises: [], tags: [], forbids: [].}
-
Sets the samplerate for the audio engine
Parameters:
- sample_rate - Sample rate enum (e.g., SAI_48KHZ, SAI_96KHZ)
proc setAudioSampleRate(this: var DaisyPatchSM; sr: cfloat) {. importcpp: "#.SetAudioSampleRate(#)", ...raises: [], tags: [], forbids: [].}
-
Sets the samplerate for the audio engine
Parameters:
- sr - Target sample rate in Hz
This will set it to the closest valid samplerate. Options being: 8kHz, 16kHz, 32kHz, 48kHz, and 96kHz
Source Edit proc setLed(this: var DaisyPatchSM; state: bool) {.importcpp: "#.SetLed(#)", ...raises: [], tags: [], forbids: [].}
-
Set onboard user LED state
Parameters:
- state - true = LED on, false = LED off
Example:
patchsm.setLed(true) # Turn LED on patchsm.delay(500) patchsm.setLed(false) # Turn LED off
Source Edit proc startAdc(this: var DaisyPatchSM) {.importcpp: "#.StartAdc()", ...raises: [], tags: [], forbids: [].}
-
Starts the Control ADCs
Note: This is started automatically when Init() is called.
Uses DMA for continuous background conversion of all 12 channels.
Source Edit proc startAudio(patchsm: var DaisyPatchSM; callback: AudioCallback) {. ...raises: [], tags: [], forbids: [].}
-
Start audio processing with multi-channel (non-interleaved) callback
The callback receives separate channels as arrays of float samples.
Example:
proc audioCallback(input, output: AudioBuffer, size: int) {.cdecl.} = for i in 0..<size: output[0][i] = input[0][i] * 0.5 # Left channel output[1][i] = input[1][i] * 0.5 # Right channel patchsm.startAudio(audioCallback)
Source Edit proc startAudio(patchsm: var DaisyPatchSM; callback: InterleavingAudioCallback) {. ...raises: [], tags: [], forbids: [].}
-
Start audio processing with interleaved callback
The callback receives interleaved samples (L, R, L, R, ...)
Example:
proc audioCallback(input, output: InterleavedAudioBuffer, size: int) {.cdecl.} = for i in 0..<size: output[i * 2] = input[i * 2] * 0.5 # Left output[i * 2 + 1] = input[i * 2 + 1] * 0.5 # Right patchsm.startAudio(audioCallback)
Source Edit proc startDac(this: var DaisyPatchSM; callback: DacCallback = nil) {. importcpp: "#.StartDac(#)", ...raises: [], tags: [], forbids: [].}
-
Starts the DAC for the CV Outputs
Parameters:
- callback - Optional custom DAC update callback (default: nil)
By default this starts by running the internal callback at 48kHz, which will update the values based on the writeCvOut() function.
Note: This is started automatically when Init() is called.
Source Edit proc stopAdc(this: var DaisyPatchSM) {.importcpp: "#.StopAdc()", ...raises: [], tags: [], forbids: [].}
-
Stops the Control ADCs
Halts DMA conversion and reduces power consumption.
Source Edit proc stopAudio(patchsm: var DaisyPatchSM) {.importcpp: "#.StopAudio()", ...raises: [], tags: [], forbids: [].}
-
Stop audio processing
Stops the audio callback and codec.
Source Edit proc stopDac(this: var DaisyPatchSM) {.importcpp: "#.StopDac()", ...raises: [], tags: [], forbids: [].}
-
Stop the DAC from updating
This will suspend the CV Outputs from changing.
Source Edit proc trig(gate: var GateIn): bool {.importcpp: "#.Trig()", header: "hid/gatein.h", ...raises: [], tags: [], forbids: [].}
-
Check if gate input has a rising edge (trigger)
Returns: true on rising edge, false otherwise
Note: Only returns true once per rising edge. Call processAllControls() regularly for accurate trigger detection.
Source Edit proc validateQSPI(this: var DaisyPatchSM; quick: bool = true): bool {. importcpp: "#.ValidateQSPI(#)", ...raises: [], tags: [], forbids: [].}
-
Tests the QSPI for validity
WARNING: This will wipe contents of QSPI when testing.
Parameters:
- quick - If true, only test 16kB starting at offset 0x400000. If false, test entire 8MB (takes over a minute).
Note: The "quick" test starts 4MB into the memory and tests 16kB of data.
Returns: true if QSPI is okay, otherwise false
Source Edit proc validateSDRAM(this: var DaisyPatchSM): bool {. importcpp: "#.ValidateSDRAM()", ...raises: [], tags: [], forbids: [].}
-
Tests entirety of SDRAM for validity
WARNING: This will wipe contents of SDRAM when testing.
Note: If using the SDRAM for the default bss, or heap, and using constructors as initializers do not call this function. Otherwise, it could overwrite changes performed by constructors.
Returns: true if SDRAM is okay, otherwise false
Source Edit proc writeCvOut(this: var DaisyPatchSM; channel: cint; voltage: cfloat) {. importcpp: "#.WriteCvOut(#, #)", ...raises: [], tags: [], forbids: [].}
-
Sets specified DAC channel to the target voltage
Parameters:
- channel - Desired channel (cast PatchSmCvOut to cint: CV_OUT_1.cint)
- voltage - Value in Volts (valid range is 0-5V)
Note: This may not be 100% accurate without calibration.
Examples:
# Set CV Out 1 to 2.5V (middle of range) patchsm.writeCvOut(CV_OUT_1.cint, 2.5) # Set both outputs to 0V patchsm.writeCvOut(CV_OUT_BOTH.cint, 0.0)
Source Edit