logo elektroda
logo elektroda
X
logo elektroda

ESP32 and Remote Control Transceiver (RMT) - tutorial, first steps, WS2812 control

p.kaczmarek2  0 4245 Cool? (+5)
📢 Listen (AI):

TL;DR

  • An ESP32 RMT tutorial shows how to generate precise transmit waveforms for WS2812B LEDs and other timing-sensitive control signals.
  • It configures RMT_TX on a GPIO pin and fills rmt_item32_t entries with separate high and low durations for each bit.
  • With APB_CLK at 80MHz and a divider of 2, one RMT cycle becomes 25 ns, so T0H 0.4us maps to 16 cycles.
  • Oscilloscope checks confirm the waveform, WS2812B control works, and the code scales to N pixels or RGBW bytes; the first color appears green because the strip expects GRB.
Generated by the language model.
Oscilloscope displaying waveforms and a WS2812B LED strip connected to the device. .
The Remote Control Transceiver (RMT) on the ESP32 is a special hardware module that allows precise generation and reception of timing signals such as IR and RF control. Here I will show how to run it and use it to operate the WS2812B diodes, which are known to need a fairly precise waveform at the DI input to obtain colour data. By the way, we will look at the generated waveforms on an oscilloscope.

To begin with, I will remind you of the previous topics in the series. The first, most important discussion of the WS2812 and similar:
PIC18F45K50 as WS2812 LED strip driver (theory+library) .
A side topic, a method with hardware SPI and DMA:
Controlling WS2812 diodes via SPI with DMA - using MOSI to generate timings .
This time I will make the program based on ESP32:
ESP32 module with USB cable connected and lit LED lights. .
I start the presentation with my classic code integrating WiFiManager and OTA (batch update via WiFi):
Code: C / C++
Log in, to see the code
.
You also need to refer to the documentation I am relying on here:
https://docs.espressif.com/projects/esp-idf/e...able/esp32/api-reference/peripherals/rmt.html
We start the adventure by including the library header:
Code: C / C++
Log in, to see the code
.
Then we need to initialise the RMT. Initialisation includes:
- mode setting (RMT_MODE_TX for transmit or RMT_MODE_RX for receive)
- channel selection (RMT_CHANNEL_0 to RMT_CHANNEL_7)
- setting the number of the GPIO pin with which the RMT will interact (GPIO_NUM_4 enumeration, which simplifies to a numeric 4)
- configuration of the number of memory blocks for data storage
- configuration of the transmit mode, including enabling/disabling loop, carrier, or setting the idle level (low or high)
- clock divider configuration, as high and low state times are expressed in cycles .
The default clock input is APB_CLK, which is 80MHz. If we set the clock divider to 2, the RMT clock is 40MHz. Then one cycle is 25 ns.
Code: C / C++
Log in, to see the code
.
Now you can transmit something, bearing in mind that one cycle is 25 ns. Since one cycle is 25 ns, let's choose a value of 50 to try, which will give us 50*25ns = 1.25 μs. So now we set up an array of objects rmt_item32_t , where each object describes one high and one low state separately. We have control over when and how long these states last. For both low and high I entered 50. We then set this data and send.
Code: C / C++
Log in, to see the code
.
I connected both functions to setup and loop respectively.
Now the oscilloscope can be connected. You will need Single mode, pulse trigger, we need to catch that moment when the ESP will send the data:
Oscilloscope displaying signal waveform with connected ESP32. .
Oscilloscope screen showing waveform signals from a Rigol oscilloscope. .
I used the Cursor function to measure the mileage. It is fine, the error is negligible (I manually set it). Everything works, now you can experiment.
Shorter low state of the first cycle:
Code: C / C++
Log in, to see the code
.
Oscilloscope screenshot showing signal waveforms from ESP32. .
It is time to convert this to support WS2812B. We start by reading the catalogue note:
Table showing data transfer times for WS2812B. .
First we have T0H - 0.4us. How many cycles will 0.4us be? 0.4us/25ns = 16.
Similarly, we count the remaining times. I came out with:
Code: C / C++
Log in, to see the code
.
The WS2812B uses 8-bit RGB. In total we have 24 bits - 8 bits per channel. We will therefore need 24 objects of the rmt_item32_t structure.
Code: C / C++
Log in, to see the code
.
I have added to the main loop:
Code: C / C++
Log in, to see the code
.
Flashes green... well, yes, but it's not an error. The format expected is GRB.
WS2812B LED strip with a lit green diode and connecting wires. .
Diagram showing the composition of 24-bit data for WS2812B .
That's two colours now:
Code: C / C++
Log in, to see the code
.
Flashing LED strip with WS2812B diodes connected to wires. .
The next step is to generalise the case for N pixels, or perhaps even better, for N bytes, as some bars are in RGBW format.
Code: C / C++
Log in, to see the code
.
This way you can do effects where each pixel has a different colour:
Code: C / C++
Log in, to see the code
.
Step is a counter which specifies the current frame of the animation.
Flashing WS2812B LED strip connected to an oscilloscope with ESP32. .
You could also use HSV and make a better animation as in WLED, but that's a bit beyond the scope of this topic:
Code: C / C++
Log in, to see the code
.
The result:
Flashing LED strip with WS2812B LEDs .
That's enough for today... The WS2812B control undoubtedly works, and you can always change the timings to suit a different type of LED if required. Handling an extra channel, white, won't be a problem either, because the code that converts bytes into RMT data doesn't "need to know" what the meaning of the bits is anyway.
I have made some simplifications in the theme, but I think this is not a problem and you can build your own WS2812 driver based on the solution shown here. I attach the full project in PlatformIO:
Attachments:
  • rmt20250331.zip (12.13 MB) You must be logged in to download this attachment.

