Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@elvis-epx
Copy link
Contributor

@elvis-epx elvis-epx commented Nov 24, 2024

Summary

The current esp32.RMT module uses a legacy API from ESP-IDF 4.x. The ESP-IDF 5.x offers a new API, which is overall better, and easier to implement the RX side in the future. This PR updates the module and the documentation, preserving the current MicroPython RMT API as much as possible.

The bitstream RMT implementation was updated as well, since ESP-IDF does not allow an image to reference legacy and new APIs at the same time (it resets right after boot with an error message, even if neither module is imported).

Testing

Tested on ESP32, ESP32-S3, ESP32-C3 and ESP32-C6. (Commenters may have tested on other variants.) We have tested all aspects of the RMT API and all seem to work fine.

@elvis-epx elvis-epx force-pushed the rmtng branch 4 times, most recently from 6b14d89 to 1cb1e6c Compare November 24, 2024 02:46
@elvis-epx elvis-epx mentioned this pull request Nov 24, 2024
@elvis-epx elvis-epx force-pushed the rmtng branch 3 times, most recently from 9247d15 to 77ed23a Compare November 25, 2024 17:11
@ricksorensen
Copy link
Contributor

Thank you.

I have been trying to figure this out but with little success. When working with neopixel strips I noticed that the legacy RMT IDF4 + Bitstream has some oddness (extra bytes, noise) and the legacy bitbang is erratic on startup. With C++ and ESPIDF I get good results with RMT IDF 5, but haven't been able to integrated with micropython or bitbang.

If I can help with testing, let me know.

Thanks again

@elvis-epx
Copy link
Contributor Author

@ricksorensen testing would be great, since my use cases are limited here (433MHz key fob emulator). At least in this application, it has been working perfectly.

The only known showstopper is calling RMT.disable() while transmitting, sometimes it panics with a WDT interrupt timeout, but only on ESP32, not in an ESP32-C3 board. Smeels more like an ESP-IDF issue.

@ricksorensen
Copy link
Contributor

ricksorensen commented Nov 28, 2024

@elvis-epx I can get some evaluation done at the end of this week.... fingers crossed.

IDF 5.2.2 is the plan (I can try others, but I have some issues with 5.2.3 and 5.3.1 that I have not resolved yet)

Today (Friday 20241129 CST) I ran a few simple experiments. Boards:

  • XIAO ESP32C3 (risc-v) ESP32_GENERIC_C3 mod mpconfigboard.h to add "rmt" to board name
  • XIAO ESP32S3 (xtensa) ESP32_GENERIC_S3/SPIRAM_OCT mod mpconfigboard.h/mpconfigvariant_SPIRAM_OCT.cmake to add "rmt" to board name
  • Compiler ESP IDF 5.2.2
    Compile:
make BOARD= xxxx   BOARD_VARIANT=xxx clean
make BOARD= xxxx   BOARD_VARIANT=xxx submodules
make BOARD= xxxx   BOARD_VARIANT=xxx
python -m esptool --port /dev/ttyACM0 --chip esp32s3 -b 460800 --before default_reset --after hard_reset write_flash --flash_mode dio --flash_size 8MB --flash_freq 80m 0x0 build-ESP32_GENERIC_S3-SPIRAM_OCT/bootloader/bootloader.bin 0x8000 build-ESP32_GENERIC_S3-SPIRAM_OCT/partition_table/partition-table.bin 0x10000 build-ESP32_GENERIC_S3-SPIRAM_OCT/micropython.bin

I then tried simple esp32.RMT, machine.bitstream, and neopixel.NeoPixel and here is the summary :

  1. 'RMT': OK using simple examples with data upto 10 bytes or so. On the C3 (2 RMT channels) I was able to release and reuse one channel.
    On the S3 (8 channels?) I was only able to get one RMT instantiated ... subsequent tries gave;
>>> r2=esp32.RMT(pin=machine.Pin(2))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: (-261, 'ESP_ERR_NOT_FOUND')
  1. 1bitstream: On both C3 and S3 first tests gave:
