Merge branch 'feature/rmt_simple_encoder' into 'master'

RMT simple encoder

See merge request espressif/esp-idf!29874
This commit is contained in:
morris 2024-04-15 15:23:18 +08:00
commit c5389ac2fd
7 changed files with 479 additions and 0 deletions

View File

@ -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
*/
@ -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
*
@ -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
*

View File

@ -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);
@ -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);
@ -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;
@ -301,6 +469,40 @@ err:
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");

View File

@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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)

View File

@ -0,0 +1,56 @@
| Supported Targets | ESP32 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- |
# RMT Transmit Example -- LED Strip
(See the README.md file in the upper level 'examples' directory for more information about examples.)
Almost any waveform can be generated by RMT peripheral, as long as a proper encoder is implemented. In this example, the simple callback RMT encoder is used to convert RGB pixels into format that can be recognized by hardware.
This example shows how to drive an addressable LED strip [WS2812](https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf) by implementing a callback that can be used by the simple callback RMT encoder.
## How to Use Example
### Hardware Required
* A development board with any supported Espressif SOC chip (see `Supported Targets` table above)
* A USB cable for Power supply and programming
* A WS2812 LED strip
Connection :
```
--- 5V
|
+
RMT_LED_STRIP_GPIO_NUM +------ +---|>| (WS2812 LED strip)
DI +
|
--- GND
```
The GPIO number used in this example can be changed according to your board, by the macro `RMT_LED_STRIP_GPIO_NUM` defined in the [source file](main/led_strip_example_main.c). The number of LEDs can be changed as well by `EXAMPLE_LED_NUMBERS`.
### Build and Flash
Run `idf.py -p PORT flash monitor` to build, flash and monitor the project.
(To exit the serial monitor, type ``Ctrl-]``.)
See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects.
## Console Output
```
I (302) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
I (323) example: Create RMT TX channel
I (343) example: Create simple callback-based encoder
I (353) example: Start LED rainbow chase
```
After you seeing this log, you should see a rainbow chasing demonstration pattern. To change the chasing speed, you can update the `EXAMPLE_ANGLE_INC_FRAME` value in [source file](main/led_strip_example_main.c). To change the density of colors, you can change `EXAMPLE_ANGLE_INC_LED` in the same file.
## Troubleshooting
For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon.

View File

@ -0,0 +1,2 @@
idf_component_register(SRCS "led_strip_example_main.c"
INCLUDE_DIRS ".")

View File

