This is an old revision of the document!
Zephyr OS is a small, open-source real-time operating system (RTOS) designed for resource-constrained embedded and IoT devices, from tiny microcontrollers up to more capable edge hardware. It focuses on predictable, low-latency behavior with a preemptive kernel, strong configurability (you build only what you need), and broad hardware support through a clean device-driver model. Zephyr includes common embedded features like threads, synchronization primitives, timers, power management, networking stacks (e.g., TCP/IP, Bluetooth LE), and security options, all maintained under the Linux Foundation with an emphasis on portability, modularity, and production use.
This tutorial walks you through:
hello_world sample for an ESP32‑S3 targetZephyr’s official Getting Started Guide is the source of truth for host setup (Ubuntu/macOS/Windows), Python environment, west, and SDK installation. Reference: https://docs.zephyrproject.org/latest/develop/getting_started/index.html
Below is a practical “do this in order” setup that follows that guide.
Follow the Install dependencies step for your OS in Zephyr’s Getting Started Guide (Ubuntu/macOS/Windows). Guide: https://docs.zephyrproject.org/latest/develop/getting_started/index.html
Typical things you’ll need include: Python 3, CMake, Ninja, Git, and system build tools.
Pick a workspace folder. In this tutorial I’ll use ~/zephyrproject on Linux/macOS.
(Windows users can use something like C:\zephyrproject.)
# Linux/macOS mkdir -p ~/zephyrproject cd ~/zephyrproject python3 -m venv .venv source .venv/bin/activate
On Windows PowerShell (example):
mkdir C:\zephyrproject cd C:\zephyrproject py -m venv .venv .\.venv\Scripts\Activate.ps1
Zephyr’s official west install guide: https://docs.zephyrproject.org/latest/develop/west/install.html
pip install -U west
# Still inside ~/zephyrproject (or your chosen folder) west init -m https://github.com/zephyrproject-rtos/zephyr --mr main west update
Export Zephyr’s CMake package (so CMake can find Zephyr):
west zephyr-export
Install Python requirements for Zephyr and its modules:
west packages pip --install
(That exact command appears in the Getting Started flow and is commonly the next step after west update.)
Zephyr SDK documentation: https://docs.zephyrproject.org/latest/develop/toolchains/zephyr_sdk.html
Install the Zephyr SDK for your OS, then ensure your environment can find it (either via the SDK setup script registering it, or via environment variables like ZEPHYR_SDK_INSTALL_DIR / ZEPHYR_TOOLCHAIN_VARIANT=zephyr).
Note: ESP32‑S3 is Xtensa based, so you’ll need the Xtensa toolchain that’s included with the Zephyr SDK.
Zephyr’s Espressif HAL requires binary blobs (RF calibration, etc.). Zephyr board docs for Espressif boards call this out and recommend fetching after west update.
Example (ESP32 DevKitC doc): it explicitly instructs west blobs fetch hal_espressif.
Run:
# From your workspace root (~/zephyrproject)
west blobs fetch hal_espressif
If you skip this, you may see build or runtime failures for ESP32 targets.
Hacktor is not (yet) an upstream Zephyr board (maybe you could help with that?), so we need a compatible Zephyr board definition to build/flash a first image.
You have two reasonable approaches:
This tutorial focuses on A to get you a successful first boot quickly, and then outlines B.
Zephyr includes the Official Espressif ESP32 S3 Devkitc board, which is explicitly documented as an ESP32-S3-WROOM-1, 8 MB Quad flash and 8MB Octal PSRAM. Board doc: https://docs.zephyrproject.org/latest/boards/espressif/esp32s3_devkitc/doc/index.html
Limitations:
This section is where the Hacktor‑specific “special care” matters:
We will:
1. create a small application folder 2. add a devicetree overlay that selects the USB serial device for console 3. build and flash
From your Zephyr workspace root:
cd ~/zephyrproject mkdir -p hacktor_hello cd hacktor_hello
Create these files:
CMakeLists.txtprj.confsrc/main.cYou can copy the sample hello_world, but I recommend creating a tiny app so you fully control the console configuration.
CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0) set(BOARD esp32s3_devkitc/esp32s3/procpu CACHE STRING "Default Zephyr board") find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(minimal_zephyr_app) target_sources(app PRIVATE src/main.c )
src/main.c
#include <zephyr/kernel.h> #include <zephyr/sys/printk.h> int main(void) { while (1) { printk("Hello World!\n"); k_sleep(K_SECONDS(1)); } }
prj.conf (minimal logging/console)
CONFIG_CONSOLE=y CONFIG_UART_CONSOLE=y CONFIG_SERIAL=y CONFIG_SERIAL_ESP32_USB=y CONFIG_PRINTK=y
On ESP32 chips with a USB Serial/JTAG device exposed to Zephyr as a UART, you can route the console by changing the devicetree chosen nodes.
A Zephyr issue discussing USB serial output on ESP32 shows an overlay pattern like:
zephyr,console to &usb_serialusb_serial node
(See Zephyr issue #60825 about using usb_serial for console on ESP32-class devices.)
In the root of the project folder, create:
app.overlay
/ {
chosen {
zephyr,console = &usb_serial;
};
};
&usb_serial {
status = "okay";
};
Why this matters for Hacktor:
Activate your Python venv first if it isn’t active:
cd ~/zephyrproject source .venv/bin/activate
Then go to your hacktor_hello folder and create this build script, name it build.sh:
#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$SCRIPT_DIR" DEFAULT_BOARD="esp32s3_devkitc/esp32s3/procpu" if [ -d "$PROJECT_ROOT/../.venv/bin" ]; then export PATH="$PROJECT_ROOT/../.venv/bin:$PATH" fi if [ -z "${ZEPHYR_BASE:-}" ]; then if [ -d "$PROJECT_ROOT/../zephyr" ]; then export ZEPHYR_BASE="$PROJECT_ROOT/../zephyr" else echo "ZEPHYR_BASE is not set and ../zephyr was not found." >&2 exit 1 fi fi BOARD="${BOARD:-$DEFAULT_BOARD}" BUILD_DIR="${BUILD_DIR:-$PROJECT_ROOT/build}" FLASH_PORT="${FLASH_PORT:-${ESPTOOL_PORT:-}}" FLASH_BAUD="${FLASH_BAUD:-}" PRISTINE=0 RUN_FLASH=0 ERASE_FLASH=0 while [ $# -gt 0 ]; do case "$1" in -p|--pristine) PRISTINE=1 ;; -f|--flash|--upload) RUN_FLASH=1 ;; --erase) ERASE_FLASH=1 ;; --port) shift if [ $# -eq 0 ]; then echo "--port requires a value." >&2 exit 1 fi FLASH_PORT="$1" ;; --baud) shift if [ $# -eq 0 ]; then echo "--baud requires a value." >&2 exit 1 fi FLASH_BAUD="$1" ;; -h|--help) cat <<EOF Usage: ./build.sh [--pristine] [--flash] [--port <device>] [--baud <rate>] [--erase] Environment overrides: BOARD=<board target> Default: $DEFAULT_BOARD BUILD_DIR=<build dir> Default: $PROJECT_ROOT/build ZEPHYR_BASE=<zephyr path> Auto-detected from ../zephyr if unset FLASH_PORT=<device> Serial port for flashing FLASH_BAUD=<rate> Serial baud rate for flashing EOF exit 0 ;; *) echo "Unknown argument: $1" >&2 exit 1 ;; esac shift done if [ "$PRISTINE" -eq 1 ]; then rm -rf "$BUILD_DIR" fi cmake -GNinja -B "$BUILD_DIR" -S "$PROJECT_ROOT" -DBOARD="$BOARD" cmake --build "$BUILD_DIR" if [ "$RUN_FLASH" -eq 1 ]; then flash_cmd=(west flash --no-rebuild -d "$BUILD_DIR" -r esp32) runner_args=() if [ -n "$FLASH_PORT" ]; then runner_args+=(--esp-device "$FLASH_PORT") fi if [ -n "$FLASH_BAUD" ]; then runner_args+=(--esp-baud-rate "$FLASH_BAUD") fi if [ "$ERASE_FLASH" -eq 1 ]; then runner_args+=(--erase) fi if [ ${#runner_args[@]} -gt 0 ]; then flash_cmd+=(-- "${runner_args[@]}") fi "${flash_cmd[@]}" fi echo echo "Build complete:" echo " Board: $BOARD" echo " Build dir: $BUILD_DIR" echo " ELF: $BUILD_DIR/zephyr/zephyr.elf" if [ "$RUN_FLASH" -eq 1 ]; then echo " Flash: completed" echo "Opening serial terminal on ${FLASH_PORT} (115200)..." exec screen "${FLASH_PORT}" 115200 fi
Connect Sparrow over USB‑C.
Then build and flash by running:
./build.sh --upload --port [your_usb_port]
You will see the app being built, uploaded and then a serial terminal will print out the Hello World message.
Clone https://github.com/dantudose/Hacktor_Basic
This is a more advance example and the starting point of your tutorial. It initializes the watch's display and touchscreen, imports the LVGL graphic library and builds a simple interactive app. Also, it initializes the shell, so you have a basic command line interface over the serial port.