Skip to content

Commit

Permalink
Merge branch 'feature/rmt_simple_encoder' into 'master'
Browse files Browse the repository at this point in the history
RMT simple encoder

See merge request espressif/esp-idf!29874
  • Loading branch information
suda-morris committed Apr 15, 2024
2 parents 3cca8ff + 0078680 commit c5389ac
Show file tree
Hide file tree
Showing 7 changed files with 479 additions and 0 deletions.
67 changes: 67 additions & 0 deletions components/esp_driver_rmt/include/driver/rmt_encoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,47 @@ struct rmt_encoder_t {
esp_err_t (*del)(rmt_encoder_t *encoder);
};

/**
* @brief Callback for simple callback encoder
*
* This will get called to encode the data stream of given length (as passed to
* rmt_transmit by the user) into symbols to be sent by the hardware.
*
* The callback will be initially called with symbol_pos=0. If the callback
* encodes N symbols and finishes, the next callback will always be with
* symbols_written=N. If the callback then encodes M symbols, the next callback
* will always be with symbol_pos=N+M, etc. The only exception is when the
* encoder is reset (e.g. to begin a new transaction) in which case
* symbol_pos will always restart at 0.
*
* If the amount of free space in the symbol buffer (as indicated by
* symbols_free) is too low, the function can return 0 as result and
* the RMT will call the function again once there is more space available.
* Note that the callback should eventually return non-0 if called with
* free space of rmt_simple_encoder_config_t::min_chunk_size or more. It
* is acceptable to return 0 for a given free space N, then on the next
* call (possibly with a larger free buffer space) return less or more
* than N symbols.
*
* When the transaction is done (all data_size data is encoded), the callback
* can indicate this by setting *done to true. This can either happen on the
* last callback call that returns an amount of symbols encoded, or on a
* callback that returns zero. In either case, the callback will not be
* called again for this transaction.
*
* @param[in] data Data pointer, as passed to rmt_transmit()
* @param[in] data_size Size of the data, as passed to rmt_transmit()
* @param[in] symbols_written Current position in encoded stream, in symbols
* @param[in] symbols_free The maximum amount of symbols that can be written into the `symbols` buffer
* @param[out] symbols Symbols to be sent to the RMT hardware
* @param[out] done Setting this to true marks this transaction as finished
* @param arg Opaque argument
* @return Amount of symbols encoded in this callback round. 0 if more space is needed.
*/
typedef size_t (*rmt_encode_simple_cb_t)(const void *data, size_t data_size,
size_t symbols_written, size_t symbols_free,
rmt_symbol_word_t *symbols, bool *done, void *arg);

