Un Software Timer es un temporizador gestionado por FreeRTOS que ejecuta un callback cuando vence un plazo, sin crear una tarea dedicada para cada espera.
Hasta ahora, si queríamos que algo ocurriera cada cierto tiempo, teníamos una receta fija: creábamos una tarea y poníamos un bucle infinito con un vTaskDelay.
void TareaParpadeo(void *p) {
for(;;) {
toggleLED();
vTaskDelay(pdMS_TO_TICKS(1000));
}
}Esto funciona, pero es caro. Crear una tarea completa (con su Stack de 2KB, su TCB y su gestión de contexto) solo para parpadear un LED o comprobar si un botón sigue pulsado es matar moscas a cañonazos.
Para estas tareas sencillas y temporizadas, FreeRTOS nos ofrece una alternativa mucho más ligera y eficiente: los Software Timers (Temporizadores por Software).
¿Qué es un software timer?
Un Software Timer permite ejecutar una función de C (llamada Callback) en un momento futuro determinado.
A diferencia de los Hardware Timers (que son periféricos físicos del chip, limitados en número y complejos de configurar), los Software Timers son gestionados por el Kernel de FreeRTOS.
- Ventaja: Puedes crear tantos como te permita la memoria RAM.
- Ventaja: Son fáciles de usar y no requieren configuración de registros.
- Limitación: Su resolución depende del Tick del sistema (normalmente 1ms). No sirven para precisiones de microsegundos.
Tipos de temporizadores
FreeRTOS define dos modos de funcionamiento:
- One-Shot (Disparo Único): El temporizador cuenta, ejecuta su callback una sola vez y se detiene (pasa a estado Dormant).
- Uso típico: “Apagar la luz de fondo de la pantalla 10 segundos después de que el usuario deje de tocar botones.”
- Auto-Reload (Periódico): El temporizador cuenta, ejecuta su callback y se reinicia automáticamente para volver a contar.
- Uso típico: “Leer el sensor de temperatura cada 500ms.”
La tarea Timer Service
Esto es CRÍTICO de entender. Cuando usas Software Timers, FreeRTOS no crea una tarea invisible por cada temporizador. Eso sería ineficiente.
En su lugar, existe una única tarea del sistema llamada Daemon Task (o Timer Service Task) que gestiona todos los temporizadores a la vez.
Cuando un temporizador vence, esta tarea Daemon despierta y ejecuta tu función Callback.
Idea importante: como todos los callbacks se ejecutan dentro de la misma Tarea Daemon:
- Nunca uses
vTaskDelaydentro de un callback. - Nunca esperes de forma bloqueante un semáforo o una cola. Puedes usar una API con tiempo de espera
0y gestionar el fallo. - Mantén el código del callback muy corto.
Si bloqueas un callback, bloqueas TODOS los temporizadores del sistema.
API de Software Timers
Creación (xTimerCreate)
TimerHandle_t xTimerCreate(
const char * const pcTimerName, // Nombre (texto debug)
const TickType_t xTimerPeriodInTicks, // Periodo
const UBaseType_t uxAutoReload, // pdTRUE (Repetir) o pdFALSE (Una vez)
void * const pvTimerID, // ID numérico (útil si compartimos callback)
TimerCallbackFunction_t pxCallbackFunction // La función a ejecutar
);Control (Start, Stop, Reset)
Estas funciones envían comandos a la cola de la Tarea Daemon.
xTimerStart(handle, ticks_espera): Arranca el temporizador. Si ya estaba corriendo, lo reinicia (vuelve a contar desde 0).xTimerStop(handle, ticks_espera): Lo detiene.xTimerReset(handle, ticks_espera): Reinicia la cuenta. Muy útil para “Watchdogs” de software.xTimerChangePeriod(handle, nuevo_periodo, ticks_espera): Cambia la duración dinámicamente.
Ejemplo práctico: one-shot y auto-reload
Vamos a montar un sistema con dos temporizadores:
- Auto-Reload: Parpadea un LED cada 500ms.
- One-Shot: Simula una “Luz de escalera”. Al pulsar un botón, se enciende un LED y se programa para apagarse solo a los 5 segundos. Si pulsas de nuevo antes de que se apague, el tiempo se reinicia.
#include <Arduino.h>
// Handles de los temporizadores
TimerHandle_t timerParpadeo;
TimerHandle_t timerApagado;
// Pines
#define LED_PARPADEO 2 // LED Integrado
#define LED_LUZ 4 // Otro LED
#define BOTON 0 // Botón BOOT
// --- Callbacks ---
// 1. Callback periódico (Auto-Reload)
void onTimerParpadeo(TimerHandle_t xTimer) {
// Simplemente invertimos el LED
static bool estado = false;
estado = !estado;
digitalWrite(LED_PARPADEO, estado);
}
// 2. Callback de disparo único (One-Shot)
void onTimerApagado(TimerHandle_t xTimer) {
// Si entramos aquí, es que han pasado 5 segundos sin pulsar
Serial.println("Tiempo agotado. Apagando luz.");
digitalWrite(LED_LUZ, LOW);
}
// --- Setup ---
void setup() {
Serial.begin(115200);
pinMode(LED_PARPADEO, OUTPUT);
pinMode(LED_LUZ, OUTPUT);
pinMode(BOTON, INPUT_PULLUP);
Serial.println("Iniciando Timers...");
// 1. Crear Timer Periódico (500ms)
timerParpadeo = xTimerCreate(
"Blinker", // Nombre
pdMS_TO_TICKS(500), // Periodo
pdTRUE, // Auto-Reload: SI
(void*)0, // ID (no usado)
onTimerParpadeo // Función
);
// 2. Crear Timer One-Shot (5000ms)
timerApagado = xTimerCreate(
"LuzEscalera",
pdMS_TO_TICKS(5000),
pdFALSE, // Auto-Reload: NO
(void*)0,
onTimerApagado
);
// Arrancamos el parpadeo ya
if (timerParpadeo == NULL || timerApagado == NULL) {
Serial.println("No se pudieron crear los timers");
return;
}
if (xTimerStart(timerParpadeo, 0) != pdPASS) {
Serial.println("No se pudo arrancar el timer de parpadeo");
}
}
void loop() {
// Simulamos la detección del botón en el loop (o en otra tarea)
if(digitalRead(BOTON) == LOW) {
Serial.println("Botón pulsado! Luz ON.");
digitalWrite(LED_LUZ, HIGH);
// RESETEAMOS el temporizador.
// Si estaba parado, arranca.
// Si ya estaba contando, vuelve a 0 (extiende el tiempo).
if (xTimerReset(timerApagado, 0) != pdPASS) {
Serial.println("La cola de comandos del timer está llena");
}
// Debounce simple
vTaskDelay(pdMS_TO_TICKS(200));
while(digitalRead(BOTON) == LOW) {
vTaskDelay(pdMS_TO_TICKS(10));
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}Análisis del comportamiento
- El Blinker: Verás el LED 2 parpadear indefinidamente. La Tarea Daemon le llama cada 500ms.
- La Luz de Escalera:
- Al pulsar el botón, encendemos el LED 4 manualmente y llamamos a
xTimerReset. - El temporizador empieza a contar 5 segundos.
- Si no haces nada, a los 5 segundos salta
onTimerApagadoy apaga el LED. - Si pulsas el botón al segundo 3,
xTimerResetpone el contador a 0 otra vez. ¡La luz tardará 8 segundos en total en apagarse!
Uso del Timer ID (Identificador)
A veces queremos usar la misma función de callback para 10 temporizadores distintos (ej. 10 LEDs). Para saber cuál de los 10 ha saltado, usamos el ID.
void onTimerGenerico(TimerHandle_t xTimer) {
// Recuperamos el ID que asignamos al crear
int id = (int)(intptr_t)pvTimerGetTimerID(xTimer);
Serial.printf("Ha saltado el Timer número: %d\n", id);
}
// Al crear:
timer1 = xTimerCreate("T1", pdMS_TO_TICKS(100), pdTRUE, (void*)(intptr_t)1, onTimerGenerico);
timer2 = xTimerCreate("T2", pdMS_TO_TICKS(100), pdTRUE, (void*)(intptr_t)2, onTimerGenerico);Prioridad de la Tarea Daemon
En FreeRTOSConfig.h (o en el menuconfig del ESP32), se define configTIMER_TASK_PRIORITY.
- Si ponemos una prioridad Baja, y tenemos tareas de usuario de prioridad Alta que consumen mucha CPU, los temporizadores pueden retrasarse (“Jitter”).
- Si ponemos una prioridad alta, los callbacks sufrirán menos espera ante tareas menos prioritarias, pero interferirán más con ellas.
En la configuración habitual de ESP32 esta tarea usa una prioridad baja, pero el valor depende de cómo se hayan construido las librerías. Subirla puede reducir el retardo de los callbacks, a costa de interferir más con otras tareas; no mejora la resolución del tick.