Ensuring idle task runs after vTaskDelete

jcwren wrote on Thursday, March 13, 2008:

If one wanted to restart a task by deleting it with vTaskDelete() followed by xTaskCreate(), is there a way to ensure the idle task gets run to clean up between the calls?

In this particular situation, I have a task that initializes a handful of variables at startup which control which of two devices the driver talks to.  The user can flip a switch to change which device is used.  The cleanest method at this particular time is to have the main control task stop and restart the driver task.  I just need (I think) to make sure the idle tasks gets run to clean up the TCBs and such.

Thanks,
–jc

1 Like

adarkar9 wrote on Friday, March 14, 2008:

Here are some ways:

- Have the idle task create the driver task if it has been deleted.
- Have the idle task send a message to your main control task when it detects that the driver task has been deleted.
- Have the idle task give a semaphore every time it runs and have the main control task pend on the semaphore after deleting the driver task.

The last method is the most generic, but it does add extra overhead to the idle task.

jcwren wrote on Friday, March 14, 2008:

The last idea was the one I had already come up with, but I was hoping there was a less hackish way of doing this.

It would seem that if vTaskDelete() and xTaskCreate() are being used, there should be some way of enforcing the clean up.  Yes, sooner or later the idle task will run, but guaranteeing when is more difficult.

–jc

adarkar9 wrote on Friday, March 14, 2008:

The idle task calls the static function prvCheckTasksWaitingTermination() to finish deleting tasks.  Perhaps you could make this public and call it after you call vTaskDelete().  You must be careful not to do this from the task you deleted, though.  Of course, you’re not sticking to the API if you do this, but it is a more elegant solution.

While this is a very old thread, the issue of deleted task TCB memory not getting freed keeps coming up because the idle task may not get a chance to run quickly enough. Of course, if it doesn’t run for a while (e.g. 5 seconds) then the watchdog timer will expire since the idle task feeds it, but one can create tasks more quickly and experience running out of memory faster so one sees that as the failure mode.

While ideally every task would be written using function calls that would have it go into a blocked state while waiting for an action, such as waiting for events in a queue or suspending itself waiting to be awakened (unblocked) from an interrupt or another task, if this isn’t done then the idle task can get starved.

Note that a taskYIELD is not sufficient as it will only allow tasks with equal or higher priority to run while the idle task usually has lowest (0) priority.

If one has a main dispatch loop from which the tasks are created or has a polling loop (servicing serial input, for example), then one can force the idle task to run by calling a function that suspends this looping task and awakens from the idle task callback, such as using xTaskNotifyWaitIndexed / xTaskNotifyIndexed. So one can call a function like the following tasknotify_run_idle_task and have the function for the idle hook:

void tasknotify_run_idle_task(void)
{
	waiting_for_idle = 1;  // will have idle_hook() notify main task when it is run

	// The following suspends the main task so that the idle task can run.
	// It waits for the idle task to finish running so releasing memory from any
	// deleted tasks and resetting the (idle) task watchdog timer.
	xTaskNotifyWaitIndexed(TASK_INDEX_IDLE_RUN, 0, 0, NULL, portMAX_DELAY);

	// waiting_for_idle is set to 0 in idle_hook() just prior to the notify
}

static uint8_t waiting_for_idle = 0;

// This function is called whenever the idle task has been run.  It's called after any task
// memory cleanup and after feeding the task watchdog timer.
void vApplicationIdleHook(void)
{
	if (waiting_for_idle) {
		waiting_for_idle = 0;  // we only want to notify once
		xTaskNotifyIndexed(mainTaskHandle, TASK_INDEX_IDLE_RUN, 0, eNoAction);
	}
}

Instead of xTaskNotifyWaitIndexed / xTaskNotifyIndexed, one can use vTaskSuspend(NULL) / vTaskResume(taskHandle) instead but needs to set the taskHandle (usually this is the main task).

It would be nice to have a FreeRTOS function that effectively did the above.

The concept of deleting the task and then recreating it is in my opinion, a very foolish concept. If the task can delete itself, it can do what is needed to instead reinitialize itself. If some other task deletes it, then you run the risk that the task wasn’t in a “clean” state when it was deleted, and you may have corrupted the system.

Generally, task will be “immortal”, and when not needed, block until they are needed again. If you need to have them terminate when not being used, to allow changing mixes of tasks, then the tasks should end themselves, and your then MUST give the idle task time to run. IF things happen so fast that you need the memory before the idle task gets to run, you are likely running your system too close to the limits to be reliable.