>>> a=b"12345678"
>>> a
b"12345678"
>>> t=(800,450,400,850)
>>> machine.bitstream(machine.Pin(1),0,t,a)
>>> machine.bitstream(machine.Pin(1),0,t,a)
>>> machine.bitstream(machine.Pin(1),0,t,a)
>>> a2=b"12"
>>> machine.bitstream(machine.Pin(1),0,t,a2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: [Errno 116] ETIMEDOUT: ESP_ERR_TIMEOUT

wiith subsequent writes giving OSError: (-261, 'ESP_ERR_NOT_FOUND')

A few experiments seem to indicate that if number of bytes sent is 1 or 2 then the timeout occurs.

On the C3 RMT printed some more debug information:

>>> import machine
>>> tim1000 = (1000, 1000, 1000, 1000)
>>> val = b"\x55\x66"
>>> machine.bitstream(machine.Pin(2), 0, tim1000, val)
E (85445) rmt: rmt_tx_wait_all_done(553): flush timeout
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: [Errno 116] ETIMEDOUT: ESP_ERR_TIMEOUT
>>> machine.bitstream(machine.Pin(2), 0, tim1000, val)
E (87455) rmt: rmt_tx_register_to_group(133): no free tx channels
E (87455) rmt: rmt_new_tx_channel(252): register channel failed
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: (-261, 'ESP_ERR_NOT_FOUND')
>>> machine.bitstream(machine.Pin(3), 0, tim1000, val)
E (188205) rmt: rmt_tx_register_to_group(133): no free tx channels
E (188205) rmt: rmt_new_tx_channel(252): register channel failed
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: (-261, 'ESP_ERR_NOT_FOUND')
>>>
  1. NeoPixel: similar results to bitstream (as to be expected). I was able to instantiate a NeoPixel with >2 pixels object multiple times so the release seems to occur okay.

  2. Soft reboot (^D): After errors, doing a soft reboot did not clear/release the RMT allocations, so no new RMT object was available

I will try some other experiments at end of weekend possibly.

Thanks again- Rick

@elvis-epx
Copy link
Contributor Author

@ricksorensen Have pushed an additional commit 9cdb7dd that should fix at least items 2.1 and 3. We weren't waiting on TX completion for long enough and were treating the timeout as a fatal error, not running the cleanup code lines right after.

@l00jj
Copy link

l00jj commented Dec 3, 2024

@elvis-epx Thank you all, this is a great feature update. hope it can be released in the stable version soon. Does the ESP32-C3 require specific IO pins to properly use the RMT function for receiving infrared signals?

@elvis-epx
Copy link
Contributor Author

@elvis-epx Thank you all, this is a great feature update. hope it can be released in the stable version soon. Does the ESP32-C3 require specific IO pins to properly use the RMT function for receiving infrared signals?

As far as I know, any pin capable of input can do RMT. I have only one C3 (devkit-rust-1) and play with the onboard led pin (Pin 7).

@ricksorensen
Copy link
Contributor

Thanks again.

Transmit concern:
With my few neopixel apps the strips are activated okay, but there is a funning behavior which may affect those using the bitstream class. Using a logic analyzer I see when transmitting, upon termination of the bit transfers, the output is raised high for a while then lowered. This may cause problems on the more generalized receiving end than neopixel.

I put in some events to look and see when this happens, and I saw that level switch to high occurs during the rmt_del_channelI() call and then seems to go low when esp_rom_gpio_connect_out_signal() is called The transmit sequence is:

rmt_transmit 
rmt_tx_wait_all_done                # first event is completion of this
rmt_del_encoder                     #   then completion of del_encoder
rmt_disable                         #   then completion of disable
rmt_del_channel                     #    then completion of del_channel (towards end of high 
esp_rom_gpio_connect_out-signal     # transition low

The bottom trace on the image are my event markers.
image

I have been working on/off (mostly off) to try and figure out how to avoid that high pulse at the end. You also see it after 'RMT.release()' . The rmt_del_channel function resets the output pin, which causes it to be PULL_UP, but disables functions.

I see same behavior with XIAO C3 and XIAO S3, with timing of the finishing events much slower on the C3.

@elvis-epx
Copy link
Contributor Author

@ricksorensen good catch, one possible workaround is to keep the channel for the bitstream after the first use (do not delete it, so not calling rmt_del_channel() and not exercising the offending behavior).

@ricksorensen
Copy link
Contributor

ricksorensen commented Dec 5, 2024 via email

@elvis-epx
Copy link
Contributor Author

@ricksorensen no, if the pin changes, we need to release the channel and create another one, so there is extra logic. We can try calling esp_rom_gpio_connect_out_signal() before rmt_del_channel(), if it works, it is a cheaper solution.

@ricksorensen
Copy link
Contributor

ricksorensen commented Dec 5, 2024 via email

@elvis-epx
Copy link
Contributor Author

@ricksorensen nice, so this moves the ball to ESP-IDF court. It is not the only issue stemming from ESP-IDF in this PR (see FIXMEs).

Have tried changing the position of esp_rom_gpio_connect_out_signal() without success (nothing changes or the problem is worsened). Delaying channel release is also not a perfect solution because a) suppose a use case where 2 or more pins are bitstream'ed in parallel b) the pull up still happens, albeit in a delayed moment, so it would only solve the problem for the rather narrow case that only 1 pin is bitstream'ed.

