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

> Control WS2812B LED strips with Zig on ESP32 using the zig-esp-idf-sample. This guide offers type-safe control via RMT or SPI backends for your addressable LEDs.

- Repository: [Matheus C. França/zig-esp-idf-sample](https://github.com/kassane/zig-esp-idf-sample)
- Tags: tutorial
- Published: 2026-03-05

---

**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`](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/idf_component.yml). The build system auto-detects the component, sets include paths, and generates `HAS_LED_STRIP` macros. | [`main/idf_component.yml`](https://github.com/kassane/zig-esp-idf-sample/blob/main/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`](https://github.com/kassane/zig-esp-idf-sample/blob/main/main/idf_component.yml):

```yaml
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:

```zig
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:

```zig
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:

```zig
// 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:

```zig
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`](https://github.com/kassane/zig-esp-idf-sample/blob/main/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`](https://github.com/kassane/zig-esp-idf-sample/blob/main/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.