esp-idf icon indicating copy to clipboard operation
esp-idf copied to clipboard

ESP32S3 - Continuous RMT transmissions (IDFGH-11922)

Open higaski opened this issue 1 year ago • 12 comments

Answers checklist.

  • [X] I have read the documentation ESP-IDF Programming Guide and the issue is not addressed there.
  • [X] I have updated my IDF branch (master or release) to the latest version and checked that the issue is present there.
  • [X] I have searched the issue tracker for a similar issue and not found a similar issue.

IDF version.

ESP-IDF v5.3-dev-1353-gb3f7e2c8a4

Espressif SoC revision.

ESP32-S3 (QFN56) (revision v0.1)

Operating System used.

Linux

How did you build your project?

Command line with idf.py

If you are using Windows, please specify command line type.

None

Development Kit.

ESP32-S3-DevKitC-1

Power Supply used.

USB

What is the expected behavior?

I've always been under the impression that the RMT peripheral should be able to do continuous transmissions when the transmission queue is specified to be greater 1. However this does not seem to be the case. The current driver does introduce a significant delay between two consecutive transmissions.

What is the actual behavior?

Take a look at the following scope image. It shows a simple bytes encoder transmitting a 1 bit as a 50us high/50 us low pulse. Inside a transmission frame those timings are kept perfect, but between two transmissions they are clearly not although the very first bit is a 1 again.

esp32s3_rmt_bug

Steps to reproduce.

Here is a reproducible example of my issue. It uses GPIO11 to output a signal with RMT and GPIO10 to get some reference toggle between two transmissions. The whole example is also available here: https://github.com/higaski/esp32s3_rmt_bug

#include <driver/gpio.h>
#include <driver/rmt_tx.h>
#include <esp_task.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <array>
#include "sdkconfig.h"

rmt_channel_handle_t channel{};
rmt_encoder_handle_t encoder{};

// Toggle GPIO10 after each RMT transmission
bool gpio10_state{};
bool IRAM_ATTR rmt_callback(rmt_channel_handle_t,
                            rmt_tx_done_event_data_t const*,
                            void*) {
  gpio_set_level(GPIO_NUM_10, gpio10_state = !gpio10_state);
  return pdFALSE;
}

void task_function(void*) {
  // Initialize RMT on GPIO11
  static constexpr rmt_tx_channel_config_t chan_config{
    .gpio_num = GPIO_NUM_11,
    .clk_src = RMT_CLK_SRC_DEFAULT,
    .resolution_hz = 1'000'000u,
    .mem_block_symbols =
      (SOC_RMT_CHANNELS_PER_GROUP * SOC_RMT_MEM_WORDS_PER_CHANNEL) /
      sizeof(rmt_symbol_word_t),  // 8 channels sharing 384x32 bit RAM
    .trans_queue_depth = 2uz,
    .intr_priority = 3,
    .flags = {
      .invert_out = false,
      .with_dma = false,
      .io_loop_back = false,
      .io_od_mode = false,
    }};
  ESP_ERROR_CHECK(rmt_new_tx_channel(&chan_config, &channel));
  ESP_ERROR_CHECK(rmt_enable(channel));

  // Some simple bytes encoder
  static constexpr rmt_bytes_encoder_config_t bytes_encoder_config{
    .bit0 =
      {
        .duration0 = 100,
        .level0 = 1,
        .duration1 = 100,
        .level1 = 0,
      },
    .bit1 =
      {
        .duration0 = 50,
        .level0 = 1,
        .duration1 = 50,
        .level1 = 0,
      },
  };
  ESP_ERROR_CHECK(rmt_new_bytes_encoder(&bytes_encoder_config, &encoder));

  // Enable RMT callback
  rmt_tx_event_callbacks_t cbs{.on_trans_done = rmt_callback};
  ESP_ERROR_CHECK(rmt_tx_register_event_callbacks(channel, &cbs, NULL));

  // Some bytes to send
  std::array<uint8_t, 3uz> bytes_to_send{0xFFu, 0x00u, 0xFFu};

  for (;;) {
    static constexpr rmt_transmit_config_t config{};
    ESP_ERROR_CHECK(rmt_transmit(
      channel, encoder, data(bytes_to_send), size(bytes_to_send), &config));
  }
}

