velxio/docs/CUSTOM_CHIPS.md

8.7 KiB

Custom Chips — Velxio Developer Guide

Velxio supports user-defined chips written in C and compiled to WebAssembly. Drop a chip on the canvas, wire its pins like any other component, and your chip runs alongside the simulated Arduino sketch.

This is Velxio's equivalent of Wokwi's Custom Chips API — but with a clean-room header (velxio-chip.h), our own runtime, and zero code from third-party simulators. The toolchain (clang + WASI-SDK) is 100 % open source.


Table of contents


What you get

A custom chip can:

Feature Use it for
Digital GPIO (vx_pin_*) Logic gates, level translators, level-triggered effects
Pin watch with edge detection (vx_pin_watch) Reactive chips: counters, debouncers, edge-triggered FSMs
Initialized output state (VX_OUTPUT_LOW/HIGH) Pin defaults to a known value at chip boot — no glitch
Analog out (DAC) (vx_pin_dac_write) Programmable voltage references, function generators
I2C slave (vx_i2c_attach) EEPROMs, RTCs, IO expanders, sensors
SPI slave (vx_spi_attach/start/stop) Shift registers, ADCs, displays, flash chips
UART (vx_uart_attach/vx_uart_write) GPS modules, BT/WiFi modems, anything that talks Serial
User-editable attributes (vx_attr_*) Knobs the user tweaks: gain, threshold, baud, etc.
Timers (vx_timer_*) Periodic events, oscillators, watchdogs
Framebuffer / display (vx_framebuffer_init/vx_buffer_write) Custom LCDs, OLEDs, dot-matrix displays
Logging (vx_log and printf) Debug output to the chip console

Every API call is documented in wiki/custom-chips-api-reference.md.


The 30-second example: an inverter

A chip with one input and one output. OUT is always the inverse of IN.

inverter.c

#include "velxio-chip.h"
#include <stdlib.h>

typedef struct {
  vx_pin in;
  vx_pin out;
} chip_state_t;

static void on_in_change(void *ud, vx_pin pin, int value) {
  chip_state_t *s = (chip_state_t*)ud;
  vx_pin_write(s->out, value ? VX_LOW : VX_HIGH);
}

void chip_setup(void) {
  chip_state_t *s = (chip_state_t*)malloc(sizeof(chip_state_t));
  s->in  = vx_pin_register("IN",  VX_INPUT);
  s->out = vx_pin_register("OUT", VX_OUTPUT);

  vx_pin_write(s->out, vx_pin_read(s->in) ? VX_LOW : VX_HIGH);
  vx_pin_watch(s->in, VX_EDGE_BOTH, on_in_change, s);

  vx_log("inverter ready");
}

inverter.chip.json

{
  "schema": "velxio-chip/v1",
  "name": "Inverter",
  "author": "you",
  "license": "MIT",
  "description": "OUT = !IN.",
  "pins": ["IN", "OUT", "GND", "VCC"],
  "attributes": []
}

Try it in the editor

  1. Open Velxio. Click Add Component → search "Custom Chip" → select.
  2. The Custom Chip Designer opens. Switch to the Examples tab.
  3. Click Inverter — code loads in the editor.
  4. Click Compile → "✓ Compiled — 61.8 KB".
  5. Click Save & Place — the chip appears on the canvas.
  6. Wire IN to your Arduino's pin 13 and OUT to a LED.
  7. Run the blink sketch → the LED toggles inverse to the built-in LED.

Anatomy of a chip

A chip is two files:

mychip.c           // C source — the logic of the chip
mychip.chip.json   // Metadata — pin layout, attributes, optional display

Lifecycle

The chip exports exactly one function:

void chip_setup(void);

Velxio calls chip_setup() once per chip instance after the simulation starts. Inside it the chip:

  1. Allocates state — typically one malloc(sizeof(chip_state_t)) per instance.
  2. Registers pins with vx_pin_register(name, mode).
  3. Attaches peripherals if needed: vx_i2c_attach, vx_uart_attach, vx_spi_attach.
  4. Subscribes to events with vx_pin_watch and/or vx_timer_create + vx_timer_start.
  5. Returns. No event loop. The chip is purely reactive.

After setup, the chip runs only inside callbacks the host invokes:

  • A pin watch fires → your callback runs → maybe writes other pins.
  • The I2C bus addresses your slave → your on_connect/read/write/stop runs.
  • A timer expires → your callback runs.
  • A UART byte arrives → your on_rx_byte runs.

This means your chip never blocks, never loops forever, and uses zero CPU between events.

State per instance

If the user drops two of your chips on the canvas, they each get their own WebAssembly.Instance with separate memory. Inside chip_setup() you malloc a new state struct — there's no shared global state to worry about.

chip.json schema

{
  "schema":      "velxio-chip/v1",
  "name":        "Display name",
  "author":      "Your name",
  "license":     "MIT",
  "description": "Short text shown in the picker tooltip",

  "pins": [
    "IN",                         // string  — auto-laid out left/right
    "OUT",
    { "name": "SCL", "x": 0, "y": 24 },   // object — explicit position
    { "name": "SDA", "x": 0, "y": 36 }
  ],

  "attributes": [
    { "name": "threshold", "type": "int",   "default": 4, "min": 1, "max": 1024 },
    { "name": "gain",      "type": "float", "default": 1.0, "min": 0, "max": 10, "step": 0.1 }
  ],

  "display": { "width": 128, "height": 64 }   // optional — for chips with a screen
}

Empty strings in pins skip a slot (so your DIP layout matches a real chip). attributes show up as sliders/inputs in the chip's properties dialog. display enables the framebuffer API.


The 11 example chips

The Custom Chip designer ships a gallery with 11 ready-to-go examples, covering every protocol the runtime supports:

Chip Protocol What it shows
Inverter GPIO + watch Simplest possible chip — start here
XOR Gate GPIO + 2 inputs Multiple watches, recompute output on any edge
CD4094 GPIO state machine Edge detection (RISING vs BOTH), multi-pin shift register
Pulse Counter GPIO + attributes User-configurable threshold via slider
74HC595 SPI slave SPI transfer with re-arm pattern
MCP3008 SPI + analog SPI ADC: read voltage, return 10-bit result
24C01 EEPROM I2C slave Tiny memory device with pointer auto-increment
24LC256 EEPROM I2C slave 16-bit addressing + page writes
PCF8574 I2C IO expander Reading/writing 8 pins atomically
DS3231 RTC I2C state 19 registers, BCD encoding, register pointer
ROT13 UART UART Receive a byte, transmit transformed byte

Each is fully explained in wiki/custom-chips-examples.md.


If you want to … Read
Understand every host function in detail API reference
See the 11 chip examples worked through Examples walkthrough
Set up the toolchain or write tests Build & test guide
Run custom chips on ESP32 (backend runtime architecture) ESP32 backend runtime
Know which boards support which protocols Board support matrix

Design philosophy

A few decisions worth knowing:

  • Reactive, not procedural. Your chip never has a loop(). The host calls your callbacks; you do small bits of work and return. This guarantees the chip can't hang the simulator.
  • Shared-nothing memory. Each chip instance gets its own WASM linear memory. Two instances of the same chip can't accidentally share state — no globals to worry about.
  • The C type system is your friend. velxio-chip.h declares concrete types (vx_pin, vx_attr, vx_i2c_config) and uses _Static_assert to guarantee the struct layouts match between your chip and the host. If you add a field, the assertion fires and you fix the runtime accordingly.
  • No dependency on Wokwi. This is a clean-room implementation. The toolchain is clang + wasi-sdk, both Apache-2.0. Our header and runtime are Velxio-original.