# `brawl:device/spi`

## 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)