extern "C" void app_main() {
  // Get some pin to toggle
  static constexpr gpio_config_t io_conf{.pin_bit_mask = 1ull << GPIO_NUM_10,
                                         .mode = GPIO_MODE_OUTPUT,
                                         .pull_up_en = GPIO_PULLUP_DISABLE,
                                         .pull_down_en = GPIO_PULLDOWN_DISABLE,
                                         .intr_type = GPIO_INTR_DISABLE};
  ESP_ERROR_CHECK(gpio_config(&io_conf));

  xTaskCreatePinnedToCore(
    task_function, NULL, 4096uz, NULL, ESP_TASK_PRIO_MAX - 1u, NULL, 1);

  for (;;) vTaskDelay(pdMS_TO_TICKS(5000u));
}

Debug Logs.

No response

More Information.

No response

higaski avatar Jan 17 '24 18:01 higaski

@higaski Thanks for your interest in the RMT.

The delay you see between RMT transmission is introduced by the software. When the previous transmission finished, we will call a callback function, then retrieve a new transmission from an internal queue, and afterward, set up the necessary registers according to the transmission description (In the function rmt_tx_do_transaction). All of them cost time and can introduce delay.

suda-morris avatar Jan 18 '24 04:01 suda-morris

BTW, a loop transmission is controlled by the hardware, so it's 100% consecutive.

suda-morris avatar Jan 18 '24 04:01 suda-morris

I need to send different packets. How can I work around this issue? Is this really a hardware limitation? Could the driver potentially load RMT symbols from the next element in queue before the current transmission is done?

Can RMT trigger a timer or something so that I can calculate how far I'm off?

/edit https://github.com/espressif/esp-idf/issues/9991 seems kinda related

higaski avatar Jan 18 '24 05:01 higaski

I'm afraid it's not easy to support the pre-load feature because the encoder is not working in a one-shot way. The encoder keeps generating new symbols and writes to the FIFO memory when the transmitter is working on the old symbols. Also we support different types of transmission (infinite loop, finite loop, no loop), they need different register configurations. So if we do a "pre-set", it will affect the current transmission.

I need to send different packets. How can I work around this issue?

Maybe you can make a customized encoder, and in the state machine, you can prepare any kinds of packets you link..

Can RMT trigger a timer or something so that I can calculate how far I'm off?

Do you mean the Event Task Matrix feature ? No, RMT doesn't have that hardware support yet.

suda-morris avatar Jan 18 '24 07:01 suda-morris

I need to send different packets. How can I work around this issue?

Maybe you can make a customized encoder, and in the state machine, you can prepare any kinds of packets you link..

You mean like combining data into a single never ending transmission? If I do that I can no longer tell when one packet ends and the next one starts... Timing between them is crucial to me as well.

/edit I already have a custom "regular" encoder. I only use the bytes encoder for a convenience example so that you don't have to pile through 500 lines of shit C code. :D

higaski avatar Jan 18 '24 07:01 higaski

Continous running transmission works in the 4.x IDE. We have a product that uses a continious transmission which consists of a fixed preamble and the changing paylaod. The payload is updated with rmt_fill_tx_items(channel, data, dataLen, preambleOffset) while the preamble is running. Of course I want to be able to update my project to the 5.x IDE eventually. Please make that possible.

Regards, Harald.

REF to source: https://github.com/DCC-EX/CommandStation-EX/blob/master/DCCRMT.cpp

habazut avatar Jan 18 '24 08:01 habazut

@habazut I see your request, we didn't expect the user will still want to update the FIFO memory dynamically in the loop mode. In the current design, we only preload the FIFO memory by the encoder for once, and the normal "one-round trans done" interrupt is disabled, because we don't want that event to happen too frequently in the loop mode if there's no data needs updated. We may reconsider this use case...

Is there a risk that while the RMT is transmitting the "IDLE" symbols, you're filling "data" to the same FIFO region?

suda-morris avatar Jan 18 '24 09:01 suda-morris

So, what's the recommended approach now? Besides that drawback I pointed out in my original post I quite like the current encoder-based driver API. I thought about creating a workaround for my issue by introducing a special "very last symbol in a transmission" state which accommodates for the delay. So basically the last pulse would be shorter...

Of course this is rather ugly since that delay is, and correct me if I'm wrong, depended on

  • the number of memory symbols reserved for the channel
  • and the complexity of the decoding function

So one would have to adjust that last symbol depending on the current CPU frequency, compiler optimizations and maybe even compiler versions....?

/edit I've just tried adjusting the mem_block_symbols size from min (48) to max (384) and to my surprise this doesn't effect the delay at all. So apparently copying of a queued transmission does not take up any processor time, it's really just the peripheral setup?

higaski avatar Jan 18 '24 09:01 higaski

With the 4.x IDE I can get continous transmission of abcdef like this

abcdefabcdefabcdefabcdef....

