Hardware Abstraction Layer (HAL)

The HAL (hardware abstraction layer) is the set of TypeScript objects you use to talk to the board — each pin, bus, and built-in feature is an object, and Cuttlefish checks that you’re using them in ways the hardware actually supports. So a wrong pin assignment, a missing setup step, or two features fighting over the same pin is caught before the firmware reaches the board.


Overview

The HAL is organized into eleven areas:

AreaWhat it covers
GPIO & Digital I/OPin configuration, read/write/toggle, interrupts
Analog & PWMADC channels, DAC channels, PWM output
Communication BusesUART, USB serial, I2C, SPI — one class per target device
Timing & ClocksTime — sleep, monotonic clocks, busy-wait
Networking (WiFi & HTTP)WiFi join/scan/AP, HTTP client with TLS (ESP32 targets)
IoT Networking (MQTT)MQTT publish/subscribe over the Zephyr networking stack
Bluetooth Low Energy (BLE)BLE GATT peripheral server
Storage & Persistent DataStore key/value settings, File whole-file storage
On-Chip PeripheralsHardware timers (Counter), watchdog
Hardware EventsGPIO interrupts with Zephyr edge/level tokens
Signal UtilitiesShift registers, RNG

Every class follows one shape: construction carries the configuration (pin flags, baud, period, broker URI — there is no begin() to call or order to get right), and every method lowers to one Zephyr call. Units are Zephyr’s — nanoseconds for PWM, raw counts for ADC, milliseconds throughout.

Compile-Time Directives

The HAL uses three compile-time functions that exist purely for code generation:

rawCpp(text)

Injects raw C++ at the call site. The TypeScript never executes.

import { rawCpp } from '@typecad/hal';
rawCpp("PORTB |= (1 << PB5)");

rawCppExpr(text) is the expression form — it injects a raw C++ expression where a value is expected.

include(text)

Adds a C++ #include to the output. No-op during type-checking.

import { include } from '@typecad/hal';
include("<Wire.h>");

board(path)

Resolves a board definition value at compile time.

import { board } from '@typecad/hal';
const resolution = board("peripherals.pwm.resolution");

Pin Model

Pins are typed objects, not integer constants. The generated board module exports named pin objects with known capabilities:

import { GPIO, ADC, PWM } from '@typecad/hal';
import { LED, A1, PB6 } from '@typecad/board';

const led = new GPIO(LED, GPIO.OUTPUT | GPIO.OUTPUT_INIT_LOW);  // set / get / toggle
const sense = new ADC(A1);                               // read / readMillivolts
const motor = new PWM(PB6, { periodNs: 20_000_000 });           // setPulse / setDuty

The generated board module exports named pins with known capabilities — using a pin for something it cannot do (analog on a digital-only pad, PWM on a non-PWM pin) is caught at build time with the valid pins named.

Timing

One Time object with a blocking sleep, monotonic clocks, and a sub-millisecond spin — every call lowers to one Zephyr kernel call:

Time.sleep(1000);        // k_msleep — blocking at the top level
await Time.sleep(50);    // inside an async function: cooperative (other tasks run)
const ms = Time.now();   // milliseconds since boot, monotonic, no wrap
Time.busyWaitUs(3);      // k_busy_wait — a spin with no schedule point

See Timing & Clocks for the clocks, the Arduino millis() mapping, and the resolution notes.

Threads

Periodic and concurrent work is a Thread — a real kernel thread (k_thread_create), not a callback timer. The construction carries the stack size and priority; start(fn) schedules it; join() blocks until it exits.

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

const blinker = new Thread(0, { stackKb: 2 });   // 0 = the first thread slot
blinker.start(() => {
  while (true) {
    led.toggle();
    Time.sleep(500);
  }
});
blinker.join();          // park main here — the thread runs forever

Each Thread index owns one stack slot, like a Counter instance owns its timer. For deterministic, sub-millisecond periodic execution use the hardware Counter instead — it fires from interrupt context and does not depend on any thread.

Async

async/await lowers to cooperative state machines — one task per async body, pumped from the main loop, no RTOS task and no heap per function. Long waits (Time.sleep, network operations) yield to everything else sharing the loop; async methods on your own classes get an owner-bound task so this keeps working across awaits.

async function heartbeat() {
  while (true) {
    led.toggle();
    await Time.sleep(500);
  }
}

Cooperative waiting

When you need to wait without blocking other tasks, use Asyncawait Async.sleep(ms) becomes a non-blocking wait instead of a busy delay:

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

await Async.sleep(1000);                              // non-blocking sleep
await Async.yield();                                  // let other tasks run once

Board Singletons

Pins and buses come from the generated board module, and the bus singletons are ready to use — UART0.writeLine(...) needs no construction, and I2C0.device(0x44) hands back a working target:

import { LED, UART0, I2C0 } from '@typecad/board';
import { GPIO } from '@typecad/hal';

const led = new GPIO(LED, GPIO.OUTPUT);
UART0.writeLine('hello');
const dev = I2C0.device(0x44);      // register verbs callable immediately

A board that lacks a controller does not export its name — that is a module-resolution error at build time, not a runtime surprise.

Getting Started

Start with GPIO & Digital I/O to learn pin basics, then explore Communication Buses for I2C/SPI/UART.