espressif / esp-idf

Espressif IoT Development Framework. Official development framework for Espressif SoCs.
Apache License 2.0
13.27k stars 7.2k forks source link

ESP32S3 - Continuous RMT transmissions (IDFGH-11922) #13003

Open higaski opened 7 months ago

higaski commented 7 months ago

Answers checklist.

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

suda-morris commented 7 months ago

@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 commented 7 months ago

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

higaski commented 7 months ago

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

suda-morris commented 7 months ago

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.

higaski commented 7 months ago

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

habazut commented 7 months ago

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

suda-morris commented 7 months ago

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

higaski commented 7 months ago

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

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?

habazut commented 2 months ago

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.

higaski commented 2 months ago

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.

habazut commented 2 months ago

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.