Zephyr

The Zephyr framework turns your TypeScript into a real Zephyr application, built with west. Pins are resolved through devicetree, timing uses the Zephyr kernel, and periodic callbacks run on kernel timers. It is Cuttlefish’s flagship framework target.

Prerequisites

  1. Install the Zephyr SDK and west following the official guideor use the installer bundled with @typecad/framework-zephyr, which sets up a micromamba environment that Cuttlefish discovers automatically (no shell activation required):

    npx --package @typecad/framework-zephyr zephyr-installer

    The installer ships Zephyr SDK 1.0.1 (Zephyr 4.4-compatible) with selective platform installs — pick only the toolchain groups you need (arm, esp32, riscv, …) instead of the full multi-gigabyte bundle, and reconfigure later with --modify.

  2. Initialize a Zephyr workspace and pull the board support you need:

    west init ~/zephyrproject
    cd ~/zephyrproject
    west update
  3. Confirm the toolchain. Watch for the version string:

    west --version

The supported Zephyr version range is >=4.3 <5.0 — builds fail fast with a clear message outside that range. The framework finds west without any shell activation by checking, in order: PATH, the venv next to $ZEPHYR_BASE, a micromamba environment (override the env name with TYPECAD_ZEPHYR_ENV), well-known workspace locations, and system pythons via -m west. Run cuttlefish doctor to see exactly how west and your board target resolve.

The framework relies on mainline Zephyr board support. The supported boards ship in the Zephyr tree, so west update is all you need.

Quick start

Scaffold an ESP32 project with the Zephyr framework:

npx @typecad/cuttlefish create my-project --target esp32s3_devkitc --framework zephyr
cd my-project

Dependencies are installed by create. The generated cuttlefish.config.ts sets framework: '@typecad/framework-zephyr' and the qualified board target. Build and upload with:

npx @typecad/cuttlefish build --compile --upload

For a different board, set the target explicitly:

import type { CuttlefishConfig } from '@typecad/cuttlefish/api';

const config: CuttlefishConfig = {
  entry: './src/main.ts',
  framework: '@typecad/framework-zephyr',
  board: 'xiao_ble/nrf52840',   // or 'esp32s3_devkitc/esp32s3/procpu', 'blackpill_f411ce/stm32f411xe', ...
  output: {
    outDir: './out',
  },
};

export default config;

Supported boards

These board targets are hardware-validated. Every board’s chip view — GPIO controllers, bus nodes, silicon routes — is resolved from the generated board manifest, so every catalog board works through the same data-driven path:

TargetBoardSoCProgramming
xiao_bleSeeed Studio XIAO nRF52840nRF52840J-Link / nrfjprog
esp32s3_devkitcESP32-S3 DevKitCESP32-S3USB / esptool
esp32_devkitcESP32 DevKitCESP32USB / esptool
esp32c3_devkitm/esp32c3ESP32-C3 DevKitMESP32-C3 (RISC-V)USB / esptool
esp32c6_devkitc/esp32c6/hpcoreESP32-C6 DevKitCESP32-C6 (RISC-V)USB / esptool
rpi_picoRaspberry Pi PicoRP2040USB / picotool
rpi_pico2/rp2350a/m33Raspberry Pi Pico 2RP2350 (ARM M33)USB / picotool
blackpill_f411ce/stm32f411xeWeAct Black Pill V2.0STM32F411 (ARM M4F)ST-Link (openocd) or USB DFU (dfu-util)

The esp32c6_devkitc target requires its /esp32c6/hpcore qualifier — the board also ships an lpcore variant and Zephyr rejects the bare name. The Black Pill is the first STM32 target: its chip data (per-port GPIO controllers, ADC channel map, TIM4 PWM channels) is harvested from the Zephyr tree at catalog-generation time, and the typeCAD Zephyr installer provides dfu-util on Windows so the USB-bootloader path works out of the box.

The xiao_ble is the default when no target is supplied. A target may be a bare board id (esp32s3_devkitc) or a board/qualifier path (esp32s3_devkitc/esp32s3/procpu). On Zephyr 4.3+, multi-core ESP32 boards require the qualified form — bare ids like esp32_devkitc are normalized to their /procpu qualified form automatically. The Pico 2 target is qualified with the SoC/CPU-cluster path (rp2350a/m33) because the bare rpi_pico2 name defaults to the Hazard3 RISC-V core — the M33 variant matches the ARM toolchain the rest of the targets use. Boards beyond these come from the generated board data pack — 1300+ Zephyr board variants with their LED/BUTTON wiring, console, and silicon routes (PWM/ADC/DAC, harvested from the SoC’s pinctrl data) extracted from the tree at generation time.

