SPI API reference

brawl:device/spi

All device APIs

Overview

The brawl:device/spi interface defines a portable, minimal Serial Peripheral Interface (SPI) API for WebAssembly (WASM) modules. It provides a consistent way to control and monitor SPI buses across diverse embedded targets - ranging from microcontrollers to emulators - while remaining fully compatible with the WASM MVP specification.

Each function follows a simple convention: it returns a signed 32-bit integer, where negative values represent standardized error codes and non-negative values represent valid results or success indicators. This design keeps the ABI deterministic, low-overhead, and portable between environments, enabling WASM applications to interact directly with physical or virtual SPI devices without callbacks, threads, or host-specific extensions.

Implementation Notes

This section outlines the expected behavior and conventions used by the brawl:device/spi interface. It provides guidance for both host implementers and guest developers to ensure consistent behavior across WASM targets.

SPI Bus Enumeration

spiCount() returns the total number of SPI buses available on the target. spiList() populates a memory buffer with a list of available bus identifiers (uint8_t) in the host environment’s canonical order. Each bus identifier can be passed directly to spiOpen() to obtain a handle for further operations.

Open and Configure

Before utilizing a SPI bus, always call spiCapabilities() to verify that it supports the desired modes. Use spiOpen() to reserve the SPI bus and obtain a handle; this handle is passed to all subsequent operations. When the SPI bus is no longer needed, release it using spiClose(). SPI buses remain inactive until configured with calls to spiSetMode(), spiSetBitOrder(). spiSetBitsPerWord(), spiSetFrequency(), spiSet3Wire() and spiUseAutoCS() or spiUseGpioCS() depending on if the bus is hardware managed (auto) or GPIO pin - to define the SPI bus behavior and electrical characteristics.

Software SPI (bit-banged SCK/MISO/MOSI)

If no hardware SPI controller exists or additional buses are needed, SPI can be emulated on a set of GPIO pins. The host enforces SPI mode (CPOL/CPHA), bit order, and bit-width in software, driving SCK/MOSI and sampling MISO. Chip-select is handled separately via GPIO or via the SPI CS API when supported.

To detect if the host supports a software implementation, call spiModuleCapabilities() check for the SPI_CAPS_SW_PORT capability, a SPI bus can then be created using the spiOpenOnPins() function.

Transactions

Transactions allow multiple SPI operations to occur under one CS assertion, spiTransactionBegin() keeps CS active during a sequence and spiTransactionEnd() releases CS when finished. This enables compound operations such as “command + read data” sequences.

Error Codes

All brawl:device/spi functions return a signed 32-bit integer (int32_t), which encodes both result and error information:

Value
ret < 0 an error occurred.
ret == 0 operation completed successfully (or no value to return).
ret > 0 operation succeeded and returned a value (e.g., SPI bus data or handle).

All memory access operations are bounds-checked by the host; going outside will return a SPI_ERROR_MEMORY_OOB error.

Constants

SPI Errors

All functions return a signed 32-bit integer (int32_t). If the value is negative, it represents an error code from the table below:

Constant
SPI_ERROR_BUSY The SPI bus is already in use or reserved by another handle.
SPI_ERROR_UNSUPPORTED The requested operation or capability is not supported on this SPI bus.
SPI_ERROR_TIMEOUT The operation did not complete within the expected time.
SPI_ERROR_INVALID_HANDLE The handle provided was invalid for the operation requested.
SPI_ERROR_INVALID_ARG The argument provided was invalid for the operation requested
SPI_ERROR_MEMORY_OOB The requested operation would result in a MEMORY_OOB Error.

A return value >= 0 indicates success - either 0 for non-returning operations or a positive value for operations that produce a result.

SPI Capabilities

Each SPI bus can support one or more capabilities, reported by spiModuleCapabilities() and spiCapabilities(). These are bit-flags and may be combined using bitwise OR.