About Author
p.kaczmarek2
p.kaczmarek2 wrote 14309 posts with rating 12194 , helped 648 times. Been with us since 2014 year.

Comments

FAQ

TL;DR: With an 80 MHz APB clock and the expert rule "one cycle is 25 ns", ESP32 RMT lets Arduino or PlatformIO users generate WS2812B waveforms precisely enough to drive single LEDs and full strips, while also making timing easy to verify on an oscilloscope. [#21502225]

Why it matters: It shows a practical, low-level way to build a precise ESP32 LED driver without relying on a heavyweight library.

Approach Signal generation method Timing detail shown here Best fit in this thread
ESP32 RMT Dedicated hardware pulses 25 ns per cycle at clk_div = 2 Precise WS2812B driving and waveform experiments
SPI + DMA MOSI used to emulate timings Not quantified here Alternative method referenced from an earlier project

Key insight: The most important step is converting WS2812B pulse widths into RMT clock cycles correctly. Once that mapping is right, the same byte-to-pulse code scales from one RGB LED to longer RGB or RGBW strips. [#21502225]

Quick Facts

  • ESP32 RMT uses APB_CLK = 80 MHz by default; with clk_div = 2, the effective RMT clock becomes 40 MHz, so 1 cycle = 25 ns. [#21502225]
  • The example WS2812B timing map is T0H = 16, T1H = 32, T0L = 34, T1L = 18, derived from dividing target pulse widths by 25 ns. [#21502225]
  • One WS2812B pixel needs 24 bits, so the transmit buffer uses 24 rmt_item32_t objects for one RGB LED. [#21502225]
  • The demo generalizes output to N bytes, building an 8 * numBytes RMT buffer so the same driver can handle longer strips or RGBW data layouts. [#21502225]
  • The oscilloscope check uses Single mode, pulse trigger, and cursor measurements to confirm the generated pulse widths match the intended timings with negligible observed error. [#21502225]

What is the ESP32 Remote Control Transceiver (RMT) module, and what kinds of signals is it designed to generate or receive?

The ESP32 RMT module is special hardware for precise timing signals. It can both generate and receive control waveforms such as IR and RF signals, and this thread uses it to drive WS2812B LEDs that need tightly timed pulses on the DI input. "RMT is a hardware peripheral that generates or captures timed pulses, with cycle-based timing control." That makes it useful when software bit-banging would be less reliable. [#21502225]

What is the rmt_item32_t structure in ESP32 RMT, and how do its high and low timing fields translate into an output waveform?

rmt_item32_t is the basic ESP32 RMT data unit for one pulse pair. Each object stores one high state and one low state, including both the logic level and the duration in clock cycles. In the example, {{{50, 1, 50, 0}}} means 50 cycles high at logic 1, then 50 cycles low at logic 0. With 25 ns per cycle, that produces 1.25 µs high and 1.25 µs low. [#21502225]

How do I initialize the ESP32 RMT peripheral in transmit mode for driving a WS2812B LED on a GPIO pin?

You initialize it by configuring TX mode, channel, GPIO, memory blocks, TX behavior, and clock divider. 1. Set RMT_MODE_TX, choose a channel such as RMT_CHANNEL_0, and assign a pin like GPIO_NUM_4. 2. Set mem_block_num = 3, disable loop and carrier, and keep idle output enabled with low idle level. 3. Set clk_div = 2, then call rmt_config(&config) and rmt_driver_install(config.channel, 0, 0). [#21502225]

How do you calculate WS2812B timing values like T0H, T1H, T0L, and T1L from the ESP32 RMT clock and clock divider settings?

You calculate them by converting the LED timing from microseconds to RMT clock cycles. The thread uses APB_CLK = 80 MHz and clk_div = 2, so the RMT clock is 40 MHz and 1 cycle = 25 ns. For example, T0H = 0.4 µs / 25 ns = 16 cycles. The posted values are T0H 16, T1H 32, T0L 34, and T1L 18, all based on that 25 ns cycle time. [#21502225]

Why does an ESP32 sketch that calls my_send(255, 0, 0) make a WS2812B LED light green instead of red?

It lights green because the LED expects GRB, not RGB, byte order. The thread shows my_send(255, 0, 0) flashing green and states that this is not an error. In that data layout, the first 8 bits map to green, the next 8 bits to red, and the final 8 bits to blue. If you want red, you must place the 255 value in the red position for the LED’s expected order. [#21502225]

How do I send a single WS2812B pixel from an ESP32 using RMT when the LED expects 24-bit GRB data?

Build a buffer of 24 rmt_item32_t entries, one for each bit, then write it through RMT. The example loops through 8 bits per color channel and assigns either the T1H/T1L waveform or the T0H/T0L waveform depending on whether each bit is 1 or 0. It then calls rmt_write_items(...) and waits with rmt_wait_tx_done(...). The key point is to serialize the pixel as 24-bit GRB data, not plain RGB. [#21502225]

What is the best way to extend an ESP32 RMT WS2812 driver from one LED to an entire strip or to RGBW LEDs?

The best extension is to send N bytes, not hard-code one pixel format. The thread replaces the single-pixel function with my_send(byte *data, int numBytes) and builds an RMT array sized to *8 numBytes. That lets the code handle any byte stream, including long RGB strips or RGBW** devices, because the byte-to-pulse conversion does not depend on what each byte means. You only change the input buffer layout. [#21502225]

How can I build a rainbow animation on a WS2812B strip with ESP32 RMT using either simple byte patterns or HSV-to-RGB conversion?

You can animate either by rotating simple byte patterns or by converting HSV to RGB for smoother color changes. The simple version fills a 30-byte buffer for 10 LEDs, offsetting color values with step. The improved version computes hue = fmod((step + i * 20) / 200.0, 1.0), converts HSV to RGB, and stores three bytes per pixel before sending them through the same RMT function. Both methods reuse one transmit path. [#21502225]

Why is the delay between WS2812B transactions important when using ESP32 RMT, and how should the reset/latch time be handled?

The delay matters because the thread explicitly warns that you must wait between transactions. The code finishes transmission with rmt_wait_tx_done(...), then leaves a TODO note to ensure spacing before the next frame. Without that gap, the LED may not interpret the previous 24-bit or multi-byte frame cleanly, which can cause unstable color updates or missed latching. In practice here, treat the end-of-frame pause as part of the protocol, not an optional delay. [#21502225]

How do I verify ESP32 RMT pulse timings on an oscilloscope using single trigger mode and cursor measurements?

Use single-shot capture and measure one transmitted burst. 1. Connect the oscilloscope to the RMT output pin and set Single mode. 2. Use a pulse trigger so the scope catches the moment the ESP32 sends data. 3. Use the Cursor function to measure pulse widths and compare them with your intended cycle counts, such as 50 cycles = 1.25 µs at 25 ns per cycle. The thread reports negligible observed error. [#21502225]

ESP32 RMT vs SPI with DMA for WS2812 control — which approach is better for timing accuracy, flexibility, and code complexity?

In this thread, RMT is the better choice for direct timing control and waveform experiments. RMT exposes pulse timing as explicit high and low cycle counts, so you can map values like 16, 32, 34, and 18 directly to WS2812B timing. SPI with DMA is presented only as an earlier alternative that uses MOSI to generate timings. That suggests SPI can work, but RMT gives clearer pulse-level control in the code shown here. [#21502225]

What does the ESP32 RMT clock divider do, and how does APB_CLK at 80 MHz affect pulse timing resolution?

The clock divider scales the RMT timing base, which sets your pulse resolution. With the default APB_CLK at 80 MHz, each raw clock period is 12.5 ns. The example sets clk_div = 2, so the RMT runs at 40 MHz and each timing unit becomes 25 ns. That means a duration field of 50 produces 50 × 25 ns = 1.25 µs. Smaller dividers increase resolution; larger dividers make each timing step coarser. [#21502225]

How do I choose the number of RMT memory blocks on ESP32 when transmitting longer LED data streams?

Choose enough memory blocks for the amount of pulse data you need to buffer. The initialization example sets mem_block_num = 3, which is a practical starting point for TX use in this project. Longer LED streams create larger RMT item arrays, because the generalized sender allocates 8 pulse items per byte. If your strip data grows, memory pressure and buffering matter more, so block count becomes part of scaling the design cleanly. [#21502225]

What common mistakes cause WS2812B control problems on ESP32 RMT, such as wrong color order, incorrect timings, or unstable output?

Three common mistakes are wrong byte order, wrong timing conversion, and missing spacing between frames. The thread shows a clear color-order trap: my_send(255, 0, 0) appears green because the LED expects GRB. It also derives pulse constants from 25 ns timing steps, so a bad divider or bad math breaks the waveform. Finally, the code warns to wait between transactions; skipping that can cause unstable updates or failed latching. [#21502225]

How can I combine WiFiManager, Arduino OTA, and an ESP32 RMT-based WS2812 driver in one Arduino or PlatformIO project without timing issues?

Use a simple loop that keeps OTA responsive and sends LED frames in controlled intervals. The project initializes WiFiManager, starts ArduinoOTA, and toggles state in loop() while calling ArduinoOTA.handle() and delay(1). LED updates run on a counter, such as every 1000 loop ticks, rather than continuously. That structure keeps Wi‑Fi and OTA active while leaving room for timed RMT transmissions, especially if you also respect the required wait between WS2812B transactions. [#21502225]
Generated by the language model.
%}