/**
* @brief Bytes encoder configuration
*/
Expand All @@ -85,6 +126,18 @@ typedef struct {
typedef struct {
} rmt_copy_encoder_config_t;

/**
* @brief Simple callback encoder configuration
*/
typedef struct {
rmt_encode_simple_cb_t callback; /*!< Callback to call for encoding data into RMT items */
void *arg; /*!< Opaque user-supplied argument for callback */
size_t min_chunk_size; /*!< Minimum amount of free space, in RMT symbols, the
encoder needs in order to guarantee it always
returns non-zero. Defaults
to 64 if zero / not given. */
} rmt_simple_encoder_config_t;

/**
* @brief Create RMT bytes encoder, which can encode byte stream into RMT symbols
*
Expand Down Expand Up @@ -126,6 +179,20 @@ esp_err_t rmt_bytes_encoder_update_config(rmt_encoder_handle_t bytes_encoder, co
*/
esp_err_t rmt_new_copy_encoder(const rmt_copy_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder);

/**
* @brief Create RMT simple callback encoder, which uses a callback to convert incoming
* data into RMT symbols.
*
* @param[in] config Simple callback encoder configuration
* @param[out] ret_encoder Returned encoder handle
* @return
* - ESP_OK: Create RMT simple callback encoder successfully
* - ESP_ERR_INVALID_ARG: Create RMT simple callback encoder failed because of invalid argument
* - ESP_ERR_NO_MEM: Create RMT simple callback encoder failed because out of memory
* - ESP_FAIL: Create RMT simple callback encoder failed because of other error
*/
esp_err_t rmt_new_simple_encoder(const rmt_simple_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder);

/**
* @brief Delete RMT encoder
*
Expand Down
202 changes: 202 additions & 0 deletions components/esp_driver_rmt/src/rmt_encoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ typedef struct rmt_copy_encoder_t {
size_t last_symbol_index; // index of symbol position in the primary stream
} rmt_copy_encoder_t;

typedef struct rmt_simple_encoder_t {
rmt_encoder_t base; // encoder base class
size_t last_symbol_index; // index of symbol position in the primary stream
rmt_encode_simple_cb_t callback; //callback to call to encode
void *arg; // opaque callback argument
rmt_symbol_word_t *ovf_buf; //overflow buffer
size_t ovf_buf_size; //size, in elements, of overflow buffer
size_t ovf_buf_fill_len; //how much actual info the overflow buffer has
size_t ovf_buf_parsed_pos; //up to where we moved info from the ovf buf to the rmt
bool callback_done; //true if we can't call the callback for more data anymore.
} rmt_simple_encoder_t;

static esp_err_t rmt_bytes_encoder_reset(rmt_encoder_t *encoder)
{
rmt_bytes_encoder_t *bytes_encoder = __containerof(encoder, rmt_bytes_encoder_t, base);
Expand Down Expand Up @@ -242,6 +254,142 @@ static size_t IRAM_ATTR rmt_encode_copy(rmt_encoder_t *encoder, rmt_channel_hand
return encode_len;
}

static size_t IRAM_ATTR rmt_encode_simple(rmt_encoder_t *encoder, rmt_channel_handle_t channel,
const void *data, size_t data_size, rmt_encode_state_t *ret_state)
{
rmt_simple_encoder_t *simple_encoder = __containerof(encoder, rmt_simple_encoder_t, base);
rmt_tx_channel_t *tx_chan = __containerof(channel, rmt_tx_channel_t, base);
rmt_encode_state_t state = RMT_ENCODING_RESET;
rmt_dma_descriptor_t *desc0 = NULL;
rmt_dma_descriptor_t *desc1 = NULL;

// where to put the encoded symbols? DMA buffer or RMT HW memory
rmt_symbol_word_t *mem_to_nc = NULL;
if (channel->dma_chan) {
mem_to_nc = tx_chan->dma_mem_base_nc;
} else {
mem_to_nc = channel->hw_mem_base;
}

if (channel->dma_chan) {
// mark the start descriptor
if (tx_chan->mem_off < tx_chan->ping_pong_symbols) {
desc0 = &tx_chan->dma_nodes_nc[0];
} else {
desc0 = &tx_chan->dma_nodes_nc[1];
}
}

// While we're not done, we need to use the callback to fill the RMT memory until it is
// exactly entirely full. We cannot do that if the RMT memory still has N free spaces
// but the encoder callback needs more than N spaces to properly encode a symbol.
// In order to work around that, if we detect that situation we let the encoder
// encode into an overflow buffer, then we use the contents of that buffer to fill
// those last N spaces. On the next call, we will first output the rest of the
// overflow buffer before again using the callback to continue filling the RMT
// buffer.

// Note the next code is in a while loop to properly handle 'unsure' callbacks that
// e.g. return 0 with a free buffer size of M, but then return less than M symbols
// when then called with a larger buffer.
size_t encode_len = 0; //total amount of symbols written to rmt memory
bool is_done = false;
while (tx_chan->mem_off < tx_chan->mem_end) {
if (simple_encoder->ovf_buf_parsed_pos < simple_encoder->ovf_buf_fill_len) {
// Overflow buffer has data from the previous encoding call. Copy one entry
// from that.
mem_to_nc[tx_chan->mem_off++] = simple_encoder->ovf_buf[simple_encoder->ovf_buf_parsed_pos++];
encode_len++;
} else {
// Overflow buffer is empty, so we don't need to empty that first.

if (simple_encoder->callback_done) {
// We cannot call the callback anymore and the overflow buffer
// is empty, so we're done with the transaction.
is_done = true;
break;
}
// Try to have the callback write the data directly into RMT memory.
size_t enc_size = simple_encoder->callback(data, data_size,
simple_encoder->last_symbol_index,
tx_chan->mem_end - tx_chan->mem_off,
&mem_to_nc[tx_chan->mem_off],
&is_done, simple_encoder->arg);
encode_len += enc_size;
tx_chan->mem_off += enc_size;
simple_encoder->last_symbol_index += enc_size;
if (is_done) {
break; // we're done, no more data to write to RMT memory.
}
if (enc_size == 0) {
// The encoder does not have enough space in RMT memory to encode its thing,
// but the RMT memory is not filled out entirely. Encode into the overflow
// buffer so the next iterations of the loop can fill out the RMT buffer
// from that.
enc_size = simple_encoder->callback(data, data_size,
simple_encoder->last_symbol_index,
simple_encoder->ovf_buf_size,
simple_encoder->ovf_buf,
&is_done, simple_encoder->arg);
simple_encoder->last_symbol_index += enc_size;
//Note we do *not* update encode_len here as the data isn't going to the RMT yet.
simple_encoder->ovf_buf_fill_len = enc_size;
simple_encoder->ovf_buf_parsed_pos = 0;
if (is_done) {
// If the encoder is done, we cannot call the callback anymore, but we still
// need to handle any data in the overflow buffer.
simple_encoder->callback_done = true;
} else {
if (enc_size == 0) {
//According to the callback docs, this is illegal.
//Report this. EARLY_LOGE as we're running from an ISR.
ESP_EARLY_LOGE(TAG, "rmt_encoder_simple: encoder callback returned 0 when fed a buffer of config::min_chunk_size!");
//Then abort the transaction.
is_done = true;
break;
}
}
}
}
}

if (channel->dma_chan) {
// mark the end descriptor
if (tx_chan->mem_off < tx_chan->ping_pong_symbols) {
desc1 = &tx_chan->dma_nodes_nc[0];
} else {
desc1 = &tx_chan->dma_nodes_nc[1];
}

// cross line, means desc0 has prepared with sufficient data buffer
if (desc0 != desc1) {
desc0->dw0.length = tx_chan->ping_pong_symbols * sizeof(rmt_symbol_word_t);
desc0->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA;
}
}

if (is_done) {
// reset internal index if encoding session has finished
simple_encoder->last_symbol_index = 0;
state |= RMT_ENCODING_COMPLETE;
} else {
// no more free memory, the caller should yield
state |= RMT_ENCODING_MEM_FULL;
}

// reset offset pointer when exceeds maximum range
if (tx_chan->mem_off >= tx_chan->ping_pong_symbols * 2) {
if (channel->dma_chan) {
desc1->dw0.length = tx_chan->ping_pong_symbols * sizeof(rmt_symbol_word_t);
desc1->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA;
}
tx_chan->mem_off = 0;
}

*ret_state = state;
return encode_len;
}

static esp_err_t rmt_del_bytes_encoder(rmt_encoder_t *encoder)
{
rmt_bytes_encoder_t *bytes_encoder = __containerof(encoder, rmt_bytes_encoder_t, base);
Expand All @@ -256,6 +404,26 @@ static esp_err_t rmt_del_copy_encoder(rmt_encoder_t *encoder)
return ESP_OK;
}

static esp_err_t rmt_simple_encoder_reset(rmt_encoder_t *encoder)
{
rmt_simple_encoder_t *simple_encoder = __containerof(encoder, rmt_simple_encoder_t, base);
simple_encoder->last_symbol_index = 0;
simple_encoder->ovf_buf_fill_len = 0;
simple_encoder->ovf_buf_parsed_pos = 0;
simple_encoder->callback_done = false;
return ESP_OK;
}

static esp_err_t rmt_del_simple_encoder(rmt_encoder_t *encoder)
{
rmt_simple_encoder_t *simple_encoder = __containerof(encoder, rmt_simple_encoder_t, base);
if (simple_encoder) {
free(simple_encoder->ovf_buf);
}
free(simple_encoder);
return ESP_OK;
}

esp_err_t rmt_new_bytes_encoder(const rmt_bytes_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder)
{
esp_err_t ret = ESP_OK;
Expand Down Expand Up @@ -301,6 +469,40 @@ esp_err_t rmt_new_copy_encoder(const rmt_copy_encoder_config_t *config, rmt_enc
return ret;
}

esp_err_t rmt_new_simple_encoder(const rmt_simple_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder)
{
esp_err_t ret = ESP_OK;
rmt_simple_encoder_t *encoder = NULL;
ESP_GOTO_ON_FALSE(config && ret_encoder, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
encoder = rmt_alloc_encoder_mem(sizeof(rmt_simple_encoder_t));
ESP_GOTO_ON_FALSE(encoder, ESP_ERR_NO_MEM, err, TAG, "no mem for simple encoder");
encoder->base.encode = rmt_encode_simple;
encoder->base.del = rmt_del_simple_encoder;
encoder->base.reset = rmt_simple_encoder_reset;
encoder->callback = config->callback;
encoder->arg = config->arg;

size_t min_chunk_size = config->min_chunk_size;
if (min_chunk_size == 0) {
min_chunk_size = 64;
}
encoder->ovf_buf = rmt_alloc_encoder_mem(min_chunk_size * sizeof(rmt_symbol_word_t));
ESP_GOTO_ON_FALSE(encoder->ovf_buf, ESP_ERR_NO_MEM, err, TAG, "no mem for simple encoder overflow buffer");
encoder->ovf_buf_size = min_chunk_size;
encoder->ovf_buf_fill_len = 0;
encoder->ovf_buf_parsed_pos = 0;

// return general encoder handle
*ret_encoder = &encoder->base;
ESP_LOGD(TAG, "new simple encoder @%p", encoder);
return ret;
err:
if (encoder) {
free(encoder);
}
return ret;
}

esp_err_t rmt_del_encoder(rmt_encoder_handle_t encoder)
{
ESP_RETURN_ON_FALSE(encoder, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
Expand Down
12 changes: 12 additions & 0 deletions docs/en/api-reference/peripherals/rmt.rst
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,18 @@ Besides the primitive encoders provided by the driver, the user can implement hi
:caption: RMT Encoder Chain
:align: center

Simple Callback Encoder
~~~~~~~~~~~~~~~~~~~~~~~

A simple callback encoder is created by calling :cpp:func:`rmt_new_simple_encoder`. The simple callback encoder allows you to provide a callback that reads data from userspace and writes symbols to the output stream without having to chain other encoders. The callback itself gets a pointer to the data passed to :cpp:func:`rmt_transmit`, a counter indicating the amount of symbols already written by the callback in this transmission, and a pointer where to write the encoded RMT symbols as well as the free space there. If the space is not enough for the callback to encode something, it can return 0 and the RMT will wait for previous symbols to be transmitted and call the callback again, now with more space available. If the callback successfully writes RMT symbols, it should return the number of symbols written.

A configuration structure :cpp:type:`rmt_simple_encoder_config_t` should be provided in advance before calling :cpp:func:`rmt_new_simple_encoder`:

- :cpp:member:`rmt_simple_encoder_config_t::callback` and :cpp:member:`rmt_simple_encoder_config_t::arg` provide the callback function and an opaque argument that will be passed to that function.
- :cpp:member:`rmt_simple_encoder_config_t::min_chunk_size` specifies the minimum amount of free space, in symbols, the encoder will be always be able to write some data to. In other words, when this amount of free space is passed to the encoder, it should never return 0 (except when the encoder is done encoding symbols).

While the functionality of an encoding process using the simple callback encoder can usually also realized by chaining other encoders, the simple callback can be more easy to understand and maintain than an encoder chain.

Customize RMT Encoder for NEC Protocol
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(led_strip_simple_encoder)
Loading

0 comments on commit c5389ac

Please sign in to comment.