velxio/docs/wiki/custom-chips-api-reference.md

620 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Custom Chips — C API Reference
Complete reference for `velxio-chip.h`. Every function, struct, enum, and
constant the chip can use.
The header is shipped at:
- `backend/sdk/velxio-chip.h` — bundled with the backend Docker image
- `test/test_custom_chips/sdk/include/velxio-chip.h` — local sandbox copy
Both are kept in sync; either works as the include path.
---
## Table of contents
- [Lifecycle](#lifecycle)
- [Pins](#pins)
- [Attributes](#attributes)
- [I2C slave](#i2c-slave)
- [SPI slave](#spi-slave)
- [UART](#uart)
- [Timers and time](#timers-and-time)
- [Display / framebuffer](#display--framebuffer)
- [Logging](#logging)
- [Type & constant cheat sheet](#type--constant-cheat-sheet)
- [ABI guarantees](#abi-guarantees)
---
## Lifecycle
```c
void chip_setup(void);
```
Required, exported. Called **once per chip instance** when the simulation
starts. Allocate state, register pins, attach peripherals, and subscribe to
events here. Do not loop.
---
## Pins
### Types and constants
```c
typedef int32_t vx_pin; // Opaque handle returned by vx_pin_register
#define VX_INPUT 0
#define VX_OUTPUT 1
#define VX_INPUT_PULLUP 2
#define VX_INPUT_PULLDOWN 3
#define VX_ANALOG 4
#define VX_OUTPUT_LOW 16 // Initialize the wired pin LOW at register time
#define VX_OUTPUT_HIGH 17 // Initialize the wired pin HIGH at register time
#define VX_LOW 0
#define VX_HIGH 1
#define VX_EDGE_RISING 1
#define VX_EDGE_FALLING 2
#define VX_EDGE_BOTH 3
```
Use `VX_OUTPUT_LOW` / `VX_OUTPUT_HIGH` instead of `VX_OUTPUT` when you want
the pin to power up at a known level. This eliminates the brief window
between `vx_pin_register` and your first `vx_pin_write` during which a plain
`VX_OUTPUT` pin would default to LOW.
### `vx_pin_register`
```c
vx_pin vx_pin_register(const char* name, vx_pin_mode mode);
```
Register a logical pin on the chip. `name` is what appears on the schematic
and what the diagram editor uses to wire your chip. Returns an opaque handle
you'll pass to all other pin functions.
Call only from `chip_setup()`.
```c
chip_state_t* s = malloc(sizeof(chip_state_t));
s->in = vx_pin_register("IN", VX_INPUT);
s->out = vx_pin_register("OUT", VX_OUTPUT_LOW); // starts LOW, no glitch
```
### `vx_pin_read`
```c
int vx_pin_read(vx_pin p);
```
Returns the digital state of a pin: `0` (LOW) or `1` (HIGH). If the pin
isn't wired to anything in the diagram, returns `0`.
### `vx_pin_write`
```c
void vx_pin_write(vx_pin p, int value);
```
Drive an OUTPUT pin to `value` (0 or 1). The host propagates the change
through the wiring graph immediately — any other chip with a `pin_watch` on
the wired pin will see the edge.
### `vx_pin_read_analog`
```c
double vx_pin_read_analog(vx_pin p);
```
Read the analog voltage of a pin (0.0 V 5.0 V on AVR, 0.0 V 3.3 V on
ESP32). Used by ADC chips to sample voltages from potentiometers or sensors.
### `vx_pin_dac_write`
```c
void vx_pin_dac_write(vx_pin p, double voltage);
```
Drive an analog voltage on a pin. Used by DAC chips.
### `vx_pin_set_mode`
```c
void vx_pin_set_mode(vx_pin p, vx_pin_mode mode);
```
Change a pin's direction after registration — useful for bidirectional
buses (e.g. open-drain protocols where you switch between input and output).
### `vx_pin_watch`
```c
void vx_pin_watch(
vx_pin p,
vx_edge edge,
void (*cb)(void* user_data, vx_pin pin, int value),
void* user_data
);
```
Subscribe to edge events on a pin. The callback fires when the pin's state
crosses the requested edge:
| `edge` | Fires on |
|---|---|
| `VX_EDGE_RISING` | LOW → HIGH only |
| `VX_EDGE_FALLING` | HIGH → LOW only |
| `VX_EDGE_BOTH` | every transition |
Inside the callback you have access to the pin handle, the new value, and
your `user_data` pointer (typically a pointer to your chip's state struct).
```c
static void on_clk(void *ud, vx_pin pin, int value) {
chip_state_t *s = (chip_state_t*)ud;
if (value) { // rising edge
s->shift_register <<= 1;
s->shift_register |= vx_pin_read(s->data);
}
}
vx_pin_watch(clk_pin, VX_EDGE_RISING, on_clk, s);
```
### `vx_pin_watch_stop`
```c
void vx_pin_watch_stop(vx_pin p);
```
Cancels every watch registered for the given pin. Useful when entering a
mode where the chip should ignore inputs (e.g. powered-down state).
---
## Attributes
User-editable parameters that show up in the Custom Chip designer's
Attributes panel as sliders or number inputs.
### Schema in `chip.json`
```json
"attributes": [
{ "name": "threshold", "label": "Pulses", "type": "int", "default": 4, "min": 1, "max": 1024 },
{ "name": "gain", "label": "Gain", "type": "float", "default": 1.0, "min": 0, "max": 10, "step": 0.1 }
]
```
| Field | Effect |
|---|---|
| `name` | Internal key — what the chip uses in `vx_attr_register` |
| `label` | Human-readable text shown next to the slider |
| `type` | `int` rounds to integer; `float`/`number` keeps decimals |
| `default` | Initial value |
| `min`/`max` | If both present, a slider is shown |
| `step` | Step size (default 1 for int, 0.01 for float) |
### `vx_attr_register`
```c
vx_attr vx_attr_register(const char* name, double default_val);
```
Register an attribute. Returns a handle. The default in `chip.json` takes
precedence over the C-side default if both are set — the C-side default
applies when an instance has no saved value yet.
### `vx_attr_read`
```c
double vx_attr_read(vx_attr a);
```
Read the current value. **Always re-read** inside callbacks — the user can
change the slider while the simulation runs and your chip should pick up
the new value on the next event.
```c
static void on_pulse(void* ud, vx_pin pin, int value) {
chip_state_t* s = (chip_state_t*)ud;
s->count++;
uint32_t threshold = (uint32_t)vx_attr_read(s->threshold); // re-read live
if (s->count >= threshold) {
s->count = 0;
vx_pin_write(s->out, !s->state);
s->state = !s->state;
}
}
```
---
## I2C slave
Velxio routes I2C bus events from the master (the Arduino sketch's
`Wire.beginTransmission(addr)`) to your chip when the address matches.
### Config struct
```c
typedef struct {
uint8_t address; /* 7-bit I2C address */
uint8_t _pad[3];
vx_pin scl;
vx_pin sda;
bool (*on_connect)(void* user_data, uint8_t addr, bool is_read);
uint8_t(*on_read) (void* user_data);
bool (*on_write) (void* user_data, uint8_t byte);
void (*on_stop) (void* user_data);
void* user_data;
uint32_t reserved[8];
} vx_i2c_config;
_Static_assert(sizeof(vx_i2c_config) == 64, "vx_i2c_config must be 64 bytes");
```
### `vx_i2c_attach`
```c
vx_i2c vx_i2c_attach(const vx_i2c_config* cfg);
```
Attach an I2C slave. Call only from `chip_setup()`. Two instances of the
same chip with different `A0`/`A1`/`A2` settings can coexist — they get
different addresses.
### Callbacks
```c
bool on_connect(void* ud, uint8_t addr, bool is_read);
```
The master started a transaction. Return `true` for ACK, `false` for NACK.
For most chips: just `return true;`. `is_read` tells you whether the master
is about to read or write.
```c
uint8_t on_read(void* ud);
```
The master is reading a byte from your chip. Return the byte to put on
SDA. Called once per byte the master clocks out.
```c
bool on_write(void* ud, uint8_t byte);
```
The master sent a byte. Return `true` to ACK, `false` to NACK (e.g. memory
full).
```c
void on_stop(void* ud);
```
The master issued STOP. Reset any "transaction in progress" state your
chip has — the next `on_connect` is a fresh transaction.
### Example: 24C01 EEPROM
```c
typedef enum { ST_IDLE, ST_HAS_POINTER } ee_state;
typedef struct {
uint8_t pointer;
uint8_t mem[128];
ee_state state;
} chip_state_t;
static bool i2c_connect(void* ud, uint8_t addr, bool is_read) {
chip_state_t* s = ud;
if (!is_read) s->state = ST_IDLE; // fresh write transaction
return true;
}
static uint8_t i2c_read(void* ud) {
chip_state_t* s = ud;
uint8_t b = s->mem[s->pointer & 0x7f];
s->pointer++;
return b;
}
static bool i2c_write(void* ud, uint8_t byte) {
chip_state_t* s = ud;
if (s->state == ST_IDLE) {
s->pointer = byte;
s->state = ST_HAS_POINTER;
} else {
s->mem[s->pointer & 0x7f] = byte;
s->pointer++;
}
return true;
}
void chip_setup(void) {
chip_state_t* s = calloc(1, sizeof(chip_state_t));
vx_i2c_config cfg = {
.address = 0x50,
.scl = vx_pin_register("SCL", VX_INPUT),
.sda = vx_pin_register("SDA", VX_INPUT),
.on_connect = i2c_connect,
.on_read = i2c_read,
.on_write = i2c_write,
.on_stop = NULL, // optional
.user_data = s,
};
vx_i2c_attach(&cfg);
}
```
---
## SPI slave
Buffer-based bidirectional transfer model. The chip pre-fills a buffer with
the bytes to send on MISO; the bus overwrites those bytes with what it
received on MOSI.
### Config struct
```c
typedef struct {
vx_pin sck;
vx_pin mosi;
vx_pin miso;
vx_pin cs; /* watched by the chip — runtime ignores this field */
uint32_t mode; /* 0..3 */
void (*on_done)(void* user_data, uint8_t* buffer, uint32_t count);
void* user_data;
uint32_t reserved[8];
} vx_spi_config;
_Static_assert(sizeof(vx_spi_config) == 60, "vx_spi_config must be 60 bytes");
```
### Functions
```c
vx_spi vx_spi_attach(const vx_spi_config* cfg);
void vx_spi_start (vx_spi s, uint8_t* buffer, uint32_t count);
void vx_spi_stop (vx_spi s);
```
### How it works
1. `vx_spi_attach` registers the chip on the bus.
2. The chip calls `vx_spi_start(handle, buf, N)` to say "I want to exchange
N bytes; here's my MISO data."
3. As the master clocks bytes, byte by byte:
- the master's MOSI byte overwrites `buf[i]`
- the chip's `buf[i]` (its MISO data) is shifted out to the master
4. After N bytes, `on_done(buf, N)` fires. `buf` now contains the N MOSI
bytes the master sent.
### Re-arming
The chip is **not** automatically armed for the next transfer. Call
`vx_spi_start` again inside `on_done` if you want continuous transfer:
```c
static void on_spi_done(void* ud, uint8_t* buffer, uint32_t count) {
chip_state_t* s = ud;
s->shift_reg = buffer[0];
vx_spi_start(s->spi, s->buf, 1); // re-arm for next byte
}
```
This is needed for chips like 74HC595 that have no real CS — they shift
on every SCK edge as long as data flows.
### Using CS for transaction boundaries
For chips with a real chip-select (e.g. MCP3008), the chip watches its CS
pin and triggers `vx_spi_start` / `vx_spi_stop` accordingly:
```c
static void on_cs_change(void* ud, vx_pin pin, int value) {
chip_state_t* s = ud;
if (value == VX_LOW) {
vx_spi_start(s->spi, s->buf, 3); // CS asserted — start exchange
} else {
vx_spi_stop(s->spi); // CS released
}
}
vx_pin_watch(s->cs, VX_EDGE_BOTH, on_cs_change, s);
```
---
## UART
### Config struct
```c
typedef struct {
vx_pin rx;
vx_pin tx;
uint32_t baud_rate;
void (*on_rx_byte) (void* user_data, uint8_t byte);
void (*on_tx_done) (void* user_data);
void* user_data;
uint32_t reserved[8];
} vx_uart_config;
_Static_assert(sizeof(vx_uart_config) == 56, "vx_uart_config must be 56 bytes");
```
### Functions
```c
vx_uart vx_uart_attach(const vx_uart_config* cfg);
bool vx_uart_write (vx_uart u, const uint8_t* buffer, uint32_t count);
```
### Example: ROT13 chip
```c
static void on_rx(void* ud, uint8_t byte) {
chip_state_t* s = ud;
uint8_t out = byte;
if (out >= 'A' && out <= 'Z') out = ((out - 'A' + 13) % 26) + 'A';
if (out >= 'a' && out <= 'z') out = ((out - 'a' + 13) % 26) + 'a';
vx_uart_write(s->uart, &out, 1); // echo back transformed byte
}
void chip_setup(void) {
chip_state_t* s = malloc(sizeof(chip_state_t));
vx_uart_config cfg = {
.rx = vx_pin_register("RX", VX_INPUT),
.tx = vx_pin_register("TX", VX_INPUT_PULLUP),
.baud_rate = 115200,
.on_rx_byte = on_rx,
.on_tx_done = NULL,
.user_data = s,
};
s->uart = vx_uart_attach(&cfg);
}
```
When the user wires the chip's `RX` pin to the Arduino's pin 1 (TX0), the
host bridges them automatically: every byte the sketch sends with
`Serial.write()` triggers your `on_rx` callback. Your `vx_uart_write` calls
land in `Serial.read()`'s buffer.
---
## Timers and time
```c
uint64_t vx_sim_now_nanos(void);
vx_timer vx_timer_create(void (*cb)(void* user_data), void* user_data);
void vx_timer_start (vx_timer t, uint64_t period_nanos, bool repeat);
void vx_timer_stop (vx_timer t);
```
Timer ticks are anchored to **simulated time** — they fire deterministically
relative to CPU cycles, not wall-clock seconds. A 1-ms timer will fire after
exactly 1 ms of simulated AVR time regardless of how fast the host actually runs.
```c
static void on_tick(void* ud) {
chip_state_t* s = ud;
vx_pin_write(s->led, !vx_pin_read(s->led)); // blink at 1 Hz
}
void chip_setup(void) {
chip_state_t* s = malloc(sizeof(chip_state_t));
s->led = vx_pin_register("LED", VX_OUTPUT_LOW);
vx_timer t = vx_timer_create(on_tick, s);
vx_timer_start(t, 500000000, true); // 500 ms, repeating
}
```
---
## Display / framebuffer
For chips that drive a screen.
### Schema in `chip.json`
```json
"display": { "width": 128, "height": 64 }
```
Adding this enables a `<canvas>` inside the chip's web component on the
canvas. The chip writes RGBA pixels to a framebuffer; the host repaints the
canvas after each write.
### Functions
```c
typedef int32_t vx_buffer;
vx_buffer vx_framebuffer_init(uint32_t* out_width, uint32_t* out_height);
void vx_buffer_write (vx_buffer buf, uint32_t offset, const void* data, uint32_t data_len);
```
### Pixel format
Row-major RGBA8888, no padding. Pixel `(x, y)` lives at byte offset
`(y * width + x) * 4`, bytes `R G B A`.
### Example
```c
uint32_t w, h;
vx_buffer fb = vx_framebuffer_init(&w, &h);
// Fill the screen green
uint8_t green[4] = {0, 0xFF, 0, 0xFF};
for (uint32_t y = 0; y < h; y++) {
for (uint32_t x = 0; x < w; x++) {
vx_buffer_write(fb, (y * w + x) * 4, green, 4);
}
}
```
For real LCDs you typically convert RGB565 → RGBA8888 inline before writing.
---
## Logging
```c
void vx_log(const char* msg);
```
Print a message to the host's chip log (browser dev console, prefixed with
`[chip:<componentId>]`).
`printf` also works — it's routed through WASI's `fd_write` syscall to the
same log.
```c
vx_log("EEPROM ready");
printf("Temperature: %.2f °C\n", temp);
```
---
## Type & constant cheat sheet
```c
// Opaque handles (all int32_t under the hood)
vx_pin // pin handle
vx_attr // attribute handle
vx_i2c // I2C device handle
vx_uart // UART handle
vx_spi // SPI handle
vx_timer // timer handle
vx_buffer // framebuffer handle
// Pin modes
VX_INPUT, VX_OUTPUT, VX_INPUT_PULLUP, VX_INPUT_PULLDOWN, VX_ANALOG
VX_OUTPUT_LOW, VX_OUTPUT_HIGH
// Pin values
VX_LOW (0), VX_HIGH (1)
// Edge mask (combine with bitwise OR if needed)
VX_EDGE_RISING (1), VX_EDGE_FALLING (2), VX_EDGE_BOTH (3)
```
---
## ABI guarantees
These are checked at compile time inside the header:
- `sizeof(vx_i2c_config) == 64`
- `sizeof(vx_uart_config) == 56`
- `sizeof(vx_spi_config) == 60`
If any of these change, your chip won't compile until the runtime side is
updated to match. This is intentional — it catches ABI drift early.
Each config struct also has a `uint32_t reserved[8]` field at the end. Zero
it out (the Velxio header initializer literally `= {.field = ...}` syntax
zeros unmentioned fields). Future versions may use those slots; today they
must be 0.