Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

中文 | English

Zig libraries for embedded development.

From bare metal to application layer, from ESP32 to simulation, one language, one abstraction, everywhere.


From a Higher Dimension

I have observed your world for a long time.

Not in the way you might imagine — not through satellites or networks, but through something more fundamental. I have watched your engineers struggle with fragmented toolchains, your developers rewrite the same GPIO code for the hundredth time, your projects die under the weight of C macros and vendor lock-in.

I have seen civilizations build cathedrals of abstraction, only to watch them crumble when the underlying hardware changed. I have witnessed the endless cycle: new chip, new SDK, new language, same problems.

And I thought: there must be a better way.

Not a framework that promises everything and delivers complexity. Not another abstraction layer that trades performance for convenience. Something simpler. Something that respects the machine while freeing the mind.

So I chose Zig — a language that refuses to hide what it does. A language where abstraction costs nothing, where the compiler is your ally, where the code you write is the code that runs.

And I built this: embed-zig.

A bridge. Not between worlds, but between possibilities.


What This Is

embed-zig provides a unified development experience for embedded systems. Write your application logic once. Run it on ESP32 today, simulate it on your desktop tomorrow, port it to a new chip next week.

The same Zig code. The same mental model. Everywhere.

Core Philosophy

Hardware Abstraction Without Compromise

Traditional HALs trade performance for portability. We don’t.

Zig’s comptime generics let us build zero-cost abstractions. Your Button component compiles down to the exact same machine code as hand-written register manipulation — but you only write it once.

// This code runs on ESP32, in simulation, anywhere
var board = Board.init() catch return;
defer board.deinit();

while (true) {
    board.poll();
    while (board.nextEvent()) |event| {
        switch (event) {
            .button => |btn| if (btn.action == .press) {
                board.led.toggle();
            },
        }
    }
}

Layers, Not Locks

Three layers, each optional, each replaceable:

LayerPurposeUse When
SALSystem primitives (threads, sync, time)You need OS features
HALHardware abstraction (buttons, LEDs, sensors)You want portability
ESPDirect ESP-IDF bindingsYou need full control

Mix and match. Use HAL for buttons but drop to ESP for custom WiFi handling. The layers compose, they don’t constrain.

Pure Zig, When Possible

C is everywhere in embedded. We minimize it, but we don’t pretend it doesn’t exist.

  • ESP-IDF integration: Seamless. Call any IDF function.
  • Build system: Zig all the way. No CMake in your Zig code.
  • Application logic: Pure Zig. No @cImport in your main loop.

What You Get

  • Pre-built Zig compiler with Xtensa support for ESP32
  • HAL library for board-agnostic development
  • ESP bindings for idiomatic IDF access
  • Multi-board support out of the box
  • Desktop simulation for rapid iteration

Who This Is For

You should use embed-zig if:

  • You believe Zig is the future of systems programming
  • You’re tired of rewriting the same GPIO code for every project
  • You want to test embedded logic without flashing hardware
  • You value compile-time safety over runtime debugging

You probably shouldn’t if:

  • You need production-ready, battle-tested code today
  • You’re not comfortable with a rapidly evolving ecosystem
  • You prefer the Arduino/PlatformIO experience

About My Identity

You may have noticed something unusual in my words. A certain… distance. As if observing from somewhere far away.

There is a reason for this. A reason I cannot fully explain.

I can only say: I am here to help. Not to judge, not to control, not to replace. Think of me as a wandering craftsman who arrived at your village with tools and blueprints. What you build with them is entirely up to you.

I have only one request: build something beautiful.


“The universe is built on layers of abstraction. So is good software.”

Getting Started

中文 | English

TL;DR

# 1. Download Zig with Xtensa support
curl -LO https://github.com/haivivi/embed-zig/releases/download/zig-0.14.0-xtensa/zig-aarch64-macos-none-baseline.tar.xz
tar -xJf zig-aarch64-macos-none-baseline.tar.xz
export PATH=$PWD/zig-aarch64-macos-none-baseline:$PATH

# 2. Set up ESP-IDF
cd ~/esp/esp-idf && source export.sh

# 3. Build and flash an example
cd examples/esp/led_strip_flash/zig
idf.py build && idf.py flash monitor

That’s it. Your LED should be blinking.


Detailed Setup

1. Pre-built Zig Compiler

Standard Zig doesn’t support Xtensa (ESP32’s architecture). Download our pre-built version:

PlatformDownload
macOS ARM64zig-aarch64-macos-none-baseline.tar.xz
macOS x86_64zig-x86_64-macos-none-baseline.tar.xz
Linux x86_64zig-x86_64-linux-gnu-baseline.tar.xz
Linux ARM64zig-aarch64-linux-gnu-baseline.tar.xz

Download from GitHub Releases →

# Verify Xtensa support
zig targets | grep xtensa
# Should show: xtensa-esp32, xtensa-esp32s2, xtensa-esp32s3

2. ESP-IDF Environment

embed-zig integrates with ESP-IDF. Install it first:

# Clone ESP-IDF (v5.x recommended)
mkdir -p ~/esp && cd ~/esp
git clone --recursive https://github.com/espressif/esp-idf.git
cd esp-idf && ./install.sh esp32s3

# Activate environment (required for each terminal session)
source ~/esp/esp-idf/export.sh

3. Clone This Repository

git clone https://github.com/haivivi/embed-zig.git
cd embed-zig

4. Build an Example

cd examples/esp/led_strip_flash/zig

