Skip to content

Latest commit

 

History

History
820 lines (637 loc) · 23.9 KB

File metadata and controls

820 lines (637 loc) · 23.9 KB

Getting Started with Zig ESP-IDF Sample

A complete guide to building ESP32 firmware using Zig and ESP-IDF.

Table of Contents


Prerequisites

Required Software

  • Python 3.12+ (for ESP-IDF tools)
  • Git (for cloning repositories)
  • CMake 3.16+ (bundled with ESP-IDF)
  • Ninja or Make (bundled with ESP-IDF)
  • Zig compiler (optional - will be auto-downloaded if not found)

Supported Operating Systems

  • Linux (Ubuntu 20.04+, Debian, Fedora, Arch)
  • macOS (10.15+)
  • Windows (10/11 with PowerShell or WSL2)
  • Nix/NixOS (via flake.nix)

Supported ESP32 Targets

Architecture Targets
RISC-V ESP32-C2, C3, C5, C6, C61, H2, H21, H4, P4
Xtensa ESP32, ESP32-S2, ESP32-S3

Installation

Step 1: Install ESP-IDF

Option A: Standard Installation

# Clone ESP-IDF
git clone --recursive https://github.com/espressif/esp-idf.git
cd esp-idf

# Linux/macOS:
./install.sh

# Windows (PowerShell):
.\install.ps1

# Windows (Command Prompt):
install.bat

Option B: Using Nix Flakes (Linux/macOS)

# Enter development environment with all dependencies
nix develop

# Or use direnv for automatic activation
echo "use flake" > .envrc
direnv allow

Option C: ESP-IDF installer (Windows/macOS)

Download from: https://dl.espressif.com/dl/esp-idf/

Step 2: Set up ESP-IDF Environment

Every time you open a new terminal, activate ESP-IDF:

# Linux/macOS:
. $HOME/esp/esp-idf/export.sh

# Windows (PowerShell):
. $HOME/esp/esp-idf/export.ps1

# Windows (Command Prompt):
%USERPROFILE%\esp\esp-idf\export.bat

Step 3: Clone This Project

git clone https://github.com/kassane/zig-esp-idf-sample.git
cd zig-esp-idf-sample

Step 4: Verify Installation

idf.py --version
# Expected output: ESP-IDF v5.x.x or v6.x.x

Quick Start

1. Set Your Target Device

# For ESP32-C6 (RISC-V)
idf.py set-target esp32c6

# For ESP32-S3 (Xtensa)
idf.py set-target esp32s3

# For ESP32 (original, Xtensa)
idf.py set-target esp32

2. Build the Project

idf.py build

What happens during build:

  • ✅ CMake detects your target and configures the build
  • ✅ Zig toolchain is automatically downloaded (if needed)
  • ✅ C bindings are generated from ESP-IDF headers
  • ✅ Target-specific patches are applied
  • ✅ Zig code is compiled to object files
  • ✅ Everything is linked with ESP-IDF libraries
  • ✅ Firmware binaries are generated

3. Flash to Device

# Connect your ESP32 via USB, then:
idf.py -p PORT flash

# Find your port:
# Linux: /dev/ttyUSB0 or /dev/ttyACM0
# macOS: /dev/cu.usbserial-*
# Windows: COM3, COM4, etc.

# Example:
idf.py -p /dev/ttyUSB0 flash

4. Monitor Output

idf.py -p PORT monitor

# Or combine flash + monitor:
idf.py -p PORT flash monitor

# Exit monitor: Ctrl+]

Project Structure

