freertos-software-timers-temporizadores

Software Timers in FreeRTOS

  • 6 min

A Software Timer is a timer managed by FreeRTOS that executes a callback when a deadline expires, without creating a dedicated task for each wait.

Until now, if we wanted something to happen periodically, we had a fixed recipe: we created a task and put an infinite loop with a vTaskDelay.

void BlinkTask(void *p) {
    for(;;) {
        toggleLED();
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}
Copied!

This works, but it’s expensive. Creating a full task (with its 2KB Stack, its TCB, and its context management) just to blink an LED or check if a button is still pressed is overkill.

For these simple, timed tasks, FreeRTOS offers a much lighter and more efficient alternative: Software Timers.

What is a software timer?

A Software Timer allows you to execute a C function (called a Callback) at a specific future moment.

Unlike Hardware Timers (which are physical peripherals on the chip, limited in number and complex to configure), Software Timers are managed by the FreeRTOS Kernel.

  • Advantage: You can create as many as your RAM allows.
  • Advantage: They are easy to use and require no register configuration.
  • Limitation: Their resolution depends on the system Tick (typically 1ms). They are not suitable for microsecond precision.

Timer types

FreeRTOS defines two operating modes:

  1. One-Shot: The timer counts down, executes its callback once, and stops (enters the Dormant state).
  • Typical use: “Turn off the display backlight 10 seconds after the user stops pressing buttons.”
  1. Auto-Reload (Periodic): The timer counts down, executes its callback, and automatically restarts to count again.
  • Typical use: “Read the temperature sensor every 500ms.”

The Timer Service Task

This is CRITICAL to understand. When you use Software Timers, FreeRTOS does not create an invisible task for each timer. That would be inefficient.

Instead, there is a single system task called the Daemon Task (or Timer Service Task) that manages all timers simultaneously.

When a timer expires, this Daemon task wakes up and executes your Callback function.

Important concept: since all callbacks execute within the same Daemon Task:

  1. Never use vTaskDelay inside a callback.
  2. Never block waiting for a semaphore or queue. You can use an API with a 0 timeout and handle the failure.
  3. Keep the callback code very short.

If you block a callback, you block ALL timers in the system.

Software Timer API

Creation (xTimerCreate)

TimerHandle_t xTimerCreate(
    const char * const pcTimerName,  // Name (debug text)
    const TickType_t xTimerPeriodInTicks, // Period
    const UBaseType_t uxAutoReload,  // pdTRUE (Repeat) or pdFALSE (Once)
    void * const pvTimerID,          // Numeric ID (useful for shared callbacks)
    TimerCallbackFunction_t pxCallbackFunction // The function to execute
);
Copied!

Control (Start, Stop, Reset)

These functions send commands to the Daemon Task’s queue.

  • xTimerStart(handle, ticks_to_wait): Starts the timer. If it was already running, it restarts it (resets to 0).
  • xTimerStop(handle, ticks_to_wait): Stops it.
  • xTimerReset(handle, ticks_to_wait): Resets the count. Very useful for software “Watchdogs”.
  • xTimerChangePeriod(handle, new_period, ticks_to_wait): Dynamically changes the duration.

Practical example: one-shot and auto-reload

Let’s build a system with two timers:

  1. Auto-Reload: Blinks an LED every 500ms.
  2. One-Shot: Simulates a “Staircase Light.” When a button is pressed, an LED turns on and is scheduled to turn off automatically after 5 seconds. If you press again before it turns off, the timer resets.
#include <Arduino.h>

// Timer handles
TimerHandle_t blinkTimer;
TimerHandle_t offTimer;

// Pins
#define LED_BLINK  2  // Built-in LED
#define LED_LIGHT  4  // Another LED
#define BUTTON     0  // BOOT Button

// --- Callbacks ---

// 1. Periodic callback (Auto-Reload)
void onBlinkTimer(TimerHandle_t xTimer) {
    // Simply toggle the LED
    static bool state = false;
    state = !state;
    digitalWrite(LED_BLINK, state);
}

// 2. One-shot callback (One-Shot)
void onOffTimer(TimerHandle_t xTimer) {
    // If we get here, 5 seconds have passed without a press
    Serial.println("Time expired. Turning light off.");
    digitalWrite(LED_LIGHT, LOW);
}

// --- Setup ---

void setup() {
    Serial.begin(115200);
    pinMode(LED_BLINK, OUTPUT);
    pinMode(LED_LIGHT, OUTPUT);
    pinMode(BUTTON, INPUT_PULLUP);

    Serial.println("Starting Timers...");

    // 1. Create Periodic Timer (500ms)
    blinkTimer = xTimerCreate(
        "Blinker",              // Name
        pdMS_TO_TICKS(500),     // Period
        pdTRUE,                 // Auto-Reload: YES
        (void*)0,               // ID (not used)
        onBlinkTimer            // Function
    );

    // 2. Create One-Shot Timer (5000ms)
    offTimer = xTimerCreate(
        "StaircaseLight",
        pdMS_TO_TICKS(5000),
        pdFALSE,                // Auto-Reload: NO
        (void*)0,
        onOffTimer
    );

    // Start the blink immediately
    if (blinkTimer == NULL || offTimer == NULL) {
        Serial.println("Could not create timers");
        return;
    }

    if (xTimerStart(blinkTimer, 0) != pdPASS) {
        Serial.println("Could not start blink timer");
    }
}

void loop() {
    // Simulate button detection in the loop (or in another task)
    if(digitalRead(BUTTON) == LOW) {
        Serial.println("Button pressed! Light ON.");
        digitalWrite(LED_LIGHT, HIGH);

        // RESET the timer.
        // If it was stopped, it starts.
        // If it was already counting, it resets to 0 (extends the time).
        if (xTimerReset(offTimer, 0) != pdPASS) {
            Serial.println("Timer command queue is full");
        }

        // Simple debounce
        vTaskDelay(pdMS_TO_TICKS(200));
        while(digitalRead(BUTTON) == LOW) {
            vTaskDelay(pdMS_TO_TICKS(10));
        }
    }
    
    vTaskDelay(pdMS_TO_TICKS(10));
}
Copied!

Behavior Analysis

  1. The Blinker: You will see LED 2 blink indefinitely. The Daemon Task calls it every 500ms.
  2. The Staircase Light:
  • When you press the button, we turn on LED 4 manually and call xTimerReset.
  • The timer starts counting down 5 seconds.
  • If you do nothing, after 5 seconds onOffTimer fires and turns off the LED.
  • If you press the button at second 3, xTimerReset sets the counter back to 0. The light will take a total of 8 seconds to turn off!

Using the Timer ID

Sometimes we want to use the same callback function for 10 different timers (e.g., 10 LEDs). To know which of the 10 expired, we use the ID.

void onGenericTimer(TimerHandle_t xTimer) {
    // Retrieve the ID assigned during creation
    int id = (int)(intptr_t)pvTimerGetTimerID(xTimer);
    
    Serial.printf("Timer number: %d has expired\n", id);
}

// When creating:
timer1 = xTimerCreate("T1", pdMS_TO_TICKS(100), pdTRUE, (void*)(intptr_t)1, onGenericTimer);
timer2 = xTimerCreate("T2", pdMS_TO_TICKS(100), pdTRUE, (void*)(intptr_t)2, onGenericTimer);
Copied!

Daemon Task Priority

In FreeRTOSConfig.h (or in the ESP32’s menuconfig), configTIMER_TASK_PRIORITY is defined.

  • If we set a Low priority, and we have user tasks with High priority that consume a lot of CPU, timers can experience delays (“Jitter”).
  • If we set a High priority, callbacks will suffer less delay from lower-priority tasks, but will interfere more with them.

In the standard ESP32 configuration, this task uses a low priority, but the value depends on how the libraries were built. Increasing it can reduce callback delay at the cost of more interference with other tasks; it does not improve the tick resolution.