Skip to content

API guide

How to do the common jobs with the driver: create a strip, set pixels, dim, resize, animate, drive several strips and handle errors.

Every snippet on this page is compiled by the host CI from tests/docs/api_tasks.cpp, where Rgb(r, g, b) packs a colour as 0xRRGGBB, the format SetPixel() takes (see Colour order). The API reference lists every member.

The shape of the API

Class Header Owns
WS2812Strip ws2812_cpp.hpp One RMT channel, the pixel buffer, brightness, timing, LED type
WS2812Animator ws2812_effects.hpp One effect on one strip: a step counter and a colour
WS2812MultiAnimator ws2812_multi_animator.hpp One animator per strip, with a shared step
ws2812::RmtTx rmt_wrapper.hpp An RMT transmit channel; raw symbols or WS2812 bytes
WS2812Strip member Does
WS2812Strip(gpio, channel, num_leds, type, t0h, t1h, t0l, t1l, brightness) Creates the RMT channel; every argument has a Kconfig default
SetPixel(index, colour) Writes one pixel (0xRRGGBB, or 0xWWRRGGBB for RGBW); out-of-range indices are ignored
Show() Scales, encodes and sends the buffer green first, then the reset; blocks until done; returns esp_err_t
SetBrightness(0-255) Global scale applied at the next Show()
SetNumLeds(n) Resizes and clears the buffer
SetTimings(t0h, t1h, t0l, t1l) New bit timing in 100 ns ticks
Length() The number of LEDs
ColorWheel(0-255) A colour on a red-green-blue wheel, packed 0xRRGGBB
Begin() ESP_OK if the constructor set the channel up, otherwise retries and returns the RMT error
GetDriverVersion() The version string, for example "1.0.0"

Create a strip

// WS2812B timing in 100 ns ticks: T0H 400 ns, T1H 800 ns, T0L 900 ns, T1L 500 ns (also the Kconfig
// defaults).
WS2812Strip make_status_strip() {
  return WS2812Strip(GPIO_NUM_8,   // data pin
                     -1,           // channel: unused, ESP-IDF picks a free one
                     16,           // LEDs
                     LedType::RGB, // 24 bits per LED (LedType::RGBW: 32)
                     4, 8, 9, 5,   // T0H, T1H, T0L, T1L in ticks
                     64);          // brightness 0-255
}

The constructor creates and enables the RMT channel at once. It does not abort when that fails (no free channel, an invalid GPIO, no memory); check Begin() before the first frame:

bool start_strip(WS2812Strip& strip) {
  // The constructor does not abort when the channel cannot be created; Begin() says whether it was,
  // and tries again if not.
  const esp_err_t err = strip.Begin();
  if (err != ESP_OK) {
    ESP_LOGE("leds", "no RMT channel: %s", esp_err_to_name(err)); // e.g. ESP_ERR_NOT_FOUND
    return false;
  }
  return true;
}

Until Begin() succeeds, Show() returns ESP_ERR_INVALID_STATE. Keep the object alive as long as you use the LEDs: the destructor releases the channel. The channel argument is accepted and unused: ESP-IDF v5's RMT driver picks a free transmit channel and has no way to request a number.

Set pixels and show them

esp_err_t show_gradient(WS2812Strip& strip) {
  const uint32_t n = strip.Length();
  const uint32_t last = n > 1 ? n - 1 : 1;
  for (uint32_t i = 0; i < n; ++i) {
    const auto level = static_cast<uint8_t>(255 * i / last);
    strip.SetPixel(i, Rgb(level, 0, 255 - level)); // blue to red
  }
  return strip.Show(); // nothing reaches the LEDs until Show()
}

SetPixel() only writes the buffer. Show() sends the whole strip every time; there is no partial update, and the LEDs keep their colours until the next frame.

Dim the strip

esp_err_t dim(WS2812Strip& strip) {
  strip.SetBrightness(16); // scales every channel of every LED at the next Show()
  return strip.Show();
}

Each channel is scaled as value × brightness / 255 while encoding. The buffer keeps the full values, so raising the brightness again restores the colours exactly. At low brightness, small values round down to 0.

Change the length

esp_err_t grow(WS2812Strip& strip) {
  strip.SetNumLeds(60); // all pixels are cleared to 0
  strip.SetPixel(59, Rgb(0, 0, 255));
  return strip.Show();
}

SetNumLeds() reallocates the pixel and symbol buffers and clears every pixel. Call it from the task that calls Show().

Change the timing

void use_ws2812_original_timing(WS2812Strip& strip) {
  // 4, 8, 9, 5 already fit both parts. To centre on the WS2812's nominal 350 / 700 / 800 / 600 ns:
  strip.SetTimings(4, 7, 8, 6); // T0H, T1H, T0L, T1L in ticks of 100 ns
}

