A delay in FreeRTOS is a wait that blocks only the current task, leaving the CPU free so that other tasks can run in the meantime.
This idea is fundamental. In classic Arduino, when we use delay(), we think “wait a second”. In an RTOS we have to think differently: this task has nothing useful to do for a second, so it can go to sleep and let others work.
The problem with delay()
In a sequential program, delay() stops the program flow. If we write this:
void loop() {
leerSensor();
delay(1000);
actualizarPantalla();
}During that second, nothing else happens within that flow. In Arduino-ESP32, delay() internally uses a FreeRTOS wait, so it blocks loopTask, not the whole system. Still, vTaskDelay() better expresses the intention within a task and works directly with ticks.
In FreeRTOS, a task should avoid busy waits like this:
while (millis() - inicio < 1000) {
// Waiting without doing anything useful
}That code consumes CPU even though it’s not doing real work. It’s like leaving someone sitting in a chair twirling a pen, occupying the entire room just because.
Quick comparison
| Function | Type of wait | Typical use |
|---|---|---|
vTaskDelay() | Relative | Simple pauses, retries, blinks |
vTaskDelayUntil() | Absolute/periodic | Periodic readings, control, sampling |
If the exact time doesn’t matter too much, vTaskDelay() is simpler. If you need a stable rhythm, use vTaskDelayUntil().
vTaskDelay
The simplest function to put a task to sleep is vTaskDelay.
void vTaskDelay(TickType_t xTicksToDelay);This function moves the task to the Blocked state for a given number of ticks. While it is blocked, it does not consume CPU.
void TareaBlink(void *pvParameters) {
for(;;) {
digitalWrite(LED_BUILTIN, HIGH);
vTaskDelay(pdMS_TO_TICKS(500));
digitalWrite(LED_BUILTIN, LOW);
vTaskDelay(pdMS_TO_TICKS(500));
}
}The macro pdMS_TO_TICKS() converts milliseconds to ticks according to the configured system tick rate.
Use pdMS_TO_TICKS(500) instead of just putting 500. This way the code remains correct even if configTICK_RATE_HZ changes.
Relative delay
vTaskDelay() performs a relative wait. That is, it waits “from now on”.
If a task takes 20ms to execute its work and then does:
vTaskDelay(pdMS_TO_TICKS(1000));The real period will be approximately:
20ms of work + 1000ms of wait = 1020msFor many things this is perfectly fine. If we only want to blink an LED, update a status, or space out non-critical readings, vTaskDelay() is sufficient.
The problem arises when we want a truly periodic task, for example reading a sensor exactly every 100ms.
vTaskDelayUntil
For periodic tasks, FreeRTOS gives us vTaskDelayUntil.
void vTaskDelayUntil(
TickType_t *pxPreviousWakeTime,
TickType_t xTimeIncrement
);This function does not wait “from now”, but until the next scheduled instant. This helps maintain a constant period even if the task’s work takes a little while.
void TareaPeriodica(void *pvParameters) {
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t periodo = pdMS_TO_TICKS(100);
for(;;) {
leerSensor();
procesarDato();
vTaskDelayUntil(&xLastWakeTime, periodo);
}
}Here the task tries to run every 100ms, not “100ms after finishing”. This difference seems small, but in timed systems it prevents the period from drifting little by little.
If the work takes longer than the period, vTaskDelayUntil() cannot recover the lost time: it will return immediately until the time reference is ahead again. It does not turn an overloaded task into a punctual one.
Watch out for ticks
FreeRTOS measures time in ticks, not directly in milliseconds. If configTICK_RATE_HZ is 1000, one tick equals 1ms. If it is 100, one tick equals 10ms.
This means we cannot request a resolution better than the tick. Furthermore, the task wakes up in Ready state: a higher priority task can delay its execution, so resolution does not equal guaranteed latency.
A delay in FreeRTOS is not a high-precision timer. For fast signals, PWM, pulse capture, or microsecond timing, we will usually need hardware peripherals or interrupts.