The one other option I see is that you create a “reaper” task at moderate to high priority, and task notify. the reaper when they are done, and then “block forever” in a clean state, and the reaper deletes the task. Note, you only need to wait for the idle task on a self-delete, as that is the case that runs into the issue of the task resources need to be availiable to run the clean-up code, but then they can’t be freed, so the Idle task takes over the final freeing.

Note, your waiting for idle hook above has a race condition, as if idle has just check for releasing memory, and seeing nothing is needed, it get interrupted, and a task deleted and the notification requested, the waiting for idle will get tested BEFORE the memory free will be checked.

Note, if your “main dispatch loop” doesn’t naturally block to allow the idle task to run, it should be also at priority level 0, so yield will work to let the idle task run.

I agree with you that it is better to use a worker pool of tasks if that is what is needed rather than creating and self-deleting them. I wasn’t proposing normally having a task, even the main task, as not using event/interrupt waits that suspend the task, but there are situations where this can occur.

One is where one is using the ESP32-C3 with its USB CDC support. In that case this isn’t one’s own UART setup with one’s own UART event handler. In this situation, one can have a message processing and event dispatcher loop that polls serial (via a non-blocking “getchar”, for example) and can do so where it is a slow poll (say, once a second, with a task suspend until a timer interrupt resumes it) until one sees a character and is a fast poll once typing is detected and then has a timeout where enough time with no typing goes back to a slow polling.

This works well for serial debugging using USB with the ESP32-C3 but leads to needing to intentionally run the idle task. Again, not the normal situation where one is in full control, but this sort of thing can happen when using other’s libraries (in this case, ESP-IDF).

As for the race condition, it’s not a fatal one if one is not dependent on every tasknotify call to be a complete idle task call. It is guaranteed that two such calls will have at least one complete one that freed TCB self-task delete resources and fed the watchdog timer because the callback occurs at the end of the idle task (so two such callbacks have to have at least one complete idle task run). If it were important to know with certainty that the full idle task was run, then one would just do the tasknotify twice in a row, but normally that isn’t needed for something like the main event serial handler loop example since the idle task will get serviced the next loop cycle. The idea is to prevent a buildup of TCB memory over time.

As for running the main loop task at priority 0 and use taskYIELD, that doesn’t prevent the race condition if the idle task were pre-empted (though a double taskYIELD would work so long as no higher priority task resumed). Similarly, instead of setting the main task priority to 0 one could temporarily set the idle task priority to 1 and do a taskYIELD in the main task and have the idle task callback set it’s priority back to 0. But this doesn’t seem much better than what I described.

Note that having all possible tasks created early can use a substantial amount of memory for all of their stacks. It should be OK to design more dynamically especially in a hardware system where most (but not all) of the hardware cannot and isn’t needed to be run simultaneously. Again, a normal main task would suspend and let the idle task run, but sometimes it’s not practical when dealing with libraries not in one’s control.

IF the code is actually “polling”, as in checking a bit, and looping until it is set, such code is not really compatible with a real RTOS environment, except perhaps as an idle priority task with a yield in the loop, at which point the idle task will still get to run.

If the support library doesn’t support an interrupt based driver with a blocking task access, then the library is really not RTOS ready, and should be updated/replaced.

I have written USB serial drivers that support that, where the USB ISR/Task puts the received data into a Queue/StreamBuffer to allow the task to block waiting for data.

I suppose part of our difference is I build purpose built systems that are expected to be reliable, and I put my name (and my companies name) on the line with the system. If a system needs to be user configured, I want a bad configuration to “fail fast” at configuration time, and not get “flaky” latter if some resource management gets into a timing issue.

The whole discussion of a “main loop” seems to be from a programming paradigm that is not based on using a RTOS, but more of a “bare metal” concept of programming that hobby systems encourage. I may have a “Command Loop” taking operational commands from the outside world, or a “GUI” but these tend to not do any real “work”, just processes request, which get forwarded to the actual operational task, which may report status messages to the GUI for updating a display.

2 Likes

First off, Merry Xmas and Happy Hanukkah!

What I described is polling but not by checking a bit but by doing a non-blocking getchar call because the stdio calls are all that were left exposed for their client-only USB CDC serial once the ESP-IDF library took that over during startup. Their library does have UART driver support that has event callbacks so that would work properly with RTOS and we use that as well as a host USB driver for their ESP32-S2, but their USB support during startup for the ESP32-C3 does not expose their interrupt event handler. They do have a component for a console that replaces or hooks into their USB CDC code internally but that has its own set of issues.

