Multi-Slave SPI Module for libDaisy
This module provides support for sharing a single SPI bus between multiple slave devices (up to 4). Each device has its own chip select (NSS) pin, while SCLK, MISO, and MOSI are shared.
Features:
- Support for up to 4 SPI slave devices on one bus
- Software-controlled chip select per device
- Blocking and DMA-based transfers
- Compatible with standard SPI peripherals (SPI1-SPI6)
- Individual device selection
Common Use Cases:
- Multiple sensor ICs on one bus (e.g., accelerometer + gyro + magnetometer)
- Flash memory + display sharing SPI bus
- Multiple DAC/ADC chips
- Modular synth with multiple peripheral boards
Hardware Setup:
MCU Device 1 Device 2 Device 3 ---- -------- -------- -------- SCLK --------+--SCLK SCLK SCLK | MOSI --------+--MOSI MOSI MOSI | MISO --------+--MISO MISO MISO NSS0 -----------CS NSS1 --------------------------CS NSS2 -----------------------------------------CS
Usage Example (Blocking):
import nimphea import nimphea/per/spi_multislave var daisy = initDaisy() # Configure for 3 devices on SPI1 var config = MultiSlaveSpiConfig( periph: SPI_1, direction: SPI_TWO_LINES, datasize: 8, clock_polarity: SPI_CLOCK_POL_LOW, clock_phase: SPI_CLOCK_PHASE_1, baud_prescaler: SPI_PS_8, num_devices: 3 ) # Configure pins config.pin_config.sclk = D7() config.pin_config.miso = D8() config.pin_config.mosi = D9() config.pin_config.nss[0] = D10() # Device 0 CS config.pin_config.nss[1] = D11() # Device 1 CS config.pin_config.nss[2] = D12() # Device 2 CS # Initialize var spi = MultiSlaveSpiHandle() if spi.init(config) != SPI_OK: echo "SPI init failed" # Communicate with device 0 var txData = [0x01'u8, 0x02, 0x03] if spi.blockingTransmit(0, txData) == SPI_OK: echo "Sent to device 0" # Communicate with device 1 var rxData: array[4, uint8] if spi.blockingReceive(1, rxData) == SPI_OK: echo "Received from device 1" # Full-duplex with device 2 if spi.blockingTransmitAndReceive(2, txData, rxData, 3) == SPI_OK: echo "Transfer with device 2 complete"
Usage Example (DMA):
# DMA buffers MUST be in D2 memory! var txBuffer {.section: ".sram1_bss".}: array[256, uint8] var rxBuffer {.section: ".sram1_bss".}: array[256, uint8] var transferDone = false proc onTransferComplete(context: pointer, result: SpiResult) {.cdecl.} = transferDone = true # Start non-blocking DMA transfer to device 0 discard spi.dmaTransmitAndReceive( device_index = 0, tx_buff = addr txBuffer[0], rx_buff = addr rxBuffer[0], size = 256, start_callback = nil, end_callback = onTransferComplete, callback_context = nil ) # CPU is free while transfer happens while not transferDone: # Do other work... discard
Important Notes:
- Maximum 4 devices per bus
- Each device can have different timing requirements, but all share the same SPI configuration (clock polarity, phase, baud rate)
- For devices with different timing needs, use separate SPI peripherals
- DMA buffers MUST be in D2 memory domain (not stack variables)
- Device indices are 0-based (0, 1, 2, 3)
Types
MultiSlaveSpiConfig {.importcpp: "daisy::MultiSlaveSpiHandle::Config", bycopy.} = object pin_config* {.importc: "pin_config".}: MultiSlaveSpiPinConfig periph* {.importc: "periph".}: SpiPeripheral direction* {.importc: "direction".}: SpiDirection datasize* {.importc: "datasize".}: culong clock_polarity* {.importc: "clock_polarity".}: SpiClockPolarity clock_phase* {.importc: "clock_phase".}: SpiClockPhase baud_prescaler* {.importc: "baud_prescaler".}: SpiBaudPrescaler num_devices* {.importc: "num_devices".}: csize_t ## Number of devices (1-4)
- Configuration for multi-slave SPI bus Source Edit
MultiSlaveSpiHandle {.importcpp: "daisy::MultiSlaveSpiHandle", byref.} = object
-
Multi-slave SPI bus handler
Manages a single SPI bus shared between multiple slave devices
Source Edit MultiSlaveSpiPinConfig {.importcpp: "daisy::MultiSlaveSpiHandle::Config::pin_config", bycopy.} = object sclk* {.importc: "sclk".}: Pin miso* {.importc: "miso".}: Pin mosi* {.importc: "mosi".}: Pin nss* {.importc: "nss".}: array[MAX_SPI_DEVICES, Pin] ## Chip select pins
- Pin configuration for multi-slave SPI Source Edit
Consts
MAX_SPI_DEVICES = 4
- Maximum number of devices on one multi-slave SPI bus Source Edit
Procs
proc blockingReceive(this: var MultiSlaveSpiHandle; device_index: csize_t; buff: ptr uint8; size: uint16; timeout: uint32 = 100): SpiResult {. importcpp: "#.BlockingReceive(@)", ...raises: [], tags: [], forbids: [].}
-
Blocking receive from a specific device
Parameters:
- device_index: Which device to communicate with (0-3)
- buff: Pointer to receive buffer
- size: Number of bytes to receive
- timeout: Timeout in milliseconds (default: 100)
Returns:
- SPI_OK on success
- SPI_ERR on failure or timeout
Warning: Blocks CPU! Do not call from audio callback.
Example:
var data: array[8, uint8] if spi.blockingReceive(1, addr data[0], 8) == SPI_OK: echo "Received: ", data
Source Edit proc blockingReceive(this: var MultiSlaveSpiHandle; device_index: int; data: var openArray[uint8]; timeout: uint32 = 100): SpiResult {. ...raises: [], tags: [], forbids: [].}
-
Convenience wrapper for receiving into arrays
Example:
var buffer: array[16, uint8] discard spi.blockingReceive(1, buffer)
Source Edit proc blockingTransmit(this: var MultiSlaveSpiHandle; device_index: csize_t; buff: ptr uint8; size: csize_t; timeout: uint32 = 100): SpiResult {. importcpp: "#.BlockingTransmit(@)", ...raises: [], tags: [], forbids: [].}
-
Blocking transmit to a specific device
This will:
- Assert the chip select for the specified device
- Transmit data over SPI
- Deassert the chip select
- Block CPU until complete or timeout
Parameters:
- device_index: Which device to communicate with (0-3)
- buff: Pointer to transmit buffer
- size: Number of bytes to send
- timeout: Timeout in milliseconds (default: 100)
Returns:
- SPI_OK on success
- SPI_ERR on failure or timeout
Warning: Blocks CPU! Do not call from audio callback.
Example:
var data = [0x01'u8, 0x02, 0x03, 0x04] if spi.blockingTransmit(0, addr data[0], 4) != SPI_OK: echo "Transmit failed"
Source Edit proc blockingTransmit(this: var MultiSlaveSpiHandle; device_index: int; data: openArray[uint8]; timeout: uint32 = 100): SpiResult {. ...raises: [], tags: [], forbids: [].}
-
Convenience wrapper for transmitting from arrays
Example:
var cmd = [0x01'u8, 0x02, 0x03] discard spi.blockingTransmit(0, cmd)
Source Edit proc blockingTransmitAndReceive(this: var MultiSlaveSpiHandle; device_index: csize_t; tx_buff: ptr uint8; rx_buff: ptr uint8; size: csize_t; timeout: uint32 = 100): SpiResult {. importcpp: "#.BlockingTransmitAndReceive(@)", ...raises: [], tags: [], forbids: [].}
-
Blocking full-duplex transfer with a specific device
Simultaneously sends and receives data (standard SPI operation).
Parameters:
- device_index: Which device to communicate with (0-3)
- tx_buff: Pointer to transmit buffer
- rx_buff: Pointer to receive buffer
- size: Number of bytes to transfer
- timeout: Timeout in milliseconds (default: 100)
Returns:
- SPI_OK on success
- SPI_ERR on failure or timeout
Warning: Blocks CPU! Do not call from audio callback.
Example:
var txData = [0x80'u8, 0x00] # Read register 0x00 var rxData: array[2, uint8] if spi.blockingTransmitAndReceive(0, addr txData[0], addr rxData[0], 2) == SPI_OK: echo "Register value: ", rxData[1]
Source Edit proc blockingTransmitAndReceive(this: var MultiSlaveSpiHandle; device_index: int; tx_data: openArray[uint8]; rx_data: var openArray[uint8]; timeout: uint32 = 100): SpiResult {....raises: [], tags: [], forbids: [].}
-
Convenience wrapper for full-duplex transfer with arrays
Example:
var tx = [0x03'u8, 0x00, 0x00, 0x00] # Read command var rx: array[4, uint8] discard spi.blockingTransmitAndReceive(0, tx, rx) echo "Read: ", rx
Source Edit proc checkError(this: var MultiSlaveSpiHandle): cint {. importcpp: "#.CheckError()", ...raises: [], tags: [], forbids: [].}
-
Check for SPI errors
Returns the result of HAL_SPI_GetError().
Returns: Error code (0 = no error)
Example:
if spi.blockingTransmit(0, data, size) != SPI_OK: let errCode = spi.checkError() echo "SPI error code: ", errCode
Source Edit proc dmaReceive(this: var MultiSlaveSpiHandle; device_index: csize_t; buff: ptr uint8; size: csize_t; start_callback: SpiStartCallbackFunctionPtr; end_callback: SpiEndCallbackFunctionPtr; callback_context: pointer): SpiResult {. importcpp: "#.DmaReceive(@)", ...raises: [], tags: [], forbids: [].}
-
Non-blocking DMA receive from a specific device
Important:
- Buffer MUST be in D2 memory domain
- Callbacks execute from interrupt context
Parameters:
- device_index: Which device to communicate with (0-3)
- buff: Pointer to receive buffer (must be in D2 memory)
- size: Number of bytes to receive
- start_callback: Called when transfer starts (or nil)
- end_callback: Called when transfer completes (or nil)
- callback_context: User data passed to callbacks (or nil)
Returns:
- SPI_OK: Transfer started successfully
- SPI_ERR: Failed to start transfer
proc dmaTransmit(this: var MultiSlaveSpiHandle; device_index: csize_t; buff: ptr uint8; size: csize_t; start_callback: SpiStartCallbackFunctionPtr; end_callback: SpiEndCallbackFunctionPtr; callback_context: pointer): SpiResult {. importcpp: "#.DmaTransmit(@)", ...raises: [], tags: [], forbids: [].}
-
Non-blocking DMA transmit to a specific device
Important:
- Buffer MUST be in D2 memory domain
- Use {.section: ".sram1_bss".} for static buffers
- Callbacks execute from interrupt context (keep them fast!)
Parameters:
- device_index: Which device to communicate with (0-3)
- buff: Pointer to transmit buffer (must be in D2 memory)
- size: Number of bytes to send
- start_callback: Called when transfer starts (or nil)
- end_callback: Called when transfer completes (or nil)
- callback_context: User data passed to callbacks (or nil)
Returns:
- SPI_OK: Transfer started successfully
- SPI_ERR: Failed to start transfer
Example:
var txBuf {.section: ".sram1_bss".}: array[256, uint8] proc onDone(ctx: pointer, result: SpiResult) {.cdecl.} = if result == SPI_OK: echo "Transfer complete" discard spi.dmaTransmit(0, addr txBuf[0], 256, nil, onDone, nil)
Source Edit proc dmaTransmitAndReceive(this: var MultiSlaveSpiHandle; device_index: csize_t; tx_buff: ptr uint8; rx_buff: ptr uint8; size: csize_t; start_callback: SpiStartCallbackFunctionPtr; end_callback: SpiEndCallbackFunctionPtr; callback_context: pointer): SpiResult {. importcpp: "#.DmaTransmitAndReceive(@)", ...raises: [], tags: [], forbids: [].}
-
Non-blocking DMA full-duplex transfer with a specific device
Important:
- Both buffers MUST be in D2 memory domain
- Callbacks execute from interrupt context
Parameters:
- device_index: Which device to communicate with (0-3)
- tx_buff: Pointer to transmit buffer (must be in D2 memory)
- rx_buff: Pointer to receive buffer (must be in D2 memory)
- size: Number of bytes to transfer
- start_callback: Called when transfer starts (or nil)
- end_callback: Called when transfer completes (or nil)
- callback_context: User data passed to callbacks (or nil)
Returns:
- SPI_OK: Transfer started successfully
- SPI_ERR: Failed to start transfer
Example:
var txBuf {.section: ".sram1_bss".}: array[128, uint8] var rxBuf {.section: ".sram1_bss".}: array[128, uint8] var done = false proc onComplete(ctx: pointer, res: SpiResult) {.cdecl.} = done = true discard spi.dmaTransmitAndReceive( device_index = 2, tx_buff = addr txBuf[0], rx_buff = addr rxBuf[0], size = 128, start_callback = nil, end_callback = onComplete, callback_context = nil ) while not done: # Do other work while transfer happens discard
Source Edit proc getConfig(this: MultiSlaveSpiHandle): MultiSlaveSpiConfig {. importcpp: "#.GetConfig()", ...raises: [], tags: [], forbids: [].}
-
Get the current SPI configuration
Returns: Current MultiSlaveSpiConfig
Source Edit proc init(this: var MultiSlaveSpiHandle; config: MultiSlaveSpiConfig): SpiResult {. importcpp: "#.Init(@)", ...raises: [], tags: [], forbids: [].}
-
Initialize the multi-slave SPI bus
Parameters:
- config: Configuration including pins, timing, and number of devices
Returns:
- SPI_OK on success
- SPI_ERR on failure
Example:
var config = MultiSlaveSpiConfig( periph: SPI_1, num_devices: 2, baud_prescaler: SPI_PS_16 ) if spi.init(config) != SPI_OK: echo "Init failed"
Source Edit proc initMultiSlaveSpi(): MultiSlaveSpiHandle {. importcpp: "daisy::MultiSlaveSpiHandle()", constructor, ...raises: [], tags: [], forbids: [].}
-
Create a new MultiSlaveSpiHandle instance
Example:
var spi = initMultiSlaveSpi()
Source Edit