My protocol does not like pauses between f and a or stretched symbols.

With the 4.x IDE I can get either an end interrupt after f (happens during a) or I can with rmt_set_tx_thr_intr_en(... 3 ...) get an interrupt during d. Or I can have both.

With the interrupt code during a I can replace symbols def to something else. Replacement is faster than one symbol time. With the interrupt code duriung d I can replace symbols abc to something else. How should I do this with the new IDE? Apparently the hardware can do it as the 4.x IDE can make the hardware do it that way. I don't want to fiddle with the RMT registers myself, that's what the HAL is for I suppose.

Regards, Harald.

habazut avatar Jul 07 '24 12:07 habazut

I agree, this is a necessary feature. Relying on workarounds by tinkering with the transmission end is not very satisfying... specially not if the hardware would potentially be able to handle such cases itself.

higaski avatar Jul 07 '24 12:07 higaski

Looking at the HW reference manuals of the ESP32 and the ESP32-S3 I want to use these two interrupts:

• RMT_CHn_TX_THR_EVENT_INT: Triggered when the amount of data the transmitter has sent matches the
value of RMT_CHn_TX_LIM_REG.
• RMT_CHn_TX_END_INT: Triggered when the transmitter has finished transmitting the signal.

Whis is explained in the manual:

...When an RMT_CHn_TX_THR_EVENT_INT interrupt is
detected by software, the already used RAM region can be updated by new pulse codes. In such way, the
transmitter can seamlessly send unlimited pulse codes...

Example code welcome. Reagards, Harald.

habazut avatar Jul 07 '24 12:07 habazut

+1. Continuous transmission also needed here. Feeding HW before it finishes seems the way to go..

andreamerello avatar Jun 17 '25 13:06 andreamerello

+1 from me as well. I‘m receiving and transmitting SENT signals and those are expected to be continually, too (although there is a logical PAUSE, but then I get „behind“ the incoming signal). Can we get this?

mickeyl avatar Jul 02 '25 12:07 mickeyl

+1 from me as well. I‘m receiving and transmitting SENT signals and those are expected to be continually, too (although there is a logical PAUSE, but then I get „behind“ the incoming signal). Can we get this?

I have given up trying to ask this from espressif and just use register access to achieve what I want. :(

mitchellcairns avatar Jul 02 '25 19:07 mitchellcairns

@mitchellcairns If you ever write a new generic low-level RMT driver based on your knowledge, please make it open ;-)

mickeyl avatar Jul 03 '25 14:07 mickeyl

I have examined the output of the RMT continious transmission again on the scope. As my first 10 transmitted symbols were all identical I did not notice this before but when you make the first and the second symbol different one can observe that in the case of IDE 4.x and continious loop mode and interrupt at end of transmission the first symbol is actually duplicated (transmitted twice). Like a "bonus" symbol. As the protocol I am transmitting (DCC) is flexible in this respect it does not matter for this particular application but it may very well matter for other protocols.

So if I ask RMT to transmit symbols

abcdef (repeated in RMT loop mode and interrupt at end)

the transmission actually will be

abcdefaabcdefaabcdefaabcdefaa....

I do not know if that is a bug in the harware or in the 4.x IDE. I do not know if not supporting loop mode in the 5.x IDE is covering up for a hardware error

@mitchellcairns : I would appreciate if you had some example code that shows how to load the registers to get continious loop mode with interrupt at end as espressif does not seem to be able to produce any API functions that make that possible, we have waited quite a long time by now.

Example code from espressif for the 5.x IDE would be welcome as well.

Regards, Harald.

habazut avatar Jul 05 '25 15:07 habazut

@mitchellcairns If you ever write a new generic low-level RMT driver based on your knowledge, please make it open ;-)

The closest thing I've done which is available is here: https://github.com/HandHeldLegend/GC-Adapter-ESP32-S3/blob/main/FW/main/joybus_itf.c

This is the esp32-s3, and this will vary greatly depending on the ESP32 chip used.

mitchellcairns avatar Jul 05 '25 18:07 mitchellcairns

Any updates on this? I can also see myself adding this feature, but at least give me some pointers...

higaski avatar Sep 03 '25 05:09 higaski

Any updates on this? I can also see myself adding this feature, but at least give me some pointers...

I don't think you will see an official implementation that offers true looping transmission. It's not wise to use an interrupt either. You basically need to service the RMT peripheral constantly, or use DMA. It will not be pretty and you will need to refer to the technical reference manual to play with the hardware in the way that you want.

mitchellcairns avatar Sep 04 '25 18:09 mitchellcairns