Constant (Flag)
SPI_CAPS_CONTROLLER Supports master/controller mode.
SPI_CAPS_DEVICE Supports device/slave mode (optional, future).
SPI_CAPS_MODE0 Supports CPOL=0, CPHA=0 (Mode 0).
SPI_CAPS_MODE1 Supports CPOL=0, CPHA=1 (Mode 1).
SPI_CAPS_MODE2 Supports CPOL=1, CPHA=0 (Mode 2).
SPI_CAPS_MODE3 Supports CPOL=1, CPHA=1 (Mode 3).
SPI_CAPS_LSB_FIRST Supports least-significant-bit-first transfers.
SPI_CAPS_CS_AUTO Provides hardware-controlled chip-select.
SPI_CAPS_CS_GPIO Allows use of external GPIO for chip-select.
SPI_CAPS_3WIRE Supports 3-wire (shared I/O, half-duplex).
SPI_CAPS_DMA Supports DMA or large-buffer transfers.
SPI_CAPS_SW_PORT Supports creating a SPI bus on arbitrary GPIO pins.

Combine capability flags with the bitwise OR operator (|) to describe a SPI bus that supports multiple features.

SPI Mode

Represents the mode of the SPI bus.

Constant
SPI_MODE_0 Clock polarity 0, phase 0.
SPI_MODE_1 Clock polarity 0, phase 1.
SPI_MODE_2 Clock polarity 1, phase 0.
SPI_MODE_3 Clock polarity 1, phase 1.

Chip-Select Polarity

Configures the chip select polarity.

Constant
SPI_CS_ACTIVE_LOW CS active when low
SPI_CS_ACTIVE_HIGH CS active when high

Bit Order

Specifies the bit transmission order transfers.

Constant
SPI_MSBFIRST Most-significant bit first.
SPI_LSBFIRST Least-significant bit first.

Application Programmer Interface (API)

//--------------------------------------------------------------------------
// brawl:device/spi

// SPI errors
enum SPI_ERROR {
                        // >= 0              no error
  SPI_ERROR_BUSY           = -1,          // SPI busy
  SPI_ERROR_UNSUPPORTED    = -2,          // SPI action unsupported
  SPI_ERROR_TIMEOUT        = -3,          // SPI operation timed out
  SPI_ERROR_INVALID_HANDLE = -4,          // invalid handle
  SPI_ERROR_INVALID_ARG    = -5,          // invalid argument
  SPI_ERROR_MEMORY_OOB     = -6           // memory OOB
};

// SPI capabilities (flags)
enum SPI_CAPS {
  SPI_CAPS_CONTROLLER      = (1u << 0),   // master role
  SPI_CAPS_DEVICE          = (1u << 1),   // (optional future) slave role
  SPI_CAPS_MODE0           = (1u << 2),
  SPI_CAPS_MODE1           = (1u << 3),
  SPI_CAPS_MODE2           = (1u << 4),
  SPI_CAPS_MODE3           = (1u << 5),
  SPI_CAPS_LSB_FIRST       = (1u << 6),
  SPI_CAPS_CS_AUTO         = (1u << 7),   // hardware CS available
  SPI_CAPS_CS_GPIO         = (1u << 8),   // allow external GPIO as CS
  SPI_CAPS_3WIRE           = (1u << 9),   // half-duplex (shared IO)
  SPI_CAPS_DMA             = (1u << 10),  // large/DMA-friendly transfers
  SPI_CAPS_SW_PORT         = (1u << 11)   // supports creating a SPI bus on an arbitrary GPIO pin
};

// SPI mode
enum SPI_MODE {
  SPI_MODE_0               = 0,           // CPOL=0, CPHA=0
  SPI_MODE_1               = 1,           // CPOL=0, CPHA=1
  SPI_MODE_2               = 2,           // CPOL=1, CPHA=0
  SPI_MODE_3               = 3            // CPOL=1, CPHA=1
};

// SPI CS polarity
enum SPI_CS_POLARITY {
  SPI_CS_ACTIVE_LOW        = 0,
  SPI_CS_ACTIVE_HIGH       = 1
};

// bit order
enum SPI_BITORDER {
  SPI_MSBFIRST             = 0,           // most significant bit first
  SPI_LSBFIRST             = 1            // least significant bit first
};

int32_t // SPI_ERROR|SPI_CAPS
spiModuleCapabilities(void);

Discovery

int32_t // SPI_ERROR|int32_t
spiCount(void);

int32_t // SPI_ERROR|int32_t
spiList(int32_t mem_ptr, int32_t max_items);