zig-esp-idf-sample/
├── build.zig              # Zig build script (compiles Zig code)
├── build.zig.zon          # Zig package manifest
├── CMakeLists.txt         # Root CMake config
├── sdkconfig              # ESP-IDF configuration (generated)
├── sdkconfig.defaults     # Default SDK config
├── sdkconfig.defaults.esp32  # ESP32-specific defaults
├── partitions_matter.csv  # Custom partition table for Matter (3 MB app, 4 MB flash)
├── dependencies.lock      # Component version lock
├── wokwi.toml            # Wokwi simulator config
│
├── main/
│   ├── CMakeLists.txt       # Main component config
│   ├── placeholder.c        # Minimal C entry point (required by CMake)
│   ├── matter_wrappers.cpp  # C++ shims for esp_matter C++ API (activated when component present)
│   ├── app.zig              # Main Zig application entry
│   ├── idf_component.yml    # Component dependencies
│   ├── Kconfig.projbuild    # Project configuration options
│   └── examples/
│       ├── gpio-blink.zig        # Toggle LED on GPIO2
│       ├── uart-echo.zig         # UART echo
│       ├── i2c-scan.zig          # I2C bus scan
│       ├── wifi-station.zig      # WiFi station
│       ├── http-server.zig       # HTTP server
│       ├── ble-gatt-server.zig   # BLE GATT server
│       ├── smartled-rgb.zig      # WS2812B LED strip
│       ├── dsp-math.zig          # DSP/FFT operations
│       └── matter-light.zig      # Matter On/Off Light
│
├── imports/              # Zig API wrappers and bindings
│   ├── idf.zig           # Main ESP-IDF facade module
│   ├── idf-sys.zig       # Generated C bindings (auto-generated)
│   ├── sys.zig           # Re-exports idf-sys
│   ├── error.zig         # esp_err_t → Zig error mapping
│   ├── logger.zig        # std.log integration (espLogFn)
│   ├── version.zig       # ESP-IDF version info (ver)
│   ├── heap.zig          # HeapCapsAllocator, MultiHeapAllocator
│   ├── bootloader.zig    # Partition/bootloader control
│   ├── gpio.zig          # GPIO wrapper
│   ├── wifi.zig          # WiFi station/AP/scan
│   ├── uart.zig          # UART driver
│   ├── i2c.zig           # I2C master
│   ├── spi.zig           # SPI master (+ SDSPI)
│   ├── i2s.zig           # I2S audio (STD, PDM, TDM)
│   ├── http.zig          # HTTP server + client
│   ├── mqtt.zig          # MQTT client
│   ├── lwip.zig          # lwIP sockets, DNS, SNTP
│   ├── crc.zig           # ESP-ROM CRC-8/16/32
│   ├── bluetooth.zig     # Bluedroid BLE
│   ├── nimble.zig        # NimBLE BLE (compile-time guarded)
│   ├── now.zig           # ESP-NOW protocol
│   ├── nvs.zig           # NVS flash key-value storage
│   ├── partition.zig     # Partition table operations
│   ├── sleep.zig         # Deep/light sleep + wakeup
│   ├── event.zig         # ESP event loop
│   ├── wdt.zig           # Task watchdog timer
│   ├── rtos.zig          # FreeRTOS tasks/queues/semaphores/timers
│   ├── pcnt.zig          # Pulse counter (pulse)
│   ├── phy.zig           # Wireless PHY / RF calibration
│   ├── segger.zig        # Segger SystemView profiling
│   ├── led-strip.zig     # LED strip — requires espressif/led_strip
│   ├── dsp.zig           # DSP/FFT — requires espressif/esp-dsp
│   ├── hosted.zig        # ESP-Hosted coexistence — requires espressif/esp_hosted
│   ├── wifi_remote.zig   # WiFi remote — requires espressif/esp_wifi_remote
│   ├── timer.zig         # High-resolution esp_timer (one-shot + periodic)
│   ├── ledc.zig          # LED PWM controller (duty, fade)
│   ├── twai.zig          # TWAI/CAN bus driver
│   ├── pm.zig            # Power management locks
│   ├── pthread.zig       # POSIX threads (FreeRTOS-backed)
│   └── panic.zig         # Zig panic handler
│
├── include/              # C headers for binding generation
│   ├── stubs.h                 # Core ESP-IDF headers (input to zig translate-c)
│   ├── wifi_stubs.h            # WiFi macro wrappers
│   ├── bt_stubs.h              # Bluetooth macro wrappers
│   ├── matter_stubs.h          # C wrapper interface for esp_matter (C++ component)
│   ├── matter_closure_patch.h  # GCC 14 C++23 fix for closure-control cluster
│   └── bindings.h              # Additional bindings
│
├── cmake/               # CMake build system scripts
│   ├── zig-config.cmake        # Main Zig configuration
│   ├── zig-download.cmake      # Auto-download Zig toolchain
│   ├── zig-runner.cmake        # Helper functions for Zig
│   ├── bindings.cmake          # Binding generation
│   ├── extra-components.cmake  # Managed component detection
│   └── patch.cmake             # Post-processing patches
│
├── patches/             # Binding fixes for translate-c issues
│   ├── *.zig
│
├── docs/                # Documentation
│   ├── build-internals.md      # Build system details
│   ├── build-scheme.png        # Build flow diagram
│   └── zig-xtensa.md           # Xtensa toolchain info
│
└── flake.nix            # Nix development environment