What it does

Cuttlefish turns each HAL operation into native Zephyr APIs. Pin handling is devicetree-aware — pins resolve to devicetree nodes, honoring the polarity set in the devicetree, and fall back to raw controller access when no devicetree node exists. These headers are always included: <zephyr/kernel.h>, <zephyr/drivers/gpio.h>, <cstdio>, and <cstdint>, plus driver and networking headers as your code needs them.

PeripheralZephyr API
GPIOgpio_pin_configure_dt, gpio_pin_set_dt, gpio_pin_get_dt, gpio_pin_toggle_dt (raw gpio_pin_*_raw via DT_NODELABEL(gpio0)/gpio1 when no DT spec)
PWMpwm_dt_spec against the pwm-led0 alias
ADCSAADC (adc) — 12-bit, ~3.0 V VREF on the XIAO
DACZephyr dac driver — ESP32 targets, channels on GPIO25/26
I2C / SPI / UARTdevicetree-bound i2c_* / spi_* / uart_* against the board’s i2c1 / spi2 / uart0-style node labels (the exact labels come from each board’s devicetree)
USB serialCDC-ACM on Zephyr’s USB device stack, composed in the generated devicetree overlay — the board’s USB connector as USB0 (Black Pill, XIAO nRF52840)
Interruptsgpio_init_callback + gpio_add_callback — any GPIO. Pins with a devicetree button alias go through the alias (pull-up and active level honored); every other pin attaches through its GPIO port controller directly
Hardware timerscounter driver (<zephyr/drivers/counter.h>) — the XIAO’s free RTC1, for example
WDTwdt_* driver
Randomsys_rand_get + xorshift32
PreferencesZephyr settings / ZMS (auto-locates storage_partition)
Filesystemlittlefs mounted at /lfs on the board’s storage_partition, formatted on first use
Workerk_work + k_sem
DisplayILI9341 / ST7796S (RGB565) and SSD1306 (mono OLED) via <zephyr/drivers/display.h> — see below
TouchFT6336U over I2C
BLENimBLE GATT peripheral
WiFi / HTTP / MQTTESP32 targets only — net_mgmt / conn_mgr, BSD sockets + http_client_req (mbedTLS TLS), mqtt over <zephyr/net/mqtt.h>

Builds are incremental: the west build directory is reused across compilations (a pristine rebuild happens only when the generated prj.conf / CMakeLists.txt content changes), and the installer environment includes ccache, which Zephyr picks up automatically.

Display and touch

The Zephyr framework provides native display and touch adapters — no Adafruit library dependency. See Display Configuration for the field reference.

  • Displays: ILI9341 and ST7796S (RGB565), plus SSD1306 (128×64 mono OLED). The ST7796S is driven over direct SPI writes rather than Zephyr’s standard display bridge, which does not work with this panel. The SSD1306 profile is for direct display.* drawing — the @typecad/ui adapter does not support mono displays, so use it for raw drawing rather than HTML/CSS UIs.
  • Touch: FT6336U only, polled over I2C against an ft6336u devicetree node. The IRQ pin is not used — the driver’s GPIO interrupt does not work reliably on the ESP32 interrupt controller, so the adapter reads the controller each frame instead.

Devicetree overlays (display/touch nodes, pinctrl remux, DMA, backlight alias) and the matching prj.conf Kconfig symbols (CONFIG_DISPLAY, CONFIG_SPI, CONFIG_I2C, CONFIG_DMA, …) are generated from a usage scan of your code.

Timing on Zephyr

The HAL timing API uses the Zephyr kernel:

HAL callZephyr C++
Time.sleep(ms)k_msleep(ms);
Time.busyWaitUs(us)k_busy_wait(us); (spins, does not yield)
Time.now()milliseconds since boot via k_uptime_get()
Time.nowUs()uptime-derived microseconds on every board
new Thread(n).start(fn)k_thread_create on a per-slot stack (concurrent with main)

Because this is an RTOS target, blocking in a task is expected — Zephyr tasks are allowed to block, so a blocking sleep does not trip the “blocking delay in loop” style warning.