Nevertheless, if I had put the serial reading into a separate task using a stdio blocking call and sent a message or a task resume when a line was received which is what it waits for currently, then that would be RTOS-friendly. I will see if their blocked stdio call behaves properly by suspending the task as it should. Then the main message event handling loop which suspends until a message/notificaiton is received can have a message/notification for serial input so would allow the idle task to run when waiting for serial without needing the tasknotify hack.

Other than that poll, the “main loop” is in not just a “command loop” but really an “event handler loop” that processes messages including those that come from interrupt routines for external input from BLE but it also processes messages from our low-level hardware code that mostly uses the paradigm of the application calling a lower-level hardware function that returns right away and then later a message comes after the (interrupt-driven) hardware response (ADC read, for example). This is so the application code can manage a timeout for such hardware calls, logging as appropriate, ignoring or restarting if needed, etc. This event loop also manages a calendar using a once-a-second RTC message because the original code from an AVR was ported to ESP32 and that part works fine for RTOS. And, it turns out, the calendar clock via gettimeofday in the ESP-IDF library is unreliable, sometimes failing returning with an error and also often losing time over the day when light or deep sleep is used so we use the always running RTC clock for our own “wake from sleep catch up the calendar” code. Finally, this same event handler loop handles light or deep sleep (using the ESP-IDF power management library when BLE is used).

Another situation is FCC certification calls as the ESP-IDF library code (stupidly) uses a loop with a yield instead of suspending when waiting. They are function calls you are expected to put into your own created task but their example code has this be at a higher priority so is useless for getting the idle code to run and they explicitly say to disable the task watchdog for the configuration! We need to initiate the FCC calls in sealed units using BLE (obviously only to start and with timeouts since regular BLE is disabled before calling their FCC cert code) so I had to hack this by 1) using a priority 1 task so their yield will let our code run and 2) use our tasknotify to have the idle code run periodically. This let us keep the watchdog timer from expiring and also let us time their code and execute a function of theirs to stop their code early (their function sets a global variable that their loop apparently interrogates).

I completely and totally understand your point, but the reality is that we need to deal with libraries from chip manufacturers (and others) that may have code that busy loops or yields but does not suspend/block and have to choose between relatively simple hacks like I did vs. modifying their source code (when available; it sometimes isn’t) which is a maintenance headache vs. replicating their code the right way which is time consuming and can also be a maintenance headache. I didn’t mean to imply there was a problem/flaw with FreeRTOS. My request was for a more standard workaround to force the idle task tickle the watchdog and free self-deleted task (TCB) memory ONLY for dealing with integration with non-RTOS library code. You can caveat this and explain no new code should be written this way – i.e. code in one’s direct control should always suspend via blocking calls in tasks.

Just looping on calling a non-block library code is just as bad. This is just an example of a general problem that I have pointed out before that many vendor supplied libraries are just not worth actually using, but using as a basis to writing a correctly functioning version, as many vendors do that sort of errors, as their library was built to build “toy” examples. It isn’t the role of the FreeRTOS developers to support all sorts of “non-compliant” libraries built contary to the standard practices of Real-Time systems. There is nothing the FreeRTOS kernal can do to get the idle task to run if some non-idle priority task won’t block. Such tasks need to be put at idle priority (and your raising the priority of the idle task is essentially the same as dropping the tasks priority to idle, only less clean and wasting resources). Note, one solution to “Non-RTOS-Compatible-Library Code” is to put ALL usages of it in a single task at idle priority with pre-emption and round-robin operation enabled to get around the issues of such code.

IF they supply the source code for their driver then it can be modified to expose what it needs to expose. I would check their criteria for FCC certification, as it likely is not restricted to just using thier library as that isn’t the way I understand FCC certification for sub-modules being done (There may be some registers that need to be set a specific way, but the full library would not be included in the test requirements).

Your “main loop” sounds very much like it isn’t designed as a “real time” application, events from things that need “real-time” handling shouldn’t be mixed into a task doing non-real-time operations or it becomes hard to verify real time requirements.

I will also add that system with non-real-time tasks probably shouldn’t use “watchdog in Idle” operation, as that gets into the sort of issues you seem to be having. You might have some support code in the idle hook to let the watchdog handler check it at a lower requirement rate.

I understand your point but even when they expose source code it’s a maintenance mess to have modified code of theirs you have to re-modify whenever you update to a newer library. It would be better for me to get Espressif to fix their FCC functions more directly and I will write a bug for that and also inquire about the USB default driver that is nonblocking.

