![]() |
HOME
–
BLOG
–
CONTACT
–
ABOUT
|
HOME / BRAWL
brawl:device/spi
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.
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.
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.
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.
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 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.
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.
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.
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.
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. |
Configures the chip select polarity.
Constant | |
---|---|
SPI_CS_ACTIVE_LOW |
CS active when low |
SPI_CS_ACTIVE_HIGH |
CS active when high |
Specifies the bit transmission order transfers.
Constant | |
---|---|
SPI_MSBFIRST |
Most-significant bit first. |
SPI_LSBFIRST |
Least-significant bit first. |
//--------------------------------------------------------------------------
// 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);
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);
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);
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);
int32_t // SPI_ERROR|void
spiTransactionBegin(int32_t handle);
int32_t // SPI_ERROR|void
spiTransactionEnd(int32_t handle);
#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;
}
2025-10 (draft)