Basic Concepts of Interrupt Handling in FreeRTOS
In FreeRTOS, interrupt handling is implemented through Interrupt Service Routines (ISRs). An ISR is a function called by the operating system when an interrupt occurs. FreeRTOS provides several functions for use in interrupt handling to ensure the correct use of the real-time operating system in the interrupt context.
Registering and Handling Interrupt Service Routines
FreeRTOS allows users to register interrupt handling functions within ISRs. This can be determined by using the xTaskGetSchedulerState
function to check the system’s state to decide whether to call FreeRTOS’s interrupt handling functions.
#include "FreeRTOS.h"
#include "task.h"
void vISRHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// Interrupt handling code
// Notify the waiting task that an event has occurred
vTaskNotifyGiveFromISR(/*task handle*/, &xHigherPriorityTaskWoken);
// If there is a task with a higher priority than the currently running task, a task switch is required
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
int main(void)
{
// Register interrupt service routine
registerISRHandler(vISRHandler);
// Create task
xTaskCreate(vTaskFunction, "Task", 1000, NULL, 1, NULL);
// Start scheduler
vTaskStartScheduler();
return 0;
}
Example: Using Interrupts in Real Projects
Suppose we are developing an embedded system where a sensor notifies the system that new data is available via an interrupt. We will use FreeRTOS to handle this interrupt event.
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
// Define task and semaphore handles
TaskHandle_t xTaskHandle;
SemaphoreHandle_t xSemaphore;
// Interrupt service routine
void vSensorISR(void)
{
// Notify the waiting task that an event has occurred
xSemaphoreGiveFromISR(xSemaphore, NULL);
}
// Task function
void vTaskFunction(void *pvParameters)
{
for (;;)
{
// Wait for interrupt event to occur
if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE)
{
// Interrupt event handling code
processInterruptEvent();
}
}
}
int main(void)
{
// Create semaphore
xSemaphore = xSemaphoreCreateBinary();
// Register interrupt service routine
registerISRHandler(vSensorISR);
// Create task
xTaskCreate(vTaskFunction, "Task", 1000, NULL, 1, &xTaskHandle);
// Start scheduler
vTaskStartScheduler();
return 0;
}
In this example, the sensor interrupt triggers the vSensorISR
ISR, which notifies the waiting task that an interrupt event has occurred by releasing the semaphore xSemaphore
. The task function vTaskFunction
handles the interrupt event by waiting for the semaphore. This method ensures the real-time nature of interrupt handling while avoiding direct calls to FreeRTOS APIs within the ISR.
In the following example, we use the STM32F103 microcontroller to trigger an interrupt via a button press. We will create a task to handle the button-triggered interrupt event and notify the task through a queue when the button is pressed.
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "stm32f10x.h"
// Define task handle and queue handle
TaskHandle_t xTaskHandle;
QueueHandle_t xQueue;
// Interrupt service routine
void EXTI0_IRQHandler(void)
{
// Clear interrupt flag
EXTI_ClearITPendingBit(EXTI_Line0);
// Send interrupt event notification to queue
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(xQueue, NULL, &xHigherPriorityTaskWoken);
// If there is a task with a higher priority than the currently running task, a task switch is required
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// Task function
void vTaskFunction(void *pvParameters)
{
for (;;)
{
// Wait for interrupt event to occur
if (xQueueReceive(xQueue, NULL, portMAX_DELAY) == pdTRUE)
{
// Interrupt event handling code
processInterruptEvent();
}
}
}
int main(void)
{
// Initialize FreeRTOS
SystemInit();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
xQueue = xQueueCreate(5, sizeof(void *));
// Configure button-triggered interrupt
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = configLIBRARY_LOWEST_INTERRUPT_PRIORITY;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// Create task
xTaskCreate(vTaskFunction, "Task", configMINIMAL_STACK_SIZE, NULL, 1, &xTaskHandle);
// Start scheduler
vTaskStartScheduler();
return 0;
}
In this example, we first initialize FreeRTOS and configure the button-triggered interrupt. In the button-triggered ISR EXTI0_IRQHandler
, we clear the interrupt flag and send an interrupt event notification to the queue. In the task function vTaskFunction
, we handle the interrupt event by waiting on the queue.
Note that when configuring the ISR, we used the portYIELD_FROM_ISR
macro to ensure a task switch occurs within the ISR. This is because in FreeRTOS, some functions must be called in a task context and cannot be called in an interrupt context. Therefore, we use portYIELD_FROM_ISR
to notify FreeRTOS to perform a task switch within the ISR.
Interrupt Handling Mechanism in ARM Cortex-M and RISC-V Processors
The ARM Cortex-M processor uses a vector table-based interrupt handling mechanism. Here are the basic steps of interrupt handling in ARM Cortex-M:
-
Interrupt Vector Table (IVT):
-
The interrupt vector table is a table that contains the addresses of interrupt handlers, with each interrupt corresponding to an entry in the table. -
After the processor resets, it loads the first address in the IVT as the initial stack pointer, then loads the second address as the entry point for the reset ISR. -
The interrupt number is automatically mapped to the corresponding location in the interrupt vector table by hardware.
Interrupt Handling Process:
-
When an interrupt occurs, the processor looks up the address of the corresponding interrupt handler in the IVT based on the interrupt number. -
The processor saves the current context, including register values and status registers, and transfers control to the ISR. -
After the ISR finishes executing, the processor restores the previously saved context and returns to the state before the interrupt occurred.
NVIC (Nested Vectored Interrupt Controller):
-
The Cortex-M processor manages interrupt priority through the NVIC. -
Each interrupt’s priority can be set, and the NVIC controls the enabling and masking of interrupts.
The RISC-V processor’s interrupt mechanism is relatively flexible, mainly implemented through the Platform-Level Interrupt Controller (PLIC) and the Core-Level Interrupt Controller (CLIC).
-
PLIC (Platform-Level Interrupt Controller):
-
PLIC is responsible for handling interrupts from peripherals and provides an interrupt number for each peripheral. -
When a peripheral generates an interrupt, the PLIC decides whether to pass the interrupt request to the processor based on priority and interrupt enable status.
PLIC Software Interrupt:
-
RISC-V introduces a software interrupt mechanism that allows software to trigger interrupts using the ebreak
instruction. -
This software interrupt can be used for system calls and exception handling.
MTVEC Register:
-
The MTVEC register is used to set the starting address of the interrupt vector table. -
It can choose between Direct Mode and Vector Mode.
MEPC Register:
-
The MEPC register saves the program counter value when an exception or interrupt occurs, used for recovery after exception handling.
Comparison
Similarities:
-
Interrupt Vector Table: Both use an interrupt vector table to map interrupt numbers to the corresponding interrupt handler addresses.
-
Interrupt Service Routines: The processor saves the current context when an interrupt occurs, executes the corresponding ISR, and then restores the previous context.
-
Interrupt Priority: Both support interrupt priority settings, allowing different priorities to be assigned to different interrupts.
Differences:
-
Interrupt Controller:
-
ARM Cortex-M uses the NVIC to manage interrupt priority and enabling. -
RISC-V uses PLIC and CLIC to handle platform-level and core-level interrupts, providing more flexible interrupt control.
Software Interrupt:
-
RISC-V introduces a software interrupt mechanism, allowing software to trigger interrupts using the ebreak
instruction, which is more flexible in system calls and exception handling.
Modes of the Interrupt Vector Table:
-
ARM Cortex-M provides two modes, Main Table and Vector Table Relocation, for different application scenarios. -
RISC-V provides Direct Mode and Vector Mode.
Exception Handling Registers:
-
RISC-V uses the MEPC register to save the program counter value during an exception or interrupt, while ARM Cortex-M uses the PSP or MSP registers to save the stack pointer.
Overall, both provide effective interrupt handling mechanisms, with the choice depending on specific application scenarios and needs. ARM Cortex-M is more standardized, suitable for a wide range of embedded systems, while RISC-V’s interrupt mechanism is relatively more flexible, suitable for scenarios requiring high configurability and customization.