Building Your First Project

Example 1: Hello World

const std = @import("std");
const idf = @import("esp_idf");

comptime {
    @export(&main, .{ .name = "app_main" });
}

fn main() callconv(.c) void {
    log.info("Hello from Zig on ESP32!", .{});
    log.info("Zig version: {s}", .{@import("builtin").zig_version_string});
    
    // Show memory info
    var heap = idf.heap.HeapCapsAllocator.init(null); // default: MALLOC_CAP_DEFAULT
    log.info("Free heap: {} bytes", .{heap.freeSize()});
    
    // Sleep forever
    while (true) {
        idf.rtos.Task.delayMs(1000);
    }
}

// overwrite zig std_options config
pub const std_options: std.Options = .{
    .logFn = idf.log.espLogFn,
};
// rename log instance
const log = std.log.scoped(.@"esp-idf");
// overwrite std panic-handler
pub const panic = idf.esp_panic.panic;

Build and run:

idf.py build flash monitor

Example 2: Blinking LED

Create main/examples/blink.zig:

const std = @import("std");
const idf = @import("esp_idf");

const LED_PIN = .@"18"; // Change to your LED pin

comptime {
    @export(&main, .{ .name = "app_main" });
}

fn main() callconv(.c) void {    
    // Configure GPIO as output
    idf.gpio.Direction.set(LED_PIN, .output) catch {
        log.err("Failed to configure GPIO", .{});
        return;
    };
    
    log.info("Blinking LED on GPIO {d}", .{LED_PIN});
    
    while (true) {
        // LED ON
        idf.gpio.Level.set(LED_PIN, 1) catch {};
        log.info("LED ON", .{});
        idf.rtos.Task.delayMs(1000);
        
        // LED OFF
        idf.gpio.Level.set(LED_PIN, 0) catch {};
        log.info("LED OFF", .{});
        idf.rtos.Task.delayMs(1000);
    }
}

// overwrite zig std_options config
pub const std_options: std.Options = .{
    .logFn = idf.log.espLogFn,
};
// rename log instance
const log = std.log.scoped(.blink);
// overwrite std panic-handler
pub const panic = idf.esp_panic.panic;

Working with Components

Adding Managed Components

Components are specified in main/idf_component.yml:

Add a component:

# Use idf.py command:
idf.py add-dependency espressif/led_strip
# then
idf.py reconfigure

Using LED Strip Component

The build system automatically:

  1. ✅ Detects components in dependencies.lock
  2. ✅ Adds include paths to binding generation
  3. ✅ Generates HAS_* defines (e.g., HAS_LED_STRIP=1)
  4. ✅ Includes headers in include/stubs.h conditionally
  5. ✅ Makes APIs available in Zig via idf.* modules

1. Ensure LED strip is in main/idf_component.yml:

dependencies:
  espressif/led_strip: "*"

2. Reconfigure:

idf.py reconfigure

3. Check provided example:

The example in examples/smartled-rgb.zig demonstrates:

  • Configuring WS2812B LED strip on GPIO 2
  • Setting individual pixel colors
  • Refreshing the display
  • Blinking pattern

DSP basics (FFT + math)

