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:
| Area | What it covers |
|---|---|
| GPIO & Digital I/O | Pin configuration, read/write/toggle, interrupts |
| Analog & PWM | ADC channels, DAC channels, PWM output |
| Communication Buses | UART, USB serial, I2C, SPI — one class per target device |
| Timing & Clocks | Time — 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 Data | Store key/value settings, File whole-file storage |
| On-Chip Peripherals | Hardware timers (Counter), watchdog |
| Hardware Events | GPIO interrupts with Zephyr edge/level tokens |
| Signal Utilities | Shift 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 / setDutyThe 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 pointSee 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 foreverEach 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 Async — await 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 onceBoard 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 immediatelyA 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.
On This Page