freertos-interrupciones-isr-reglas-oro

Interrupts in FreeRTOS and FromISR Functions

  • 6 min

A interrupt is a hardware event that temporarily stops normal execution to handle something urgent via an ISR.

We inaugurate the final block of the course by entering a delicate area. Until now, everything we have programmed (Tasks, Queues, Timers) occurred within the world managed by the FreeRTOS Kernel.

The real world is asynchronous. A button is pressed, a WiFi packet arrives, an I2C sensor finishes reading. These events trigger interrupts (ISRs - Interrupt Service Routines).

An interrupt causes the core to pause the current task and execute a specific routine. Depending on the architecture and configured priorities, another, higher priority interrupt can still interrupt that ISR.

In an RTOS, interrupts are delicate: they are necessary for responsiveness, but if we don’t follow some basic rules, we can cause random resets (Guru Meditation Error on ESP32).

Today we are going to learn how to tame ISRs.

API from Task and ISR

ActionStandard Function (Task)ISR Function (Interrupt)
Semaphore GivexSemaphoreGivexSemaphoreGiveFromISR
Semaphore TakexSemaphoreTakeFORBIDDEN (Blocking)
Queue SendxQueueSendxQueueSendFromISR
Queue ReceivexQueueReceivexQueueReceiveFromISR
NotificationxTaskNotifyxTaskNotifyFromISR
DelayvTaskDelayFORBIDDEN

Basic Rules of an ISR

When we write code inside an interrupt function (for example, the function we call with attachInterrupt), we are outside the control of the Scheduler. Therefore, very strict rules apply:

Keep the ISR as short as possible

This is a universal law in embedded systems, but it is especially important in an RTOS. While you are in an ISR, no task is running. If your ISR takes 5ms, you have frozen the system for 5ms.

  • Bad: Printing over Serial, doing complex calculations, reading slow sensors.
  • Good: Read a register, clear a flag, notify a task, and exit.

This pattern is called Deferred Interrupt Processing: The ISR only signals (“Hey, something happened!”), and a Task handles the heavy work.

Do not use blocking functions

An ISR is not a Task. It has no TCB, no full context managed by the Scheduler. Therefore, it cannot go to sleep.

If you call a function that tries to block, the system will crash.

  • Forbidden: vTaskDelay, vTaskDelayUntil.
  • Forbidden: xQueueReceive with a timeout.
  • Forbidden: xSemaphoreTake with a timeout.
  • Forbidden: Trying to take a Mutex (because Mutexes imply blocking and priority inheritance, concepts that do not exist in an ISR).
Use the FromISR functions

This is the rule that causes the most compilation and runtime errors for beginners.

The standard FreeRTOS API (xQueueSend, xSemaphoreGive) is not safe inside an interrupt. FreeRTOS provides a parallel API specifically for ISRs, which always ends in ...FromISR.

  • Instead of xSemaphoreGive use xSemaphoreGiveFromISR.
  • Instead of xQueueSend use xQueueSendFromISR.
  • Instead of xTaskNotify use xTaskNotifyFromISR.

What is portYIELD_FROM_ISR for?

Many FromISR functions receive a parameter called pxHigherPriorityTaskWoken.

What is it for? Let’s understand the flow:

  1. The system is executing a Low Priority task.
  2. The Interrupt fires.
  3. The ISR gives a Semaphore to a High Priority task that was waiting.
  4. Now the High Priority task is Ready.

If the ISR ends without doing anything else, the Scheduler does not find out immediately. The CPU will return to the Low Priority task, and the High task will not run until the next system Tick (which could be 1ms away). We have lost responsiveness!

To avoid this, the FromISR functions “tell us” if we have woken up an important task.

// Boolean variable (BaseType_t in FreeRTOS)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;

// The function modifies this variable to pdTRUE if it woke an important task
xSemaphoreGiveFromISR(mySemaphore, &xHigherPriorityTaskWoken);

// If it is TRUE, we request a VOLUNTARY and IMMEDIATE context switch
if(xHigherPriorityTaskWoken == pdTRUE) {
    portYIELD_FROM_ISR(); 
}
Copied!

By calling portYIELD_FROM_ISR, we ask the Scheduler to decide again upon exiting the interrupt. If appropriate, the CPU continues with the newly unblocked task. Latency still depends on the ISR, higher priority interrupts, and active critical sections.

Complete Example

Let’s implement the deferred processing pattern using a button and a binary semaphore, applying everything we’ve learned.

On the ESP32, IRAM_ATTR places the function in RAM. It is necessary when the ISR must remain available with the Flash cache disabled, but it is not sufficient by itself: all functions and data it accesses must also be safe for that context.

#include <Arduino.h>

// Global resources
SemaphoreHandle_t buttonSemaphore;
#define BUTTON_PIN 0

// --- ISR (Interrupt) ---
// Rule 1: Short. Rule 2: Non-blocking. Rule 3: FromISR API.
void IRAM_ATTR isrButton() {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    // Send the signal
    xSemaphoreGiveFromISR(buttonSemaphore, &xHigherPriorityTaskWoken);

    // Request a context switch if necessary
    if(xHigherPriorityTaskWoken) {
        portYIELD_FROM_ISR();
    }
}

// --- Deferred Task (Processing) ---
void HandlerTask(void *pvParameters) {
    for(;;) {
        // Here we CAN block waiting
        if(xSemaphoreTake(buttonSemaphore, portMAX_DELAY) == pdTRUE) {
            
            // Heavy work (e.g., writing to SD, Serial, Network)
            Serial.println("Interrupt received. Processing...");
            
            // Simulate workload
            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(BUTTON_PIN, INPUT_PULLUP);

    // Create binary semaphore
    buttonSemaphore = xSemaphoreCreateBinary();

    if (buttonSemaphore == NULL) {
        Serial.println("Failed to create semaphore");
        return;
    }

    // Create task with HIGH priority
    // We want that as soon as the button is pressed, this task interrupts the rest
    xTaskCreate(HandlerTask, "Handler", 2048, NULL, 3, NULL);

    // Setup the interrupt
    attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), isrButton, FALLING);
}

void loop() {
    // Low priority background task
    Serial.println("Loop doing irrelevant things...");
    vTaskDelay(pdMS_TO_TICKS(1000));
}
Copied!

Critical Sections

Sometimes, the ISR and a Task share a simple variable (e.g., a volatile int counter) and we don’t want to use the overhead of a semaphore or queue.

If the Task is reading the variable and the ISR fires right in the middle and modifies it, we will have data corruption.

To prevent an ISR from interrupting a sensitive piece of code, we use Critical Sections.

portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;

// Inside a task: takes the spinlock and masks
// interrupts managed by this mechanism on the current core.
portENTER_CRITICAL(&mux);

// --- Atomic and very fast code ---
globalCounter++; 
// ----------------------------------

// 2. Exit critical section (Re-enable interrupts)
portEXIT_CRITICAL(&mux);
Copied!

On a dual-core ESP32, the spinlock prevents the other core from entering a section protected by the same portMUX_TYPE. From an ISR, the variants portENTER_CRITICAL_ISR and portEXIT_CRITICAL_ISR are used.

Use critical sections with extreme caution. They increase the latency of masked interrupts and cause the other core to spin if it competes for the same spinlock. Reserve this mechanism for very short, non-blocking operations.