nimphea/boards/daisy_patch_sm

Search:
Group by:
Source   Edit  

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)
Source   Edit  
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
Source   Edit  
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)
Source   Edit  
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 state(gate: var GateIn): bool {.importcpp: "#.State()",
                                     header: "hid/gatein.h", ...raises: [],
                                     tags: [], forbids: [].}

Check if gate input is currently HIGH

Returns: true if gate is HIGH, false if LOW

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