Interrupts and RTOS: Instant Response to Real-World Events
What Is an Interrupt and Why You Need One
An interrupt pauses the currently executing code, runs a short handler function, and returns to the original code exactly where it left off. Instead of constantly polling, the MCU reacts instantly when an event occurs.
In a factory, a motor stall sensor must trigger emergency stop within microseconds. The NVIC on Cortex-M manages up to 240 interrupt sources with configurable priorities. Higher-priority interrupts preempt lower-priority ones, so safety-critical events always take precedence.
External Interrupts: Instant Response to Events
External interrupts trigger on GPIO pin edges — rising, falling, or both. This pattern counts pulses from flow meters, RPM sensors, and encoders:
volatile uint32_t pulse_count = 0;
void exti_init(void) {
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_0; gpio.Mode = GPIO_MODE_IT_RISING;
gpio.Pull = GPIO_PULLDOWN;
HAL_GPIO_Init(GPIOA, &gpio);
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
void HAL_GPIO_EXTI_Callback(uint16_t pin) {
if (pin == GPIO_PIN_0) pulse_count++;
}
The main loop periodically reads and resets the counter to calculate flow rate or RPM.
Timer Interrupts: Precise Periodic Tasks
Timer interrupts fire at exact intervals for deterministic sensor sampling and control loops. The handler sets a flag rather than performing heavy work — keeping interrupts short is critical.
volatile bool sensor_flag = false;
void TIM7_IRQHandler(void) {
if (__HAL_TIM_GET_FLAG(&htim7, TIM_FLAG_UPDATE)) {
__HAL_TIM_CLEAR_FLAG(&htim7, TIM_FLAG_UPDATE);
sensor_flag = true; // Signal main loop to read sensors
}
}
Interrupt Pitfalls: Race Conditions and Volatile
Interrupts introduce concurrency. The volatile keyword forces the compiler to read actual memory each time, preventing optimization bugs with shared variables.
// WRONG: compiler may cache the value
uint32_t counter = 0;
// CORRECT: volatile forces re-read from memory
volatile uint32_t counter = 0;
Critical sections protect multi-byte reads from corruption when an interrupt fires mid-access:
uint32_t safe_read_counter(void) {
__disable_irq();
uint32_t value = pulse_count;
__enable_irq();
return value;
}
Rules for interrupt handlers: keep them short, never call blocking functions, never allocate memory, always use volatile for shared variables, and use critical sections for multi-byte data.
FreeRTOS: A Minimal Real-Time Operating System
When bare-metal firmware grows too complex, FreeRTOS provides structured multitasking with priorities. It runs on Cortex-M with as little as 4 KB of RAM.
#include "FreeRTOS.h"
#include "task.h"
void sensor_task(void *params) {
while (1) {
float temp = read_temperature(&hadc1);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
xTaskCreate(sensor_task, "Sensors", 256, NULL, 2, NULL);
vTaskStartScheduler();
while (1) {}
}
Tasks, Priorities, and Queues in FreeRTOS
FreeRTOS uses preemptive scheduling. Industrial priority hierarchy: safety interlocks (4), motor control (3), sensor reading (2), logging (1). Queues safely pass data between tasks:
typedef struct { float temperature, current; } SensorData;
QueueHandle_t sensor_queue;
void sensor_task(void *p) {
while (1) {
SensorData d = { read_temperature(&hadc1), read_current(&hadc2) };
xQueueSend(sensor_queue, &d, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
Practical Example: Multi-Task System Reading Sensors and Controlling Motors
This example uses an external interrupt for emergency stop (highest priority) and a FreeRTOS task for PID motor control.
SemaphoreHandle_t emergency_sem;
void HAL_GPIO_EXTI_Callback(uint16_t pin) {
if (pin == GPIO_PIN_1) {
BaseType_t w = pdFALSE;
xSemaphoreGiveFromISR(emergency_sem, &w);
portYIELD_FROM_ISR(w);
}
}
void safety_task(void *p) {
while (1) {
xSemaphoreTake(emergency_sem, portMAX_DELAY);
motor_set_speed(&motor, 0);
}
}
void control_task(void *p) {
PidController pid;
pid_init(&pid, 2.0f, 0.5f, 0.1f);
while (1) {
float duty = pid_compute(&pid, 45.0f, read_temperature(&hadc1), 0.01f);
motor_set_speed(&motor, (int16_t)duty);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
emergency_sem = xSemaphoreCreateBinary();
xTaskCreate(safety_task, "Safety", 256, NULL, 4, NULL);
xTaskCreate(control_task, "Control", 512, NULL, 3, NULL);
vTaskStartScheduler();
while (1) {}
}
Summary
Interrupts let the MCU respond instantly to hardware events without wasting CPU cycles on polling. External interrupts react to GPIO edges, timer interrupts create precise periodic schedules. Shared data requires volatile and critical sections to prevent race conditions. FreeRTOS provides structured multitasking with prioritized tasks, queues for safe data passing, and semaphores for event signaling. Industrial systems assign priorities by criticality — safety first, then control, then communication. The next lesson introduces ESP32 and wireless IIoT with WiFi and MQTT.