The values are RMT ticks of 100 ns. Signal timing has the windows for the WS2812 and WS2812B.

Animate

void led_task(void* arg) {
  auto& strip = *static_cast<WS2812Strip*>(arg);
  WS2812Animator anim(strip);
  anim.SetEffect(WS2812Animator::Effect::Rainbow);
  for (;;) {
    anim.Tick(); // computes one frame and calls strip.Show()
    vTaskDelay(pdMS_TO_TICKS(20));
  }
}
Effect Each Tick()
Off Clears the strip
SolidColor Sets every LED to the colour
Rainbow Spreads ColorWheel() over the strip and shifts it by one step
Chase Lights every third LED and moves the pattern by one
Blink Alternates between the colour and off
Breath Ramps the strip brightness up and down in steps of 4 (overwrites SetBrightness())
Larson One lit LED that moves end to end and back

Every Tick() ends with a Show(), so the tick rate is the frame rate; 20 ms (50 Hz) is smooth. Tick() ignores the result of Show(); check transmit errors on the strip if you need them. SetVirtualLength() stretches an effect over a longer imaginary strip, and SetStep() / Step() read and set its position.

Drive several strips

void two_strips(WS2812Strip& front, WS2812Strip& rear) {
  // unified = true: both strips run the effect over the longer strip's length.
  // sync = true: one step counter for both.
  WS2812MultiAnimator multi({&front, &rear}, true, true);
  multi.SetEffect(WS2812Animator::Effect::Chase, Rgb(255, 80, 0));
  multi.SetEffect(1, WS2812Animator::Effect::Blink, Rgb(255, 0, 0)); // rear strip only
  for (int frame = 0; frame < 500; ++frame) {
    multi.Tick();
    vTaskDelay(pdMS_TO_TICKS(50));
  }
}

Each strip has its own RMT channel and one memory block, so the number of strips is the chip's number of transmit channels (Porting). One strip too many fails in Begin() with ESP_ERR_NOT_FOUND. The strips are sent one after the other in each Tick().

Handle errors

bool show_or_report(WS2812Strip& strip) {
  const esp_err_t err = strip.Show();
  if (err != ESP_OK) {
    // The RMT driver's code, or ESP_ERR_INVALID_STATE when the channel was never set up.
    ESP_LOGE("leds", "Show failed: %s", esp_err_to_name(err));
    return false;
  }
  return true;
}
Signal Meaning What to do
Begin() returns an error rmt_new_tx_channel(), rmt_enable() or the encoder failed: ESP_ERR_NOT_FOUND (no free channel), a bad GPIO, no memory Fix the configuration, or free a channel and call Begin() again
Show() returns ESP_ERR_INVALID_STATE The channel was never set up Check Begin()
Show() returns another esp_err_t The RMT driver refused the transfer Log it with esp_err_to_name(); the frame was not sent
Wrong colours, all white, flicker Timing, colour order or signal level, not a software error Troubleshooting

Send bytes directly

esp_err_t send_raw_grb(ws2812::RmtTx& tx) {
  // Bytes go out as they are, MSB first, with the WS2812B timing built into RmtTx (10 MHz
  // resolution): already in wire order, and no reset is appended.
  const uint8_t frame[] = {0x00, 0xFF, 0x00,  // LED 0: red   (G, R, B)
                           0xFF, 0x00, 0x00}; // LED 1: green
  return tx.TransmitWs2812(frame, sizeof(frame));
}

ws2812::RmtTx can also be used on its own: TransmitWs2812() sends bytes as they are with the WS2812B timing, Transmit() sends prepared symbols and TransmitBytes() takes your own 0 and 1 symbols. All three block until the transfer is done, send no reset of their own, and return ESP_ERR_INVALID_STATE while IsReady() is false; InitStatus() holds the set-up error and Init() retries it.

The C interface

The C functions ws2812_control_init(), ws2812_write_leds() and ws2812_set_brightness() drive one strip whose LED type, length and timing come from Kconfig. struct led_state holds one 0xRRGGBB (or 0xWWRRGGBB) value per LED; ws2812_write_leds() scales them by the brightness, sends them green first with the Kconfig timing, appends the 300 µs reset and blocks until done. It returns ESP_ERR_INVALID_STATE before ws2812_control_init(), which can be called once (a second call returns ESP_ERR_INVALID_STATE) and cleans up after itself when it fails. The channel argument is unused. Prefer WS2812Strip for more than one strip.

Threads

The classes are not thread-safe. Give each strip one owner task, or guard every call on a strip (and on any animator that uses it) with one mutex.