-
-
Notifications
You must be signed in to change notification settings - Fork 3.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
Added multithreading support for ESP32s #3310
Conversation
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.
Hi, thanks for this interesting proposal.
Below please find some first comments - I'll need some more time to go through everything...
Please also study how multi-tasking is managed in four-line-display(ALT) and in the audioreactive usermod.
void doBackgroundWork(){ | ||
backgroundIndex++; | ||
Serial.println("B: " + String(backgroundIndex)); | ||
} |
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.
Please generally(!) avoid fastled EVERY_N_MILLIS as it does a very bad job .... for example, if the N_MILLIS time is missed by a single microsecond, it does nothing.
You might be able to utilize vTaskDelayUntil(). There are examples in the fourLineDisplay_ALT or audioreactive usermods.
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 thought EVERY_N... was clean, I will move to vTaskDelayUntil, thank you for that.
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.
The benefit of every_n_... is that it does not block as it avoids calling delay().
But you can easily substitute it with a few lines like this:
const unsigned long task1_period = 40; // to run every 40 millisecond
static unsigned long task1_last_run = 0; // "static" needed so the value is preserved
if ((millis() - task1_last_run) >= task1_period) { // activate after at least 40 millis have passed
task1_last_run = millis(); // remember last runtime
task1_do_work(); // do work
}
// do something else, or just wait
In contrast to 8266 or other small arduino platforms, on esp32 millis()
and delay()
do work as expected even when you are outside of the main looptask (i.e. in a seperate task). They are still "forbidden" for interrupt service routines (ISR).
vTaskDelayUntil()
will suspend your main task until a specific time, its for "nothing to do" situations.
} | ||
|
||
void loop() { | ||
EVERY_N_MILLISECONDS(1000) { doMainWork(); } |
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.
See my previous comment
@@ -11,7 +11,7 @@ | |||
|
|||
# CI binaries | |||
; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth # ESP32 variant builds are temporarily excluded from CI due to toolchain issues on the GitHub Actions Linux environment | |||
default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB, esp32s3dev_8MB_PSRAM_opi | |||
; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB, esp32s3dev_8MB_PSRAM_opi |
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.
Please revert this
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 am happy to revert this part. I usually use platform_overrides. Is that the preferred method for adding new functionality?
I am not a RTOS expert or have deep knowledge about task scheduling on ESP but these crossed my mind nevertheless:
I wonder what @softhack007 will say as he is better at low level stuff. |
I fully support this point. Not all libraries are thread-safe, and the typical usermod is not thread-safe either. |
@@ -285,6 +285,31 @@ DEBUG_PRINTLN(F("Watchdog: disabled")); | |||
#endif | |||
} | |||
|
|||
#if defined(WLED_ENABLE_BACKGROUND) |
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.
Can you move these two functions (background main loop, and creation of the new task) into your main usermod?
I think its not strictly needed that this is added into the WLED core. You can achieve the same thing in the "setup" function of your main UsermodBackground::Setup(). Similar to what usermods/audioreactive does.
WLED/usermods/audioreactive/audio_reactive.h
Line 1173 in 45d7e66
if (enabled) onUpdateBegin(false); // create FFT task |
WLED/usermods/audioreactive/audio_reactive.h
Line 1356 in 45d7e66
void onUpdateBegin(bool init) |
WLED/usermods/audioreactive/audio_reactive.h
Lines 1379 to 1405 in 45d7e66
if (init && FFT_Task) { | |
vTaskSuspend(FFT_Task); // update is about to begin, disable task to prevent crash | |
if (udpSyncConnected) { // close UDP sync connection (if open) | |
udpSyncConnected = false; | |
fftUdp.stop(); | |
} | |
} else { | |
// update has failed or create task requested | |
if (FFT_Task) { | |
vTaskResume(FFT_Task); | |
connected(); // resume UDP | |
} else | |
// xTaskCreatePinnedToCore( | |
xTaskCreate( // no need to "pin" this task to core #0 | |
FFTcode, // Function to implement the task | |
"FFT", // Name of the task | |
5000, // Stack size in words | |
NULL, // Task input parameter | |
1, // Priority of the task | |
&FFT_Task // Task handle | |
// , 0 // Core where the task should run | |
); | |
} | |
micDataReal = 0.0f; // just to be sure | |
if (enabled) disableSoundProcessing = false; | |
} | |
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 will look at audio reactive for sure. I am not trying to reinvent the wheel, but make writing usermods easier. By adding backgroundLoop to wled.cpp and the other core files, all devs need to do is implement the backgroundLoop function. Also this strategy can support multiple usermods all with main and background loops. That said, it is never my goal to reinvent the wheel, just make the usermod experience easier for new devs.
The stack size is large, but settable via the BACKGROUND_STACK_SIZE setting.
for (;;) | ||
{ | ||
// instance->usermods.backgroundLoop(); | ||
usermods.backgroundLoop(); |
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.
Sorry, but this way of "running" stuff in the background is very problematic.
Together with the activation conditions (every_n_milliseconds) in each of your background usermods, this basically implements a busy-wait-loop that will compete for 100% CPU cycles with all other tasks on core#0 (e.g. audioreactive, system tasks (like wifi or tcp), and ISRs).
Your code would even compile on -C3 and -S2 (that only have core #0), but it will cause instabilities and may even heat up cheaper boards (=fire hazard).
Normally in FreeRTOS a task only uses the CPU when it has something to do, and the task goes into "waiting" state (vTaskDelay, waiting on semaphores or queues, etc) when nothing is scheduled. This allows other tasks at the same priority to activate in time to meat their task deadlines.
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.
To improve, I would suggest that each backgroundLoop()
should return the number of millis until it wants to be activated again, similar to return FRAMETIME;
used in effects (FX.cpp). This would allow your scheduler to identify "nothing to do" situations, and free the CPU by vTaskDelay()
if no background loop is due for running.
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.
Think this is going to change how I write multithreaded apps for the foreseeable future. Thank you
Thank you for all the comments. I was not aware that the four line display and audioreactive implemented multithreading. Your comments are all insightful and grounded in reality. There are some bits in there I wish I knew years ago. I'll go back to the drawing board and see if there is need for this at all. |
don't give up .... its still an interesting feature :-) Maybe just the final details will be different from your first design. cheers 🥇 |
@softhack007 I am not giving up, I was stoked for the feedback. Given all the comments, I need to go back and rework my solution and try again. |
I would suggest a complete re-think of how usermods are called. I think modifying add() function and adding a few semaphores would do the trick. |
Added optional multithreading support to core WLED classes. WLED, UsermodManager and Usermod. The actual multithreading is done in custom usermods. Any usermod with a backgroundLoop, will be executed on the background thread.