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

620 lines
15 KiB
Markdown
Raw Normal View History

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