/*
TasmotaLEDPusherRMT.cpp - Implementation to push Leds via RMT channel
Copyright (C) 2024 Stephan Hadinger
This library is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#ifdef ESP32
#include "TasmotaLEDPusher.h"
#include "TasmotaLED.h"
#if TASMOTALED_HARDWARE_RMT
#include
#include
//**************************************************************************************************************
// enable AddLog support within a C++ library
extern void AddLog(uint32_t loglevel, PGM_P formatP, ...);
enum LoggingLevels {LOG_LEVEL_NONE, LOG_LEVEL_ERROR, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG, LOG_LEVEL_DEBUG_MORE};
//**************************************************************************************************************
/*******************************************************************************************\
* Implementation for TasmotaLEDPusherRMT
*
* Code mostly copied from Tasmota patch to NeoPixelBus applied to support esp-idf 5.x
* itself inspired from esp-idf example for RMT encoder from
* https://github.com/espressif/esp-idf/tree/v5.3.1/examples/peripherals/rmt/ir_nec_transceiver
\*******************************************************************************************/
#define RMT_LED_STRIP_RESOLUTION_HZ 40000000 // 40MHz resolution, steps of 25 nanoseconds
// structure used to pass arguments to `rmt_new_led_strip_encoder`
// currently only the encoder resolution in Hz
typedef struct {
uint32_t resolution; /*!< Encoder resolution, in Hz */
} led_strip_encoder_config_t;
// structure used to store all the necessary information for the RMT encoder
typedef struct {
rmt_encoder_t base;
rmt_encoder_t *bytes_encoder;
rmt_encoder_t *copy_encoder;
int32_t state;
rmt_symbol_word_t reset_code;
} rmt_led_strip_encoder_t;
static IRAM_ATTR size_t rmt_encode_led_strip(rmt_encoder_t *encoder, rmt_channel_handle_t channel, const void *primary_data, size_t data_size, rmt_encode_state_t *ret_state)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_encoder_handle_t bytes_encoder = led_encoder->bytes_encoder;
rmt_encoder_handle_t copy_encoder = led_encoder->copy_encoder;
rmt_encode_state_t session_state = RMT_ENCODING_RESET;
rmt_encode_state_t state = RMT_ENCODING_RESET;
size_t encoded_symbols = 0;
switch (led_encoder->state) {
case 0: // send RGB data
encoded_symbols += bytes_encoder->encode(bytes_encoder, channel, primary_data, data_size, &session_state);
if (session_state & RMT_ENCODING_COMPLETE) {
led_encoder->state = 1; // switch to next state when current encoding session finished
}
if (session_state & RMT_ENCODING_MEM_FULL) {
state = static_cast(static_cast(state) | static_cast(RMT_ENCODING_MEM_FULL));
goto out; // yield if there's no free space for encoding artifacts
}
// fall-through
case 1: // send reset code
encoded_symbols += copy_encoder->encode(copy_encoder, channel, &led_encoder->reset_code, sizeof(led_encoder->reset_code), &session_state);
if (session_state & RMT_ENCODING_COMPLETE) {
led_encoder->state = RMT_ENCODING_RESET; // back to the initial encoding session
state = static_cast(static_cast(state) | static_cast(RMT_ENCODING_COMPLETE));
}
if (session_state & RMT_ENCODING_MEM_FULL) {
state = static_cast(static_cast(state) | static_cast(RMT_ENCODING_MEM_FULL));
goto out; // yield if there's no free space for encoding artifacts
}
}
out:
*ret_state = state;
return encoded_symbols;
}
static esp_err_t rmt_del_led_strip_encoder(rmt_encoder_t *encoder) {
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_del_encoder(led_encoder->bytes_encoder);
rmt_del_encoder(led_encoder->copy_encoder);
delete led_encoder;
return ESP_OK;
}
static esp_err_t rmt_led_strip_encoder_reset(rmt_encoder_t *encoder) {
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_encoder_reset(led_encoder->bytes_encoder);
rmt_encoder_reset(led_encoder->copy_encoder);
led_encoder->state = RMT_ENCODING_RESET;
return ESP_OK;
}
static esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder, rmt_symbol_word_t bit0, rmt_symbol_word_t bit1, rmt_symbol_word_t reset_code) {
static const char* TAG = "TASMOTA_RMT";
esp_err_t ret = ESP_OK;
rmt_led_strip_encoder_t *led_encoder = NULL;
rmt_bytes_encoder_config_t bytes_encoder_config;
rmt_copy_encoder_config_t copy_encoder_config = {};
ESP_GOTO_ON_FALSE(config && ret_encoder, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
led_encoder = new rmt_led_strip_encoder_t();
ESP_GOTO_ON_FALSE(led_encoder, ESP_ERR_NO_MEM, err, TAG, "no mem for led strip encoder");
led_encoder->base.encode = rmt_encode_led_strip;
led_encoder->base.del = rmt_del_led_strip_encoder;
led_encoder->base.reset = rmt_led_strip_encoder_reset;
led_encoder->reset_code = reset_code;
bytes_encoder_config.bit0 = bit0;
bytes_encoder_config.bit1 = bit1;
bytes_encoder_config.flags.msb_first = 1; // WS2812 transfer bit order: G7...G0R7...R0B7...B0 - TODO: more checks
ESP_GOTO_ON_ERROR(rmt_new_bytes_encoder(&bytes_encoder_config, &led_encoder->bytes_encoder), err, TAG, "create bytes encoder failed");
ESP_GOTO_ON_ERROR(rmt_new_copy_encoder(©_encoder_config, &led_encoder->copy_encoder), err, TAG, "create copy encoder failed");
*ret_encoder = &led_encoder->base;
return ret;
err:
AddLog(LOG_LEVEL_INFO, "RMT: could not init led encoder");
if (led_encoder) {
if (led_encoder->bytes_encoder) { rmt_del_encoder(led_encoder->bytes_encoder); }
if (led_encoder->copy_encoder) { rmt_del_encoder(led_encoder->copy_encoder); }
delete led_encoder;
}
return ret;
}
TasmotaLEDPusherRMT::~TasmotaLEDPusherRMT() {
if (_channel) {
rmt_tx_wait_all_done(_channel, 10000 / portTICK_PERIOD_MS);
rmt_del_channel(_channel);
_channel = nullptr;
}
if (_pin >= 0) {
gpio_matrix_out(_pin, 0x100, false, false);
pinMode(_pin, INPUT);
_pin = -1;
}
}
TasmotaLEDPusherRMT::TasmotaLEDPusherRMT(int8_t pin) : _pin(pin) {
esp_err_t ret = ESP_OK;
rmt_tx_channel_config_t config = {};
config.clk_src = RMT_CLK_SRC_DEFAULT;
config.gpio_num = static_cast(_pin);
config.mem_block_symbols = 192; // memory block size, 64 * 4 = 256 Bytes
config.resolution_hz = RMT_LED_STRIP_RESOLUTION_HZ; // 40 MHz tick resolution, i.e., 1 tick = 0.025 µs or 25 ns
config.trans_queue_depth = 4; // set the number of transactions that can pend in the background
config.flags.invert_out = false; // do not invert output signal
config.flags.with_dma = false; // do not need DMA backend
_err = rmt_new_tx_channel(&config, &_channel);
if (_err == ESP_OK) {
_initialized = true;
}
}
bool TasmotaLEDPusherRMT::Begin(uint16_t pixel_count, uint16_t pixel_size, const TasmotaLED_Timing * led_timing) {
if (!_initialized) { return false; }
TasmotaLEDPusher::Begin(pixel_count, pixel_size, led_timing);
led_strip_encoder_config_t encoder_config = {
.resolution = RMT_LED_STRIP_RESOLUTION_HZ,
};
_tx_config.loop_count = 0; // no loop
rmt_symbol_word_t RmtBit0 = {
.duration0 = (uint16_t) (led_timing->T0H * (RMT_LED_STRIP_RESOLUTION_HZ / 1000000) / 1000),
.level0 = 1,
.duration1 = (uint16_t) (led_timing->T0L * (RMT_LED_STRIP_RESOLUTION_HZ / 1000000) / 1000),
.level1 = 0,
};
rmt_symbol_word_t RmtBit1 = {
.duration0 = (uint16_t) (led_timing->T1H * (RMT_LED_STRIP_RESOLUTION_HZ / 1000000) / 1000),
.level0 = 1,
.duration1 = (uint16_t) (led_timing->T1L * (RMT_LED_STRIP_RESOLUTION_HZ / 1000000) / 1000),
.level1 = 0,
};
rmt_symbol_word_t RmtReset = {
.duration0 = (uint16_t) (led_timing->Reset * (RMT_LED_STRIP_RESOLUTION_HZ / 1000000) / 1000),
.level0 = 0,
.duration1 = 50 * (RMT_LED_STRIP_RESOLUTION_HZ / 1000000) / 1000,
.level1 = 1,
};
// AddLog(LOG_LEVEL_INFO, "RMT: RmtBit0 0x%08X RmtBit1 0x%08X RmtReset 0x%08X", RmtBit0.val, RmtBit1.val, RmtReset.val);
_err = rmt_new_led_strip_encoder(&encoder_config, &_led_encoder, RmtBit0, RmtBit1, RmtReset);
if (_err != ESP_OK) {
// AddLog(LOG_LEVEL_INFO, "RMT: cannot initialize led strip encoder err=%i", ret);
return false;
}
_err = rmt_enable(_channel);
if (_err != ESP_OK) {
// AddLog(LOG_LEVEL_INFO, "RMT: cannot enable channel err=%i", ret);
return false;
}
return true;
}
bool TasmotaLEDPusherRMT::CanShow(void) {
if (_channel && _initialized) {
return (ESP_OK == rmt_tx_wait_all_done(_channel, 0));
} else {
return false;
}
}
bool TasmotaLEDPusherRMT::Push(uint8_t *buf) {
if (!_initialized) { return false; }
// wait for not actively sending data
// this will time out at 1 second, an arbitrarily long period of time
// and do nothing if this happens
esp_err_t ret = rmt_tx_wait_all_done(_channel, 1000 / portTICK_PERIOD_MS);
if (ESP_OK == ret) {
// now start the RMT transmit with the editing buffer before we swap
ret = rmt_transmit(_channel, _led_encoder, buf, _pixel_count * _pixel_size, &_tx_config);
if (ESP_OK != ret) {
AddLog(LOG_LEVEL_DEBUG, "RMT: cannot transmit err=%i", ret);
return false;
}
}
return true;
}
#endif // TASMOTALED_HARDWARE_RMT
#endif // ESP32