Introduction
Have you ever encountered a scenario where the system suddenly freezes under high load, and after debugging, you find that it was due to directly parsing JSON data in the serial port receive interrupt? Or have you noticed that the response time of a certain interrupt is too long, causing delays in executing other important interrupts?
This is a classic problem in embedded development: executing complex tasks in Interrupt Service Routines (ISR). What seems like a simple and direct approach can lead to severe consequences such as sluggish system response, decreased real-time performance, and even deadlocks.
Today, we will delve into: why complex tasks should not be handled in interrupts? What are the elegant solutions? How to choose the most suitable handling method based on specific scenarios? This article will provide you with a complete toolbox of solutions.
Fundamentals: The Special Nature of Interrupt Execution Environment
To understand why complex tasks cannot be handled in interrupts, we first need to understand the special nature of the interrupt execution environment.
Limitations of Interrupt Context
Hardware Peripheral Interrupt Service Routine Main Task Hardware Peripheral Interrupt Service Routine Main Task Task is interrupted, context saved Limited execution environment Restore context, continue execution Normal execution Hardware interrupt triggers Interrupt handling Interrupt handling completed Normal execution
The Interrupt Service Routine runs in a special execution environment with the following limitations:
- 1. Cannot sleep or block: Functions that may cause task switching are not allowed to be called in the interrupt context
- 2. Limited stack space: Typically uses a separate interrupt stack, which is much smaller than the task stack
- 3. Non-preemptive: Interrupts of the same or lower priority are masked
- 4. Execution time sensitive: Affects the real-time response performance of the entire system
Real-time Requirements
// Incorrect example: Performing complex calculations in an interrupt
void UART_IRQHandler(void) {
if (UART_GetFlag(UART_FLAG_RXNE)) {
char data = UART_ReceiveData();
// ❌ Incorrect: Complex processing in interrupt
if (data == '{') {
json_parse_start(); // May take several milliseconds
}
parse_protocol_data(data); // Complex state machine processing
calculate_checksum(); // High computational cost
UART_ClearFlag(UART_FLAG_RXNE);
}
}
When the interrupt handling time is too long, it can create a chain reaction:
- • Increased interrupt latency: The response time of other interrupts becomes longer
- • Decreased system real-time performance: Task scheduling is delayed
- • Risk of data loss: High-frequency interrupts may not be processed in time
Core Issue: Risks of Complex Tasks
Problem Classification Analysis
Risks of Complex Tasks in Interrupts
Delay Issues
Interrupt response time too long
Affects other interrupt handling
Real-time performance degradation
Resource contention
Insufficient stack space
Memory allocation conflicts
Shared data contention
System stability
Priority inversion
Deadlock risk
Unpredictable execution time
Typical Problem Scenarios
Scenario 1: Data Processing Delay
// ❌ Problem code
void SPI_IRQHandler(void) {
uint16_t adc_value = SPI_ReadData();
// Complex digital filtering algorithm - may take 100μs+
float filtered_value = kalman_filter(adc_value);
process_sensor_data(filtered_value);
}
Scenario 2: Stack Overflow Risk
// ❌ Problem code
void CAN_IRQHandler(void) {
CAN_Message msg;
CAN_Receive(&msg);
// Large array occupies stack space
char buffer[512];
sprintf(buffer, "CAN ID:%d Data:%d", msg.id, msg.data);
// Recursive call, stack usage is unpredictable
parse_can_frame_recursive(&msg);
}
Scenario 3: Data Contention
// ❌ Problem code
volatile int global_counter = 0;
void Timer_IRQHandler(void) {
// Competes with the main task for access to the global variable
global_counter++;
if (global_counter > 100) {
reset_system(); // May reset while the main task is using counter
}
}
Solution Toolbox
For different application scenarios, we have four main solutions, each with its applicable scenarios and pros and cons.
Solution 1: Semaphore Notification Mechanism
Applicable Scenarios: Simple event notifications, small data volume, relatively simple processing logic.
// ✅ Correct approach: Semaphore notification
#include "FreeRTOS.h"
#include "semphr.h"
SemaphoreHandle_t uart_data_sem;
// Interrupt Service Routine: Only performs basic data acquisition and notification
void UART_IRQHandler(void) {
if (UART_GetFlag(UART_FLAG_RXNE)) {
uart_rx_buffer[rx_index++] = UART_ReceiveData();
// Notify processing task of new data
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(uart_data_sem, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
UART_ClearFlag(UART_FLAG_RXNE);
}
}
// Processing task: Perform complex processing in task context
void uart_process_task(void *param) {
while (1) {
// Wait for interrupt notification
if (xSemaphoreTake(uart_data_sem, portMAX_DELAY)) {
// Safely process data in task context
parse_protocol_data(uart_rx_buffer, rx_index);
rx_index = 0; // Reset buffer index
}
}
}
Advantages: Simple implementation, low overhead, good real-time performanceDisadvantages: Can only pass notifications, cannot directly pass complex data
Solution 2: Work Queue Mechanism
Applicable Scenarios: Requires passing structured data, complex processing logic, allows for slight delays.
// ✅ Work queue solution
typedef struct {
uint8_t sensor_id;
uint16_t raw_value;
uint32_t timestamp;
} SensorData_t;
QueueHandle_t sensor_queue;
// Interrupt: Quickly collect data and enqueue
void ADC_IRQHandler(void) {
SensorData_t sensor_data;
sensor_data.sensor_id = current_channel;
sensor_data.raw_value = ADC_GetValue();
sensor_data.timestamp = xTaskGetTickCountFromISR();
// Send data to queue
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(sensor_queue, &sensor_data, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// Processing task: Retrieve data from queue and process
void sensor_process_task(void *param) {
SensorData_t received_data;
while (1) {
if (xQueueReceive(sensor_queue, &received_data, portMAX_DELAY)) {
// Complex data processing
float calibrated_value = calibrate_sensor(
received_data.sensor_id,
received_data.raw_value
);
// Digital filtering
float filtered_value = apply_filter(calibrated_value);
// Data storage and reporting
store_sensor_data(received_data.sensor_id, filtered_value);
report_to_cloud(filtered_value);
}
}
}
Advantages: Supports data passing, good decoupling, supports multiple producersDisadvantages: Has some memory overhead, risk of queue being full
Solution 3: Double Buffering Mechanism
Applicable Scenarios: High-frequency continuous data acquisition, high real-time requirements, large data volume.
// ✅ Double buffering (Ping-Pong Buffer) solution
#define BUFFER_SIZE 1024
typedef struct {
uint16_t buffer_a[BUFFER_SIZE];
uint16_t buffer_b[BUFFER_SIZE];
volatile uint16_t *active_buffer;
volatile uint16_t *process_buffer;
volatile uint32_t buffer_index;
volatile bool buffer_ready;
} DualBuffer_t;
DualBuffer_t dual_buffer = {
.active_buffer = dual_buffer.buffer_a,
.process_buffer = dual_buffer.buffer_b,
.buffer_index = 0,
.buffer_ready = false
};
// Interrupt: Quickly fill the current buffer
void DMA_IRQHandler(void) {
// DMA transfer complete interrupt
if (DMA_GetFlag(DMA_FLAG_TC)) {
// Swap buffers
if (dual_buffer.active_buffer == dual_buffer.buffer_a) {
dual_buffer.active_buffer = dual_buffer.buffer_b;
dual_buffer.process_buffer = dual_buffer.buffer_a;
} else {
dual_buffer.active_buffer = dual_buffer.buffer_a;
dual_buffer.process_buffer = dual_buffer.buffer_b;
}
dual_buffer.buffer_ready = true;
dual_buffer.buffer_index = 0;
// Notify processing task
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(buffer_ready_sem, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
DMA_ClearFlag(DMA_FLAG_TC);
}
}
// Processing task: Process filled buffer data
void data_process_task(void *param) {
while (1) {
if (xSemaphoreTake(buffer_ready_sem, portMAX_DELAY)) {
// Process complete buffer data
fft_analysis(dual_buffer.process_buffer, BUFFER_SIZE);
frequency_analysis();
dual_buffer.buffer_ready = false;
}
}
}
Advantages: No data copy overhead, suitable for high-frequency acquisition, good real-time performanceDisadvantages: High memory consumption, complex implementation
Solution 4: State Machine Pattern
Applicable Scenarios: Complex protocol parsing with clear state transition logic.
// ✅ State machine driven solution
typedef enum {
STATE_WAIT_HEADER,
STATE_WAIT_LENGTH,
STATE_WAIT_DATA,
STATE_WAIT_CHECKSUM,
STATE_PROCESSING
} ProtocolState_t;
typedef struct {
ProtocolState_t state;
uint8_t buffer[256];
uint16_t index;
uint16_t expected_length;
uint8_t checksum;
} ProtocolParser_t;
ProtocolParser_t parser = {STATE_WAIT_HEADER, {0}, 0, 0, 0};
QueueHandle_t protocol_queue;
// Interrupt: Simple state machine advancement
void UART_IRQHandler(void) {
uint8_t data = UART_ReceiveData();
bool frame_complete = false;
switch (parser.state) {
case STATE_WAIT_HEADER:
if (data == 0xAA) {
parser.state = STATE_WAIT_LENGTH;
parser.index = 0;
}
break;
case STATE_WAIT_LENGTH:
parser.expected_length = data;
parser.state = STATE_WAIT_DATA;
break;
case STATE_WAIT_DATA:
parser.buffer[parser.index++] = data;
parser.checksum += data;
if (parser.index >= parser.expected_length) {
parser.state = STATE_WAIT_CHECKSUM;
}
break;
case STATE_WAIT_CHECKSUM:
if (data == parser.checksum) {
frame_complete = true;
}
parser.state = STATE_WAIT_HEADER;
parser.checksum = 0;
break;
}
// Complete frame passed to processing task via queue
if (frame_complete) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(protocol_queue, parser.buffer,
&xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
Advantages: Clear state, easy to debug, supports complex protocolsDisadvantages: Complex state machine design, memory usage needs careful planning
Selection Guide: How to Choose the Right Solution
Decision Process
Simple event notification
Small structured data
Large continuous data
Complex protocol parsing
Extremely high
General
Strict
Loose
Sufficient
Insufficient
Simple
Complex
Need to handle complex tasks in interrupts
Data characteristics?
Semaphore notification
Work queue
Double buffering mechanism
State machine pattern
Real-time requirements?
Memory constraints?
Processing capability?
State complexity?
✅ Recommended for use
Consider work queue
Consider semaphore
✅ Recommended for use
✅ Recommended for use
Consider work queue
Can be simplified to work queue
✅ Recommended for use
Solution Comparison Matrix
| Solution | Implementation Complexity | Memory Overhead | CPU Overhead | Real-time Performance | Data Passing Capability | Applicable Scenarios |
|---|---|---|---|---|---|---|
| Semaphore Notification | ⭐ | ⭐ | ⭐ | ⭐⭐⭐⭐⭐ | ⭐ | Simple events, extremely high real-time performance |
| Work Queue | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Structured data, medium complexity |
| Double Buffering | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | High-frequency large data, continuous acquisition |
| State Machine | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | Complex protocols, state transitions |
Selection Recommendations
- • Data volume < 16 bytes, extremely high real-time requirements → Semaphore notification
- • Data volume 16-256 bytes, medium real-time performance → Work queue
- • Data volume > 1KB, continuous high-frequency acquisition → Double buffering mechanism
- • Complex state transition logic → State machine pattern
Performance Optimization and Trap Avoidance
Performance Monitoring Techniques
// Measure interrupt latency
void measure_interrupt_latency(void) {
static uint32_t last_tick = 0;
uint32_t current_tick = get_system_tick();
uint32_t latency = current_tick - last_tick;
// Record maximum latency
if (latency > max_interrupt_latency) {
max_interrupt_latency = latency;
}
last_tick = current_tick;
}
Common Traps and Countermeasures
- 1. Stack Overflow Prevention
// Avoid large arrays and recursive calls in interrupts
#define MAX_ISR_STACK_USAGE 128 // Bytes
void check_stack_usage(void) {
extern uint32_t _interrupt_stack_start;
uint32_t *stack_ptr = &_interrupt_stack_start;
// Check stack usage
}
- 2. Priority Configuration Principles
// Follow: Data acquisition > Data processing > User interaction
#define DATA_CAPTURE_PRIORITY (configMAX_PRIORITIES - 1)
#define DATA_PROCESS_PRIORITY (configMAX_PRIORITIES - 2)
#define UI_UPDATE_PRIORITY (configMAX_PRIORITIES - 4)
- 3. Memory Management Considerations
// Interrupt-safe memory allocation (if necessary)
void* interrupt_safe_malloc(size_t size) {
void* ptr = NULL;
taskENTER_CRITICAL();
ptr = pvPortMalloc(size);
taskEXIT_CRITICAL();
return ptr;
}
Conclusion
Interrupt handling is at the core of embedded systems, and the correct handling method directly affects the system’s real-time performance and stability. The four solutions provided in this article each have their unique features:
- • Semaphore Notification: Lightweight, suitable for simple scenarios
- • Work Queue: Good flexibility, suitable for medium complexity applications
- • Double Buffering Mechanism: Excellent performance, suitable for high-frequency data acquisition
- • State Machine Pattern: Clear structure, suitable for complex protocol processing
Choosing the right solution requires a comprehensive consideration of data characteristics, real-time requirements, memory constraints, and other factors. Regardless of which solution is chosen, always adhere to the basic principle of “quick entry and exit from interrupts,” moving complex processing to task context execution.
By mastering these techniques, you will be able to design more stable and efficient embedded systems. Remember, an elegant system architecture is not achieved overnight; it requires continuous optimization and improvement in practice.
Follow me for more practical tips on embedded development!