@ricksorensen
Copy link
Contributor

ricksorensen commented Dec 5, 2024

I have an ad hoc work around which only affects machine_bitstream.c. It simply replaces rmt_del_channel(channel); with channel->del(channel);. To get this to compile rmt_private.h must be included, but I do not know the elegant way to get it on the include path. For my testing I used a hard coded include path (new/modified lines marked with //RJS)

#include "/work/tools/espv5/v522/components/driver/rmt/rmt_private.h" //RJS`
#include "driver/rmt_tx.h"
#include "driver/rmt_encoder.h"
.....
    //rmt_del_channel(channel);    //RJS  reset gpio pin (adds pull-up) and deletes channel
    channel->del(channel);   // RJS   skip gpio pin reset just delete channel
....

EDIT: @elvis-epx I think I figured out how to include the rmt_private.h file without having to use an absolute path. I have modified machine_bitstream.c and esp32_common.cmake and here are the git diffs:

$ git diff machine_bitstream.c
diff --git a/ports/esp32/machine_bitstream.c b/ports/esp32/machine_bitstream.c
index 0148959c1..52c6997d8 100644
--- a/ports/esp32/machine_bitstream.c
+++ b/ports/esp32/machine_bitstream.c
@@ -93,6 +93,9 @@ static void IRAM_ATTR machine_bitstream_high_low_bitbang(mp_hal_pin_obj_t pin, u
 /******************************************************************************/
 // RMT implementation
 
+#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5,3,0)
+#include "rmt_private.h" //RJS  need to add rmt_private path in _common.cmake
+#endif
 #include "driver/rmt_tx.h"
 #include "driver/rmt_encoder.h"
 
@@ -154,7 +157,11 @@ static void machine_bitstream_high_low_rmt(mp_hal_pin_obj_t pin, uint32_t *timin
     // Disable and release channel.
     check_esp_err(rmt_del_encoder(encoder));
     rmt_disable(channel);
-    rmt_del_channel(channel);
+#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5,3,0)
+    channel->del(channel);
+#else
+    rmt_del_channel(channel);    // RJS untested
+#endif
 
     // Cancel RMT output to GPIO pin.
     esp_rom_gpio_connect_out_signal(pin, SIG_GPIO_OUT_IDX, false, false);
$ git diff esp32_common.cmake
diff --git a/ports/esp32/esp32_common.cmake b/ports/esp32/esp32_common.cmake
index 9d51a03aa..85b68b0b8 100644
--- a/ports/esp32/esp32_common.cmake
+++ b/ports/esp32/esp32_common.cmake
@@ -237,6 +237,14 @@ target_link_options(${MICROPY_TARGET} PUBLIC
 target_include_directories(${MICROPY_TARGET} PUBLIC
     ${IDF_PATH}/components/bt/host/nimble/nimble
 )
+if (IDF_VERSION VERSION_LESS "5.3")
+# Additional include directories needed for private RMT header.
+#  IDF 5.x versions before 5.3.1
+  message(STATUS "Using private rmt headers for ${IDF_VERSION}")
+  target_include_directories(${MICROPY_TARGET} PRIVATE
+    ${IDF_PATH}/components/driver/rmt
+  )
+endif()
 
 # Add additional extmod and usermod components.
 target_link_libraries(${MICROPY_TARGET} micropy_extmod_btree)

I have not verified the 5.3.1 config.

@seanmpuckett
Copy link

seanmpuckett commented Dec 6, 2024

I have keen anticipation for this as my application requires high speed temporal dithering of dozens of neopixels. A solution that does not block the main core while data is streaming is critical. Any solution that also minimizes setup and teardown associated with each stream is ideal, as there need to be hundreds of streams per second for flicker-free dithering Thank you for your efforts.

@ricksorensen
Copy link
Contributor

@seanmpuckett Rember there are only a finite number of RMT channels- I believe 8 on ESP32 and ESP32S3 and only 2 on ESP32C3 (single mcu too) .

@elvis-epx elvis-epx force-pushed the rmtng branch 2 times, most recently from 6bcd481 to 4d77fa6 Compare December 6, 2024 19:29
@elvis-epx
Copy link
Contributor Author

@ricksorensen pushed additonal commit with your suggested changes, confirmed they fixed the issue here as well.

@elvis-epx
Copy link
Contributor Author

@ricksorensen tested with ESP-IDF 5.3.2 with the help of PR #15733. The long pulse problem reappears since gpio_reset_pin() was moved from rmt_common.c to rmt_tx.c, and calling base->destroy() directly no longer avoids it.
Screenshot from 2024-12-09 18-26-13

projectgus

This comment was marked as resolved.

elvis-epx and others added 3 commits October 22, 2025 08:24
Replace deprecated parameter.

Co-authored-by: Angus Gratton <[email protected]>
Signed-off-by: Elvis Pfützenreuter <[email protected]>
Fix parameter name.

Co-authored-by: Angus Gratton <[email protected]>
Signed-off-by: Elvis Pfützenreuter <[email protected]>
Make text more general.

Co-authored-by: Angus Gratton <[email protected]>
Signed-off-by: Elvis Pfützenreuter <[email protected]>
@ricksorensen
Copy link
Contributor

ricksorensen commented Oct 22, 2025

This relates and if the case points to an ESP IDF issue:

https://forum.arduino.cc/t/esp32s3-rmt-no-free-tx-channels-opening-third-channel/1310503
https://esp32.com/viewtopic.php?f=13&t=42301

My prior builds all used 5.4.2 , trying to build with 5.5.1 to see if the behavior changes. ... no change sadly.

@projectgus
Copy link
Contributor

This relates and if the case points to an ESP IDF issue:

Thanks @ricksorensen, that does sound like the same issue. If so then seems like it'd be enough for MicroPython to change the num_symbols default (and minimum) to SOC_RMT_MEM_WORDS_PER_CHANNEL. That will improve the situation now, and the rest will be fixed when whatever ESP-IDF bug is fixed (although a year-old forum post makes me think maybe no one is tracking that, somoene might need to open an issue for it.)

@ricksorensen
Copy link
Contributor

Better check with @elvis-epx about the reason for setting it to 64

@projectgus
Copy link
Contributor

Better check with @elvis-epx about the reason for setting it to 64

I believe it's always been 64 in MicroPython, because this is the RAM block size on original ESP32 (and maybe S2?) Block size of 48 is a hardware change on the newer SoCs.

@elvis-epx
Copy link
Contributor Author

elvis-epx commented Oct 22, 2025

Better check with @elvis-epx about the reason for setting it to 64

I believe it's always been 64 in MicroPython, because this is the RAM block size on original ESP32 (and maybe S2?) Block size of 48 is a hardware change on the newer SoCs.

The ESP-IDF documentation recommends the minimum to be 64 when DMA is not used. https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/rmt.html#rmt-resource-allocation "If DMA is not used, this field controls the size of the dedicated memory block owned by the channel, which should be at least 64."

We can change the minimum/defaults to SOC_RMT_MEM_WORDS_PER_CHANNEL which sounds like a natural size for each hardware.

@ricksorensen
Copy link
Contributor

Thanks for that information @elvis-epx and @projectgus. If you look at https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-reference/peripherals/rmt.html#rmt-resource-allocation "If DMA is not used, this field controls the size of the dedicated memory block owned by the channel, which should be at least 48."

I think @elvis-epx suggestion to use SOC_RMT_MEM_WORDS_PER_CHANNEL seems appropriate.

@dpgeorge dpgeorge added this to the release-1.27.0 milestone Oct 23, 2025
@elvis-epx
Copy link
Contributor Author

Thanks for that information @elvis-epx and @projectgus. If you look at https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-reference/peripherals/rmt.html#rmt-resource-allocation "If DMA is not used, this field controls the size of the dedicated memory block owned by the channel, which should be at least 48."

I think @elvis-epx suggestion to use SOC_RMT_MEM_WORDS_PER_CHANNEL seems appropriate.

Just sent a commit amending this.

@ricksorensen
Copy link
Contributor

Should this be updated in machine_bitstream.c also for the (rare) case of someone trying to use it while another RMT is active? (e.g. IR transponder with RGB light)

The RMT instance in machine_bitstream is short lived so may not be as important.

@elvis-epx
Copy link
Contributor Author

Should this be updated in machine_bitstream.c also for the (rare) case of someone trying to use it while another RMT is active? (e.g. IR transponder with RGB light)

The RMT instance in machine_bitstream is short lived so may not be as important.

Good catch, amended the commit.

Use the SOC_RMT_MEM_WORDS_PER_CHANNEL macro which is a natural default
(and recommended minimum) size for the RMT symbol buffer, instead of 64.

Signed-off-by: Elvis Pfutzenreuter <[email protected]>
Copy link
Contributor

@projectgus projectgus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me, thanks for persisting with this PR @elvis-epx!

Have also confirmed no more issues allocating both TX channels on ESP32-C6.

Fix a couple documentation issues related to resolution_hz.

Signed-off-by: Elvis Pfutzenreuter <[email protected]>
Documentation was still referring to an RMT.disable() method that
was removed in favor of RMT.active([boolean]).

Signed-off-by: Elvis Pfutzenreuter <[email protected]>
- Style issues.
- Make sure RMT.active() returns True only when there is an ongoing
  transmission.
- RMT object pollable as POLLOUT instead of POLLIN.

Signed-off-by: Elvis Pfutzenreuter <[email protected]>
@elvis-epx elvis-epx requested a review from dpgeorge October 30, 2025 11:17
Documentation was mentioning a clock_div property that is no longer
printed in the string representation of an RMT object.

Signed-off-by: Elvis Pfutzenreuter <[email protected]>
- Fix type handling of optional int keyword args.
- Test tx_ongoing properly (true only if positive).
- Fix error message, should start with lowercase.
- Do not return poll bits that have not been requested.

Signed-off-by: Elvis Pfutzenreuter <[email protected]>
@elvis-epx elvis-epx requested a review from dpgeorge November 13, 2025 23:36
Let it fall through to outside the ifdef when RMT is unavailable.

Signed-off-by: Elvis Pfutzenreuter <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants