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:
| Layer | Purpose | Use When |
|---|---|---|
| SAL | System primitives (threads, sync, time) | You need OS features |
| HAL | Hardware abstraction (buttons, LEDs, sensors) | You want portability |
| ESP | Direct ESP-IDF bindings | You 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
@cImportin 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:
| Platform | Download |
|---|---|
| macOS ARM64 | zig-aarch64-macos-none-baseline.tar.xz |
| macOS x86_64 | zig-x86_64-macos-none-baseline.tar.xz |
| Linux x86_64 | zig-x86_64-linux-gnu-baseline.tar.xz |
| Linux ARM64 | zig-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
| Board | Parameter | Features |
|---|---|---|
| ESP32-S3-DevKitC | esp32s3_devkit | GPIO button, single LED |
| ESP32-S3-Korvo-2 | korvo2_v3 | ADC 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:
- Merges Espressif’s LLVM Xtensa patches
- Builds Zig against this patched LLVM
- 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
| Example | Description | HAL Components | Boards |
|---|---|---|---|
| gpio_button | Button input with LED toggle | Button, LedStrip | ①②③ |
| led_strip_flash | RGB LED strip blinking | LedStrip | ①②③ |
| led_strip_anim | LED animation effects | LedStrip | ①②③ |
| adc_button | ADC-based button matrix | ButtonGroup | ② |
| timer_callback | Hardware timer callbacks | LedStrip + idf.timer | ① |
| pwm_fade | LED brightness fading | Led (PWM) | ① |
| temperature_sensor | Internal temp sensor | TempSensor | ①② |
| nvs_storage | Persistent key-value storage | Kvs | ①② |
| wifi_dns_lookup | WiFi + DNS resolution | (direct idf) | ①② |
| http_speed_test | HTTP download benchmark | (direct idf) | ①② |
| memory_attr_test | PSRAM/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:
| Board | Port (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.Buttonwith debouncehal.LedStripcontrolBoard.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.LedStripbasic 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.ButtonGroupfor 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.timerintegration- 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.Ledwith 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.TempSensorusage- 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.Kvsfor 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:
linksectionfor 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
| Module | Purpose |
|---|---|
thread | Task creation and management |
sync | Mutex, Semaphore, Event |
time | Sleep, delays, timestamps |
queue | Thread-safe message queues |
log | Structured 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:
| Platform | Implementation | Location |
|---|---|---|
| ESP32 | FreeRTOS wrappers | lib/esp/src/sal/ |
| Desktop | std.Thread wrappers | lib/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
| Peripheral | Description | Required Driver Methods |
|---|---|---|
RtcReader | Uptime/timestamp (required) | init, deinit, uptime |
Button | GPIO button with debounce | init, deinit, read |
ButtonGroup | ADC button matrix | init, deinit, read |
LedStrip | RGB LED strip | init, deinit, setColor |
Led | Single LED with PWM | init, deinit, setBrightness |
TempSensor | Temperature sensor | init, deinit, readCelsius |
Kvs | Key-value storage | init, 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
| Module | ESP-IDF Component |
|---|---|
gpio | driver/gpio.h |
adc | esp_adc/adc_oneshot.h |
ledc | driver/ledc.h |
led_strip | led_strip |
nvs | nvs_flash |
wifi | esp_wifi |
http | esp_http_client |
timer | esp_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
- Create
boards/my_board.zig - Implement required drivers
- Add to
BoardTypeenum inbuild.zig - 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、传感器) | 需要可移植性 |
| ESP | ESP-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 ARM64 | zig-aarch64-macos-none-baseline.tar.xz |
| macOS x86_64 | zig-x86_64-macos-none-baseline.tar.xz |
| Linux x86_64 | zig-x86_64-linux-gnu-baseline.tar.xz |
| Linux ARM64 | zig-aarch64-linux-gnu-baseline.tar.xz |
# 验证 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-DevKitC | esp32s3_devkit | GPIO 按钮,单色 LED |
| ESP32-S3-Korvo-2 | korvo2_v3 | ADC 按钮,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 内核。
我们维护一个分支:
- 合并 Espressif 的 LLVM Xtensa 补丁
- 基于打过补丁的 LLVM 编译 Zig
- 为常见平台提供预编译二进制
ESP32-C3/C6 使用 RISC-V,可以用标准 Zig。但对于 Xtensa 芯片,你需要我们的构建版本。
如果你想自己编译,参见 bootstrap/。
示例
中文 | English
所有示例位于 examples/。每个示例演示特定的 HAL 组件或 ESP-IDF 功能。
快速索引
| 示例 | 说明 | HAL 组件 | 板子 |
|---|---|---|---|
| gpio_button | 按钮输入控制 LED | Button, LedStrip | ①②③ |
| led_strip_flash | RGB LED 闪烁 | LedStrip | ①②③ |
| led_strip_anim | LED 动画效果 | LedStrip | ①②③ |
| adc_button | ADC 按钮矩阵 | ButtonGroup | ② |
| timer_callback | 硬件定时器回调 | LedStrip + idf.timer | ① |
| pwm_fade | LED 亮度渐变 | Led (PWM) | ① |
| temperature_sensor | 内部温度传感器 | TempSensor | ①② |
| nvs_storage | 持久化键值存储 | Kvs | ①② |
| wifi_dns_lookup | WiFi + DNS 解析 | (直接 idf) | ①② |
| http_speed_test | HTTP 下载测速 | (直接 idf) | ①② |
| memory_attr_test | PSRAM/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.ButtonGroupADC 按钮- 电压阈值配置
- 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.LedPWM 控制- 硬件渐变支持
- 亮度控制 (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.KvsNVS 访问- 读写 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 | 任务创建和管理 |
sync | Mutex、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 是一个接口。实际实现来自平台:
| 平台 | 实现 | 位置 |
|---|---|---|
| ESP32 | FreeRTOS 封装 | 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 |
Button | GPIO 按钮带消抖 | init, deinit, read |
ButtonGroup | ADC 按钮矩阵 | init, deinit, read |
LedStrip | RGB LED 灯带 | init, deinit, setColor |
Led | 单 LED 带 PWM | init, 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 组件 |
|---|---|
gpio | driver/gpio.h |
adc | esp_adc/adc_oneshot.h |
ledc | driver/ledc.h |
led_strip | led_strip |
nvs | nvs_flash |
wifi | esp_wifi |
http | esp_http_client |
timer | esp_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()
添加新板子
- 创建
boards/my_board.zig - 实现必需的驱动
- 在
build.zig的BoardType枚举中添加 - 更新 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/ 的模拟示例。