GPIO API reference

brawl:device/gpio

All device APIs

Overview

The brawl:device/gpio interface defines a minimal, hardware-agnostic GPIO API for WebAssembly (WASM) modules. It provides a consistent way to control and monitor digital and analog I/O 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 pins without callbacks, threads, or host-specific extensions.

Implementation Notes

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

GPIO Enumeration

pinCount() returns the total number of GPIO-capable pins available on the target. pinList() populates a memory buffer with a list of available pin identifiers (uint8_t) in the host environment’s canonical order. Each pin identifier can be passed directly to pinOpen() to obtain a handle for further operations.

Interfacing With a GPIO Pin

Before reserving a pin, always call pinCapabilities() to verify that it supports the desired modes. Use pinOpen() to reserve the pin and obtain a handle; this handle is passed to all subsequent operations. When the pin is no longer needed, release it using pinClose(). Pins remain inactive and unpredictable until configured with either pinModeIn() or pinModeOut() to define their behavior and electrical characteristics.

Analog Measurements

pinAnalogRead() returns a value between 0 and (2^resolution) - 1, where the resolution is set using pinAnalogReadResolution(). pinAnalogWrite() accepts a value between 0 and (2^resolution) - 1, where the resolution is set using pinAnalogWriteResolution(). The returned value is proportional to the selected reference voltage (pinAnalogReference()).

Pulse Measurement

pinPulseIn() returns the duration of a pulse in microseconds (µs). Frequencies and PWM parameters are expressed in Hertz (Hz).

Interrupts (no callbacks)

When interrupts are available - they are polled, not event-driven. pinInterruptState() returns a GPIO_EDGE value when an edge has been detected and clears the latched state. Edges remain latched until pinInterruptState() is called. Changing the configured edge type using pinAttachInterrupt() automatically resets the latch.

Error Codes

All brawl:device/gpio 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., a pin level or handle).

Constants

GPIO 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
GPIO_ERROR_BUSY The GPIO pin is already in use or reserved by another handle.
GPIO_ERROR_UNSUPPORTED The requested operation or capability is not supported on this pin.
GPIO_ERROR_TIMEOUT The operation did not complete within the expected time.
GPIO_ERROR_INVALID_HANDLE The handle provided was invalid for the operation requested.
GPIO_ERROR_INVALID_ARG The argument provided was invalid for the operation requested.

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

GPIO Capabilities

Each GPIO pin can support one or more capabilities, reported by pinCapabilities(). These are bit-flags and may be combined using bitwise OR.

Constant (Flag)
GPIO_CAPS_DIGITAL_IN Supports digital input.
GPIO_CAPS_DIGITAL_OUT Supports digital output.
GPIO_CAPS_ANALOG_IN Supports analog input.
GPIO_CAPS_ANALOG_OUT Supports analog output.
GPIO_CAPS_PULL_UP Internal pull-up resistor available (input mode).
GPIO_CAPS_PULL_DOWN Internal pull-down resistor available (input mode).
GPIO_CAPS_PUSH_PULL Push-pull output driver (actively drives both HIGH and LOW).
GPIO_CAPS_OPEN_DRAIN Open-drain output (drives LOW, requires external pull-up for HIGH).
GPIO_CAPS_PWM Supports pulse-width modulation or tone generation.
GPIO_CAPS_IRQ_RISING Can trigger an interrupt on a LOW to HIGH edge.
GPIO_CAPS_IRQ_FALLING Can trigger an interrupt on a HIGH to LOW edge.
GPIO_CAPS_IRQ_CHANGE Can trigger an interrupt on any change (both edges).
GPIO_CAPS_LED Pin is connected to an onboard or indicator LED.

Combine capability flags with the bitwise OR operator (|) to describe a pin that supports multiple features. Pins marked as GPIO_CAPS_LED typically drive an onboard indicator.

Digital State

Represents the logical level of a digital pin.

Constant
GPIO_STATE_LOW Logic LOW / GND.
GPIO_STATE_HIGH Logic HIGH / VCC (3V3 or 5V).

Digital Pull (Input)

Configures the internal resistor behavior when a pin is set as an input.

Constant
GPIO_PULL_NONE No internal pull resistor (floating input).
GPIO_PULL_UP Enables a weak internal pull-up resistor.
GPIO_PULL_DOWN Enables a weak internal pull-down resistor.

Digital Drive (Output)

Selects how a digital output drives its line.

Constant
GPIO_DRIVE_DEFAULT Use the platform’s default drive mode (typically push-pull).
GPIO_DRIVE_PUSH_PULL Actively drives the line HIGH or LOW (standard output).
GPIO_DRIVE_OPEN_DRAIN Only drives LOW; releases the line for external pull-up when HIGH.