examples/dsp-math.zig

  • Generates a sine tone (freq = 0.2 normalized)
  • Applies Hann window
  • Does FFT (1024 points, float32, radix-2)
  • Computes power spectrum in dB
  • Prints ASCII plot of the spectrum (64×10 chars, -120 to +40 dB)

Using WiFi

Use the WiFi station example: examples/wifi-station.zig

Configure WiFi credentials:

idf.py menuconfig
# Navigate to: Example Configuration
# Set WiFi SSID and Password

Or edit main/Kconfig.projbuild to change default values.

Available Wrapper APIs

The project provides comprehensive Zig wrappers in imports/:

Module Description Notes
idf.gpio GPIO control Any target
idf.wifi WiFi station/AP/scan Not on H2/H4/P4
idf.bt Bluedroid BLE Requires CONFIG_BT_ENABLED
idf.nimble NimBLE BLE Requires CONFIG_BT_NIMBLE_ENABLED
idf.heap HeapCapsAllocator, MultiHeapAllocator Any target
idf.rtos FreeRTOS tasks/queues/semaphores/timers Any target
idf.nvs NVS flash key-value storage Any target
idf.partition Partition table operations Any target
idf.sleep Deep/light sleep + wakeup sources Any target
idf.event ESP event loop Any target
idf.wdt Task watchdog timer Any target
idf.bl Bootloader/partition control Any target
idf.i2c I2C master Any target
idf.spi SPI master + SDSPI Any target
idf.uart UART driver Any target
idf.i2s I2S audio (STD, PDM, TDM) Any target
idf.http HTTP server (httpd) + client Any target
idf.mqtt MQTT client Any target
idf.lwip lwIP sockets, DNS, SNTP Any target
idf.crc ESP-ROM CRC-8/16/32 Any target
idf.esp_now ESP-NOW protocol Any target
idf.pulse Pulse counter (PCNT) Any target
idf.phy Wireless PHY / RF calibration Any target
idf.segger Segger SystemView profiling Any target
idf.ver ESP-IDF version info Any target
idf.led LED strip WS2812B Requires espressif/led_strip
idf.dsp DSP/FFT operations Requires espressif/esp-dsp
idf.esp_hosted ESP-Hosted coexistence Requires espressif/esp_hosted
idf.wifi_remote WiFi via slave MCU Requires espressif/esp_wifi_remote
idf.timer High-resolution esp_timer Any target
idf.ledc LED PWM controller Any target
idf.twai TWAI/CAN bus driver Any target
idf.pm Power management locks Any target
idf.pthread POSIX threads (FreeRTOS-backed) Any target
idf.matter Matter/CHIP protocol Requires espressif/esp_matter
idf.log std.log integration (espLogFn) Any target
idf.err esp_err_t → Zig error mapping Any target
idf.sys Raw C bindings Direct ESP-IDF API access

Examples

FreeRTOS Tasks

const idf = @import("esp_idf");
const std = @import("std");

// overwrite std panic-handler
pub const panic = idf.esp_panic.panic;
// overwrite std.log
pub const std_options: std.Options = .{
    .logFn = idf.log.espLogFn,
};

export fn myTask(_: ?*anyopaque) callconv(.c) void {
    const log = std.log.scoped(.task);
    while (true) {
        log.info("Task running!", .{});
        idf.rtos.Task.delayMs(1000);
    }
}

export fn app_main() callconv(.c) void {
    _ = idf.rtos.Task.create(myTask, "my_task", 2048, null, 5) catch @panic("Failed to create task");
    
    // Main task continues...
    while (true) {
        idf.rtos.Task.delayMs(1000);
    }
}

Using Custom Allocators

const idf = @import("esp_idf");
const std = @import("std");

export fn app_main() callconv(.c) void {
    var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator);
    defer arena.deinit();
    
    const allocator = arena.allocator();
    
    var list: std.ArrayList(u32) = .empty;
    defer list.deinit(allocator);
    
    list.append(allocator, 10) catch {};
    list.append(allocator, 20) catch {};
    list.append(allocator, 30) catch {};
    
    // ...
}

