Setting Up Peripherals Before You Use Them

A peripheral (a serial port, an I2C bus, a timer) has to be configured before it’s used. Cuttlefish tracks that for you: it knows whether each one has been set up yet, and warns you if you try to use one before it’s ready.


Peripheral Lifecycle

HAL peripherals follow an initialization lifecycle:

  1. Uninitialized — the peripheral exists as an import but is not configured for use
  2. Initializedbegin() has been called with the required parameters

Using a peripheral before calling begin() may trigger a transpile-time diagnostic depending on the validation mode.


Bus Initialization

Each bus type has a begin() method that configures the hardware:

I2C

import { I2C0 } from '@typecad/board-arduino-uno';

I2C0.begin(100000);  // Start I2C at 100kHz (standard mode)
// Now safe to use I2C0.device(...)

Options:

  • 100000 — Standard mode (100 kHz)
  • 400000 — Fast mode (400 kHz)

You can also start as a slave:

I2C0.beginSlave(0x42);  // Start as slave at address 0x42

SPI

import { SPI0 } from '@typecad/board-arduino-uno';

SPI0.begin();  // Start SPI with default settings (MSB first, mode 0, 4MHz)
// Now safe to use SPI0.device(...)

Custom settings:

SPI0.setMode(1);          // SPI mode 1
SPI0.setBitOrder('lsb');  // LSB first
SPI0.setFrequency(8000000); // 8 MHz

UART / Serial

import { UART0 } from '@typecad/board-arduino-uno';

UART0.begin(9600);  // Start serial at 9600 baud
// Now safe to use UART0.print(...)

// With full configuration
UART0.begin(115200, { parity: 'none', stopBits: 1, flowControl: 'none' });

Ending a Bus

Use end() to release a peripheral and free its pins:

I2C0.begin(100000);
// ... use I2C ...
I2C0.end();  // Release I2C, SDA (A4) and SCL (A5) become available for GPIO

After end(), the peripheral returns to the uninitialized state.


Pin Mode Narrowing

Pin objects change their TypeScript type after calling a mode method. This is the type-state pattern — the type system tracks the pin’s state:

import { D13, D7, A0 } from '@typecad/board-arduino-uno';

// Untyped pin — limited operations available
const pin = D13;

// Narrow to OutputPin
const led = pin.asOutput();   // Now has: high(), low(), toggle(), write(), pwm(), tone()

// Narrow to InputPin
const sensor = A0.asInput();  // Now has: read(), readAnalog(), readVoltage(), onFalling()

// Narrow to InputPin with pull-up
const button = D7.asInputPullUp(); // Now has: read(), onFalling(), onRising(), onChange()

Calling toggle() on an untyped pin, or read() on an OutputPin, is a compile error caught by TypeScript — not just at transpile time, but in your editor.


Peripheral Ownership with take() / release()

For advanced use cases, buses support an ownership model via take() and release(). Once you opt in by calling take(), all subsequent bus access must occur within a taken section:

// Opt into ownership mode
const bus = I2C0.take();

// Use the bus — all I/O is within an owned section
bus.write(0x50, [0x01, 0x02]);
const data = bus.read(0x50, 2);

// Release when done
I2C0.release();

Ownership Rules

After take() is used once on a bus, the validator enforces:

RuleSeverityDiagnostic
Double take() without release()errorhal-ownership-double-take
release() without prior take()warninghal-ownership-unowned-release
I/O outside a taken sectionerrorhal-ownership-unowned-access
Bus taken but never releasedwarninghal-ownership-leak

.begin() calls are allowed outside ownership — initialization always works.

⚙️ Advanced details — bus usage inside emit()

Because emit() drops raw C++ into your output, Cuttlefish also scans those strings for bus names (Wire., SPI., Serial) so the ownership checks still apply:

emit("Wire.begin()");  // Detected — counts as I2C0 usage

Complete Example: I2C Sensor

import { A0, D13, I2C0 } from '@typecad/board-arduino-uno';
import { delay, millis } from '@typecad/hal';

// Configure pins
const led = D13.asOutput();
const analogSensor = A0.asInput();

// Initialize I2C
I2C0.begin(100000);

// Access sensor at address 0x68
const sensor = I2C0.device(0x68);

// Read a register
const whoAmI = sensor.readByte(0x75);  // WHO_AM_I register

// Configure the sensor
sensor.writeByte(0x6B, 0x00);  // Wake up
sensor.writeByte(0x19, 0x07);  // Set sample rate

// Main loop
while (true) {
  const accelX = (sensor.readByte(0x3B) << 8) | sensor.readByte(0x3C);
  const analogValue = analogSensor.readAnalog();

  led.toggle();
  delay(100);
}

In this example:

  • I2C0.begin() must be called before I2C0.device()
  • Pin types are narrowed by .asOutput() and .asInput()
  • The transpiler validates the entire initialization flow

Other Initialization Patterns

Watchdog Timer

import { WDT } from '@typecad/hal';

WDT.enable(4000);  // Enable with 4-second timeout
// Must call WDT.reset() within 4 seconds to prevent reset

Hardware Timers

import { Timer1 } from '@typecad/hal';

Timer1.setFrequency(1000);            // 1 kHz
Timer1.onOverflow(() => { /* ISR */ });
Timer1.start();

EEPROM

import { EEPROM } from '@typecad/hal';

EEPROM.write(0, 42);
const value = EEPROM.read(0);  // → 42

ADC with Configuration

import { ADC } from '@typecad/hal';

ADC.setReference('internal');  // Use internal 1.1V reference
const value = ADC.read(0);    // Read channel 0 with new reference