Communication Buses

Buses are the connections your board uses to talk to other chips. Cuttlefish supports the three common ones: serial (UART), I2C, and SPI. Each is a TypeScript object, and Cuttlefish makes sure you’ve set it up before you use it — so you can’t call .read() before .begin(), for example. That check happens at build time, before the code reaches the board.


UART (Serial)

UART (universal asynchronous receiver-transmitter) is the usual way to do serial communication with a computer or between two microcontrollers — you often see it just called “serial.” Cuttlefish gives you the usual print methods, plus buffered reads and async waiting.

Basic Usage

Initialize a serial port with a baud rate (the speed of a serial link) to make its methods available.

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

// Initialize UART0 at 115200 baud
const serial = UART0.begin(115200);

serial.println("Cuttlefish Serial Initialized");

while (true) {
  serial.println(`Millis: ${millis()}`);
  delay(1000);
}

Reading Data

Cuttlefish provides several ways to read incoming data, from single bytes to entire lines.

import { UART0, delay, millis } from '@typecad/board';
const serial = UART0.begin(115200);

while (true) {
    if (serial.available() > 0) {
        const line = serial.readLine();
        serial.print(`Received: ${line}\n`);
    }
}

Async Connections

For USB-CDC serial ports (serial-over-USB, common on modern boards), you can wait for a host computer to connect before proceeding.

// Wait for terminal to be opened on the PC
await serial.waitForConnection();
serial.println("Hello, PC! Connection established.");

I2C (Inter-Integrated Circuit)

I2C is used for communicating with sensors, displays, and port expanders over two wires (SDA/SCL). Cuttlefish automates address management and register access.

Accessing a Device

Instead of manually sending start/stop bits and handling the ACK/NACK (acknowledge / not-acknowledge replies on I2C) for every byte, you define a device accessor.

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

const bus = I2C0.begin();
const sensor = bus.device(0x76); // Typical address for a BME280 sensor

// Read a single byte from a specific register
const chipId = sensor.readByte(0xD0);

// Write a configuration byte to a register
sensor.writeByte(0xF4, 0x27);

Bus Configuration & Recovery

You can adjust the clock speed — for example, switching to Fast Mode (a faster I2C speed) — or attempt to recover a “stuck” bus (where a slave device is incorrectly holding the data line LOW).

bus.setClock(400000); // 400kHz Fast Mode
bus.recover();        // Attempt to unstick SDA/SCL lines via clock toggling

SPI (Serial Peripheral Interface)

SPI is a high-speed bus used for displays, SD cards, and high-performance sensors. It is Full Duplex (data flows both ways at once). Cuttlefish manages the Chip Select (CS) line automatically for you.

Automatic CS Management

When using .device(csPin), Cuttlefish automatically asserts/deasserts the chip-select pin (the pin that tells a chip “I’m talking to you”) for every operation — pulling it LOW to start and HIGH to end — ensuring the slave is active only during data transfer.

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

const bus = SPI0.begin();
const display = bus.device(D10); // Use D10 as Chip Select

// Transfer data (Full Duplex: sends 0x42 and returns what was received)
const response = display.transfer(0x42);

// Direct register operations
display.writeRegister(0x01, 0xFF);

Transactions

For performance and safety, you can group multiple operations into a transaction with specific hardware settings.

bus.beginTransaction({
  frequency: 10000000, // 10MHz
  mode: 0,
  bitOrder: 'msb'
});

display.write(0xAA);
display.write(0xBB);

bus.endTransaction(); // Returns settings to default

API Reference

UART / Serial Handle

MethodDescription
print(args)Sends data without a trailing newline.
println(args)Sends data with a trailing newline.
printf(fmt, args)Formatted output (C-style formatting).
read()Reads one byte from the buffer (-1 if empty).
readLine()Reads until a \n or \r\n is encountered.
available()Returns the number of bytes waiting to be read.

I2C Accessor

MethodDescription
readByte(reg)Reads one byte from a register.
readBytes(reg, n)Reads n bytes into a Uint8Array.
writeByte(reg, val)Writes one byte to a register.
writeBytes(reg, data)Writes a buffer of bytes to a register.

SPI Device Handle

MethodDescription
transfer(data)Simultaneous read/write (full duplex).
write(data)Sends data and ignores the return signal.
readRegister(reg, n)Reads n bytes starting from a specific register address.
writeRegister(reg, val)Writes one byte to a specific register address.

⚙️ Advanced details — bus ownership on multi-core boards

Advanced Feature: Bus Ownership

In multi-threaded environments (like ESP32), you can use .take() and .release() to ensure exclusive access to a shared bus. This prevents a high-priority task from interrupting a sensor reading transaction on another task.

// Wait for exclusive access to I2C0
const bus = I2C0.take(); 

if (bus) {
  // Bus is now locked for this task
  bus.device(0x42).readByte(0x00);
  bus.release(); // Unlock the bus for other tasks
}