Skip to content

Update esp32.RMT module to the new API #16293

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

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

Once this PR is accepted, we plan to submit a follow-up PR to add RX capabilities to the RMT module. We already have working code for it, see [1].

[1] https://github.com/elvis-epx/micropython/blob/rmt_rx/ports/esp32/esp32_rmt2.c

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.

Trade-offs and Alternatives

The RMT channel used by bitstream cannot be a simple number anymore since the channel is an opaque struct in the new API. In this PR, we allocate and deallocate a channel every time bitstream is used, and this may fail in case all RMT channels are in use.

In this PR, bitstream no longer falls back to bitbanging (should it, anyway?). A possible change is to reserve a RMT channel just for bitstream i.e. create it upon boot and leave it there, or reserve it upon first usage of bitstream.

@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

@ricksorensen
Copy link
Contributor

ricksorensen commented Dec 9, 2024 via email

Signed-off-by: Elvis Pfutzenreuter <elvis.pfutzenreuter@gmail.com>
Signed-off-by: Elvis Pfutzenreuter <elvis.pfutzenreuter@gmail.com>
No point in trying to calculate a maximum safe waiting time.

Signed-off-by: Elvis Pfutzenreuter <epxx@epxx.co>
The commit [1] fixed issues when mpy was moved from core 1 to core 0,
but since then it has been moved back to core 1.

Fixes a issue with the new RMT API: hangups when a RMT channel is
repeatedly created and released (e.g. calling bitstream in a tight
loop).

[1] micropython@e754c2e

Signed-off-by: Elvis Pfutzenreuter <epxx@epxx.co>
Workaround collaborated by @ricksorensen for an issue in ESP-IDF < 5.3
where channel deletion pulls the logic level HI.

Signed-off-by: Elvis Pfutzenreuter <epxx@epxx.co>
@ricksorensen
Copy link
Contributor

@elvis-epx It was long week I guess. I finally got around to rechecking this, and tried with IDF 5.4. The extended high pulse seems to be gone again! For my builds:

IDF 5.2.2 Looks good, no high pulse at end
IDF 5.3.2 Not so good, high pulse at end
IDF 5.4 Looks good, no high pulse at end.

I have a micropython clone, created a branch with your updates for RMT and synched to the 20250224 micropython version. I tried XIAO C3 and XIAO S3 with same results for each.

Here is my playing repository https://github.com/ricksorensen/micropython.git and the branch is esp32_bitstream

Here is my test code:

import sys
import platform
import machine
import neopixel
import time

pinu = None
pf = sys.platform
if "samd" in pf:
    pinu = machine.Pin("A0_D0", mode=machine.Pin.OUT)
if "rp2" in pf:
    pinu = machine.Pin("GP26")
if "esp32" in pf:
    if "S3" in sys.implementation._machine:
        pinu = machine.Pin(1)
    if "C3" in sys.implementation._machine:
        pinu = machine.Pin(2)

print("  platform:       ", sys.platform)
print("  version:        ", sys.version)
print("  implementation: ", sys.implementation)
print("  build:          ", platform.platform())
print(" Pin ", pinu)

# t=(400, 850, 800, 450)   1.25 uS/bit


def t1(b, t=(400, 850, 800, 450)):
    global pinu
    machine.bitstream(pinu, 0, t, b)


# 3 pixels, 24 bits/pixel, 72bits total
#                          90uS @ 1.25uS/bit
pixt = [
    (0xFF, 0x00, 0x55),
    (0x00, 0xAA, 0xFF),
    (0x55, 0xFF, 0xAA),
]
b6 = b"abcdef"  # 6*8   = 48 bits    60uS @ 1.25uS/bit
b300 = bytearray(300)  # 300*8 = 2400 bits,   3000uS=3ms @ 1.25uS/bit
p = neopixel.NeoPixel(pinu, len(pixt))
for i in range(len(p)):
    p[i] = pixt[i]


# total: nc*( 65 us + 100us) = nc*165us
#        nc=4 --> 660us
def runall(nc=4):
    n = nc
    while n > 0:
        t1(b6)
        # time.sleep_us(5)
        n = n - 1
    n = nc
    while n > 0:
        p.write()
        # time.sleep_us(10)
        n = n - 1

@elvis-epx
Copy link
Contributor Author

@ricksorensen so I guess we can remove the "RFC" off the title and consider it worthy?

@ricksorensen
Copy link
Contributor

I think it can be a full pull request. Caveat is removal of bitbanging mode. Thanks again.

@elvis-epx elvis-epx changed the title RFC: update esp32.RMT module to the new API Update esp32.RMT module to the new API Feb 25, 2025
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.

5 participants