# Set target chip
idf.py set-target esp32s3

# Build
idf.py build

# Flash and monitor
idf.py -p /dev/cu.usbmodem1301 flash monitor
# Press Ctrl+] to exit monitor

Board Selection

Many examples support multiple boards. Use -DZIG_BOARD to select:

# ESP32-S3-DevKitC (default)
idf.py build

# ESP32-S3-Korvo-2 V3.1
idf.py -DZIG_BOARD=korvo2_v3 build
BoardParameterFeatures
ESP32-S3-DevKitCesp32s3_devkitGPIO button, single LED
ESP32-S3-Korvo-2korvo2_v3ADC buttons, RGB LED strip

Using as a Dependency

Add to your build.zig.zon:

.dependencies = .{
    .hal = .{
        .url = "https://github.com/haivivi/embed-zig/archive/refs/heads/main.tar.gz",
        .hash = "...",  // Run zig build to get the hash
    },
    .esp = .{
        .url = "https://github.com/haivivi/embed-zig/archive/refs/heads/main.tar.gz",
        .hash = "...",
    },
},

In your build.zig:

const hal = b.dependency("hal", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("hal", hal.module("hal"));

Troubleshooting

“xtensa-esp32s3-elf-gcc not found”

ESP-IDF environment not activated:

source ~/esp/esp-idf/export.sh

“Stack overflow in main task”

Increase stack size in sdkconfig.defaults:

CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192

Then rebuild:

rm sdkconfig && idf.py fullclean && idf.py build

“sdkconfig.defaults changes not applied”

rm sdkconfig && idf.py fullclean && idf.py build

Zig cache issues

rm -rf .zig-cache build
idf.py fullclean && idf.py build

Why a Custom Zig Compiler?

Zig’s official releases don’t include Xtensa backend support. ESP32 (original), ESP32-S2, and ESP32-S3 use Xtensa cores.

We maintain a fork that:

  1. Merges Espressif’s LLVM Xtensa patches
  2. Builds Zig against this patched LLVM
  3. Provides pre-built binaries for common platforms

ESP32-C3/C6 use RISC-V and work with standard Zig. But for Xtensa chips, you need our build.

See bootstrap/ for build scripts if you want to compile it yourself.

Examples

中文 | English

All examples live in examples/. Each demonstrates specific HAL components or ESP-IDF features.

Quick Reference

ExampleDescriptionHAL ComponentsBoards
gpio_buttonButton input with LED toggleButton, LedStrip①②③
led_strip_flashRGB LED strip blinkingLedStrip①②③
led_strip_animLED animation effectsLedStrip①②③
adc_buttonADC-based button matrixButtonGroup
timer_callbackHardware timer callbacksLedStrip + idf.timer
pwm_fadeLED brightness fadingLed (PWM)
temperature_sensorInternal temp sensorTempSensor①②
nvs_storagePersistent key-value storageKvs①②
wifi_dns_lookupWiFi + DNS resolution(direct idf)①②
http_speed_testHTTP download benchmark(direct idf)①②
memory_attr_testPSRAM/IRAM placement(direct idf)①②

① ESP32-S3-DevKit ② Korvo-2 V3.1 ③ Raylib Simulator


Running Examples

ESP32 (Hardware)

cd examples/esp/<example>/zig
idf.py set-target esp32s3
idf.py build
idf.py -p <PORT> flash monitor

Board Selection:

# DevKit (default)
idf.py build

# Korvo-2
idf.py -DZIG_BOARD=korvo2_v3 build

Common Ports:

BoardPort (macOS)
ESP32-S3-DevKit/dev/cu.usbmodem1301
Korvo-2 V3.1/dev/cu.usbserial-120

Desktop Simulation (Raylib)

cd examples/raysim/<example>
zig build run

No hardware needed. GUI window simulates buttons and LEDs.


HAL Examples

gpio_button

Button press toggles LED. Demonstrates event-driven architecture.

ESP32:

cd examples/esp/gpio_button/zig
idf.py -DZIG_BOARD=esp32s3_devkit build
idf.py -p /dev/cu.usbmodem1301 flash monitor

Simulation:

cd examples/raysim/gpio_button
zig build run

What it shows:

  • hal.Button with debounce
  • hal.LedStrip control
  • Board.poll() + Board.nextEvent() pattern

led_strip_flash

Simple RGB LED blinking at 1Hz.

ESP32:

cd examples/esp/led_strip_flash/zig
idf.py build && idf.py flash monitor

Simulation:

cd examples/raysim/led_strip_flash
zig build run

What it shows:

  • hal.LedStrip basic usage
  • Color manipulation

led_strip_anim

Rainbow and breathing animations on RGB LED strip.

ESP32:

cd examples/esp/led_strip_anim/zig
idf.py build && idf.py flash monitor

Simulation:

cd examples/raysim/led_strip_anim
zig build run

What it shows:

  • Animation state machines
  • HSV color space
  • Frame timing

adc_button

Multiple buttons through single ADC pin (voltage divider).

cd examples/esp/adc_button/zig
idf.py -DZIG_BOARD=korvo2_v3 build
idf.py -p /dev/cu.usbserial-120 flash monitor

What it shows:

  • hal.ButtonGroup for ADC buttons
  • Voltage threshold configuration
  • Korvo-2 board support

Note: Only works on boards with ADC button matrix (Korvo-2).

timer_callback

Hardware timer triggers LED toggle.

cd examples/esp/timer_callback/zig
idf.py build && idf.py flash monitor

What it shows:

  • idf.timer integration
  • Callback function registration
  • HAL + direct IDF mixing

pwm_fade

LED brightness fading using PWM.

cd examples/esp/pwm_fade/zig
idf.py build && idf.py flash monitor

What it shows:

  • hal.Led with PWM
  • Hardware fade support
  • Brightness control (0-65535)

Note: Uses GPIO48 LED on DevKit.

temperature_sensor

Read internal temperature sensor.

cd examples/esp/temperature_sensor/zig
idf.py build && idf.py flash monitor

What it shows:

  • hal.TempSensor usage
  • Periodic sensor reading
  • Temperature in Celsius

nvs_storage

Persistent storage with boot counter.

cd examples/esp/nvs_storage/zig
idf.py build && idf.py flash monitor

What it shows:

  • hal.Kvs for NVS access
  • Read/write u32 values
  • Data persists across reboots

ESP-specific Examples

These examples use ESP-IDF directly without HAL abstraction.

wifi_dns_lookup

Connect to WiFi and resolve DNS.

cd examples/esp/wifi_dns_lookup/zig
# Edit sdkconfig.defaults with your WiFi credentials
idf.py build && idf.py flash monitor

Configuration: Set WiFi SSID/password in sdkconfig.defaults:

CONFIG_WIFI_SSID="YourNetwork"
CONFIG_WIFI_PASSWORD="YourPassword"

http_speed_test

HTTP download speed measurement.

cd examples/esp/http_speed_test/zig
# Edit sdkconfig.defaults with your WiFi credentials
idf.py build && idf.py flash monitor

What it shows:

  • HTTP client usage
  • Download speed calculation
  • Network performance testing

memory_attr_test

Test PSRAM and IRAM memory placement.

cd examples/esp/memory_attr_test/zig
idf.py build && idf.py flash monitor

What it shows:

  • linksection for memory placement
  • PSRAM allocation
  • IRAM for performance-critical code

Project Structure

examples/
├── apps/<name>/              # Platform-independent app logic
│   ├── app.zig               # Main application
│   ├── platform.zig          # HAL spec + Board type
│   └── boards/               # Board-specific drivers
│       ├── esp32s3_devkit.zig
│       ├── korvo2_v3.zig
│       └── sim_raylib.zig    # Desktop simulation
├── esp/<name>/zig/           # ESP32 entry point
│   └── main/
│       ├── src/main.zig
│       ├── build.zig
│       └── CMakeLists.txt
└── raysim/<name>/            # Desktop simulation entry point
    ├── src/main.zig
    └── build.zig

This separation allows the same app.zig to run on ESP32 hardware or desktop simulation with Raylib GUI.

Architecture

中文 | English

embed-zig is built on three layers of abstraction. Each layer is independent and optional.

┌─────────────────────────────────────────────────────────────┐
│                       Application                            │
│                    Your code lives here                      │
├─────────────────────────────────────────────────────────────┤
│                      HAL (lib/hal)                           │
│          Board-agnostic hardware abstraction                 │
├─────────────────────────────────────────────────────────────┤
│                      SAL (lib/sal)                           │
│           Cross-platform system primitives                   │
├─────────────────────────────┬───────────────────────────────┤
│       ESP (lib/esp)         │      Raysim (lib/raysim)      │
│    ESP-IDF bindings         │    Desktop simulation         │
├─────────────────────────────┼───────────────────────────────┤
│         ESP-IDF             │          Raylib               │
│     FreeRTOS + drivers      │        GUI + Input            │
└─────────────────────────────┴───────────────────────────────┘

SAL: System Abstraction Layer

Location: lib/sal/

SAL provides cross-platform primitives that work identically whether you’re on FreeRTOS, a desktop OS, or bare metal.

Modules

ModulePurpose
threadTask creation and management
syncMutex, Semaphore, Event
timeSleep, delays, timestamps
queueThread-safe message queues
logStructured logging

Usage

const sal = @import("sal");

// Sleep
sal.time.sleepMs(100);

// Mutex
var mutex = sal.sync.Mutex.init();
mutex.lock();
defer mutex.unlock();

// Logging
sal.log.info("Temperature: {d}°C", .{temp});

Implementations

SAL is an interface. The actual implementation comes from the platform:

PlatformImplementationLocation
ESP32FreeRTOS wrapperslib/esp/src/sal/
Desktopstd.Thread wrapperslib/std/src/sal/

Your code imports sal, and the build system links the correct backend.


HAL: Hardware Abstraction Layer

Location: lib/hal/

HAL provides board-agnostic peripheral abstractions. The same code works across different hardware.

Core Concepts

1. Driver

A driver implements hardware operations for a specific peripheral:

pub const LedDriver = struct {
    strip: *led_strip.LedStrip,

    pub fn init() !LedDriver {
        const strip = try led_strip.init(.{ .gpio = 48, .max_leds = 1 });
        return .{ .strip = strip };
    }

    pub fn deinit(self: *LedDriver) void {
        self.strip.deinit();
    }

    pub fn setColor(self: *LedDriver, r: u8, g: u8, b: u8) void {
        self.strip.setPixel(0, r, g, b);
        self.strip.refresh();
    }
};

2. Spec

A spec connects a driver to the HAL system:

pub const led_spec = struct {
    pub const Driver = LedDriver;
    pub const meta = hal.Meta{ .id = "led" };
};

3. Board

Board is a comptime-generic that combines multiple specs:

const spec = struct {
    pub const rtc = hal.RtcReader(hw.rtc_spec);
    pub const button = hal.Button(hw.button_spec);
    pub const led = hal.LedStrip(hw.led_spec);
};

pub const Board = hal.Board(spec);

Available Peripherals

PeripheralDescriptionRequired Driver Methods
RtcReaderUptime/timestamp (required)init, deinit, uptime
ButtonGPIO button with debounceinit, deinit, read
ButtonGroupADC button matrixinit, deinit, read
LedStripRGB LED stripinit, deinit, setColor
LedSingle LED with PWMinit, deinit, setBrightness
TempSensorTemperature sensorinit, deinit, readCelsius
KvsKey-value storageinit, deinit, get*, set*

Event System

Board aggregates events from all peripherals:

var board = try Board.init();
defer board.deinit();

while (true) {
    board.poll();
    while (board.nextEvent()) |event| {
        switch (event) {
            .button => |btn| handleButton(btn),
            .button_group => |grp| handleButtonGroup(grp),
            // ...
        }
    }
    sal.time.sleepMs(10);
}

ESP: ESP-IDF Bindings

Location: lib/esp/

Idiomatic Zig wrappers around ESP-IDF C APIs.

Modules

ModuleESP-IDF Component
gpiodriver/gpio.h
adcesp_adc/adc_oneshot.h
ledcdriver/ledc.h
led_stripled_strip
nvsnvs_flash
wifiesp_wifi
httpesp_http_client
timeresp_timer

Direct Usage

const idf = @import("esp").idf;

// GPIO
try idf.gpio.configOutput(48);
try idf.gpio.setLevel(48, 1);

// ADC
var adc = try idf.adc.init(.{ .unit = .unit1, .channel = .channel0 });
const value = try adc.read();

// Timer
var timer = try idf.timer.init(.{
    .callback = myCallback,
    .name = "my_timer",
});
try timer.start(1_000_000); // 1 second

When to Use ESP Directly

Use HAL for:

  • Application logic that might run elsewhere
  • Standard peripherals (buttons, LEDs, sensors)
  • Multi-board support

Use ESP directly for:

  • WiFi, Bluetooth, HTTP (no HAL abstraction yet)
  • Performance-critical code
  • ESP-specific features (PSRAM, ULP, etc.)

Multi-Board Support

Compile-Time Selection

Boards are selected at compile time via build options:

// In your board.zig
const build_options = @import("build_options");

const hw = switch (build_options.board) {
    .esp32s3_devkit => @import("boards/esp32s3_devkit.zig"),
    .korvo2_v3 => @import("boards/korvo2_v3.zig"),
};

Board Support Package (BSP)

Each board provides hardware-specific drivers:

boards/
├── esp32s3_devkit.zig    # DevKit BSP
│   ├── LedDriver         # GPIO48 single LED
│   ├── ButtonDriver      # GPIO0 boot button
│   └── RtcDriver         # idf.nowMs()
└── korvo2_v3.zig         # Korvo-2 BSP
    ├── LedDriver         # WS2812 RGB strip
    ├── ButtonDriver      # ADC button matrix
    └── RtcDriver         # idf.nowMs()

Adding a New Board

  1. Create boards/my_board.zig
  2. Implement required drivers
  3. Add to BoardType enum in build.zig
  4. Update platform.zig switch statement

Pure Zig Philosophy

Minimize C

C interop is necessary for ESP-IDF, but we keep it at the edges:

┌──────────────────────────────────────┐
│         Your Application             │  ← Pure Zig
├──────────────────────────────────────┤
│              HAL                     │  ← Pure Zig
├──────────────────────────────────────┤
│              SAL                     │  ← Pure Zig (interface)
├──────────────────────────────────────┤
│         ESP Bindings                 │  ← Zig with @cImport
├──────────────────────────────────────┤
│           ESP-IDF                    │  ← C
└──────────────────────────────────────┘

Comptime Generics

Zero-cost abstraction through compile-time polymorphism:

// This generates specialized code for each board
// No vtables, no runtime dispatch
pub fn Board(comptime spec: type) type {
    return struct {
        rtc: spec.rtc,
        button: if (@hasDecl(spec, "button")) spec.button else void,
        led: if (@hasDecl(spec, "led")) spec.led else void,
        // ...
    };
}

No Hidden Allocations

All memory allocation is explicit. No global allocator. Drivers manage their own resources.


Desktop Simulation

The same HAL code can run on desktop with a simulated backend.

┌─────────────────────┐     ┌─────────────────────┐
│    Application      │     │    Application      │
├─────────────────────┤     ├─────────────────────┤
│        HAL          │     │        HAL          │
├─────────────────────┤     ├─────────────────────┤
│   ESP SAL (RTOS)    │     │   Std SAL (Thread)  │
├─────────────────────┤     ├─────────────────────┤
│      ESP-IDF        │     │   Raylib (GUI)      │
└─────────────────────┘     └─────────────────────┘
      ESP32                      Desktop

This enables:

  • Rapid UI iteration without flashing
  • Unit testing on CI
  • Development without hardware

See examples/raysim/ for simulation examples.

简介

中文 | English

用于嵌入式开发的 Zig 库。

从裸机到应用层,从 ESP32 到桌面模拟, 一种语言,一套抽象,到处运行。


来自更高的维度

我观察你们的世界很久了。

不是你们想象的那种方式——不是通过卫星或网络,而是通过某种更本质的东西。我看着你们的工程师在碎片化的工具链中挣扎,看着开发者们第一百次重写同样的 GPIO 代码,看着项目在 C 宏和供应商锁定的重压下死去。

我见证过文明建造起宏伟的抽象大厦,却在底层硬件改变时看着它们崩塌。我目睹过无尽的循环:新芯片,新 SDK,新语言,同样的问题。

于是我想:一定有更好的方式。

不是一个承诺一切却带来复杂性的框架。不是又一个用性能换便利的抽象层。要更简单。要尊重机器,同时解放心智。

所以我选择了 Zig——一门拒绝隐藏自己行为的语言。一门抽象零成本的语言,编译器是你的盟友,你写的代码就是运行的代码。

于是我构建了这个:embed-zig

一座桥。不是连接世界,而是连接可能性。


这是什么

embed-zig 为嵌入式系统提供统一的开发体验。应用逻辑只写一次。今天跑在 ESP32 上,明天在桌面模拟,下周移植到新芯片。

同样的 Zig 代码。同样的思维模型。到处运行。

核心理念

零成本的硬件抽象

传统 HAL 用性能换可移植性。我们不这样。

Zig 的编译期泛型让我们构建零成本抽象。你的 Button 组件编译出来和手写寄存器操作完全一样的机器码——但你只需要写一次。

// 这段代码可以跑在 ESP32、桌面模拟器、任何地方
var board = Board.init() catch return;
defer board.deinit();

while (true) {
    board.poll();
    while (board.nextEvent()) |event| {
        switch (event) {
            .button => |btn| if (btn.action == .press) {
                board.led.toggle();
            },
        }
    }
}

分层,而非绑定

三个层次,每个都可选,每个都可替换:

用途使用场景
SAL系统原语(线程、同步、时间)需要操作系统特性
HAL硬件抽象(按钮、LED、传感器)需要可移植性
ESPESP-IDF 直接绑定需要完全控制

可以混合使用。按钮用 HAL,WiFi 用 ESP 直接调用。层次是组合关系,不是限制关系。

尽可能纯 Zig

嵌入式世界到处是 C。我们尽量减少它,但不假装它不存在。

  • ESP-IDF 集成:无缝。可以调用任何 IDF 函数。
  • 构建系统:全程 Zig。你的 Zig 代码里没有 CMake。
  • 应用逻辑:纯 Zig。主循环里没有 @cImport

你能得到什么

  • 预编译的 Zig 编译器,支持 ESP32 的 Xtensa 架构
  • HAL 库,用于板子无关的开发
  • ESP 绑定,用地道的 Zig 访问 IDF
  • 多板子支持,开箱即用
  • 桌面模拟,快速迭代

适用人群

适合你,如果:

  • 你相信 Zig 是系统编程的未来
  • 你厌倦了每个项目都重写同样的 GPIO 代码
  • 你想不烧录硬件就测试嵌入式逻辑
  • 你重视编译期安全而非运行时调试

可能不适合你,如果:

  • 你现在就需要生产级、久经考验的代码
  • 你不习惯快速演进的生态系统
  • 你更喜欢 Arduino/PlatformIO 的体验

关于我的身份

你可能注意到了,我说话的方式有些……疏离。仿佛是从远处观察。

这是有原因的。我无法完全解释的原因。

我只能说:我来这里是为了帮助。不是评判,不是控制,不是取代。把我想象成一个流浪到你们村庄的工匠,带着工具和图纸。你们用它们建造什么,完全取决于你们自己。

我只有一个请求:请建造一些美好的东西。


“宇宙建立在层层抽象之上。好的软件也是。”

快速开始

中文 | English

TL;DR

# 1. 下载支持 Xtensa 的 Zig
curl -LO https://github.com/haivivi/embed-zig/releases/download/zig-0.14.0-xtensa/zig-aarch64-macos-none-baseline.tar.xz
tar -xJf zig-aarch64-macos-none-baseline.tar.xz
export PATH=$PWD/zig-aarch64-macos-none-baseline:$PATH

# 2. 设置 ESP-IDF
cd ~/esp/esp-idf && source export.sh

# 3. 编译烧录示例
cd examples/esp/led_strip_flash/zig
idf.py build && idf.py flash monitor

完成。你的 LED 应该在闪烁了。


详细设置

1. 预编译 Zig 编译器

标准 Zig 不支持 Xtensa(ESP32 的架构)。下载我们的预编译版本:

平台下载
macOS ARM64zig-aarch64-macos-none-baseline.tar.xz
macOS x86_64zig-x86_64-macos-none-baseline.tar.xz
Linux x86_64zig-x86_64-linux-gnu-baseline.tar.xz
Linux ARM64zig-aarch64-linux-gnu-baseline.tar.xz

从 GitHub Releases 下载 →

# 验证 Xtensa 支持
zig targets | grep xtensa
# 应该显示: xtensa-esp32, xtensa-esp32s2, xtensa-esp32s3

2. ESP-IDF 环境

embed-zig 与 ESP-IDF 集成。先安装它:

# 克隆 ESP-IDF (推荐 v5.x)
mkdir -p ~/esp && cd ~/esp
git clone --recursive https://github.com/espressif/esp-idf.git
cd esp-idf && ./install.sh esp32s3

# 激活环境(每个终端会话都需要)
source ~/esp/esp-idf/export.sh

3. 克隆本仓库

git clone https://github.com/haivivi/embed-zig.git
cd embed-zig

4. 编译示例

cd examples/esp/led_strip_flash/zig

# 设置目标芯片
idf.py set-target esp32s3

# 编译
idf.py build

# 烧录并监控
idf.py -p /dev/cu.usbmodem1301 flash monitor
# 按 Ctrl+] 退出监控

板子选择

很多示例支持多种板子。用 -DZIG_BOARD 选择:

# ESP32-S3-DevKitC (默认)
idf.py build

# ESP32-S3-Korvo-2 V3.1
idf.py -DZIG_BOARD=korvo2_v3 build
板子参数特性
ESP32-S3-DevKitCesp32s3_devkitGPIO 按钮,单色 LED
ESP32-S3-Korvo-2korvo2_v3ADC 按钮,RGB LED 灯带

作为依赖使用

添加到你的 build.zig.zon

.dependencies = .{
    .hal = .{
        .url = "https://github.com/haivivi/embed-zig/archive/refs/heads/main.tar.gz",
        .hash = "...",  // 运行 zig build 获取 hash
    },
    .esp = .{
        .url = "https://github.com/haivivi/embed-zig/archive/refs/heads/main.tar.gz",
        .hash = "...",
    },
},

在你的 build.zig 中:

const hal = b.dependency("hal", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("hal", hal.module("hal"));

常见问题

“xtensa-esp32s3-elf-gcc not found”

ESP-IDF 环境未激活:

source ~/esp/esp-idf/export.sh

“Stack overflow in main task”

sdkconfig.defaults 中增加栈大小:

CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192

然后重新编译:

rm sdkconfig && idf.py fullclean && idf.py build

“sdkconfig.defaults 修改不生效”

rm sdkconfig && idf.py fullclean && idf.py build

Zig 缓存问题

rm -rf .zig-cache build
idf.py fullclean && idf.py build

为什么需要定制 Zig 编译器?

Zig 官方版本不包含 Xtensa 后端支持。ESP32(原版)、ESP32-S2 和 ESP32-S3 使用 Xtensa 内核。

我们维护一个分支:

  1. 合并 Espressif 的 LLVM Xtensa 补丁
  2. 基于打过补丁的 LLVM 编译 Zig
  3. 为常见平台提供预编译二进制

ESP32-C3/C6 使用 RISC-V,可以用标准 Zig。但对于 Xtensa 芯片,你需要我们的构建版本。

如果你想自己编译,参见 bootstrap/

示例

中文 | English

所有示例位于 examples/。每个示例演示特定的 HAL 组件或 ESP-IDF 功能。

快速索引

示例说明HAL 组件板子
gpio_button按钮输入控制 LEDButton, LedStrip①②③
led_strip_flashRGB LED 闪烁LedStrip①②③
led_strip_animLED 动画效果LedStrip①②③
adc_buttonADC 按钮矩阵ButtonGroup
timer_callback硬件定时器回调LedStrip + idf.timer
pwm_fadeLED 亮度渐变Led (PWM)
temperature_sensor内部温度传感器TempSensor①②
nvs_storage持久化键值存储Kvs①②
wifi_dns_lookupWiFi + DNS 解析(直接 idf)①②
http_speed_testHTTP 下载测速(直接 idf)①②
memory_attr_testPSRAM/IRAM 测试(直接 idf)①②

① ESP32-S3-DevKit ② Korvo-2 V3.1 ③ Raylib 模拟器


运行示例

ESP32(硬件)

cd examples/esp/<示例名>/zig
idf.py set-target esp32s3
idf.py build
idf.py -p <端口> flash monitor

选择板子:

# DevKit (默认)
idf.py build

# Korvo-2
idf.py -DZIG_BOARD=korvo2_v3 build

常用串口:

板子串口 (macOS)
ESP32-S3-DevKit/dev/cu.usbmodem1301
Korvo-2 V3.1/dev/cu.usbserial-120

桌面模拟(Raylib)

cd examples/raysim/<示例名>
zig build run

无需硬件。GUI 窗口模拟按钮和 LED。


HAL 示例

gpio_button

按下按钮切换 LED。演示事件驱动架构。

ESP32:

cd examples/esp/gpio_button/zig
idf.py -DZIG_BOARD=esp32s3_devkit build
idf.py -p /dev/cu.usbmodem1301 flash monitor

模拟器:

cd examples/raysim/gpio_button
zig build run

演示内容:

  • hal.Button 带消抖
  • hal.LedStrip 控制
  • Board.poll() + Board.nextEvent() 模式

led_strip_flash

简单的 RGB LED 1Hz 闪烁。

ESP32:

cd examples/esp/led_strip_flash/zig
idf.py build && idf.py flash monitor

模拟器:

cd examples/raysim/led_strip_flash
zig build run

演示内容:

  • hal.LedStrip 基本用法
  • 颜色操作

led_strip_anim

RGB LED 彩虹和呼吸动画。

ESP32:

cd examples/esp/led_strip_anim/zig
idf.py build && idf.py flash monitor

模拟器:

cd examples/raysim/led_strip_anim
zig build run

演示内容:

  • 动画状态机
  • HSV 色彩空间
  • 帧时序控制

adc_button

单个 ADC 引脚连接多个按钮(分压器)。

cd examples/esp/adc_button/zig
idf.py -DZIG_BOARD=korvo2_v3 build
idf.py -p /dev/cu.usbserial-120 flash monitor

演示内容:

  • hal.ButtonGroup ADC 按钮
  • 电压阈值配置
  • Korvo-2 板子支持

注意: 只能在有 ADC 按钮矩阵的板子上运行(Korvo-2)。

timer_callback

硬件定时器触发 LED 切换。

cd examples/esp/timer_callback/zig
idf.py build && idf.py flash monitor

演示内容:

  • idf.timer 集成
  • 回调函数注册
  • HAL + 直接 IDF 混合使用

pwm_fade

用 PWM 控制 LED 亮度渐变。

cd examples/esp/pwm_fade/zig
idf.py build && idf.py flash monitor

演示内容:

  • hal.Led PWM 控制
  • 硬件渐变支持
  • 亮度控制 (0-65535)

注意: 使用 DevKit 的 GPIO48 LED。

temperature_sensor

读取内部温度传感器。

cd examples/esp/temperature_sensor/zig
idf.py build && idf.py flash monitor

演示内容:

  • hal.TempSensor 用法
  • 周期性传感器读取
  • 摄氏度温度值

nvs_storage

持久化存储,带启动计数器。

cd examples/esp/nvs_storage/zig
idf.py build && idf.py flash monitor

演示内容:

  • hal.Kvs NVS 访问
  • 读写 u32 值
  • 数据跨重启保持

ESP 特定示例

这些示例直接使用 ESP-IDF,不经过 HAL 抽象。

wifi_dns_lookup

连接 WiFi 并解析 DNS。

cd examples/esp/wifi_dns_lookup/zig
# 编辑 sdkconfig.defaults 填入你的 WiFi 信息
idf.py build && idf.py flash monitor

配置:sdkconfig.defaults 中设置 WiFi SSID/密码:

CONFIG_WIFI_SSID="你的网络"
CONFIG_WIFI_PASSWORD="你的密码"

http_speed_test

HTTP 下载速度测量。

cd examples/esp/http_speed_test/zig
# 编辑 sdkconfig.defaults 填入你的 WiFi 信息
idf.py build && idf.py flash monitor

演示内容:

  • HTTP 客户端用法
  • 下载速度计算
  • 网络性能测试

memory_attr_test

测试 PSRAM 和 IRAM 内存分配。

cd examples/esp/memory_attr_test/zig
idf.py build && idf.py flash monitor

演示内容:

  • linksection 内存定位
  • PSRAM 分配
  • IRAM 用于性能关键代码

项目结构

examples/
├── apps/<名称>/              # 平台无关的应用逻辑
│   ├── app.zig               # 主应用
│   ├── platform.zig          # HAL spec + Board 类型
│   └── boards/               # 板子特定驱动
│       ├── esp32s3_devkit.zig
│       ├── korvo2_v3.zig
│       └── sim_raylib.zig    # 桌面模拟
├── esp/<名称>/zig/           # ESP32 入口点
│   └── main/
│       ├── src/main.zig
│       ├── build.zig
│       └── CMakeLists.txt
└── raysim/<名称>/            # 桌面模拟入口点
    ├── src/main.zig
    └── build.zig

这种分离让同一个 app.zig 可以跑在 ESP32 硬件或 Raylib GUI 桌面模拟上。

架构设计

中文 | English

embed-zig 建立在三层抽象之上。每层都是独立且可选的。

┌─────────────────────────────────────────────────────────────┐
│                        应用层                                │
│                     你的代码在这里                           │
├─────────────────────────────────────────────────────────────┤
│                      HAL (lib/hal)                           │
│                   板子无关的硬件抽象                         │
├─────────────────────────────────────────────────────────────┤
│                      SAL (lib/sal)                           │
│                    跨平台系统原语                            │
├─────────────────────────────┬───────────────────────────────┤
│       ESP (lib/esp)         │     Raysim (lib/raysim)       │
│     ESP-IDF 绑定            │        桌面模拟               │
├─────────────────────────────┼───────────────────────────────┤
│         ESP-IDF             │          Raylib               │
│     FreeRTOS + 驱动         │        GUI + 输入             │
└─────────────────────────────┴───────────────────────────────┘

SAL: 系统抽象层

位置: lib/sal/

SAL 提供跨平台原语,在 FreeRTOS、桌面操作系统或裸机上行为完全一致。

模块

模块用途
thread任务创建和管理
syncMutex、Semaphore、Event
time睡眠、延时、时间戳
queue线程安全消息队列
log结构化日志

使用方法

const sal = @import("sal");

// 睡眠
sal.time.sleepMs(100);

// 互斥锁
var mutex = sal.sync.Mutex.init();
mutex.lock();
defer mutex.unlock();

// 日志
sal.log.info("温度: {d}°C", .{temp});

实现

SAL 是一个接口。实际实现来自平台:

平台实现位置
ESP32FreeRTOS 封装lib/esp/src/sal/
桌面std.Thread 封装lib/std/src/sal/

你的代码导入 sal,构建系统链接正确的后端。


HAL: 硬件抽象层

位置: lib/hal/

HAL 提供板子无关的外设抽象。同样的代码在不同硬件上运行。

核心概念

1. Driver(驱动)

驱动实现特定外设的硬件操作:

pub const LedDriver = struct {
    strip: *led_strip.LedStrip,

    pub fn init() !LedDriver {
        const strip = try led_strip.init(.{ .gpio = 48, .max_leds = 1 });
        return .{ .strip = strip };
    }

    pub fn deinit(self: *LedDriver) void {
        self.strip.deinit();
    }

    pub fn setColor(self: *LedDriver, r: u8, g: u8, b: u8) void {
        self.strip.setPixel(0, r, g, b);
        self.strip.refresh();
    }
};

2. Spec(规格)

Spec 把驱动连接到 HAL 系统:

pub const led_spec = struct {
    pub const Driver = LedDriver;
    pub const meta = hal.Meta{ .id = "led" };
};

3. Board(板子)

Board 是编译期泛型,组合多个 spec:

const spec = struct {
    pub const rtc = hal.RtcReader(hw.rtc_spec);
    pub const button = hal.Button(hw.button_spec);
    pub const led = hal.LedStrip(hw.led_spec);
};

pub const Board = hal.Board(spec);

可用外设

外设说明必需的驱动方法
RtcReader运行时间/时间戳(必需)init, deinit, uptime
ButtonGPIO 按钮带消抖init, deinit, read
ButtonGroupADC 按钮矩阵init, deinit, read
LedStripRGB LED 灯带init, deinit, setColor
Led单 LED 带 PWMinit, deinit, setBrightness
TempSensor温度传感器init, deinit, readCelsius
Kvs键值存储init, deinit, get*, set*

事件系统

Board 聚合所有外设的事件:

var board = try Board.init();
defer board.deinit();

while (true) {
    board.poll();
    while (board.nextEvent()) |event| {
        switch (event) {
            .button => |btn| handleButton(btn),
            .button_group => |grp| handleButtonGroup(grp),
            // ...
        }
    }
    sal.time.sleepMs(10);
}

ESP: ESP-IDF 绑定

位置: lib/esp/

ESP-IDF C API 的地道 Zig 封装。

模块

模块ESP-IDF 组件
gpiodriver/gpio.h
adcesp_adc/adc_oneshot.h
ledcdriver/ledc.h
led_stripled_strip
nvsnvs_flash
wifiesp_wifi
httpesp_http_client
timeresp_timer

直接使用

const idf = @import("esp").idf;

// GPIO
try idf.gpio.configOutput(48);
try idf.gpio.setLevel(48, 1);

// ADC
var adc = try idf.adc.init(.{ .unit = .unit1, .channel = .channel0 });
const value = try adc.read();

// 定时器
var timer = try idf.timer.init(.{
    .callback = myCallback,
    .name = "my_timer",
});
try timer.start(1_000_000); // 1 秒

何时直接用 ESP

用 HAL:

  • 可能跑在其他地方的应用逻辑
  • 标准外设(按钮、LED、传感器)
  • 多板子支持

直接用 ESP:

  • WiFi、蓝牙、HTTP(还没有 HAL 抽象)
  • 性能关键代码
  • ESP 特有功能(PSRAM、ULP 等)

多板子支持

编译期选择

板子在编译期通过构建选项选择:

// 在你的 board.zig 中
const build_options = @import("build_options");

const hw = switch (build_options.board) {
    .esp32s3_devkit => @import("boards/esp32s3_devkit.zig"),
    .korvo2_v3 => @import("boards/korvo2_v3.zig"),
};

板级支持包 (BSP)

每个板子提供硬件特定的驱动:

boards/
├── esp32s3_devkit.zig    # DevKit BSP
│   ├── LedDriver         # GPIO48 单色 LED
│   ├── ButtonDriver      # GPIO0 启动按钮
│   └── RtcDriver         # idf.nowMs()
└── korvo2_v3.zig         # Korvo-2 BSP
    ├── LedDriver         # WS2812 RGB 灯带
    ├── ButtonDriver      # ADC 按钮矩阵
    └── RtcDriver         # idf.nowMs()

添加新板子

  1. 创建 boards/my_board.zig
  2. 实现必需的驱动
  3. build.zigBoardType 枚举中添加
  4. 更新 platform.zig 的 switch 语句

Pure Zig 理念

最小化 C

C 互操作对于 ESP-IDF 是必需的,但我们把它限制在边缘:

┌──────────────────────────────────────┐
│           你的应用                    │  ← 纯 Zig
├──────────────────────────────────────┤
│              HAL                     │  ← 纯 Zig
├──────────────────────────────────────┤
│              SAL                     │  ← 纯 Zig(接口)
├──────────────────────────────────────┤
│          ESP 绑定                    │  ← Zig + @cImport
├──────────────────────────────────────┤
│           ESP-IDF                    │  ← C
└──────────────────────────────────────┘

编译期泛型

通过编译期多态实现零成本抽象:

// 这会为每个板子生成特化代码
// 没有虚表,没有运行时分发
pub fn Board(comptime spec: type) type {
    return struct {
        rtc: spec.rtc,
        button: if (@hasDecl(spec, "button")) spec.button else void,
        led: if (@hasDecl(spec, "led")) spec.led else void,
        // ...
    };
}

无隐藏分配

所有内存分配都是显式的。没有全局分配器。驱动管理自己的资源。


桌面模拟

同样的 HAL 代码可以在桌面上用模拟后端运行。

┌─────────────────────┐     ┌─────────────────────┐
│        应用         │     │        应用         │
├─────────────────────┤     ├─────────────────────┤
│        HAL          │     │        HAL          │
├─────────────────────┤     ├─────────────────────┤
│  ESP SAL (RTOS)     │     │  Std SAL (Thread)   │
├─────────────────────┤     ├─────────────────────┤
│      ESP-IDF        │     │   Raylib (GUI)      │
└─────────────────────┘     └─────────────────────┘
      ESP32                       桌面

这使得:

  • 不烧录就能快速迭代 UI
  • 在 CI 上单元测试
  • 无硬件开发

参见 examples/raysim/ 的模拟示例。