How to Drive Addressable LEDs with the ESP32-C6's RMT Peripheral
The RGB LED on the ESP32-C6-DevKitC-1 lights up only after it receives its color as a stream of pulses on one data pin. Those pulses last fractions of a microsecond, which the CPU cannot guarantee when it also runs a radio. In this guide, you’ll hand that timing to the ESP32-C6’s Remote Control Transceiver (RMT), light the LED, and time a frame from the serial console to confirm the bit period.
How the LED Works
The LED on your ESP32-C6 board is the WS2812, a part that contains red, green, blue elements, and a small controller behind one data pin. Every bit arriving on that pin is one high pulse followed by one low pulse, and the difference between a 0 and a 1 is how long the line stays high. The WS2812B datasheet defines pulse widths in microseconds (µs) and nanoseconds (ns).
| Bit | High time | Low time |
|---|---|---|
| 0 | 0.4 µs ± 150 ns | 0.85 µs ± 150 ns |
| 1 | 0.8 µs ± 150 ns | 0.45 µs ± 150 ns |
Each LED consumes 24 bits, 8 each for green, red, and blue in that order, most significant bit first. Once it holds its 24 bits, it forwards every later bit to its data output, which is how a chain of LEDs can share one wire. When the line then rests low for 50 µs or more, every LED latches the color it received and lights up. That rest is the reset, and a frame is the run of bits between two resets.
Why the CPU Cannot Drive the LED Directly
While the CPU can toggle a pin fast enough to make a 0.4 µs pulse, it cannot guarantee a predictable pulse width for every bit. The problem is that the ESP32-C6 runs FreeRTOS, a real-time operating system that shares the CPU between tasks. With FreeRTOS, the Wi-Fi and Bluetooth stacks can independently trigger interrupts, which instantly pause the CPU to allow a handler to run. If an interrupt happens mid-pulse, a 0.4 µs high can be stretched to 0.8 µs, which will flip the bit and tell the LED to show the wrong color.
How RMT Solves This Problem
The RMT is a peripheral, a block of hardware beside the CPU that does one job independently. Espressif built it for infrared remote signals, so it can output any waveform made of timed pulses. The RMT needs you to describe a waveform as a list of symbols in order for it to do its job.
Every symbol is a 32-bit word that holds two pulses, each with a 15-bit duration and a 1-bit level. The duration is counted in ticks, and you set the tick length when you create the channel. This guide runs the channel at 10 MHz, so one tick will last 100 ns. A 0 bit becomes 4 ticks high then 8 ticks low, and a 1 bit becomes 8 ticks high then 4 ticks low. Every bit takes 12 ticks, or 1.2 µs on the wire.
The ESP32-C6 has four RMT channels, two that transmit and two that receive, and each owns a memory block of 48 symbols, which is two LEDs’ worth of data. The driver fills the block, starts the hardware, and refills the first half from an interrupt while the hardware plays the second half. The RMT hardware is in charge of maintaining the pulse widths, so the CPU only has to stay ahead of it. The driver fills the block through an encoder, a small object that turns your bytes into symbols on demand. The stock bytes encoder maps each bit to one of two symbols you define, which is exactly what a WS2812 needs.
Use the RMT to Drive the LED
Before you begin, make sure you have ESP-IDF v5.4 or later installed on your PC.
Create a new project within ESP-IDF and set the target chip to esp32c6. Then, add the esp_driver_rmt and esp_timer components to main/CMakeLists.txt to enable the RMT driver and microsecond timer in your firmware.
idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES esp_driver_rmt esp_timer)Add the following code in your project’s main.c. It opens one transmit channel on GPIO8, builds a bytes encoder with the WS2812 timing, and cycles the LED through three colors.
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/rmt_tx.h"
#include "driver/rmt_encoder.h"
#include "esp_log.h"
static const char *TAG = "rmt_led";
#define LED_GPIO 8 // the ESP32-C6 wires its RGB LED to GPIO8
#define LED_COUNT 1 // raise this for a strip on a header pin
#define RMT_RES_HZ 10000000 // 10 MHz, so one tick lasts 100 ns
// Three bytes per LED. Static so the buffer lives for the whole program,
// because the driver keeps reading it from an interrupt after
// rmt_transmit() has already returned.
static uint8_t pixels[LED_COUNT * 3];
void app_main(void)
{
// One transmit channel on the LED pin. mem_block_symbols is the
// 48-symbol memory block the ESP32-C6 gives each channel.
// trans_queue_depth is how many frames may wait in line, and one is
// enough because this loop waits for each frame to finish.
rmt_channel_handle_t chan = NULL;
rmt_tx_channel_config_t chan_cfg = {
.gpio_num = LED_GPIO,
.clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = RMT_RES_HZ,
.mem_block_symbols = 48,
.trans_queue_depth = 1,
};
ESP_ERROR_CHECK(rmt_new_tx_channel(&chan_cfg, &chan));
// The bytes encoder turns each bit into one symbol. Durations count
// ticks of 100 ns, so 4 ticks is 0.4 us and 8 ticks is 0.8 us. The
// WS2812 reads the most significant bit of each byte first.
rmt_encoder_handle_t enc = NULL;
rmt_bytes_encoder_config_t enc_cfg = {
.bit0 = { .level0 = 1, .duration0 = 4, .level1 = 0, .duration1 = 8 },
.bit1 = { .level0 = 1, .duration0 = 8, .level1 = 0, .duration1 = 4 },
.flags.msb_first = 1,
};
ESP_ERROR_CHECK(rmt_new_bytes_encoder(&enc_cfg, &enc));
// The pin stays idle until the channel is enabled.
ESP_ERROR_CHECK(rmt_enable(chan));
// loop_count 0 plays the frame once. The pin then rests low, and that
// rest is the 50 us reset the LED needs before it latches.
rmt_transmit_config_t tx_cfg = { .loop_count = 0 };
// Each color is green, red, blue, because green is the byte the LED
// reads first. 32 out of 255 keeps the LED comfortable to look at.
const uint8_t colors[3][3] = { {0, 32, 0}, {32, 0, 0}, {0, 0, 32} };
const char *names[3] = { "red", "green", "blue" };
for (int i = 0;; i = (i + 1) % 3) {
for (int led = 0; led < LED_COUNT; led++) {
memcpy(&pixels[led * 3], colors[i], 3);
}
ESP_ERROR_CHECK(rmt_transmit(chan, enc, pixels, sizeof(pixels), &tx_cfg));
// Block until the last symbol has left the pin, so the next write
// to pixels[] cannot land while the driver is still reading it.
ESP_ERROR_CHECK(rmt_tx_wait_all_done(chan, portMAX_DELAY));
ESP_LOGI(TAG, "%s", names[i]);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}Build the program, flash it, and open the serial terminal.
You should see your board’s LED rotate through red, green, and blue once a second, and the terminal print these changes in sync.

Measure the Bit Period
Why Measure
Say you wanted to take this firmware to control another LED or even a multi-LED strip. You may notice the new LED stays dark or the strip lights up with the wrong colors at the far end, even as the terminal prints the correct colors. Your firmware seems correct, yet you see these problems. What happened?
One problem is how loose the LED is about timing. The WS2812 judges each bit only by the length of its high pulse, and the datasheet allows that pulse to be off by 150 ns, so a pulse 25 percent longer than you designed will still light your onboard LED correctly. In contrast, a stricter LED, a different brand, or a part running hot can reject the same pulse.
The second problem is a gap in the frame. The CPU has to refill the RMT’s 48-symbol memory about every 29 µs while a frame plays. If it falls behind, the pin could go quiet. You won’t notice this problem with one LED, but with a strip, any quiet gap over 50 µs is interpreted as the end of the frame, making every LED past that point show the wrong color.
To catch these problems early, measure the bit period, or the length of one full bit on the wire with its high and low pulses added together. This one measurement can apply to every pulse because the RMT will always reproduce tick counts exactly. The 4-tick and 8-tick pulses you typed into the encoder will always be 4 and 8 ticks, so the only thing that can move them is the length of a tick, and a bit is 12 ticks.
Measure with Firmware
One LED is too short a frame to time well, because the driver’s own overhead is about the same size as the 29 µs the 24 bits take. Thus, it’s better to measure the bit period by driving 100 LEDs at once and calculating the average.
Add #include "esp_timer.h" with the other includes, and set LED_COUNT to 100. Then add the following code into the for loop to measure how long it takes for the RMT to drive 100 LEDs:
// esp_timer_get_time() counts microseconds since boot in a 64-bit value.
int64_t start_us = esp_timer_get_time();
ESP_ERROR_CHECK(rmt_transmit(chan, enc, pixels, sizeof(pixels), &tx_cfg));
ESP_ERROR_CHECK(rmt_tx_wait_all_done(chan, portMAX_DELAY));
int64_t frame_us = esp_timer_get_time() - start_us;
ESP_LOGI(TAG, "%s, %d LEDs in %lld us", names[i], LED_COUNT, frame_us);Rebuild your firmware, reflash, and look back at your serial terminal. The on-board LED keeps the first 24 bits and forwards the other 2,376 to its unconnected output, so you should still see the LED rotate through the same three colors like before. Here is an example of the terminal output:

The frame has 2,400 bits, and with 1.2 µs for each bit, the total time should be 2,880 µs. However, your output should be several microseconds above that because the estimate doesn’t include the time the driver spends in its interrupt handler and waking your task.
Troubleshooting
A reading within a few tens of microseconds of 2,880 µs means your pulse widths are what you designed and the CPU kept up, so the same firmware will work for an LED strip. A reading that is off by a large, steady amount means the tick is the wrong length, and the place to look is .resolution_hz and .clk_src in the channel configuration. A reading that jumps around from frame to frame means the CPU fell behind on refills, and the fix is a larger .mem_block_symbols, a higher .intr_priority, or less interrupt load from the rest of your application. Each of those is a one-line change, and the frame time tells you which one you need.
Next Steps
You now know how to drive a WS2812 with the RMT and measure a timed frame that confirms the bit period.
Keep the timer in the code and check the reading again whenever you change the firmware around it. When your firmware changes how it uses Wi-Fi, Bluetooth, or flash writes, a steady bit period can quickly become unpredictable. Make sure your bit period is stable before you use this firmware to drive a different LED or an LED strip.
Ready to connect your devices anywhere on Earth? Get started with Hubble Network for free →