int32_t // SPI_ERROR|SPI_CAPS
spiCapabilities(int32_t spiBus);

Open and Configure

int32_t // SPI_ERROR|int32_t
spiOpen(int32_t spiBus);

int32_t // SPI_ERROR|int32_t
spiOpenOnPins(int32_t clk_pin, int32_t miso_pin, int32_t mosi_pin);

int32_t // SPI_ERROR|void
spiSetMode(int32_t handle, enum SPI_MODE mode);

int32_t // SPI_ERROR|void
spiSetBitOrder(int32_t handle, enum SPI_BITORDER bitorder);

int32_t // SPI_ERROR|void
spiSetBitsPerWord(int32_t handle, int32_t bits);

int32_t // SPI_ERROR|void
spiSetFrequency(int32_t handle, int32_t hz);

int32_t // SPI_ERROR|void
spiSet3Wire(int32_t handle, int32_t enable);

int32_t // SPI_ERROR|void
spiUseAutoCS(int32_t handle, int32_t hw_cs_index, enum SPI_CS_POLARITY polarity);

int32_t // SPI_ERROR|void
spiUseGpioCS(int32_t handle, int32_t gpio_handle, enum SPI_CS_POLARITY polarity);

int32_t // SPI_ERROR|void
spiClose(int32_t handle);

Transfers

int32_t // SPI_ERROR|int32_t
spiTransfer(int32_t handle, int32_t tx_ptr, int32_t rx_ptr, int32_t len, int32_t timeout_us);

int32_t // SPI_ERROR|int32_t
spiWrite(int32_t handle, int32_t tx_ptr, int32_t len, int32_t timeout_us);

int32_t // SPI_ERROR|int32_t
spiRead(int32_t handle, int32_t rx_ptr, int32_t len, int32_t timeout_us);

Transactions

int32_t // SPI_ERROR|void
spiTransactionBegin(int32_t handle);

int32_t // SPI_ERROR|void
spiTransactionEnd(int32_t handle);

Example - Read JEDEC ID (C)

#include <stdint.h>
#include <stdio.h>

#include "brawl-device-spi.h"

#define MAX_SPI    8
#define TX_LEN     4
#define RX_LEN     4
#define TIMEOUT_US 1000

static void spi_err(int32_t rc, const char *what) {
    if (rc < 0) fprintf(stderr, "%s failed: %d\n", what, rc);
}

int main(void) {

    int32_t n, rc;
    uint8_t ids[MAX_SPI];
    int32_t spi_bus_id, spi_h;
    int32_t caps;

    // enumerate SPI buses (WASM MVP: caller-allocated buffer)
    n = spiCount();
    if (n < 0) { spi_err(n, "spiCount"); return 1; }
    if (n > MAX_SPI) n = MAX_SPI;

    rc = spiList((int32_t)(uintptr_t)ids, n);
    if (rc < 0) { spi_err(rc, "spiList"); return 1; }

    // do we have any SPI buses?
    if (rc > 0) {

        // use first available SPI bus
        spi_bus_id = ids[0];
        spi_h = spiOpen(spi_bus_id);
        if (spi_h < 0) { spi_err(spi_h, "spiOpen"); return 1; }

        // configure standard mode, 8-bit, 1Mhz
        spiSetMode(spi_h, SPI_MODE_0);
        spiSetBitOrder(spi_h, SPI_MSBFIRST);
        spiSetBitsPerWord(spi_h, 8);
        spiSetFrequency(spi_h, 1000000);

        // example: read JEDEC ID (4 bytes)
        {
            uint8_t tx[TX_LEN] = {0x9F, 0x00, 0x00, 0x00};
            uint8_t rx[RX_LEN] = {0};

            rc = spiTransfer(spi_h, (int32_t)(uintptr_t)tx, (int32_t)(uintptr_t)rx, TX_LEN, TIMEOUT_US);
            if (rc < 0) { spi_err(rc, "spiTransfer"); spiClose(spi_h); return 1; }
        }

        // close the SPI bus
        rc = spiClose(spi_h);
        if (rc < 0) { spi_err(spi_h, "spiClose"); return 1; }
    }

    return 0;
}

Revision

2025-10 (draft)