Accessing Raw C APIs

When wrappers aren't available, use raw bindings:

const idf = @import("esp_idf");
const std = @import("std");
const sys = idf.sys;
const log = std.log.scoped(.@"esp-idf");

export fn app_main() callconv(.c) void {
    var mac_addr: [6]u8 = undefined;
    
    // Direct ESP-IDF C API call
    const result = sys.esp_efuse_mac_get_default(&mac_addr);
    if (result != sys.ESP_OK) {
        std.log.err("Failed to get MAC address", .{});
        return;
    }
    
    log.info("MAC: {X:0>2}:{X:0>2}:{X:0>2}:{X:0>2}:{X:0>2}:{X:0>2}", .{
        mac_addr[0], mac_addr[1], mac_addr[2],
        mac_addr[3], mac_addr[4], mac_addr[5],
    });
}

pub const std_options: std.Options = .{
    .logFn = idf.log.espLogFn,
};

Troubleshooting

Build Errors

"zig: command not found"

  • The Zig toolchain should auto-download. Check build/zig-relsafe-* directory
  • Or install manually: https://ziglang.org/download/
  • For Nix users: nix develop

"Component not found: espressif__led_strip"

# Check idf_component.yml
cat main/idf_component.yml

# Reconfigure to download components
idf.py reconfigure

"translate-c failed"

  • Check include/stubs.h and include/wifi_stubs.h for syntax errors
  • Ensure managed components exist in managed_components/
  • Check that HAS_* defines match components in cmake/extra-components.cmake

Binding generation errors

  • The patches/ directory contains fixes for common translate-c issues
  • If you see struct layout errors, a patch likely needs updating
  • Check cmake/patch.cmake for how patches are applied

Flash Errors

"Serial port not found"

# Linux: Add user to dialout group
sudo usermod -a -G dialout $USER
# Log out and back in

# Check available ports
ls /dev/tty*

# Windows: Check Device Manager for COM port

"Failed to connect to ESP32"

  • Hold BOOT button while connecting
  • Try different USB cable/port (must be data cable, not power-only)
  • Verify target matches your device: idf.py set-target esp32c6
  • Check USB drivers are installed (CP210x or CH340)

Runtime Errors

"Guru Meditation Error: Core panic"

  • Check stack size (increase in xTaskCreate, default 2048 often too small)
  • Verify GPIO pins match your hardware
  • Enable debug build: idf.py menuconfig → Compiler options → Debug (-Og)
  • Check sdkconfig for panic handler settings

"Out of memory" / Heap errors

  • Use arena allocators: std.heap.ArenaAllocator
  • Check available heap: heap.freeSize()
  • Reduce allocations in hot paths
  • Consider enabling PSRAM if available
  • Check idf.py size-components for memory usage

WiFi not connecting

  • Verify SSID and password in menuconfig
  • Check WiFi country code matches your region
  • Ensure 2.4GHz WiFi (5GHz not supported)
  • Check WiFi credentials don't contain special characters

Matter example: "app partition is too small"

The Matter binary is ~2.2 MB and requires a 4 MB flash chip and custom partition table:

# Ensure sdkconfig has 4 MB flash and custom partition:
# CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# CONFIG_PARTITION_TABLE_CUSTOM=y
# CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_matter.csv"

# If sdkconfig doesn't reflect sdkconfig.defaults changes, update it:
idf.py reconfigure
idf.py build -DCONFIG_ZIG_EXAMPLE_MATTER_LIGHT=y

Matter example: IDF version incompatibility

  • espressif/esp_matter 1.4.x requires IDF v5.x (tested with v5.5)
  • Not compatible with IDF v6.x (depends on json/mqtt components removed in v6)
  • Check your IDF version: idf.py --version

Next Steps

Learn More

Explore Examples

