ESP32 RMT reception, looking for idle

I was thinking to use the RMT peripheral to look for an idle period. Basically i will be looking for the DMX-break, which is a minimum of 88us. Any data can be discarded, but when the idle interrupt is triggered action does need to be taken. I find it very hard to find complete examples for RMT reception. AI tends to throw some stuff and snippets that give an indication, but no solution.
Google does not find what i am looking for. Probably compounding the issue is that in esp core 3.x.x the RMT has changed a bit and i am still using the 2.0.11 core, but still if anybody has any ideas on the matter ?

I have never used RMT with arduino IDE, only on Esphome with arduino framework.
Anyway, did you have a look at these examples:

Yeah i did see those at some point. It's for core 3.x.x i suppose, since compile errors show up.
This came fairly close to the end goal

// Copyright 2023 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @brief This example demonstrate how to use a C++ Class to read several GPIO RMT signals
 * calling a data processor when data is available in background, using tasks.
 *
 * The output is the last RMT data read in the GPIO, just to illustrate how it works.
 *
 */

class MyProcessor {
private:
  uint32_t buff;  // rolling buffer of most recent 32 bits.
  int at = 0;
  size_t rx_num_symbols = RMT_MEM_NUM_BLOCKS_1 * RMT_SYMBOLS_PER_CHANNEL_BLOCK;
  rmt_data_t rx_symbols[RMT_MEM_NUM_BLOCKS_1 * RMT_SYMBOLS_PER_CHANNEL_BLOCK];

public:
  int8_t gpio = -1;

  MyProcessor(uint8_t pin, uint32_t rmtFreqHz) {
    if (!rmtInit(pin, RMT_RX_MODE, RMT_MEM_NUM_BLOCKS_1, rmtFreqHz)) {
      Serial.println("init receiver failed\n");
      return;
    }
    gpio = pin;
  }

  void begin() {
    // Creating RMT RX Callback Task
    xTaskCreate(readTask, "MyProcessor", 2048, this, 4, NULL);
  }

  static void readTask(void *args) {
    MyProcessor *me = (MyProcessor *)args;

    while (1) {
      // blocks until RMT has read data
      rmtRead(me->gpio, me->rx_symbols, &me->rx_num_symbols, RMT_WAIT_FOR_EVER);
      // process the data like a callback whenever there is data available
      process(me->rx_symbols, me->rx_num_symbols, me);
    }
    vTaskDelete(NULL);
  }

  static void process(rmt_data_t *data, size_t len, void *args) {
    MyProcessor *me = (MyProcessor *)args;
    uint32_t *buff = &me->buff;

    for (int i = 0; len; len--) {
      if (data[i].duration0 == 0) {
        break;
      }
      *buff = (*buff << 1) | (data[i].level0 ? 1 : 0);
      i++;

      if (data[i].duration1 == 0) {
        break;
      }
      *buff = (*buff << 1) | (data[i].level1 ? 1 : 0);
      i++;
    };
  }

  uint32_t val() {
    return buff;
  }
};

// Attach 3 processors to GPIO 4, 5 and 10 with different tick/speeds.
MyProcessor mp1 = MyProcessor(4, 1000000);
MyProcessor mp2 = MyProcessor(5, 1000000);
MyProcessor mp3 = MyProcessor(10, 2000000);

void setup() {
  Serial.begin(115200);
  mp1.begin();
  mp2.begin();
  mp3.begin();
}

void loop() {
  // The reading values will come from the 3 tasks started by setup()
  Serial.printf("GPIO %d: %08lx | %d: %08lx | %d: %08lx\n", mp1.gpio, mp1.val(), mp2.gpio, mp2.val(), mp3.gpio, mp3.val());
  delay(500);
}

And some things are just not declared

Arduino: 1.8.19 (Windows 10), Board: "Dmagix Esp32, Disabled, Disabled, Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS), 240MHz (WiFi/BT), QIO, 80MHz, 4MB (32Mb), 921600, Core 1, Core 0, None, Disabled"





















sketch_jul18f:28:25: error: 'RMT_MEM_NUM_BLOCKS_1' was not declared in this scope

   rmt_data_t rx_symbols[RMT_MEM_NUM_BLOCKS_1 * RMT_SYMBOLS_PER_CHANNEL_BLOCK];

                         ^~~~~~~~~~~~~~~~~~~~

sketch_jul18f:28:48: error: 'RMT_SYMBOLS_PER_CHANNEL_BLOCK' was not declared in this scope

   rmt_data_t rx_symbols[RMT_MEM_NUM_BLOCKS_1 * RMT_SYMBOLS_PER_CHANNEL_BLOCK];

                                                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~

sketch_jul18f:27:27: error: 'RMT_MEM_NUM_BLOCKS_1' was not declared in this scope

   size_t rx_num_symbols = RMT_MEM_NUM_BLOCKS_1 * RMT_SYMBOLS_PER_CHANNEL_BLOCK;

                           ^~~~~~~~~~~~~~~~~~~~

