Controlling LED Strips with Zig on ESP32: A Complete Guide to WS2812B Control

The zig-esp-idf-sample repository provides a production-ready Zig wrapper around the ESP-IDF LED strip component, enabling type-safe control of WS2812B and compatible addressable LEDs via RMT or SPI backends.

This guide walks through the complete stack for driving LED strips with Zig on ESP32 microcontrollers. Based on the kassane/zig-esp-idf-sample project, you will learn how to configure the build system, initialize hardware backends, and implement real-time LED control patterns using idiomatic Zig patterns with full ESP-IDF integration.

Architecture Overview

The implementation follows a layered architecture that bridges Zig's type system with the underlying C-based ESP-IDF LED strip driver.

Layer Description Key Source
Zig-to-C Bindings Auto-generated C bindings (idf.sys) expose ESP-IDF APIs. The imports/led-strip.zig module wraps sys.led_strip_* types as safe Zig abstractions. imports/led-strip.zig
Component Integration The espressif/led_strip dependency is declared in main/idf_component.yml. The build system auto-detects the component, sets include paths, and generates HAS_LED_STRIP macros. main/idf_component.yml
High-Level API Factory functions (newRmtDevice, newSpiDevice) and pixel helpers (setPixel, setPixelRgbw, refresh, clear) provide an idiomatic Zig interface with error handling via idf.err. imports/led-strip.zig
Runtime Support FreeRTOS task wrappers (idf.rtos.Task) and ESP logging integration (idf.log.espLogFn) enable concurrent LED patterns with unified debugging. examples/smartled-rgb.zig

Setting Up the LED Strip Component

Before writing Zig code, you must register the ESP-IDF LED strip component as a managed dependency. This ensures the build system downloads the correct headers and links the C implementation.

Create or modify main/idf_component.yml:

dependencies:
  espressif/led_strip:
    version: "^2.5.0"

During the idf.py reconfigure step, the build system automatically:

  1. Resolves the component from the ESP component registry
  2. Adds include paths to the Zig compiler invocation
  3. Defines HAS_LED_STRIP preprocessor macros for conditional compilation

Configuring WS2812B LED Strips in Zig

The imports/led-strip.zig module provides type-safe configuration structs that map to the underlying C driver structures.

RMT Backend Configuration

The RMT (Remote Control) peripheral is the preferred backend for WS2812B timing-sensitive protocols. Configure it using the high-level factory pattern:

const led = @import("imports/led-strip.zig");

fn configureLED() !led.LedStripHandle {
    // 1. Define strip parameters: GPIO 2, 24 LEDs, WS2812 model
    const strip_config = led.LedStripConfig.ws2812(2, 24);

    // 2. Configure RMT backend (10 MHz default clock)
    var rmt_config = led.LedStripRmtConfig.default;
    rmt_config.mem_block_symbols = 64; // Adjust for LED count
    
    // 3. Create device handle
    var led_strip: led.LedStripHandle = null;
    const handle = try led.newRmtDevice(&strip_config, &rmt_config, &led_strip);
    
    return handle;
}

The newRmtDevice function wraps the ESP-IDF C function led_strip_new_rmt_device, translating Zig errors via idf.err and returning an opaque handle to the driver instance.

SPI Backend Alternative

For applications where RMT channels are exhausted, the SPI backend provides an alternative using the MOSI line:

const spi_config = led.LedStripSpiConfig{
    .clk_src = @import("idf").sys.SPI_CLK_SRC_DEFAULT,
    .spi_bus = @import("idf").sys.SPI2_HOST,
};

var led_handle: led.LedStripHandle = null;
const strip = try led.newSpiDevice(&strip_config, &spi_config, &led_handle);

Controlling LEDs in Real-Time

Once initialized, manipulate pixels using the high-level API and drive patterns from a FreeRTOS task.

Pixel Manipulation API

The wrapper provides buffer-safe pixel operations:

// Set individual pixel RGB values (0-255)
try led.setPixel(handle, 0, 255, 0, 0);    // Red at index 0
try led.setPixel(handle, 1, 0, 255, 0);    // Green at index 1

