The basics
ESP-IDF uses FreeRTOS and this is a “tasks” based Operating System.
The FreeRTOS default tick rate is 100Hz (you can configure this, but 100 is a good tradeoff between ISR overhead and responsiveness).
This means portTICK_PERIOD_MS is 10 (10ms per tick).
If you use a vTaskDelay < 10mS, it is effectively a 0 tick delay, ie no delay at all. This is interpreted as “yield to higher priority tasks, otherwise keep running”. So any task which does this will starve lower priority tasks (like the FreeRTOS default IDLE task) from running.
So, a default ESP-IDF program needs to have any looping task in it idle for a minimum of 10mS. like this:
vTaskDelay(10 / portTICK_PERIOD_MS);
IDLE0, IDLE1
ESP-IDF creates a task for each CPU core (called “IDLE0”, and “IDLE 1” if there is a second core) whose sole purpose is to do nothing, ie idle. They are given the lowest possible priority that the framework allows, so that any other task created will always have a higher priority.
These idle tasks actually do a bit more than nothing, it is important they are allowed to run at least occasionally, as they do the following:
- Reset the watchdog timeout that has been activated that IDLE# process.
- Cleanup of deleted tasks
Note that the FreeRTOS timer task also runs at low priority.
Task breaks
If any task having a higher priority never takes a break (such as by calling vTaskDelay with a value >= portTICK_PERIOD_MS), then no task of lower priority (including the CPU core IDLE tasks), can “run”.
Changing the tick rate
You could set the tick rate to 1000Hz. FreeRTOS will spend more time in context switch interrupts, so you have a few less CPU cycles available for your program, but you’d be able to delay a task for >= 1ms.
Resources
https://www.freertos.org/taskandcr.html
Tasks in more depth
A RTOS application can be structured as a set of independent tasks
Only one task within the application can be executing at any point in time and the real time RTOS scheduler is responsible for deciding which task this should be.
The RTOS scheduler may repeatedly start and stop each task as the application executes. The scheduler ensures that the processor context (register values, stack contents, etc) when a task is swapped in is exactly that as when the same task was swapped out. To achieve this each task is has its own stack. When the task is swapped out the execution context is saved to its stack.