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:
- Uninitialized — the peripheral exists as an import but is not configured for use
- Initialized —
begin()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 0x42SPI
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 MHzUART / 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 GPIOAfter 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:
| Rule | Severity | Diagnostic |
|---|---|---|
Double take() without release() | error | hal-ownership-double-take |
release() without prior take() | warning | hal-ownership-unowned-release |
| I/O outside a taken section | error | hal-ownership-unowned-access |
| Bus taken but never released | warning | hal-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 usageComplete 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 beforeI2C0.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 resetHardware 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); // → 42ADC 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 referenceOn This Page