As for FCC certification, they used to and still optionally have a full code replacement method and a tool for testing but that is a binary (no source) hard-coded to work only over specific UART (not USB) serial over a pair of GPIO pins often used for other purposes in the ESP32-C3. Again, that doesn’t work well for sealed systems. They more recently created functions (API) that can be included directly in user code so works with USB and also allows us to initiate test commands via BLE so works with real product sealed units, but still has the stupid loop with task yield instead of suspend/block. Their new functions still do what you described to be able to do continuous wave (CW) unmodulated tests as well as single channel packet tests for both WiFi and BLE. It’s identical in API function to their standalone code they had before, but is more flexible.

By the way, I looked at why I used polling for the USB case and it’s because of the following for their USB Serial JTAG/Controller Console (I’m a new user that can’t post links but this is from Espressif online documentation):

The non-blocking (default) driver and the VFS implementation will flush automatically after a newline. The blocking (interrupt-based) driver will automatically flush when its transmit buffer becomes empty.

While they refer to a blocking interrupt-based driver, I couldn’t find how to initiate that in place of their default for USB. They do describe UART drivers for serial but those don’t support USB.

Nevertheless, I may be able to use fcntl and disable O_NONBLOCK though that might not be enough given their code, but I’ll try that (I may have tried it unsuccessfully before – I don’t remember as it was more than 2 years ago).

As for the main loop, it isn’t handling “real-time” requirements in the sense of what low-level code for hardware needs to deal with, but it does need to be responsive. We do want serial and BLE inputs to respond reasonably quickly while the other messages are really nothing more than delayed responses from hardware requests like reading a photodiode value. The call in application code that starts that isn’t real-time – the low-level code is real-time doing the hardware I2C or register initiation and has an interrupt for getting a response. Only the response result (a value the application wants) then gets put into a message (usually in the interrupt routine that gets the response) for the application code. It’s really nothing more than a delayed return value for most calls. But during that delay, other activities can occur though in a restricted way such as BLE that may get a response saying to wait (because generally two things can’t be done at the same time though there are some exceptions like interrupting a current activity for debugging).

So the call to the message handler does those details of waiting for the message with appropriate timeout and logging warnings/errors and recovery and even doing sleep. The paradigm is “low_level_function(…)” followed by “msg_handler(expected_message_type)”. The message handler returns with the expected message content and handles any other messages that may be coming in asynchronously such as from BLE or from the once-a-second clock timer. While it is waiting, it is usually suspended using xTaskNotifyWaitIndexed (except for the serial polling we talked about but even that suspends one second at a time when there isn’t recent serial input). So this works with RTOS (except for the fast-response serial polling mode).

Espressif has some slow low-level hardware tasks like erasing Flash in a loop for blocks but they were smart enough to do a vTaskDelay(1) (the “1” is configurable but is the default) during their erasure to allow the idle task to run though it would have been better for them to use other suspend calls during the Flash write that would resume when the write completed. Still, large Flash erasure is slow enough that 1 tick isn’t terribly inefficient.

As for non-real-time tasks, we have our own timer table (we started in AVR with 8 KB RAM and 128 KB Flash before porting to ESP32) where the message handler looking at the once-a-second RTC messages updates the table efficiently (i.e. updates one variable until there is a trigger or change that then updates the full table) and dispatches to the appropriate registered function call but almost all of these call low-level code (either ours or Espressif library hardware code). But the task watchdog is not a problem for this at all. In fact, there is absolutely nothing wrong with the task watchdog. The problem is ONLY dealing with 3rd party code that, as you aptly put it, isn’t RTOS-compliant. The ask is for being able to explicitly do what the idle task does as a workaround, whether that’s forcing the idle task to run or whether that’s calling its functionality (freeing self-deleted task memory and feeding the task watchdog timer).

Again, if it weren’t for 3rd party code we wouldn’t be having this discussion as I’d have everything doing appropriate suspend including for serial and for FCC code and it would all work well with FreeRTOS. Well, I suppose a developer could during their development/testing have a tight loop that didn’t suspend, but that’s their problem not a FreeRTOS one. In my test-like CLI commands for application code all my loops have some command that calls msg_handler and if I don’t explicitly call a low-level command I still call msg_handler for an RTC message or I call a short-timer (down to 1 ms) that also has me call msg_handler. This is somewhat analogous do doing a yield but is better since it will generally do a suspend allowing the idle task to run (or will call my hack if the serial workaround or FCC workaround for their library flaws is being done). Alternatively, if no msg_handler is done I can just call vTaskDelay(1) but that will prevent asynchronous messages from getting serviced.