@ -0,0 +1,134 @@
/*
* SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <string.h>
#include <math.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/rmt_tx.h"
#define RMT_LED_STRIP_RESOLUTION_HZ 10000000 // 10MHz resolution, 1 tick = 0.1us (led strip needs a high resolution)
#define RMT_LED_STRIP_GPIO_NUM 8
#define EXAMPLE_LED_NUMBERS 24
#define EXAMPLE_FRAME_DURATION_MS 20
#define EXAMPLE_ANGLE_INC_FRAME 0.02
#define EXAMPLE_ANGLE_INC_LED 0.3
static const char *TAG = "example";
static uint8_t led_strip_pixels[EXAMPLE_LED_NUMBERS * 3];
static const rmt_symbol_word_t ws2812_zero = {
.level0 = 1,
.duration0 = 0.3 * RMT_LED_STRIP_RESOLUTION_HZ / 1000000, // T0H=0.3us
.level1 = 0,
.duration1 = 0.9 * RMT_LED_STRIP_RESOLUTION_HZ / 1000000, // T0L=0.9us
};
static const rmt_symbol_word_t ws2812_one = {
.level0 = 1,
.duration0 = 0.9 * RMT_LED_STRIP_RESOLUTION_HZ / 1000000, // T1H=0.9us
.level1 = 0,
.duration1 = 0.3 * RMT_LED_STRIP_RESOLUTION_HZ / 1000000, // T1L=0.3us
};
//reset defaults to 50uS
static const rmt_symbol_word_t ws2812_reset = {
.level0 = 1,
.duration0 = RMT_LED_STRIP_RESOLUTION_HZ / 1000000 * 50 / 2,
.level1 = 0,
.duration1 = RMT_LED_STRIP_RESOLUTION_HZ / 1000000 * 50 / 2,
};
static size_t encoder_callback(const void *data, size_t data_size,
size_t symbols_written, size_t symbols_free,
rmt_symbol_word_t *symbols, bool *done, void *arg)
{
// We need a minimum of 8 symbol spaces to encode a byte. We only
// need one to encode a reset, but it's simpler to simply demand that
// there are 8 symbol spaces free to write anything.
if (symbols_free < 8) {
return 0;
}
// We can calculate where in the data we are from the symbol pos.
// Alternatively, we could use some counter referenced by the arg
// parameter to keep track of this.
size_t data_pos = symbols_written / 8;
uint8_t *data_bytes = (uint8_t*)data;
if (data_pos < data_size) {
// Encode a byte
size_t symbol_pos = 0;
for (int bitmask = 0x80; bitmask != 0; bitmask >>= 1) {
if (data_bytes[data_pos]&bitmask) {
symbols[symbol_pos++] = ws2812_one;
} else {
symbols[symbol_pos++] = ws2812_zero;
}
}
// We're done; we should have written 8 symbols.
return symbol_pos;
} else {
//All bytes already are encoded.
//Encode the reset, and we're done.
symbols[0] = ws2812_reset;
*done = 1; //Indicate end of the transaction.
return 1; //we only wrote one symbol
}
}
void app_main(void)
{
ESP_LOGI(TAG, "Create RMT TX channel");
rmt_channel_handle_t led_chan = NULL;
rmt_tx_channel_config_t tx_chan_config = {
.clk_src = RMT_CLK_SRC_DEFAULT, // select source clock
.gpio_num = RMT_LED_STRIP_GPIO_NUM,
.mem_block_symbols = 64, // increase the block size can make the LED less flickering
.resolution_hz = RMT_LED_STRIP_RESOLUTION_HZ,
.trans_queue_depth = 4, // set the number of transactions that can be pending in the background
};
ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &led_chan));
ESP_LOGI(TAG, "Create simple callback-based encoder");
rmt_encoder_handle_t simple_encoder = NULL;
const rmt_simple_encoder_config_t simple_encoder_cfg = {
.callback = encoder_callback
//Note we don't set min_chunk_size here as the default of 64 is good enough.
};
ESP_ERROR_CHECK(rmt_new_simple_encoder(&simple_encoder_cfg, &simple_encoder));
ESP_LOGI(TAG, "Enable RMT TX channel");
ESP_ERROR_CHECK(rmt_enable(led_chan));
ESP_LOGI(TAG, "Start LED rainbow chase");
rmt_transmit_config_t tx_config = {
.loop_count = 0, // no transfer loop
};
float offset = 0;
while (1) {
for (int led = 0; led < EXAMPLE_LED_NUMBERS; led++) {
// Build RGB pixels. Each color is an offset sine, which gives a
// hue-like effect.
float angle = offset + (led * EXAMPLE_ANGLE_INC_LED);
const float color_off = (M_PI * 2) / 3;
led_strip_pixels[led * 3 + 0] = sin(angle + color_off * 0) * 127 + 128;
led_strip_pixels[led * 3 + 1] = sin(angle + color_off * 1) * 127 + 128;
led_strip_pixels[led * 3 + 2] = sin(angle + color_off * 2) * 117 + 128;;
}
// Flush RGB values to LEDs
ESP_ERROR_CHECK(rmt_transmit(led_chan, simple_encoder, led_strip_pixels, sizeof(led_strip_pixels), &tx_config));
ESP_ERROR_CHECK(rmt_tx_wait_all_done(led_chan, portMAX_DELAY));
vTaskDelay(pdMS_TO_TICKS(EXAMPLE_FRAME_DURATION_MS));
//Increase offset to shift pattern
offset += EXAMPLE_ANGLE_INC_FRAME;
if (offset > 2 * M_PI) {
offset -= 2 * M_PI;
}
}
}