When using GPIO_DRIVE_OPEN_DRAIN, writing LOW drives the line low, while writing HIGH releases it (requires a pull-up resistor).

Digital Interrupt Edge (Input)

Defines which signal transitions generate an interrupt event.

Constant
GPIO_EDGE_NONE No interrupt triggered.
GPIO_EDGE_RISING Trigger on LOW to HIGH transition.
GPIO_EDGE_FALLING Trigger on HIGH to LOW transition.
GPIO_EDGE_CHANGE Trigger on any change (both directions).

Interrupt edges are polled via pinInterruptState(), which returns a GPIO_EDGE and clears the latched state.

Bit Order

Specifies the bit transmission order for pinShiftIn() and pinShiftOut() operations.

Constant
GPIO_MSBFIRST Most-significant bit first.
GPIO_LSBFIRST Least-significant bit first.

Application Programming Interface (API)

//--------------------------------------------------------------------------
// brawl:device/gpio

// GPIO errors
enum GPIO_ERROR {
                         // >= 0              no error
  GPIO_ERROR_BUSY           = -1,          // GPIO pin is in use
  GPIO_ERROR_UNSUPPORTED    = -2,          // GPIO pin action unsupported
  GPIO_ERROR_TIMEOUT        = -3,          // GPIO pin operation timed out
  GPIO_ERROR_INVALID_HANDLE = -4,          // invalid handle
  GPIO_ERROR_INVALID_ARG    = -5           // invalid argument
};

// GPIO capabilities (flags)
enum GPIO_CAPS {
  GPIO_CAPS_DIGITAL_IN      = (1UL << 0),  // supports digitalRead()
  GPIO_CAPS_DIGITAL_OUT     = (1UL << 1),  // supports digitalWrite()
  GPIO_CAPS_ANALOG_IN       = (1UL << 2),  // supports analogRead()
  GPIO_CAPS_ANALOG_OUT      = (1UL << 3),  // supports analogWrite()
  GPIO_CAPS_PULL_UP         = (1UL << 4),  // DIGITAL IN: pull up
  GPIO_CAPS_PULL_DOWN       = (1UL << 5),  // DIGITAL IN: pull down
  GPIO_CAPS_PUSH_PULL       = (1UL << 6),  // DIGITAL OUT: push/pull between GND and VCC
  GPIO_CAPS_OPEN_DRAIN      = (1UL << 7),  // DIGITAL OUT: drives LOW, external resistor drives HIGH
  GPIO_CAPS_PWM             = (1UL << 8),  // supports pulse width modulation
  GPIO_CAPS_IRQ_RISING      = (1UL << 9),  // irq: on LOW   -> HIGH transition
  GPIO_CAPS_IRQ_FALLING     = (1UL << 10), // irq: on HIGH  -> LOW  transition
  GPIO_CAPS_IRQ_CHANGE      = (1UL << 11), // irq: on HIGH <-> LOW  transition
  GPIO_CAPS_LED             = (1UL << 12)  // pin is connected to an LED
};

// digital state
enum GPIO_STATE {
  GPIO_STATE_LOW            = 0,           // GND
  GPIO_STATE_HIGH           = 1            // VCC (3v3 or 5V)
};

// digital pull (input)
enum GPIO_PULL {
  GPIO_PULL_NONE            = 0,
  GPIO_PULL_UP              = 1,           // internal pull up
  GPIO_PULL_DOWN            = 2            // internal pull down
};

// digital drive (output)
enum GPIO_DRIVE {
  GPIO_DRIVE_DEFAULT        = 0,
  GPIO_DRIVE_PUSH_PULL      = 1,           // connected to either (GND or VCC)
  GPIO_DRIVE_OPEN_DRAIN     = 2            // drives LOW, external resistor drives HIGH
};

// digital interrupt edge (input)
enum GPIO_EDGE {
  GPIO_EDGE_NONE            = 0,
  GPIO_EDGE_RISING          = 1,           // trigger interrupt on change from LOW  -> HIGH
  GPIO_EDGE_FALLING         = 2,           // trigger interrupt on change from HIGH -> LOW
  GPIO_EDGE_CHANGE          = 3            // trigger interrupt on any change
};

// bit order
enum GPIO_BITORDER {
  GPIO_MSBFIRST             = 0,           // most significant bit first
  GPIO_LSBFIRST             = 1            // least significant bit first
};

Discovery and Mode

int32_t // GPIO_ERROR|int32_t
pinCount(void);

int32_t // GPIO_ERROR|int32_t
pinList(int32_t mem_ptr, int32_t max_items);