sketch_jul18f:27:50: error: 'RMT_SYMBOLS_PER_CHANNEL_BLOCK' was not declared in this scope

   size_t rx_num_symbols = RMT_MEM_NUM_BLOCKS_1 * RMT_SYMBOLS_PER_CHANNEL_BLOCK;

                                                  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~

C:\Users\deva_\AppData\Local\Temp\arduino_modified_sketch_988224\sketch_jul18f.ino: In constructor 'MyProcessor::MyProcessor(uint8_t, uint32_t)':

sketch_jul18f:34:36: error: 'RMT_MEM_NUM_BLOCKS_1' was not declared in this scope

     if (!rmtInit(pin, RMT_RX_MODE, RMT_MEM_NUM_BLOCKS_1, rmtFreqHz)) {

                                    ^~~~~~~~~~~~~~~~~~~~

C:\Users\deva_\AppData\Local\Temp\arduino_modified_sketch_988224\sketch_jul18f.ino: In static member function 'static void MyProcessor::readTask(void*)':

sketch_jul18f:51:29: error: 'class MyProcessor' has no member named 'rx_symbols'; did you mean 'rx_num_symbols'?

       rmtRead(me->gpio, me->rx_symbols, &me->rx_num_symbols, RMT_WAIT_FOR_EVER);

                             ^~~~~~~~~~

                             rx_num_symbols

sketch_jul18f:51:62: error: 'RMT_WAIT_FOR_EVER' was not declared in this scope

       rmtRead(me->gpio, me->rx_symbols, &me->rx_num_symbols, RMT_WAIT_FOR_EVER);

                                                              ^~~~~~~~~~~~~~~~~

C:\Users\deva_\AppData\Local\Temp\arduino_modified_sketch_988224\sketch_jul18f.ino:51:62: note: suggested alternative: 'SPINLOCK_WAIT_FOREVER'

       rmtRead(me->gpio, me->rx_symbols, &me->rx_num_symbols, RMT_WAIT_FOR_EVER);

                                                              ^~~~~~~~~~~~~~~~~

                                                              SPINLOCK_WAIT_FOREVER

sketch_jul18f:53:19: error: 'class MyProcessor' has no member named 'rx_symbols'; did you mean 'rx_num_symbols'?

       process(me->rx_symbols, me->rx_num_symbols, me);

                   ^~~~~~~~~~

                   rx_num_symbols

exit status 1

'RMT_MEM_NUM_BLOCKS_1' was not declared in this scope



This report would have more information with
"Show verbose output during compilation"
option enabled in File -> Preferences.

I will try later on a device that does have a later core installed.

Ah went back to core 2.x.x and found an example that does something similar

#include "Arduino.h"
#include "esp32-hal.h"

extern "C" void receive_trampoline(uint32_t *data, size_t len, void * arg);

class MyProcessor {
  private:
    rmt_obj_t* rmt_recv = NULL;
    float realNanoTick;
    uint32_t buff; // rolling buffer of most recent 32 bits.
    int at = 0;

  public:
    MyProcessor(uint8_t pin, float nanoTicks) {
      if ((rmt_recv = rmtInit(pin, RMT_RX_MODE, RMT_MEM_192)) == NULL)
      {
        Serial.println("init receiver failed\n");
      }

      realNanoTick = rmtSetTick(rmt_recv, nanoTicks);
    };
    void begin() {
      rmtRead(rmt_recv, receive_trampoline, this);
    };

    void process(rmt_data_t *data, size_t len) {
      for (int i = 0; len; len--) {
        if (data[i].duration0 == 0)
          break;
        buff = (buff << 1) | (data[i].level0 ? 1 : 0);
        i++;

        if (data[i].duration1 == 0)
          break;
        buff = (buff << 1) | (data[i].level1 ? 1 : 0);
        i++;
      };
    };
    uint32_t val() {
      return buff;
    }
};

void receive_trampoline(uint32_t *data, size_t len, void * arg)
{
  MyProcessor * p = (MyProcessor *)arg;
  p->process((rmt_data_t*) data, len);
}

// Attach 3 processors to GPIO 4, 5 and 10 with different tick/speeds.
MyProcessor mp1 = MyProcessor(4, 1000);
MyProcessor mp2 = MyProcessor(5, 1000);
MyProcessor mp3 = MyProcessor(10, 500);

void setup()
{
  Serial.begin(115200);
  mp1.begin();
  mp2.begin();
  mp3.begin();
}

void loop()
{
  Serial.printf("GPIO 4: %08x 5: %08x 10: %08x\n", mp1.val(), mp2.val(), mp3.val());
  delay(500);
}

And that actually compiles, so there is progress.
I do like the task method of the newer example, maybe i can manage to create one out of two. Thanx for the help. Apparently it is possible to attach input lines of different peripherals to the same gpio, but that will be a different step again.

So after a bit more research and gaining some understanding of what is actually going on and looking through the core library files to find the actual function (where there is also information about them of course)

I came to this :

#include "Arduino.h"
#include "esp32-hal.h"  // esp32-hal-rmt.h & .c  contain rmtRead() & rmtInit()
#include "driver/rmt.h"  //  "hal/rmt_types.h" is included in this and contains 
                         //  Macros and types
                         // in rmt.h most other functions are defined
#include "driver/periph_ctrl.h"
#include "soc/rmt_reg.h"

#define LED_GREEN 13

/**
 * @brief Default configuration for RX channel
 *

#define RMT_DEFAULT_CONFIG_RX(gpio, channel_id) \
    {                                           \
        .rmt_mode = RMT_MODE_RX,                \
        .channel = channel_id,                  \
        .gpio_num = gpio,                       \
        .clk_div = 80,                          \  1MHx = 1us per tick
        .mem_block_num = 1,                     \
        .flags = 0,                             \
        .rx_config = {                          \
            .idle_threshold = 12000,            \
            .filter_ticks_thresh = 100,         \
            .filter_en = true,                  \
        }                                       \
    }
*/

  
extern "C" void idleCalback(uint32_t *data, size_t len, void * arg);

class rxRmt {
  private:
    rmt_obj_t* rmt_recv = NULL;
    float realNanoTick;

  public:
    rxRmt(uint8_t pin, uint16_t thresh, uint8_t filt) {
      if ((rmt_recv = rmtInit(pin, RMT_RX_MODE, RMT_MEM_64)) == NULL) {
        Serial.println("init receiver failed\n");
      }
      realNanoTick = rmtSetTick(rmt_recv, 1000);
      if (rmt_set_rx_idle_thresh(RMT_CHANNEL_0, thresh) != ESP_OK) {
        Serial.println("Idle Treshold not set\n");
      }
      bool enableFilter = false;
      if (filt) enableFilter = true;
      //if (rmt_set_rx_filter(RMT_CHANNEL_0, enableFilter, filt) != ESP_OK) {
      if (rmtSetRxThreshold(rmt_recv, thresh) != ESP_OK) { // edit !
        Serial.println("Filter not set\n");
      }
    };
    void begin() {
      rmtRead(rmt_recv, idleCalback, this);
    };
};

IRAM_ATTR void idleCalback(uint32_t *data, size_t len, void * arg) {
  static bool ledOn = false;
  ledOn = !ledOn;
  if (ledOn) digitalWrite(LED_GREEN, HIGH);
  else digitalWrite(LED_GREEN, LOW);
}

rxRmt pulseRmt = rxRmt(35, 80, 42);  // attaches RMT_Rx to GPIO 35, idle threshold 80, filter at 42


void setup() {  
  Serial.begin(500000);
  Serial.println("\n");
  Serial.println("RMT Testing Sketch, looking for idle pulse");
  pinMode(LED_GREEN, OUTPUT);
  pulseRmt.begin();
}

void loop() {

}

And it works !! can successfully detect the break in between DMX frames. I am not processing any of the other pulses, and in fact even went as far as to filter any pulses out that are shorter than 42us. Any byte will have a start-bit 8 data bits and should have 2 stop bits. At 250kbps a whole byte would be 44us. Since i do not intend to process the RMT data, i did not see a point in storing it to begin with. Mind you that storing is done through DMA and when i disabled the filter it also worked as expected. With my controller (which has a fairly long frame-reset time) i measured the idle break, and found it was near 180us (at 200us it was no longer detected)

There are some Serial debug message (as there are in the example) but i guess they would never be transmitted, since the Serial object is not initialized at the time the constructor is run (??)

Anyway i have gained a better understanding of this RMT thing, and of course to implement it for receiving DMX there are more step and considerations.

  • Is the callback going to be fired quickly enough (probably yes, but who knows for sure) And what the priority is. Basically it has to be fired before the next Serial interrupt comes in, but it isn't an ISR, do i have to stop interrupts explicitely
  • If there is still data in the Serial buffer, it doesn't make sense to reset the index yet. (data needs to be read and stored first, maybe even within the callback
  • Check to see how big the Serial-rx buffer actually is and possibly increase it to make sure it will fit at least 513 bytes
  • Possibly just copy all of the data in that buffer the moment the callback is fired. There is a function to read multiple bytes that uses memcpy() i think.
  • Does it actually work to have both a UART rx & RMT rx connected to the same GPIO ? (or do i need to use 2 pins)

(and i marked my own post as a solution for once)

The result of the use of RMT for break detection for DMX frame reset in a library for ESP32 boards. Using the same pin for RMT Rx & UART Rx worked straight out of the box in core version 2.0.x And is working with the help of ESP32 core collaborators in core 3.3.x (older core 3 version have a fatal bug in the RMT driver)

I'm am happy with the result and am using it as part of programs.

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.