WS2812 RMT Driver
API reference
Loading...
Searching...
No Matches
rmt_wrapper.hpp
Go to the documentation of this file.
1// rmt_wrapper.hpp — High‑level C++ RAII wrapper for the ESP‑IDF v5.3 RMT driver
2// on ESP32‑C6 Copyright (c) 2025 Nebiyu Tadesse SPDX‑License‑Identifier:
3// GPL‑3.0-or-later
9#pragma once
10
11#include "esp_idf_version.h"
12
13#if !defined(ESP_IDF_VERSION)
14#error "ESP_IDF_VERSION macro is not defined. Is <esp_idf_version.h> included?"
15#elif ESP_IDF_VERSION_MAJOR < 5
16#error "This wrapper requires ESP‑IDF v5.0 or newer (uses the new RMT driver)."
17#endif
18
19#include <array>
20#include <atomic>
21#include <cstddef>
22#include <cstdint>
23#include <type_traits>
24#include <utility>
25
26#include "esp_check.h"
27#include "esp_log.h"
28#include "esp_private/esp_clk.h"
29
30#include "driver/rmt_encoder.h"
31#include "driver/rmt_rx.h"
32#include "driver/rmt_tx.h"
33#include "freertos/FreeRTOS.h"
34#include "freertos/queue.h"
35#include "ws2812_rmt_caps.h"
36
37namespace ws2812 {
38
39namespace detail {
40[[nodiscard]] inline esp_err_t log_if_error(esp_err_t err, const char* tag, const char* msg) {
41 if (err != ESP_OK) {
42 ESP_LOGE(tag, "%s: %s", msg, esp_err_to_name(err));
43 }
44 return err;
45}
46
47} // namespace detail
48
49//=============================================================================
50// RmtTx — Transmit‑only wrapper
51//=============================================================================
52class RmtTx {
53public:
56
58 static constexpr int32_t kWaitForever = -1;
59
66 static constexpr int32_t TicksToTimeoutMs(TickType_t ticks,
67 uint32_t tick_rate_hz = configTICK_RATE_HZ) {
68 if (ticks == portMAX_DELAY) {
69 return kWaitForever;
70 }
71 const uint64_t ms = static_cast<uint64_t>(ticks) * 1000U / tick_rate_hz;
72 return ms > static_cast<uint64_t>(INT32_MAX) ? INT32_MAX : static_cast<int32_t>(ms);
73 }
74
87 explicit RmtTx(gpio_num_t gpio, uint32_t resolution_hz = 10'000'000,
88 size_t mem_symbols = kDefaultMemSymbols, bool with_dma = false,
89 uint32_t queue_depth = 4, rmt_clock_source_t clk_src = RMT_CLK_SRC_DEFAULT,
90 int channel_id = -1) {
91 // ESP-IDF v5's RMT driver allocates channels itself: channel_id cannot be honoured.
92 (void)channel_id;
93 cfg_.gpio_num = gpio;
94 cfg_.clk_src = clk_src;
95 cfg_.mem_block_symbols = mem_symbols;
96 cfg_.resolution_hz = resolution_hz;
97 cfg_.trans_queue_depth = queue_depth;
98 cfg_.flags.invert_out = false;
99 cfg_.flags.with_dma = with_dma;
100 (void)Init(); // the result is kept in InitStatus(); no abort on failure
101 }
102
103 RmtTx(const RmtTx&) = delete;
104 RmtTx& operator=(const RmtTx&) = delete;
105
106 RmtTx(RmtTx&& other) noexcept {
107 *this = std::move(other);
108 }
109 RmtTx& operator=(RmtTx&& other) noexcept {
110 std::swap(cfg_, other.cfg_);
111 std::swap(init_err_, other.init_err_);
112 std::swap(handle_, other.handle_);
113 std::swap(copy_encoder_, other.copy_encoder_);
114 std::swap(ws_encoder_, other.ws_encoder_);
115 return *this;
116 }
117
119 Release();
120 }
121
122 //-------------------------------------------------------------------------
132 esp_err_t Init() {
133 constexpr char TAG[] = "RmtTx";
134 if (handle_ != nullptr) {
135 return ESP_OK;
136 }
137 rmt_channel_handle_t channel = nullptr;
138 esp_err_t err = detail::log_if_error(rmt_new_tx_channel(&cfg_, &channel), TAG,
139 "failed to create TX channel");
140 if (err == ESP_OK) {
141 handle_ = channel;
142 }
143 if (err == ESP_OK) {
144 rmt_copy_encoder_config_t copy_cfg = {};
145 err = detail::log_if_error(rmt_new_copy_encoder(&copy_cfg, &copy_encoder_), TAG,
146 "failed to create copy encoder");
147 }
148 if (err == ESP_OK) {
149 err = detail::log_if_error(rmt_enable(handle_), TAG, "enable TX");
150 }
151 if (err != ESP_OK) {
152 Release();
153 }
154 init_err_ = err;
155 return err;
156 }
157
159 esp_err_t InitStatus() const {
160 return init_err_;
161 }
162
164 bool IsReady() const {
165 return handle_ != nullptr;
166 }
167
168 //-------------------------------------------------------------------------
175 esp_err_t Transmit(const rmt_symbol_word_t* symbols, size_t count,
176 int32_t timeout_ms = kWaitForever) const {
177 if (handle_ == nullptr) {
178 return ESP_ERR_INVALID_STATE;
179 }
180 rmt_transmit_config_t tx_cfg = {};
181 tx_cfg.loop_count = 0;
182 esp_err_t err =
183 rmt_transmit(handle_, copy_encoder_, symbols, count * sizeof(rmt_symbol_word_t), &tx_cfg);
184 if (err != ESP_OK)
185 return err;
186 return rmt_tx_wait_all_done(handle_, timeout_ms);
187 }
188
190 template <typename Ticks, std::enable_if_t<std::is_same_v<Ticks, TickType_t>, int> = 0>
191 [[deprecated("RmtTx timeouts are int32_t milliseconds (-1 = forever)")]] esp_err_t Transmit(
192 const rmt_symbol_word_t* symbols, size_t count, Ticks timeout) const {
193 return Transmit(symbols, count, TicksToTimeoutMs(timeout));
194 }
195
196 //-------------------------------------------------------------------------
204 esp_err_t TransmitBytes(const uint8_t* data, size_t length, const rmt_symbol_word_t& bit0,
205 const rmt_symbol_word_t& bit1, int32_t timeout_ms = kWaitForever) {
206 if (handle_ == nullptr) {
207 return ESP_ERR_INVALID_STATE;
208 }
209 // Create a throw‑away bytes encoder (kept only for this call)
210 rmt_bytes_encoder_config_t be_cfg = {};
211 be_cfg.bit0 = bit0;
212 be_cfg.bit1 = bit1;
213 be_cfg.flags.msb_first = true;
214 rmt_encoder_handle_t bytes_enc = nullptr;
215 ESP_RETURN_ON_ERROR(rmt_new_bytes_encoder(&be_cfg, &bytes_enc), "RmtTx", "new bytes enc");
216
217 rmt_transmit_config_t tx_cfg = {};
218 tx_cfg.loop_count = 0;
219 esp_err_t err = rmt_transmit(handle_, bytes_enc, data, length, &tx_cfg);
220 if (err == ESP_OK)
221 err = rmt_tx_wait_all_done(handle_, timeout_ms);
222 rmt_del_encoder(bytes_enc);
223 return err;
224 }
225
227 template <typename Ticks, std::enable_if_t<std::is_same_v<Ticks, TickType_t>, int> = 0>
228 [[deprecated("RmtTx timeouts are int32_t milliseconds (-1 = forever)")]] esp_err_t TransmitBytes(
229 const uint8_t* data, size_t length, const rmt_symbol_word_t& bit0,
230 const rmt_symbol_word_t& bit1, Ticks timeout) {
231 return TransmitBytes(data, length, bit0, bit1, TicksToTimeoutMs(timeout));
232 }
233
234 //-------------------------------------------------------------------------
241 esp_err_t TransmitWs2812(const uint8_t* grb, size_t length, int32_t timeout_ms = kWaitForever) {
242 if (handle_ == nullptr) {
243 return ESP_ERR_INVALID_STATE;
244 }
245 if (!ws_encoder_) {
246 rmt_bytes_encoder_config_t ws_cfg = {};
247 ws_cfg.bit0 = makeWs2812Bit0();
248 ws_cfg.bit1 = makeWs2812Bit1();
249 ws_cfg.flags.msb_first = true;
250 ESP_RETURN_ON_ERROR(rmt_new_bytes_encoder(&ws_cfg, &ws_encoder_), "RmtTx", "new ws2812 enc");
251 }
252 rmt_transmit_config_t tx_cfg = {};
253 tx_cfg.loop_count = 0;
254 esp_err_t err = rmt_transmit(handle_, ws_encoder_, grb, length, &tx_cfg);
255 if (err == ESP_OK)
256 err = rmt_tx_wait_all_done(handle_, timeout_ms);
257 return err;
258 }
259
261 template <typename Ticks, std::enable_if_t<std::is_same_v<Ticks, TickType_t>, int> = 0>
262 [[deprecated("RmtTx timeouts are int32_t milliseconds (-1 = forever)")]] esp_err_t TransmitWs2812(
263 const uint8_t* grb, size_t length, Ticks timeout) {
264 return TransmitWs2812(grb, length, TicksToTimeoutMs(timeout));
265 }
266
267 rmt_channel_handle_t Handle() const {
268 return handle_;
269 }
270
271private:
272 void Release() {
273 if (ws_encoder_) {
274 rmt_del_encoder(ws_encoder_);
275 ws_encoder_ = nullptr;
276 }
277 if (copy_encoder_) {
278 rmt_del_encoder(copy_encoder_);
279 copy_encoder_ = nullptr;
280 }
281 if (handle_) {
282 rmt_disable(handle_);
283 rmt_del_channel(handle_);
284 handle_ = nullptr;
285 }
286 }
287
288 static constexpr rmt_symbol_word_t makeWs2812Bit0(uint32_t resolution_hz = 10'000'000) {
289 // T0H=0.4 μs, T0L≈0.85 μs
290 uint32_t ticks_h = (400'000'000ULL / resolution_hz + 9) / 10; // round‑nearest
291 uint32_t ticks_l = (850'000'000ULL / resolution_hz + 9) / 10;
292 return {
293 .duration0 = (uint16_t)ticks_h, .level0 = 1, .duration1 = (uint16_t)ticks_l, .level1 = 0};
294 }
295
296 static constexpr rmt_symbol_word_t makeWs2812Bit1(uint32_t resolution_hz = 10'000'000) {
297 // T1H=0.8 μs, T1L≈0.45 μs
298 uint32_t ticks_h = (800'000'000ULL / resolution_hz + 9) / 10;
299 uint32_t ticks_l = (450'000'000ULL / resolution_hz + 9) / 10;
300 return {
301 .duration0 = (uint16_t)ticks_h, .level0 = 1, .duration1 = (uint16_t)ticks_l, .level1 = 0};
302 }
303
304 rmt_tx_channel_config_t cfg_ = {};
305 esp_err_t init_err_ = ESP_ERR_INVALID_STATE;
306 rmt_channel_handle_t handle_ = nullptr;
307 rmt_encoder_handle_t copy_encoder_ = nullptr;
308 rmt_encoder_handle_t ws_encoder_ = nullptr;
309};
310
311//=============================================================================
312// RmtRx — Receive‑only wrapper
313//=============================================================================
314class RmtRx {
315public:
316 explicit RmtRx(gpio_num_t gpio, uint32_t resolution_hz = 10'000'000, size_t mem_symbols = 64,
317 uint32_t idle_threshold_us = 1000, uint32_t filter_ns = 200,
318 rmt_clock_source_t clk_src = RMT_CLK_SRC_DEFAULT, int channel_id = -1) {
319 constexpr char TAG[] = "RmtRx";
320
321 if (channel_id < 0) {
322 channel_id = 0;
323 }
324
325 rmt_rx_channel_config_t cfg = {};
326 cfg.gpio_num = gpio;
327 cfg.clk_src = clk_src;
328 cfg.mem_block_symbols = mem_symbols;
329 cfg.resolution_hz = resolution_hz;
330 cfg.flags.invert_in = false;
331 ESP_ERROR_CHECK(detail::log_if_error(rmt_new_rx_channel(&cfg, &_handle), TAG, "create RX"));
332
333 rmt_rx_event_callbacks_t cbs = {};
334 cbs.on_recv_done = &RmtRx::rx_done_cb_static;
335 ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(_handle, &cbs, this));
336 ESP_ERROR_CHECK(rmt_enable(_handle));
337
338 // Create queue for ISR -> task communication
339 _queue = xQueueCreate(4, sizeof(size_t));
340
341 // Configure RX receive behavior (can be changed later)
342 _rcv_cfg.signal_range_max_ns = idle_threshold_us * 1000ULL;
343 _rcv_cfg.signal_range_min_ns = filter_ns;
344 }
345
346 RmtRx(const RmtRx&) = delete;
347 RmtRx& operator=(const RmtRx&) = delete;
348 RmtRx(RmtRx&& o) noexcept {
349 *this = std::move(o);
350 }
351 RmtRx& operator=(RmtRx&& o) noexcept {
352 std::swap(_handle, o._handle);
353 std::swap(_queue, o._queue);
354 std::swap(_rcv_cfg, o._rcv_cfg);
355 return *this;
356 }
357
359 if (_handle)
360 rmt_disable(_handle), rmt_del_channel(_handle);
361 if (_queue)
362 vQueueDelete(_queue);
363 }
364
370 esp_err_t receive(rmt_symbol_word_t* buffer, size_t buffer_symbols, size_t* out_symbols,
371 TickType_t timeout = portMAX_DELAY) {
372 if (!buffer || !out_symbols)
373 return ESP_ERR_INVALID_ARG;
374
375 esp_err_t err =
376 rmt_receive(_handle, buffer, buffer_symbols * sizeof(rmt_symbol_word_t), &_rcv_cfg);
377 if (err != ESP_OK)
378 return err;
379
380 // Wait until ISR posts the symbol count
381 if (xQueueReceive(_queue, out_symbols, timeout) != pdPASS) {
382 return ESP_ERR_TIMEOUT;
383 }
384 return ESP_OK;
385 }
386
387 rmt_channel_handle_t handle() const {
388 return _handle;
389 }
390
391private:
392 static bool rx_done_cb_static(rmt_channel_handle_t /*chan*/,
393 const rmt_rx_done_event_data_t* edata, void* user_ctx) {
394 auto* self = static_cast<RmtRx*>(user_ctx);
395 BaseType_t HPW = pdFALSE;
396 xQueueSendFromISR(self->_queue, &edata->num_symbols, &HPW);
397 return HPW == pdTRUE; // context switch request
398 }
399
400 rmt_channel_handle_t _handle = nullptr;
401 QueueHandle_t _queue = nullptr;
402 rmt_receive_config_t _rcv_cfg = {};
403};
404
405} // namespace ws2812
Definition rmt_wrapper.hpp:314
RmtRx(RmtRx &&o) noexcept
Definition rmt_wrapper.hpp:348
RmtRx(const RmtRx &)=delete
RmtRx & operator=(const RmtRx &)=delete
esp_err_t receive(rmt_symbol_word_t *buffer, size_t buffer_symbols, size_t *out_symbols, TickType_t timeout=portMAX_DELAY)
Start a single receive transaction and block until it completes.
Definition rmt_wrapper.hpp:370
RmtRx & operator=(RmtRx &&o) noexcept
Definition rmt_wrapper.hpp:351
~RmtRx()
Definition rmt_wrapper.hpp:358
RmtRx(gpio_num_t gpio, uint32_t resolution_hz=10 '000 '000, size_t mem_symbols=64, uint32_t idle_threshold_us=1000, uint32_t filter_ns=200, rmt_clock_source_t clk_src=RMT_CLK_SRC_DEFAULT, int channel_id=-1)
Definition rmt_wrapper.hpp:316
rmt_channel_handle_t handle() const
Definition rmt_wrapper.hpp:387
Definition rmt_wrapper.hpp:52
esp_err_t Transmit(const rmt_symbol_word_t *symbols, size_t count, int32_t timeout_ms=kWaitForever) const
Transmit raw RMT symbols (blocking until done).
Definition rmt_wrapper.hpp:175
esp_err_t Transmit(const rmt_symbol_word_t *symbols, size_t count, Ticks timeout) const
Definition rmt_wrapper.hpp:191
esp_err_t Init()
Create and enable the channel if that has not succeeded yet.
Definition rmt_wrapper.hpp:132
esp_err_t InitStatus() const
Result of the last Init(): ESP_OK when the channel is ready.
Definition rmt_wrapper.hpp:159
RmtTx(RmtTx &&other) noexcept
Definition rmt_wrapper.hpp:106
esp_err_t TransmitWs2812(const uint8_t *grb, size_t length, int32_t timeout_ms=kWaitForever)
Helper dedicated to WS2812/NeoPixel @ 800 kHz. Uses 10 MHz or faster resolution to achieve accurate t...
Definition rmt_wrapper.hpp:241
~RmtTx()
Definition rmt_wrapper.hpp:118
static constexpr int32_t kWaitForever
Definition rmt_wrapper.hpp:58
RmtTx & operator=(const RmtTx &)=delete
bool IsReady() const
True when the channel was created and enabled.
Definition rmt_wrapper.hpp:164
RmtTx(gpio_num_t gpio, uint32_t resolution_hz=10 '000 '000, size_t mem_symbols=kDefaultMemSymbols, bool with_dma=false, uint32_t queue_depth=4, rmt_clock_source_t clk_src=RMT_CLK_SRC_DEFAULT, int channel_id=-1)
Create a Transmit channel.
Definition rmt_wrapper.hpp:87
rmt_channel_handle_t Handle() const
Definition rmt_wrapper.hpp:267
RmtTx & operator=(RmtTx &&other) noexcept
Definition rmt_wrapper.hpp:109
RmtTx(const RmtTx &)=delete
static constexpr size_t kDefaultMemSymbols
Definition rmt_wrapper.hpp:55
esp_err_t TransmitBytes(const uint8_t *data, size_t length, const rmt_symbol_word_t &bit0, const rmt_symbol_word_t &bit1, Ticks timeout)
Definition rmt_wrapper.hpp:228
esp_err_t TransmitWs2812(const uint8_t *grb, size_t length, Ticks timeout)
Definition rmt_wrapper.hpp:262
esp_err_t TransmitBytes(const uint8_t *data, size_t length, const rmt_symbol_word_t &bit0, const rmt_symbol_word_t &bit1, int32_t timeout_ms=kWaitForever)
Transmit an arbitrary byte stream with custom bit timing.
Definition rmt_wrapper.hpp:204
static constexpr int32_t TicksToTimeoutMs(TickType_t ticks, uint32_t tick_rate_hz=configTICK_RATE_HZ)
Convert a FreeRTOS tick count to the millisecond timeout the transmit calls take.
Definition rmt_wrapper.hpp:66
esp_err_t log_if_error(esp_err_t err, const char *tag, const char *msg)
Definition rmt_wrapper.hpp:40
Definition rmt_wrapper.hpp:37
RMT resources the driver requests per strip (shared by the C and C++ APIs).
#define WS2812_RMT_MEM_BLOCK_SYMBOLS
Definition ws2812_rmt_caps.h:18