int32_t // GPIO_ERROR|GPIO_CAPS
pinCapabilities(int32_t pinNum);

int32_t // GPIO_ERROR|int32_t
pinOpen(int32_t pinNum);

int32_t // GPIO_ERROR|void
pinModeIn(int32_t handle, enum GPIO_PULL pull);

int32_t // GPIO_ERROR|void
pinModeOut(int32_t handle, enum GPIO_DRIVE drive);

int32_t // GPIO_ERROR|void
pinClose(int32_t handle);

Analog

int32_t // GPIO_ERROR|void
pinAnalogReadResolution(int32_t handle, int32_t bits);

int32_t // GPIO_ERROR|void
pinAnalogWriteResolution(int32_t handle, int32_t bits);

int32_t // GPIO_ERROR|int32_t
pinAnalogRead(int32_t handle);

int32_t // GPIO_ERROR|int32_t
pinAnalogReference(int32_t handle, int32_t mV);

int32_t // GPIO_ERROR|void
pinAnalogWrite(int32_t handle, int32_t value);

Digital

int32_t // GPIO_ERROR|GPIO_STATE
pinDigitalRead(int32_t handle);

int32_t // GPIO_ERROR|void
pinDigitalWrite(int32_t handle, enum GPIO_STATE state);

int32_t // GPIO_ERROR|void
pinAttachInterrupt(int32_t handle, enum GPIO_EDGE edge);

int32_t // GPIO_ERROR|void
pinDetachInterrupt(int32_t handle);

int32_t // GPIO_ERROR|GPIO_EDGE
pinInterruptState(int32_t handle);

PWM / bit-banging

int32_t // GPIO_ERROR|void
pinToneStart(int32_t handle, int32_t freq, int32_t duration);

int32_t // GPIO_ERROR|void
pinToneStop(int32_t handle);

int32_t // GPIO_ERROR|int32_t
pinPulseIn(int32_t handle, enum GPIO_STATE state, int32_t timeout_us);

int32_t // GPIO_ERROR|int32_t
pinShiftIn(int32_t handle_data, int32_t handle_clock, int32_t bit_count, enum GPIO_BITORDER bit_order);

int32_t // GPIO_ERROR|void
pinShiftOut(int32_t handle_data, int32_t handle_clock, int32_t bit_count, enum GPIO_BITORDER bit_order, int32_t value);
#include <stdint.h>
#include <stdio.h>

#include "brawl-device-gpio.h"

#define MAX_PINS 32

static void gpio_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_PINS];
    int32_t led_pin, led_h;
    int32_t caps;
    enum GPIO_DRIVE drive;

    // enumerate pins (WASM MVP: caller-allocated buffer)
    n = pinCount();
    if (n < 0) { gpio_err(n, "pinCount"); return 1; }
    if (n > MAX_PINS) n = MAX_PINS;

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

    // find a pin that supports LED
    led_pin = -1;
    for (int i = 0; i < rc; ++i) {
        caps = pinCapabilities(ids[i]);
        if (caps < 0) continue;

        // check the capabilities, break on first LED found
        if (caps & GPIO_CAPS_LED) { led_pin = ids[i]; break; }
    }

    // did we find an LED?
    if (led_pin >= 0) {

        // open and configure drive mode (prefer open drain if advertised)
        led_h = pinOpen(led_pin);
        if (led_h < 0) { gpio_err(led_h, "pinOpen"); return 1; }

        caps = pinCapabilities(led_pin);
        drive = (caps & GPIO_CAPS_OPEN_DRAIN) ? GPIO_DRIVE_OPEN_DRAIN : GPIO_DRIVE_PUSH_PULL;

        rc = pinModeOut(h, drive);
        if (rc < 0) { gpio_err(rc, "pinModeOut"); pinClose(led_h); return 1; }

        // this is where we do the business, toggle the pin repeatedly
        while (1)
        {
            // toggle the pin safely with a delay
            rc = pinDigitalWrite(led_h, GPIO_STATE_HIGH);
            if (rc < 0) gpio_err(rc, "pinDigitalWrite(GPIO_STATE_HIGH)");
            delay(250);

            // toggle the pin safely with a delay
            rc = pinDigitalWrite(led_h, GPIO_STATE_LOW);
            if (rc < 0) gpio_err(rc, "pinDigitalWrite(GPIO_STATE_LOW)");
            delay(250);
        }

        // close access to the pin (shouldn't get here)
        rc = pinClose(led_h);
        if (rc < 0) gpio_err(rc, "pinClose");
    }

    // if we get here, we couldn't find an LED
    return 1;
}

Revision

2025-10 (draft)