SPI (Serial Peripheral Interface) support for libDaisy Nim wrapper
This module provides SPI communication support for the Daisy Audio Platform.
⚠️ IMPORTANT - Blocking vs DMA Functions:
- Blocking functions (write, read, transfer) will stall the CPU while waiting for the SPI transaction to complete. This can cause audio glitches if called from the audio callback or main loop during audio processing.
- DMA functions (dmaTransmit, dmaReceive, dmaTransmitAndReceive) use Direct Memory Access to transfer data in the background without blocking the CPU. These are safe to use during audio processing if your buffers are in the correct memory region.
DMA Buffer Requirements:
- Buffers must be in D2 memory domain (not stack variables!)
- Use {.section: ".sram1_bss".} or allocate on heap
- Or use dsy_dma_clear_cache_for_buffer() before transfer (advanced)
Example - Simple SPI master (blocking):
import nimphea, per/spi var daisy = initDaisy() var spi = initSPI(SPI_1, D8(), D9(), D10()) # Write bytes (BLOCKS - don't use in audio callback!) discard spi.write([0x01'u8, 0x02, 0x03, 0x04]) # Read bytes (BLOCKS) var buffer: array[4, uint8] discard spi.read(buffer) # Full-duplex transfer (BLOCKS) let txData = [0xAA'u8, 0xBB, 0xCC] var rxData: array[3, uint8] discard spi.transfer(txData, rxData)
Example - SPI with register access (blocking):
# Write to register (BLOCKS) discard spi.writeRegister(0x20, 0xFF) # Read from register (BLOCKS) let (result, value) = spi.readRegister(0x21)
Example - Non-blocking DMA transfer:
import nimphea, per/spi # DMA buffers MUST be in D2 memory, not on stack! var txBuffer {.section: ".sram1_bss".}: array[256, uint8] var transferComplete = false proc onTransferComplete(context: pointer, result: SpiResult) {.cdecl.} = # Called from interrupt - keep this FAST! transferComplete = true var daisy = initDaisy() var spi = initSPI(SPI_1, D8(), D9(), D10()) # Start DMA transfer (non-blocking) discard spi.dmaTransmit(txBuffer, nil, onTransferComplete, nil) # CPU is free to do other work while transfer happens in background while not transferComplete: # Do audio processing or other tasks discard
Types
SpiBaudPrescaler {.importcpp: "daisy::SpiHandle::Config::BaudPrescaler", size: 4.} = enum SPI_PS_2 = 0, ## Clock / 2 SPI_PS_4, ## Clock / 4 SPI_PS_8, ## Clock / 8 SPI_PS_16, ## Clock / 16 SPI_PS_32, ## Clock / 32 SPI_PS_64, ## Clock / 64 SPI_PS_128, ## Clock / 128 SPI_PS_256 ## Clock / 256
- Source Edit
SpiClockPhase {.importcpp: "daisy::SpiHandle::Config::ClockPhase", size: 4.} = enum SPI_CLOCK_PHASE_1 = 0, ## Data sampled on first edge SPI_CLOCK_PHASE_2 ## Data sampled on second edge
- Source Edit
SpiClockPolarity {.importcpp: "daisy::SpiHandle::Config::ClockPolarity", size: 4.} = enum SPI_CLOCK_POL_LOW = 0, ## Clock idle state is low SPI_CLOCK_POL_HIGH ## Clock idle state is high
- Source Edit
SpiConfig {.importcpp: "daisy::SpiHandle::Config", bycopy.} = object periph* {.importc: "periph".}: SpiPeripheral mode* {.importc: "mode".}: SpiMode direction* {.importc: "direction".}: SpiDirection datasize* {.importc: "datasize".}: culong clock_polarity* {.importc: "clock_polarity".}: SpiClockPolarity clock_phase* {.importc: "clock_phase".}: SpiClockPhase nss* {.importc: "nss".}: SpiNSS baud_prescaler* {.importc: "baud_prescaler".}: SpiBaudPrescaler pin_config* {.importc: "pin_config".}: SpiPinConfig
- Source Edit
SpiDirection {.importcpp: "daisy::SpiHandle::Config::Direction", size: 4.} = enum SPI_TWO_LINES = 0, ## Full duplex SPI_TWO_LINES_TX_ONLY, ## Half duplex transmit only SPI_TWO_LINES_RX_ONLY, ## Half duplex receive only SPI_ONE_LINE ## Single wire bidirectional
- Source Edit
SpiDmaDirection {.importcpp: "daisy::SpiHandle::DmaDirection", size: 4.} = enum SPI_DMA_RX = 0, ## DMA receive only SPI_DMA_TX, ## DMA transmit only SPI_DMA_RX_TX ## DMA receive and transmit
- Source Edit
SpiEndCallbackFunctionPtr = proc (context: pointer; result: SpiResult) {.cdecl.}
- Source Edit
SpiHandleImpl {.importcpp: "daisy::SpiHandle::Impl".} = object
- Source Edit
SpiMode {.importcpp: "daisy::SpiHandle::Config::Mode", size: 4.} = enum SPI_MASTER = 0, SPI_SLAVE
- Source Edit
SpiNSS {.importcpp: "daisy::SpiHandle::Config::NSS", size: 4.} = enum SPI_NSS_SOFT = 0, ## Software NSS management SPI_NSS_HARD_INPUT, ## Hardware NSS input SPI_NSS_HARD_OUTPUT ## Hardware NSS output
- Source Edit
SpiPeripheral {.importcpp: "daisy::SpiHandle::Config::Peripheral", size: 4.} = enum SPI_1 = 0, SPI_2, SPI_3, SPI_4, SPI_5, SPI_6
- Source Edit
SpiPinConfig {.importcpp: "daisy::SpiHandle::Config::pin_config", bycopy.} = object sclk* {.importc: "sclk".}: Pin miso* {.importc: "miso".}: Pin mosi* {.importc: "mosi".}: Pin nss* {.importc: "nss".}: Pin
- Source Edit
SpiStartCallbackFunctionPtr = proc (context: pointer) {.cdecl.}
- Source Edit
Consts
SPI_MODE_0 = (SPI_CLOCK_POL_LOW, SPI_CLOCK_PHASE_1)
- CPOL=0, CPHA=0 Source Edit
SPI_MODE_1 = (SPI_CLOCK_POL_LOW, SPI_CLOCK_PHASE_2)
- CPOL=0, CPHA=1 Source Edit
SPI_MODE_2 = (SPI_CLOCK_POL_HIGH, SPI_CLOCK_PHASE_1)
- CPOL=1, CPHA=0 Source Edit
SPI_MODE_3 = (SPI_CLOCK_POL_HIGH, SPI_CLOCK_PHASE_2)
- CPOL=1, CPHA=1 Source Edit
Procs
proc BlockingTransmit(this: var SpiHandle; buff: ptr uint8; size: csize_t; timeout: uint32 = 100): SpiResult {. importcpp: "#.BlockingTransmit(@)", header: "daisy_seed.h", importcpp, ...raises: [], tags: [], forbids: [].}
- Source Edit
proc dmaReceive(spi: var SpiHandle; buffer: var openArray[uint8]; startCallback: SpiStartCallbackFunctionPtr = nil; endCallback: SpiEndCallbackFunctionPtr = nil; context: pointer = nil): SpiResult {....raises: [], tags: [], forbids: [].}
-
Non-blocking DMA receive
⚠️ CRITICAL: Buffer MUST be in D2 memory domain:
- Use {.section: ".sram1_bss".} pragma on buffer declaration
- Or allocate on heap with alloc/create
- DO NOT use stack variables (will cause DMA errors)
Parameters: buffer: Buffer to receive data into (must be in D2 memory!) startCallback: Called when transfer starts (from interrupt, keep fast!) endCallback: Called when transfer completes (from interrupt, keep fast!) context: User data pointer passed to callbacks
Returns: SPI_OK if transfer queued successfully, SPI_ERR on error
Example:
var rxBuf {.section: ".sram1_bss".}: array[256, uint8] proc onComplete(ctx: pointer, res: SpiResult) {.cdecl.} = # Process received data discard discard spi.dmaReceive(rxBuf, nil, onComplete, nil)
Source Edit proc dmaTransmit(spi: var SpiHandle; buffer: var openArray[uint8]; startCallback: SpiStartCallbackFunctionPtr = nil; endCallback: SpiEndCallbackFunctionPtr = nil; context: pointer = nil): SpiResult {....raises: [], tags: [], forbids: [].}
-
Non-blocking DMA transmit
⚠️ CRITICAL: Buffer MUST be in D2 memory domain:
- Use {.section: ".sram1_bss".} pragma on buffer declaration
- Or allocate on heap with alloc/create
- DO NOT use stack variables (will cause DMA errors)
Parameters: buffer: Data to transmit (must be in D2 memory!) startCallback: Called when transfer starts (from interrupt, keep fast!) endCallback: Called when transfer completes (from interrupt, keep fast!) context: User data pointer passed to callbacks
Returns: SPI_OK if transfer queued successfully, SPI_ERR on error
Example:
var txBuf {.section: ".sram1_bss".}: array[256, uint8] proc onComplete(ctx: pointer, res: SpiResult) {.cdecl.} = echo "Transfer done!" discard spi.dmaTransmit(txBuf, nil, onComplete, nil)
Source Edit proc dmaTransmitAndReceive(spi: var SpiHandle; txBuffer: var openArray[uint8]; rxBuffer: var openArray[uint8]; startCallback: SpiStartCallbackFunctionPtr = nil; endCallback: SpiEndCallbackFunctionPtr = nil; context: pointer = nil): SpiResult {....raises: [], tags: [], forbids: [].}
-
Non-blocking DMA full-duplex transfer (transmit and receive simultaneously)
⚠️ CRITICAL: Both buffers MUST be in D2 memory domain:
- Use {.section: ".sram1_bss".} pragma on buffer declarations
- Or allocate on heap with alloc/create
- DO NOT use stack variables (will cause DMA errors)
Parameters: txBuffer: Data to transmit (must be in D2 memory!) rxBuffer: Buffer to receive data into (must be in D2 memory!) startCallback: Called when transfer starts (from interrupt, keep fast!) endCallback: Called when transfer completes (from interrupt, keep fast!) context: User data pointer passed to callbacks
Returns: SPI_OK if transfer queued successfully, SPI_ERR on error
Note: txBuffer and rxBuffer must be the same length
Example:
var txBuf {.section: ".sram1_bss".}: array[256, uint8] var rxBuf {.section: ".sram1_bss".}: array[256, uint8] proc onComplete(ctx: pointer, res: SpiResult) {.cdecl.} = # Transfer complete, process rxBuf discard discard spi.dmaTransmitAndReceive(txBuf, rxBuf, nil, onComplete, nil)
Source Edit proc initSPI(peripheral: SpiPeripheral; sclkPin, misoPin, mosiPin: Pin; nssPin: Pin = Pin(); speed: SpiBaudPrescaler = SPI_PS_8; mode: int = 0): SpiHandle {....raises: [], tags: [], forbids: [].}
-
Initialize SPI interface
Parameters: peripheral: SPI_1, SPI_2, SPI_3, SPI_4, SPI_5, or SPI_6 sclkPin: Clock pin (e.g., D8()) misoPin: Master In Slave Out pin mosiPin: Master Out Slave In pin nssPin: Chip select pin (optional, use Pin() for software CS) speed: Clock prescaler (SPI_PS_2 to SPI_PS_256) mode: SPI mode 0-3 (sets clock polarity and phase)
Example:
var spi = initSPI(SPI_1, D8(), D9(), D10())
Source Edit proc readRegister(spi: var SpiHandle; regAddr: uint8; timeout: uint32 = 100): tuple[ result: SpiResult, value: uint8] {....raises: [], tags: [], forbids: [].}
- Read from a register Source Edit
proc readRegisters(spi: var SpiHandle; regAddr: uint8; buffer: var openArray[uint8]; timeout: uint32 = 100): SpiResult {. ...raises: [], tags: [], forbids: [].}
- Read multiple bytes from consecutive registers into provided buffer Source Edit
proc transferByte(spi: var SpiHandle; txByte: uint8; timeout: uint32 = 100): tuple[ result: SpiResult, rxByte: uint8] {.inline, ...raises: [], tags: [], forbids: [].}
- Transfer a single byte (full duplex) Source Edit
proc writeRegister(spi: var SpiHandle; regAddr: uint8; value: uint8; timeout: uint32 = 100): SpiResult {....raises: [], tags: [], forbids: [].}
- Write to a register (common SPI device pattern) Source Edit