nimphea/per/spi

Search:
Group by:
Source   Edit  

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  
SpiHandle {.importcpp: "daisy::SpiHandle".} = object
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  
SpiResult {.importcpp: "daisy::SpiHandle::Result", size: 4.} = enum
  SPI_OK = 0, SPI_ERR
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 read(spi: var SpiHandle; buffer: var openArray[uint8];
          timeout: uint32 = 100): SpiResult {.inline, ...raises: [], tags: [],
    forbids: [].}
Read data via SPI into provided buffer Source   Edit  
proc readByte(spi: var SpiHandle; timeout: uint32 = 100): tuple[
    result: SpiResult, data: uint8] {.inline, ...raises: [], tags: [], forbids: [].}
Read a single byte 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 transfer(spi: var SpiHandle; txData: openArray[uint8];
              rxBuffer: var openArray[uint8]; timeout: uint32 = 100): SpiResult {.
    inline, ...raises: [], tags: [], forbids: [].}
Full-duplex transfer (transmit and receive simultaneously) txData and rxBuffer must be same length 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 write(spi: var SpiHandle; data: openArray[uint8]; timeout: uint32 = 100): SpiResult {.
    inline, ...raises: [], tags: [], forbids: [].}
Write data via SPI Source   Edit  
proc writeByte(spi: var SpiHandle; data: uint8; timeout: uint32 = 100): SpiResult {.
    inline, ...raises: [], tags: [], forbids: [].}
Write a single byte 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