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
Install the Zephyr SDK and
westfollowing the official guide — or 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-installerThe 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.Initialize a Zephyr workspace and pull the board support you need:
west init ~/zephyrproject cd ~/zephyrproject west updateConfirm 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-projectDependencies 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 --uploadFor 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:
| Target | Board | SoC | Programming |
|---|---|---|---|
xiao_ble | Seeed Studio XIAO nRF52840 | nRF52840 | J-Link / nrfjprog |
esp32s3_devkitc | ESP32-S3 DevKitC | ESP32-S3 | USB / esptool |
esp32_devkitc | ESP32 DevKitC | ESP32 | USB / esptool |
esp32c3_devkitm/esp32c3 | ESP32-C3 DevKitM | ESP32-C3 (RISC-V) | USB / esptool |
esp32c6_devkitc/esp32c6/hpcore | ESP32-C6 DevKitC | ESP32-C6 (RISC-V) | USB / esptool |
rpi_pico | Raspberry Pi Pico | RP2040 | USB / picotool |
rpi_pico2/rp2350a/m33 | Raspberry Pi Pico 2 | RP2350 (ARM M33) | USB / picotool |
blackpill_f411ce/stm32f411xe | WeAct Black Pill V2.0 | STM32F411 (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.
| Peripheral | Zephyr API |
|---|---|
| GPIO | gpio_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) |
| PWM | pwm_dt_spec against the pwm-led0 alias |
| ADC | SAADC (adc) — 12-bit, ~3.0 V VREF on the XIAO |
| DAC | Zephyr dac driver — ESP32 targets, channels on GPIO25/26 |
| I2C / SPI / UART | devicetree-bound i2c_* / spi_* / uart_* against the board’s i2c1 / spi2 / uart0-style node labels (the exact labels come from each board’s devicetree) |
| USB serial | CDC-ACM on Zephyr’s USB device stack, composed in the generated devicetree overlay — the board’s USB connector as USB0 (Black Pill, XIAO nRF52840) |
| Interrupts | gpio_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 timers | counter driver (<zephyr/drivers/counter.h>) — the XIAO’s free RTC1, for example |
| WDT | wdt_* driver |
| Random | sys_rand_get + xorshift32 |
| Preferences | Zephyr settings / ZMS (auto-locates storage_partition) |
| Filesystem | littlefs mounted at /lfs on the board’s storage_partition, formatted on first use |
| Worker | k_work + k_sem |
| Display | ILI9341 / ST7796S (RGB565) and SSD1306 (mono OLED) via <zephyr/drivers/display.h> — see below |
| Touch | FT6336U over I2C |
| BLE | NimBLE GATT peripheral |
| WiFi / HTTP / MQTT | ESP32 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
ft6336udevicetree 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 call | Zephyr 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 dfuThe 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 staticstatic_array/static_stringimplementations instead. - Free heap reports 0 unless you enable
CONFIG_SYS_HEAP_RUNTIME_STATSin yourprj.conf. - DAC needs the right board. DAC lowering uses the Zephyr
dacdriver 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
sw0alias in mainline Zephyr’sxiao_bleboard (nogpio-keysnode).GPIO.onInterruptstill 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 agpio-keysoverlay 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.