// Set RGBW for SK6812 strips
try led.setPixelRgbw(handle, 2, 0, 0, 255, 128); // Blue + White

// Push buffer to hardware
try led.refresh(handle);

// Clear all pixels (set to black)
try led.clear(handle);

FreeRTOS Task Implementation

The examples/smartled-rgb.zig file demonstrates concurrent LED control using the idf.rtos wrapper:

fn ledStripTask(led_strip_ptr: ?*anyopaque) callconv(.c) void {
    const led_strip: led.LedStripHandle = @ptrCast(@alignCast(led_strip_ptr.?));
    var led_on_off = false;

    while (true) {
        if (led_on_off) {
            // Set all LEDs to dim white
            var i: u32 = 0;
            while (i < 24) : (i += 1) {
                led.setPixel(led_strip, i, 5, 5, 5) catch {};
            }
            led.refresh(led_strip) catch {};
        } else {
            // Turn off
            led.clear(led_strip) catch {};
        }
        
        led_on_off = !led_on_off;
        idf.rtos.Task.delayMs(500); // 500ms toggle
    }
}

The task uses idf.rtos.Task.delayMs for non-blocking delays and integrates with ESP-IDF logging via idf.log.espLogFn for unified output.

Key Source Files and API Reference

File Purpose
imports/led-strip.zig High-level Zig API wrapping ESP-IDF LED strip driver. Exports LedStripConfig, LedStripRmtConfig, newRmtDevice, setPixel, refresh, etc.
main/idf_component.yml Component manifest declaring espressif/led_strip dependency for build system integration.
examples/smartled-rgb.zig Complete working example showing RMT initialization, FreeRTOS task creation, and blinking pattern.
patches/led_strip/led_strip_struct_format_layout_15.zig Struct layout compatibility shim ensuring Zig memory layout matches C definitions.

Summary

  • The zig-esp-idf-sample repository provides a complete Zig wrapper for ESP32 LED strip control via the imports/led-strip.zig module.
  • Component management is handled through main/idf_component.yml, which auto-downloads the espressif/led_strip driver during build configuration.
  • Hardware backends include RMT (preferred for WS2812B timing) and SPI (alternative when RMT channels are unavailable), configured via LedStripRmtConfig and LedStripSpiConfig.
  • Runtime control uses high-level functions like setPixel, refresh, and clear, all returning Zig errors for idiomatic error handling with try/catch.
  • Concurrency is achieved through FreeRTOS tasks using the idf.rtos.Task wrapper, as demonstrated in examples/smartled-rgb.zig.

Frequently Asked Questions

How do I change the GPIO pin for my LED strip?

Modify the first parameter to led.LedStripConfig.ws2812(). For example, to use GPIO 5 instead of GPIO 2, call led.LedStripConfig.ws2812(5, 24) where the second argument is your LED count. Ensure the selected GPIO supports the RMT peripheral (or SPI, if using that backend) according to your ESP32 variant's datasheet.

Can I use RGBW SK6812 strips instead of WS2812B?

Yes. Use led.setPixelRgbw() instead of led.setPixel() to specify red, green, blue, and white channel values. When configuring the strip, ensure your LedStripConfig specifies the correct pixel format (RGBW) if the wrapper exposes format selection, or verify that the underlying ESP-IDF driver auto-detects the strip type correctly.

What is the difference between the RMT and SPI backends?

The RMT backend (led.newRmtDevice) uses the Remote Control peripheral to generate precise timing signals required by WS2812B protocols, making it the preferred choice for addressable LEDs. The SPI backend (led.newSpiDevice) repurposes the SPI MOSI line to bit-bang LED data, serving as a fallback when RMT channels are exhausted or unavailable. RMT generally provides better timing accuracy and lower CPU overhead.

How do I handle errors when refreshing the LED strip?

All high-level functions in imports/led-strip.zig return Zig errors that can be handled with try or catch. For example, try led.refresh(handle) will propagate errors to the caller, while led.refresh(handle) catch |err| { log.err("Refresh failed: {}", .{err}); } allows graceful degradation. The underlying ESP-IDF error codes are translated via idf.err to provide idiomatic Zig error semantics.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →