Una interrupción es un evento de hardware que detiene temporalmente la ejecución normal para atender algo urgente mediante una ISR.
Inauguramos el último bloque del curso entrando en una zona delicada. Hasta ahora, todo lo que hemos programado (Tareas, Colas, Timers) ocurría dentro del mundo gestionado por el Kernel de FreeRTOS.
El mundo real es asíncrono. Un botón se pulsa, un paquete WiFi llega, un sensor I2C termina de leer. Estos eventos disparan interrupciones (ISRs - Interrupt Service Routines).
Una interrupción hace que el núcleo pause la tarea actual y ejecute una rutina específica. Según la arquitectura y las prioridades configuradas, otra interrupción más prioritaria todavía puede interrumpir esa ISR.
En un RTOS, las interrupciones son delicadas: son necesarias para la reactividad, pero si no seguimos unas reglas básicas, podemos provocar reinicios aleatorios (Guru Meditation Error en ESP32).
Hoy vamos a aprender a domar las ISRs.
API desde tarea e ISR
| Acción | Función Estándar (Tarea) | Función ISR (Interrupción) |
|---|---|---|
| Semáforo Give | xSemaphoreGive | xSemaphoreGiveFromISR |
| Semáforo Take | xSemaphoreTake | PROHIBIDO (Bloqueante) |
| Cola Send | xQueueSend | xQueueSendFromISR |
| Cola Receive | xQueueReceive | xQueueReceiveFromISR |
| Notificación | xTaskNotify | xTaskNotifyFromISR |
| Delay | vTaskDelay | PROHIBIDO |
Reglas básicas de una ISR
Cuando escribimos código dentro de una función de interrupción (por ejemplo, la función que llamamos con attachInterrupt), estamos fuera del control del Scheduler. Por tanto, aplican reglas muy estrictas:
Mantén la ISR lo más corta posible
Esto es ley universal en sistemas embebidos, pero en RTOS es especialmente importante. Mientras estás en una ISR, ninguna tarea se ejecuta. Si tu ISR tarda 5ms, has congelado el sistema 5ms.
- Mal: Imprimir por
Serial, hacer cálculos complejos, leer sensores lentos. - Bien: Leer un registro, limpiar un flag, avisar a una tarea y salir.
Este patrón se llama Procesamiento Diferido (Deferred Interrupt Processing): La ISR solo avisa (“¡Eh, ha pasado algo!”), y una Tarea se encarga del trabajo pesado.
No uses funciones bloqueantes
Una ISR no es una Tarea. No tiene TCB, no tiene su propio contexto completo gestionado por el Scheduler. Por tanto, no puede dormirse.
Si llamas a una función que intenta bloquearse, el sistema fallará.
- Prohibido:
vTaskDelay,vTaskDelayUntil. - Prohibido:
xQueueReceivecon tiempo de espera. - Prohibido:
xSemaphoreTakecon tiempo de espera. - Prohibido: Intentar tomar un Mutex (porque los Mutex implican bloqueo y herencia de prioridad, conceptos que no existen en una ISR).
Usa las funciones FromISR
Esta es la regla que más errores de compilación y ejecución causa a los principiantes.
La API estándar de FreeRTOS (xQueueSend, xSemaphoreGive) no es segura dentro de una interrupción. FreeRTOS proporciona una API paralela específica para ISRs, que siempre termina en ...FromISR.
- En lugar de
xSemaphoreGiveusaxSemaphoreGiveFromISR. - En lugar de
xQueueSendusaxQueueSendFromISR. - En lugar de
xTaskNotifyusaxTaskNotifyFromISR.
Para qué sirve portYIELD_FROM_ISR
Muchas funciones FromISR reciben un parámetro llamado pxHigherPriorityTaskWoken.
¿Para qué sirve? Entendamos el flujo:
- El sistema está ejecutando una tarea de Baja Prioridad.
- Salta la Interrupción.
- La ISR le da un Semáforo a una tarea de Alta Prioridad que estaba esperando.
- Ahora la tarea de Alta Prioridad está Ready.
Si la ISR termina sin hacer nada más, el Scheduler no se entera inmediatamente. La CPU volverá a la tarea de Baja Prioridad y la tarea Alta no se ejecutará hasta el próximo Tick del sistema (que puede tardar 1ms). ¡Hemos perdido reactividad!
Para evitar esto, las funciones FromISR nos “chivan” si hemos despertado a alguien importante.
// Variable booleana (BaseType_t en FreeRTOS)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// La función modifica esta variable a pdTRUE si despertó a una tarea importante
xSemaphoreGiveFromISR(miSemaforo, &xHigherPriorityTaskWoken);
// Si es TRUE, solicitamos un cambio de contexto VOLUNTARIO e INMEDIATO
if(xHigherPriorityTaskWoken == pdTRUE) {
portYIELD_FROM_ISR();
}Al llamar a portYIELD_FROM_ISR, pedimos al Scheduler que decida de nuevo al salir de la interrupción. Si corresponde, la CPU continúa con la tarea recién desbloqueada. La latencia sigue dependiendo de la ISR, de interrupciones más prioritarias y de las secciones críticas activas.
Ejemplo completo
Vamos a implementar el patrón de procesamiento diferido usando un botón y un semáforo binario, aplicando todo lo aprendido.
En ESP32, IRAM_ATTR coloca la función en RAM. Es necesario cuando la ISR debe seguir disponible con la caché de Flash deshabilitada, pero no basta por sí solo: todas las funciones y los datos a los que acceda también deben ser seguros para ese contexto.
#include <Arduino.h>
// Recursos globales
SemaphoreHandle_t semaforoBoton;
#define PIN_BOTON 0
// --- ISR (Interrupción) ---
// Regla 1: Corta. Regla 2: No bloqueante. Regla 3: API FromISR.
void IRAM_ATTR isrBoton() {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// Enviamos la señal
xSemaphoreGiveFromISR(semaforoBoton, &xHigherPriorityTaskWoken);
// Solicitamos cambio de contexto si es necesario
if(xHigherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
// --- Tarea Diferida (Procesamiento) ---
void TareaManejadora(void *pvParameters) {
for(;;) {
// Aquí SÍ podemos bloquearnos esperando
if(xSemaphoreTake(semaforoBoton, portMAX_DELAY) == pdTRUE) {
// Trabajo pesado (ej. escribir en SD, Serial, Network)
Serial.println("Interrupción recibida. Procesando...");
// Simulamos carga
for(int i=0; i<5; i++) {
digitalWrite(LED_BUILTIN, HIGH);
vTaskDelay(pdMS_TO_TICKS(50));
digitalWrite(LED_BUILTIN, LOW);
vTaskDelay(pdMS_TO_TICKS(50));
}
}
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
pinMode(PIN_BOTON, INPUT_PULLUP);
// Crear semáforo binario
semaforoBoton = xSemaphoreCreateBinary();
if (semaforoBoton == NULL) {
Serial.println("No se pudo crear el semáforo");
return;
}
// Crear tarea con prioridad ALTA
// Queremos que en cuanto pulse el botón, esta tarea interrumpa al resto
xTaskCreate(TareaManejadora, "Handler", 2048, NULL, 3, NULL);
// Configurar interrupción
attachInterrupt(digitalPinToInterrupt(PIN_BOTON), isrBoton, FALLING);
}
void loop() {
// Tarea de fondo de baja prioridad
Serial.println("Loop haciendo cosas irrelevantes...");
vTaskDelay(pdMS_TO_TICKS(1000));
}Secciones críticas
A veces, la ISR y una Tarea comparten una variable simple (ej. un contador volatile int contador) y no queremos usar la sobrecarga de un semáforo o cola.
Si la Tarea está leyendo la variable y la ISR salta justo en medio y la modifica, tendremos corrupción de datos.
Para evitar que una ISR interrumpa un trozo de código sensible, usamos Secciones Críticas.
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
// Dentro de una tarea: toma el spinlock y enmascara
// las interrupciones gestionadas por este mecanismo en el núcleo actual.
portENTER_CRITICAL(&mux);
// --- Código atómico y muy rápido ---
contadorGlobal++;
// ----------------------------------
// 2. Salir de sección crítica (Rehabilita interrupciones)
portEXIT_CRITICAL(&mux);En un ESP32 de dos núcleos, el spinlock evita que el otro núcleo entre en una sección protegida por el mismo portMUX_TYPE. Desde una ISR se usan las variantes portENTER_CRITICAL_ISR y portEXIT_CRITICAL_ISR.
Usa las secciones críticas con extrema precaución. Aumentan la latencia de las interrupciones enmascaradas y hacen girar al otro núcleo si compite por el mismo spinlock. Reserva este mecanismo para operaciones muy cortas y no bloqueantes.