-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
RMT Buffer Allocation Fix (Thread-Safe) for Issue #375 #394
RMT Buffer Allocation Fix (Thread-Safe) for Issue #375 #394
Conversation
…eing of RMT resources (Thread-Safe)
Updated code to move |
I like this. Thank you, teknynja.
…On Sat, Jun 15, 2024 at 2:07 PM teknynja ***@***.***> wrote:
Updated code to move espShow()'s mutex initialization to the
Adafruit_NeoPixel constructor, allowing users to avoid a race condition.
—
Reply to this email directly, view it on GitHub
<#394 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACCSD3ZNOUTWXBT74UFP2Z3ZHSGHVAVCNFSM6AAAAABJGXOECWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCNZQGUZTKMJXGQ>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NOTE NOTE NOTE I AM ALSO JUST A KIBITZER DO NOT ASSUME I KNOW WHAT I AM DOING
I think this change can be simplified a great deal, and doing so will make it much easier to review and merge. See the individual comments, but in short
- remove spurious whitespace changes
- use a function-local
static
to initialize the mutex - simplify the interaction with
rmtInit()
andrmtDeinit()
for specific hardware/library versions | ||
*/ | ||
#if defined(ESP32) | ||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[minor] Again, maybe always have an espInit()
for ESP32, just have it do nothing as appropriate based on ESP-IDF version? BUT see comments below, I think we can actually remove espInit()
entirely?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
later...
if (show_mutex && xSemaphoreTake(show_mutex, SEMAPHORE_TIMEOUT_MS / portTICK_PERIOD_MS) == pdTRUE) { | ||
uint32_t requiredSize = numBytes * 8; | ||
if (requiredSize > led_data_size) { | ||
free(led_data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CONSIDER instead, for diagnostics and conciseness:
const int requiredBytes = requiredSize * sizeof(rmt_data_t);
led_data = (rmt_data_t *)realloc(led_data, requiredBytes);
if (!led_data) {
log_e("NeoPixel RMT buffer allocation (%d bytes) failed", requiredBytes);
xSemaphoreGive(show_mutex);
return;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
later...
It's unfortunate that the inertia behind this (much needed) PR seems to have been lost...again. Is there anyone with commit privileges that can deliver specific reasons this shouldn't be applied or that can produce a list of required changes to get it applied? I don't particularly have a direct interest in this project, but if it "just" needs a programmer to jump through acceptance hoops and confirm that a specific test case works, I'm willing to build up a test fixture and help. Crashing at 70-odd pixels on a robust SOC is kinda embarrassing. I just hate to see this languish for another couple of quarters. How can we move this PR toward that that "merge" button? I'm willing to commit to (probably)more-than-moderately-qualified brain (and equipment) cycles to moving this PR forward. What will it take? Let's get the kibitizers (?) and the approvers together and get some action on this, please. I'd like to see someone with submit approval comment on what it would take to move this to completion. Whether Tekninja or another of us takes the PR and moves it to the goal, the ESP32 world really needs to see this landed. How can we all help get this submitted? |
I forked your changes and tried it with an Arduino Nano ESP32 with ESP32 board 3.0.4 |
@ednieuw - The only thing I'm noticing when doing a quick look at your code is that you are creating three instances of the NeoPixel driver. The first one isn't really allocating much memory because it's using default constructor, but the other two are allocating space for 256 pixels each at 3 & 4 bytes per pixel. That's probably not too much memory for the driver's normal pixel buffers, but it does end up trying to allocate 32768 bytes from the heap for the RMT buffer to be shared by both instances - I don't know if that's a problem for the underlying libraries or not, but you might try paring back the number of pixels to the required minimum to see if that helps. Digging into this problem did reveal another problem in my changes though (although it's likely not related to your issue). If a user creates all instances of the NeoPixel driver using the default constructor and then configures/uses those instance(s) later on in the code, the mutex object is not instantiated, which would probably negatively impact the operation of the esp32 RMT driver. The state of my patch seems to be in limbo at this point 🤷, so unless somebody hits this particular code path, I'm probably just going to leave things as they are for now. |
I found out the size of the RMT-buffer is large enough for 512 LEDs. Espressif has a nice example with WS2812 LED strips: I tested it with a with 256 and 512 WS2812 LEDs and that works. My effort to adapt the coding for a SK6812 strip needs some more time. The timing is not correct and it still write 3 bytes instead of four. But it is not on my priority list yet.
|
Not willing to wait long for an working update of the Neopixel library I wrote a library for WS2812 and SK6812 LEDs to be used with an Arduino Nano ESP32 with Espressif board ESP32 V3.0. |
Nice, Ed.
Since this library seems abandoned, working around it seems appropriate.
…On Mon, Sep 23, 2024 at 8:38 AM Ed Nieuwenhuys ***@***.***> wrote:
Not willing to wait long for an working update of the Neopixel library I
wrote a library for WS2812 and SK6812 LEDs to be used with an Arduino Nano
ESP32 with Espressif board ESP32 V3.0.
It uses the RMT driver and It will probably also work with ESP32-S3 and
other ESP32's
https://github.com/ednieuw/EdSoftLED
It is crude version 1.0.0
—
Reply to this email directly, view it on GitHub
<#394 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ACCSD36JE6YOTQXJTSPMDS3ZYAKW3AVCNFSM6AAAAABJGXOECWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDGNRYGI4TKMBUGI>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Thanks @teknynja, I was struggling with my build of https://github.com/BikeBeamer/BikeBeamer because it uses 256 WS2812B LEDs. This fixed my issue! |
I am looking at #402, which provoked #392 and #394. @teknynja and @robertlipe: I have some questions for you (and anyone else who has technical suggestions).
Thanks for your comments. |
I guess thread-safety is a general expectation I have for these kinds of libraries, but it may not be an issue for most use-cases in Arduino
This PR was really intended as more of a patch to get things working for people trying to use the library until something more elegant came along. Without looking at the details, it sounds like using the higher-level
I think this PR ran out of steam once people starting discussing more extensive, long-term solutions to the problem and the lack of consensuses on how to move forward. It sounds like going with something like the implementation from CircuitPython is probably the way to actually resolve this issue. |
@dhalbert I would encourage you to use your judgment to move as fast as you can to a fix, whether it be adopting this PR as a stopgap (it's a fine stopgap imho), or making your own PR similar to this one as a stopgap, or just heading straight to a streaming solution. I think we're all on the same page here. Here's my implementation for reference (depends on some of my utility libraries so not easy to run directly, though I'm happy to share those if you care for some reason) #include "ok_led_strip.h"
#include "Arduino.h"
#include "ok_logging.h"
#include "ok_logging_esp.h"
static OkLoggingContext const OK_CONTEXT("ok_led_strip");
#if defined(ARDUINO_ARCH_ESP32)
#include "esp_idf_version.h"
#include "soc/soc_caps.h"
#if SOC_RMT_SUPPORTED && ESP_IDF_VERSION_MAJOR >= 5
#define INCLUDE_ESP32_RMT_DRIVER 1
#endif
#endif
#if INCLUDE_ESP32_RMT_DRIVER
#include <atomic>
#include "driver/gpio.h"
#include "driver/rmt_tx.h"
#include "esp32-hal-periman.h"
#include "esp_attr.h"
#include "esp_check.h"
static constexpr uint32_t RMT_RESOLUTION = 10000000; // 1 MHz
static constexpr rmt_symbol_word_t END_SYM = {
.duration0 = RMT_RESOLUTION * 280 / 1000000, .level0 = 0, // 280us
.duration1 = 0, .level1 = 0,
};
struct context {
rmt_encoder_t main_encoder; // So rmt_encoder_t* casts to context*
rmt_encoder_t* data_encoder; // Byte encoder for LED data
rmt_encoder_t* end_encoder; // Copy encoder that delays a bit at the end
rmt_channel_handle_t rmt_channel;
bool at_end;
std::atomic<bool> running;
};
static size_t IRAM_ATTR on_encode(
rmt_encoder_t* encoder, rmt_channel_handle_t channel,
void const* data, size_t data_size,
rmt_encode_state_t* status) {
auto* const ctx = (context*) encoder;
size_t symbols = 0;
*status = RMT_ENCODING_RESET;
if (!ctx->at_end) {
rmt_encode_state_t data_status = RMT_ENCODING_RESET;
symbols += ctx->data_encoder->encode(
ctx->data_encoder, channel, data, data_size, &data_status);
if (data_status & RMT_ENCODING_COMPLETE) ctx->at_end = true;
if (data_status & RMT_ENCODING_MEM_FULL) *status = RMT_ENCODING_MEM_FULL;
}
if (ctx->at_end && !(*status & RMT_ENCODING_MEM_FULL)) {
symbols += ctx->end_encoder->encode(
ctx->end_encoder, channel, &END_SYM, sizeof(END_SYM), status);
}
return symbols;
}
static esp_err_t IRAM_ATTR on_reset_encoder(rmt_encoder_t* encoder) {
auto* const ctx = (context*) encoder;
if (ctx->data_encoder != nullptr) rmt_encoder_reset(ctx->data_encoder);
if (ctx->end_encoder != nullptr) rmt_encoder_reset(ctx->end_encoder);
ctx->at_end = false;
return ESP_OK;
}
static bool IRAM_ATTR on_trans_done(
rmt_channel_handle_t channel,
rmt_tx_done_event_data_t const* ev, void* vctx) {
auto* const ctx = (context*) vctx;
OK_FATAL_IF(!ctx->running);
ctx->at_end = false;
ctx->running = false; // Atomic write allows main code to continue
return false; // No task woken up.
}
class OkLedStripEsp32Rmt : public OkLedStrip {
public:
OkLedStripEsp32Rmt() {}
~OkLedStripEsp32Rmt() {
OK_DETAIL("Stopping RMT LED strip driver (pin %d)", pin);
if (ctx.rmt_channel != nullptr && enabled) rmt_disable(ctx.rmt_channel);
if (ctx.rmt_channel != nullptr) rmt_del_channel(ctx.rmt_channel);
if (ctx.data_encoder != nullptr) rmt_del_encoder(ctx.data_encoder);
if (ctx.end_encoder != nullptr) rmt_del_encoder(ctx.end_encoder);
}
bool setup(int pin) {
OK_FATAL_IF(ctx.running);
OK_FATAL_IF(this->pin != -1);
OK_FATAL_IF(pin < 0);
this->pin = pin;
OK_DETAIL("Preparing RMT LED strip driver (pin %d)", pin);
if (OK_ERROR_IF(!perimanClearPinBus(pin))) return false;
ctx.main_encoder.encode = on_encode;
ctx.main_encoder.reset = on_reset_encoder;
rmt_bytes_encoder_config_t const data_cf = {
.bit0 = {
.duration0 = RMT_RESOLUTION * 3 / 10000000, .level0 = 1, // 0.3us
.duration1 = RMT_RESOLUTION * 9 / 10000000, .level1 = 0, // 0.9us
},
.bit1 = {
.duration0 = RMT_RESOLUTION * 9 / 10000000, .level0 = 1, // 0.9us
.duration1 = RMT_RESOLUTION * 3 / 10000000, .level1 = 0, // 0.3us
},
.flags = {.msb_first = 1},
};
if (OK_LOG_ESP_ERRORS(rmt_new_bytes_encoder(&data_cf, &ctx.data_encoder)))
return false;
rmt_copy_encoder_config_t const end_cf = {};
if (OK_LOG_ESP_ERRORS(rmt_new_copy_encoder(&end_cf, &ctx.end_encoder)))
return false;
rmt_tx_channel_config_t const rmt_cf = {
.gpio_num = (gpio_num_t) pin,
.clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = RMT_RESOLUTION,
.mem_block_symbols = 256, // Max on ESP32-S2
.trans_queue_depth = 1,
// .flags = {.with_dma = true},
};
if (OK_LOG_ESP_ERRORS(rmt_new_tx_channel(&rmt_cf, &ctx.rmt_channel)))
return false;
rmt_tx_event_callbacks_t const cb = {.on_trans_done = on_trans_done};
if (
OK_LOG_ESP_ERRORS(rmt_tx_register_event_callbacks(
ctx.rmt_channel, &cb, &ctx)) ||
OK_LOG_ESP_ERRORS(rmt_enable(ctx.rmt_channel))) {
return false;
}
OK_DETAIL("RMT LED strip driver enabled (pin %d)", pin);
enabled = true;
return true;
}
virtual void start_sending(std::span<uint8_t const> data) override {
if (ctx.running.exchange(true)) {
OK_FATAL("Pin %d is still busy", pin);
}
rmt_transmit_config_t transmit_cf = {};
if (OK_LOG_ESP_ERRORS(rmt_transmit(
ctx.rmt_channel, &ctx.main_encoder,
&data[0], data.size(), &transmit_cf))) {
ctx.running = false;
}
}
virtual bool busy() const override { return ctx.running; }
private:
int pin = -1;
bool enabled = false;
context ctx = {};
};
#endif // INCLUDE_ESP32_RMT_DRIVER
std::unique_ptr<OkLedStrip> ok_led_strip(int pin) {
#if INCLUDE_ESP32_RMT_DRIVER
auto esp32_rmt_strip = std::make_unique<OkLedStripEsp32Rmt>();
if (esp32_rmt_strip->setup(pin)) return std::move(esp32_rmt_strip);
#endif
OK_ERROR("No LED strip driver available for pin %d", pin);
return {};
} |
I think that everyone with a qualified opinion was pretty happy with the second patch. I don't know if the 'resolved' button was clicked on everything, but the last time I looked at it, I was as happy as I get. We just can't get anyone with appropriate permission to press the 'merge' button, leading me to suspect this library has become abandoned. (The last commit was six months ago.) ESP32 users may thus be better served by Espressif's own WS281x library, which, of course, tracks all the latest thrash in the ESP-IDF RMT layer: https://components.espressif.com/components/espressif/led_strip/ Threading, TMK, doesn't exist in pure Arduino Land because it's geared for 8-bit processors with tiny RAM. Threading on ESP32, however, is extremely common, as most of those units are even multi-core, and all of them are fast enough to multithread around waiting for peripherals and such. Users continue to suffer: If you need a working version of THIS code, I would totally go with Tekninja's PR. Threading or not, what's in the trunk now is totally broken for most non-trival cases. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tested this code on a spliced-together strip of 24+30+30 Neopixels, with strandtest. It works fine. We'll merge and release this now, and check on adapting the CircuitPython code to keep it simpler. We'll also check on the current ESP-IDF library.
for specific hardware/library versions | ||
*/ | ||
#if defined(ESP32) | ||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
later...
if (show_mutex && xSemaphoreTake(show_mutex, SEMAPHORE_TIMEOUT_MS / portTICK_PERIOD_MS) == pdTRUE) { | ||
uint32_t requiredSize = numBytes * 8; | ||
if (requiredSize > led_data_size) { | ||
free(led_data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
later...
all good, tyeth will do so when up next :) |
Thanks Dan, released as v.1.12.4 |
NOTE: This is a thread-safe version of pull-request #392: It is essentially the same code with a mutex added to protect the code from multiple threads.
This pull request addresses issue #375 [pixel.show() crash with more than 73 pixel on ESP32s3]
After reviewing the code and also getting some helpful feedback from the Espressif Forums (https://www.esp32.com/viewtopic.php?f=13&t=40270) and @robertlipe it was determined that the code in esp.c for handling the RMT item buffers when using the IDF v5 framework was allocating too much space on the stack when using more than 70ish pixels.
This pull request addresses that issue by allocating the RMT buffers from the heap instead. It will attempt to allocate a single block of memory to accommodate the largest configured instance (sharing the buffer between instances is fine, as the buffer is completely populated each each time the
espShow()
method is called).I also took the time to improve the channel allocation management, previously the RMT channels were initialized on each call to
espShow()
, now the RMT channels are only de-initialized and re-initialized whenever the output pin is changed.Finally, I was concerned about problems that may be caused by allocating large buffers on the heap without giving the user any way to free that memory, so the code allows a user to free that memory (and also release the RMT channels) by setting the number of pixels to zero using
.updateLength(0)
and then calling.show()
. This will de-allocate the heap memory used for the RMT buffers and release the RMT channels held by driver. They will automatically be re-allocated if needed when setting the number of pixels back to a non-zero value.