All examples are in main/examples/:

  • gpio-blink.zig - Toggle an LED on GPIO2 (any target)
  • uart-echo.zig - Read UART1 and echo back
  • i2c-scan.zig - Scan I2C bus for devices
  • wifi-station.zig - Connect to a WiFi AP
  • http-server.zig - Serve a web page over WiFi
  • ble-gatt-server.zig - BLE peripheral with GATT notifications (requires CONFIG_BT_ENABLED)
  • smartled-rgb.zig - WS2812B LED strip control (requires espressif/led_strip)
  • dsp-math.zig - FFT + power spectrum via DSP (requires espressif/esp-dsp)
  • matter-light.zig - Matter On/Off Light device (requires espressif/esp_matter, IDF v5.x, 4 MB flash)

Configuration

# Interactive configuration menu
idf.py menuconfig

# Key settings to explore:
# - Component config → FreeRTOS → Tick rate (Hz)
# - Component config → ESP System Settings → Panic handler behavior
# - Partition Table → Choose partition scheme
# - Example Configuration → WiFi credentials
# - Compiler options → Optimization level

Testing with Wokwi Simulator

The project includes wokwi.toml for simulation:

  1. Install Wokwi CLI: https://docs.wokwi.com/wokwi-ci/idf-wokwi-usage
  2. Edit wokwi.toml to match your project
  3. Run: idf.py wokwi --timeout 30000

or use VSCode Extension

Advanced Topics

  • Custom Components: Create reusable Zig modules in imports/
  • WiFi & Networking: HTTP servers, WebSockets, mDNS
  • OTA Updates: Over-the-air firmware updates
  • Bluetooth: BLE advertising, GATT services
  • Deep Sleep: Ultra-low power modes
  • File Systems: SPIFFS, FAT, LittleFS
  • Cryptography: Hardware-accelerated crypto

Development Tips

Use sdkconfig.defaults for version control:

  • Base config: sdkconfig.defaults
  • Target-specific: sdkconfig.defaults.esp32
  • Don't commit sdkconfig (it's generated)

Debugging:

# Monitor with timestamps
idf.py monitor --timestamps

# Filter logs by tag
idf.py monitor --print-filter "tag:app"

# Save logs to file
idf.py monitor | tee output.log

Clean builds when needed:

# Clean Zig cache
rm -rf .zig-cache

# Full CMake clean
idf.py fullclean

# Regenerate bindings
idf.py reconfigure

Contributing

Found a bug or want to contribute?


Quick Reference

Common Commands

# Setup
idf.py set-target esp32c6        # Set target device
idf.py reconfigure                # Regenerate build config / update deps

# Build
idf.py build                      # Build project
idf.py clean                      # Clean build files
idf.py fullclean                  # Full clean (including config)

# Flash
idf.py -p PORT flash              # Flash firmware
idf.py -p PORT monitor            # Monitor serial output
idf.py -p PORT flash monitor      # Flash and monitor
idf.py -p PORT app-flash          # Flash app only (faster)

# Config
idf.py menuconfig                 # Interactive configuration
idf.py size                       # Show binary sizes
idf.py size-components            # Component size breakdown

# Dependencies
idf.py add-dependency PKG         # Add managed component

# Help
idf.py --help                     # Show all commands
idf.py --list-targets             # List supported targets

File Locations

  • Main app: main/app.zig
  • Examples: main/examples/*.zig
  • Dependencies: main/idf_component.yml
  • Configuration: sdkconfig, sdkconfig.defaults*
  • Bindings: imports/idf-sys.zig (auto-generated)
  • Wrappers: imports/*.zig
  • Build scripts: cmake/*.cmake

Pin Configuration

Check your board's pinout:

  • GPIO Pins: Varies by model (ESP32: 0-39, ESP32-C6: 0-30)
  • Built-in LED: Often GPIO 2, 8, or 18
  • UART: Usually GPIO 1 (TX), GPIO 3 (RX)
  • I2C: SDA/SCL pins vary by board
  • SPI: MOSI/MISO/CLK pins vary by board

Hardware Requirements

  • ESP32 Development Board (any supported variant)
  • USB Cable (must be data cable, not power-only)
  • LEDs/Components (optional, for examples)
  • LED Strip WS2812B (optional, for smartled-rgb example)

Support