I think as long as you are able to do that in your application, you are good. We would like to avoid adding code in the kernel that promotes bad designs. Let us know if you need anything else.

1 Like

As I said, the standard answers to the need to use “non-compliant” code is to use it in a single task at priority 0, and either make sure your yield often enough or allow preemption and enable round-robin scheduling. That way the polling loop won’t lock up the system.

So what is the best way for a chip manufacturer’s library to be RTOS compliant in a situation like they have when erasing Flash for a large region (blocks) that takes time and would otherwise starve the idle task? What Espressif does is a loop with single block erase calls where in between they have a vTaskDelay(1) so they delay for 1 tick between block erasures. While that works, it also needlessly causes a small delay (default is 10 ms/tick) for every block. The reason a suspend and wait for signal from interrupt isn’t used is that this is not supported in the hardware.

During the hardware call itself (writing to register to initiate chip Flash erase), there is no interrupt associated with a completion signal so instead there is a busy loop checking for a status register. When the hardware itself does not provide an interrupt for completion, how do you propose making this RTOS-compatible? Is a 1 tick vTaskDelay the only solution RTOS provides? That seems lame.

Do you see why I still think it makes sense to be able for an RTOS to have a way to say “I know you can safely feed the task watchdog timer or perhaps temporarily set it to a higher timer value and do other safe cleanup (freeing TCB memory of self-deleted tasks)”. The fact is that most real-world hardware itself is not RTOS-compliant for some functions in that they do not provide interrupts for completion so force busy-loop register examination. For RTOS to not have a way to run the idle task while waiting for such hardware seems foolish to me. To require the entire main task of virtually every product (if the vTaskDelay approach were not done and a yield done instead) to run at priority 0 and do a periodic yield in the busy loop does not seem like a good solution. A better approach would be a temporary suspend that resumes automatically after the idle task has run. This would ensure that only if all tasks are in a suspend state that the idle task is run. This is basically what my hack does – it does a suspend of the calling task and resumes that task after the idle task is run (and yes, it isn’t a guaranteed “complete” idle task run until two such calls).

aggarg, please see my response to richard-damon and consider that most hardware has some functions that do not have interrupts for completion and instead require busy loops for checking a status register. A periodic (every so many iterations in the busy loop) temporary suspend of that task that is resumed when the idle task is run would not pay any vTaskDelay time penalty. That is essentially what my hack does (i.e. suspend task until idle task is run), but hardware status register busy loops are not uncommon and long Flash erasures and other potentially slow hardware responses are a reality so I think RTOS should have a better way to deal with this fact.

I believe I answered my own question when I just ran into the FreeRTOS thread “Having delays shorter than 1 millisecond with FreeRTOS” and the GitHub project mickeyl/esp-microsleep.

As the GitHub project notes: “Due to the way FreeRTOS works, it is impossible to achieve fine-grained delays in the millisecond or sub-millisecond region. Busy waiting is no alternative, since you will a) burn cycles and b) might trigger the FreeRTOS task watchdog.”

If I’m going to try to get Espressif to be more RTOS-compliant, I need to tell them how to handle common hardware situations like busy loops on status registers. Telling them to use vTaskDelay (once every so many loops or time) is inefficient (longer delay than may be needed) and telling them to create a priority 0 task with yield or to have the main task at priority 0 isn’t good either. This esp_microsleep that uses the esp_timer with interrupt handler and task notification is a decent approach to turn a busy wait loop into a suspend (estimated) timed interrupt resume.

While ESP-IDF has a finer-grained esp_timer, you might consider having that in RTOS as an implementation-dependent option to more readily make hardware support libraries more easily RTOS compliant. Or do you consider this something that the hardware port needs to do (i.e. Espressif should be doing this themselves and using it properly in their library code) even though it will be a common need to every hardware port that has a status register and no “done” interrupt for at least one hardware function?

For hardware that doesn’t have interrupt capability (like your the flash erase), the anser is what I have said, it is done at task level 0 (Idle Priority) and the loop. would have a yield between tests. That is how you make polling loops not break an rtos. An alternative would be to place a test in the idle hook that does the test and sets a signal of some sort when done. ALL polling type operations should be at task priority level 0 (idle) unless known to be a very short poll (like there is a couple of clock cycles of possible delay due to clock synchronization).

