Communication Buses

One thin class per target: a UART port, a USBConsole, an I2CTarget at one address, an SPITarget behind one chip-select. Construction carries the configuration — baud, buffer sizes, bus speed, SPI mode — and every verb maps to one Zephyr driver call. There is no begin(), no transaction dance, and no Wire vocabulary.

The board module exports each controller as a ready-to-use singleton, so the common case needs no construction at all:

import { UART0, I2C0, SPI0, PA4 } from '@typecad/board';

UART0.writeLine('hello');                  // the controller, default 115200
const dev = I2C0.device(0x44);             // working I2CTarget at 0x44
const id = dev.readReg(0xD0);
const flash = SPI0.device(PA4);            // working SPITarget behind CS

A board without the controller does not export the name — using it is a build error at import, not a runtime surprise. Explicit construction (below) is for non-default facts: a different baud, a bigger receive ring, a faster bus.

For managed sensor drivers over these buses, see Sensors — one generic class covers every Zephyr sensor driver from a generated part catalog.


UART — UART

The UART0 / UART1 … singletons are the direct surface. Construction names the controller and carries the baud (applied once on first use) and the receive-ring size — the first argument may be the board instance or its name:

import { UART, UART0 } from '@typecad/board';

UART0.writeLine("hello");                        // default 115200 — no construction

const gps = new UART(UART0, { baud: 9600, rxBufferBytes: 128 });

gps.write("$PMTK220,1000*2F\r\n");   // no newline appended
gps.writeLine("hello");               // with a newline

if (gps.available() > 0) {            // bytes waiting in the ring
  const b = gps.read();               // pop the oldest byte (-1 when empty)
  const next = gps.peek();            // look without consuming
}

read() and peek() return one byte or −1. There is no line API — frame messages in your program.

USB Serial — USBConsole

On boards with a USB device connector, USBConsole is the CDC port the host sees as a COM/tty device. One CDC-specific fact matters: output written before the host opens the port is dropped by most hosts. Gate early output on linked(), or block once with waitLinked():

import { USB0 } from '@typecad/board';

const usb = USB0;                    // the board exports it ready to use
if (!usb.linked()) {
  usb.waitLinked(3000);        // bounded poll — returns once the host opens it
}
usb.writeLine('hello, host');

USB0 is board-gated: the board must declare a USB device controller, otherwise usb.* operations fail at build time with a diagnostic naming the missing board data.

I2C — I2CTarget

One address on a bus, Zephyr’s register verbs verbatim. I2C0.device(0x44) is the common form — it hands back a working target with no further construction, and the same object is what new Sensor(...) takes:

import { I2C0 } from '@typecad/board';

const sensor = I2C0.device(0x44);

sensor.writeReg(0xF4, 0x27);            // one-byte register write
const id = sensor.readReg(0xD0);        // one-byte register read
sensor.updateReg(0xF5, 0x0F, 0x02);     // read-modify-write, no read-back race
sensor.write([0x2C, 0x06]);             // raw bytes

The address is the 7-bit form (0x44) — never the left-shifted 8-bit form. A custom bus speed rides the explicit constructor — new I2CTarget(I2C0, 0x44, { hz: 400000 }) — applied once; 100k/400k/1M map to Zephyr’s speed tiers. Explicit construction takes the board instance or its name and is otherwise equivalent to device(...).

To attach a managed sensor driver, pass the same target — new Sensor(SENSOR.sensirion_sht3xd, I2C0.device(0x44)) — which generates the devicetree node and typed channels. I2CTarget is for register-level access to parts the catalog does not cover.

SPI — SPITarget

One chip-select on a bus. The configuration (CS pin, frequency, mode bits) becomes a devicetree spec at build time; CS asserts and deasserts around every operation, so there is no transaction API:

import { SPI0, PA4 } from '@typecad/board';

const display = SPI0.device(PA4, { hz: 10000000, mode: 0 });

const rx = new Uint8Array(4);
display.transceive([0x42, 0x00, 0x00, 0x00], rx);   // full duplex
display.write([0xAA, 0xBB]);                         // write only
const id = display.readReg(0x00);                    // one-byte register read

Multiple targets on one bus with different CS pins is the normal shape — one SPI0.device(...) per chip. Explicit construction is equivalent: new SPITarget(SPI0, PA4, { hz, mode }).

⚙️ Advanced details — bus conflicts

Two peripherals claiming the same pins fail the build with the conflict named. The optional exclusive-claim markers (I2C0.take() / I2C0.release()) add compile-time ownership validation for programs that share a bus across tasks — see Ownership.


API Reference

MemberDescription
UART0 / UART1Board singletons — verbs callable with no construction.
new UART(UART0, opts?)baud (default 115200), rxBufferBytes (default 64); takes the instance or its name.
write(v) / writeLine(v) / available() / read() / peek()Poll TX (v is text, a number, or a boolean); IRQ-backed RX ring (byte or −1).
USB0The CDC console singleton, ready to use; the board must declare USB.
write(v) / writeLine(v) / linked() / waitLinked(ms) / read() / available()CDC port verbs (v is text, a number, or a boolean).
I2C0.device(address)Working I2CTarget at a 7-bit address.
new I2CTarget(I2C0, address, opts?)Explicit equivalent; hz sets the bus speed once.
writeReg(r, v) / readReg(r) / updateReg(r, mask, v) / write(bytes)Zephyr register verbs.
SPI0.device(cs, opts?)Working SPITarget behind one chip-select; hz, mode (0–3) become the devicetree spec.
new SPITarget(SPI0, cs, opts?)Explicit equivalent.
transceive(tx, rx?) / write(tx) / readReg(r)Full-duplex and write transfers; one-byte register read.