Zephyr-specific configuration

The zephyr block in cuttlefish.config.ts passes build details straight to the Zephyr toolchain:

const config = {
  // ...
  zephyr: {
    kconfig: { CONFIG_MAIN_STACK_SIZE: '2048' },  // extra prj.conf symbols
    cmakeArgs: ['-DEXTRA_DTC_OVERLAY_FILE=mysensor.overlay'],
    runner: 'nrfjprog',                            // west flash runner override
    runnerArgs: ['--reset'],                       // extra west flash flags, verbatim
  },
};

export default config;

Choosing the upload method

Name the hardware on your desk and the framework works out the rest. The soc descriptors carry a table of named probe methods; zephyr.probe picks one and cuttlefish create asks which you want when the board has options. One probe method serves both flashing and debugging — the same attach session, the same quirks.

To attach a Black Pill over an ST-Link (SWD) instead of its USB bootloader, add one key to your config:

const config = {
  // ...
  zephyr: {
    probe: 'stlink',
  },
};

export default config;

That is the whole config, and it drives west flash and west debug alike. The method carries its own quirks — the stlink entry brings the reset_config none flag openocd needs when the SRST reset line is unwired, so you never paste runner flags you found in a forum. Run cuttlefish doctor in the project to list what the board supports; the Black Pill answers stlink, dfu, jlink (debug-capable: stlink, jlink — a bootloader is not a debugger), the XIAO answers jlink, openocd, uf2.

For a one-off, skip the config entirely:

npx cuttlefish build --compile --upload --probe dfu

The lower level is still there when you need it. zephyr.runner names the west flash runner directly (openocd, dfu-util, jlink, …) and zephyr.runnerArgs passes any additional flags to west flash after the runner — runner and probe are two ways of choosing, so set one, not both. User runnerArgs are appended after a method’s own flags, which means they can override them (argparse takes the last value). The openocd binary ships with the Zephyr SDK and Cuttlefish puts it on the spawned west’s PATH automatically.

Leave the whole zephyr block out and the board’s default runner applies — for the Black Pill that is DFU over the USB cable, BOOT0 held while you tap reset.

doctor and licenses

cuttlefish doctor checks the Zephyr environment end to end: west discovery (and where it was found), the effective ZEPHYR_BASE, the Zephyr version against the supported range, and whether the configured board target exists — suggesting west boards when it doesn’t. cuttlefish licenses audits the Zephyr kernel and every west list manifest project; project scope is reconstructed from compile_commands.json, so build once before auditing (or use --all for the whole workspace).

⚙️ Advanced details — limitations
  • Radio targets. WiFi, HTTP, and MQTT require an ESP32 target. The nRF52840, RP2040/RP2350, and STM32 boards have no radio, and using a networking HAL op on a radioless chip is flagged at build time.
  • Explicitly unsupported categories. These have no Zephyr lowering: OTA, USB, TWAI/CAN, hardware crypto, I2S, Ethernet, ESP-NOW, PCNT, MCPWM, RMT, capacitive touch as a HAL op. Use the devicetree / driver directly via rawCpp() for these.
  • No stdlib in emitted code. No exceptions, no RTTI, no std::vector/std::string/std::iostream. Cuttlefish uses its static static_array / static_string implementations instead.
  • Free heap reports 0 unless you enable CONFIG_SYS_HEAP_RUNTIME_STATS in your prj.conf.
  • DAC needs the right board. DAC lowering uses the Zephyr dac driver on ESP32 (GPIO25/26) and ESP32-S2 (GPIO17/18), 8-bit. Boards without a DAC — nRF52840, ESP32-S3 — are flagged at build time with a hint to switch targets.
  • XIAO user button. The user button on P0.04 has no sw0 alias in mainline Zephyr’s xiao_ble board (no gpio-keys node). GPIO.onInterrupt still works — the pin attaches through its GPIO port controller directly — but the board’s pull-up and active-low wiring live in that missing node, so configure them yourself (new GPIO(pin, GPIO.INPUT | GPIO.PULL_UP)) or add a gpio-keys overlay node to get them applied for you.

What’s different

Pick Zephyr when you want a real RTOS on a supported board — devicetree-driven pin resolution, Zephyr kernel services, and west builds. For desktop testing and simulations with no hardware in the loop, use Native.