So yes, the RTOS HAS a way to handle the situation, one you seem to not be willing to use, that is put the operation at task priority level 0, and include a yield. What is wrong with that? You couldn’t put tasks at a lower priority level and expect them to be able to run, so it should be at the lowest level, which does effectively the exact same thing as what you ask. (You will also what to add the flag so Idle yields on each of its exectuion loops).

Also very few pieces of hardware do not support interupts. Flash programming is perhaps the most obvious case, but for many processors, writting to the internal flash while running your main program out of flash can be problematic to start with. SOME have the ability to write to one sector while reading from another, but that is far from universal. I am not familiar with any other peripheral commonly on a chip that doesn’t implement an interrupt suitable for usage (except for chips with eratta that the interrupt just doesn;'t work right).

Your comments about a “micro-timer” is very hardware specific, and on many processors, will be very application specific (espcially in the choice of what timer to use). Internally we use a generic timer library with Implementations for the common processors we use to implement this sort of function.

Thank you. I do need to explain to Espressif what they should do so for the Flash erase or any other non-interrupt busy loop situation (that is long enough to justify) they should (when called) create a priority 0 task that has a periodic Yield and for the caller that is normally a priority 1 (or higher) task they should suspend the calling task until notified from the priority 0 task that it is complete. They can pass in the argument to the priority 0 task the caller’s thread and notify index so it will know who to notify. Sounds pretty clean and efficient.

typedef struct {
    TaskHandle_t taskHandle;
    BaseType_t indexToNotify;
} taskParams_t;
... esp_flash_erase_function(...)
{
    taskParams_t taskParams = {
        .taskHandle = xTaskGetCurrentHandle();
        .indexToNotify = unusedIndex;  // this is tricky to avoid conflict with user code
    }
    TaskHandle_t createdTask = NULL;
 
    xTaskCreate(&low_priority_func, "low_priority_func", stackDepth, &taskParams, 0, &createdTask);
    xTaskNotifyWaitIndexed(index, 0, 0, NULL, portMAX_DELAY);
    vTaskDelete(createdTask);  // this frees TCB memory immediately
    return ...
}
void low_priority_func(void *params)
{
    taskParams_t *taskParams = (TaskHandle_t *)params;
    
    start_slow_flash_call();
    for (;;) {  // busy loop
        check status register and exit loop if done
        taskYIELD();  // will run other priority 0 tasks including idle task
    }
    xTaskNotifyIndexed(taskParams->taskHandle, taskParams->indexToNotify, 0, eNoAction);
}

I think I got hung up on thinking you were requiring the main task calling the Espressif flash erase call needing to be priority 0, but that isn’t the case. Espressif can create the priority 0 task instead. They would need to be careful to use a task notify index that wouldn’t conflict with one a user could use which might be one reason why they didn’t do it this way and used vTaskDelay(1) instead. Thanks again.

They could use the same index 0 notify, with no bits, used by StreamBuffers. If they know the task has blocked on that request (so you could use task notify) then that shouldn’t affect the user code unless it also uses no bit notifies, and only those that might be sent before the task waits for it.

Another option is to use a semaphore or event group to avoid needing to know the task id.

I also wouldn’t create the task at every request, but make a static task to handle these sort of requests. They may also want to “claim” the idle hook, and add their own way for user applications to use an alternate idle hook to avoid the overhead of creating the task, and to that work in their idle hook. My own library provides a way for multiple parts of the user program to add some of their own code (via a call-back) to the idle hook.

Understood. Yes, given that Flash erasure is common for NVS and OTA they can create that as a priority 0 task when flash_init is called (and delete it if flash_deinit is called) where it is just waiting most of the time. Your option of using a dedicated semaphore for this particular purpose is even cleaner and simpler than the notification (though a little slower) and avoids user collision so thank you for that suggestion.

They do in fact have their own idle hook in addition to the FreeRTOS one already. But are you suggesting their alternate idle hook would (if semaphore is set) check the semaphore and call the “check status register and clear semaphore when erase is done” to avoid needing another task? That sounds clean enough to be able to convince them this would be reasonable to do and avoids the separate task.

While for their RF (FCC) certification code they should really fix that more directly (assuming they have some kind of interrupt which I figure for RF code they probably do have), the simple workaround is for me to create a priority 0 task that calls their function since they already are doing a yield. Here again, the main task need not be changed in priority. I’d still need to see if I can get their USB serial to work with interrupts/blocked calls and I think I finally found how to do that similar to what they are using for their console component.