9. Dealing with Interrupts
This tutorial addresses the use of the Cortex-M hardware interrupts together with FreeRTOS. As a basic example, we'll get a task out of the blocked state upon an interrupt event. Let us illustrate this with the user push-button EXTI interrupt.
1. Interrupt setup
Make sure that you have an init function for the push-button, that enables EXTI events on the corresponding pin (PC13):
/*
* BSP_PB_Init()
* - Initialize Push-Button pin (PC13) as input without Pull-up/Pull-down
* - Enable EXTI13 interrupt on PC13 falling edge
*/
void BSP_PB_Init()
{
// Enable GPIOC clock
RCC->AHBENR |= RCC_AHBENR_GPIOCEN;
// Configure PC13 as input
GPIOC->MODER &= ~GPIO_MODER_MODER13_Msk;
GPIOC->MODER |= (0x00 <<GPIO_MODER_MODER13_Pos);
// Disable PC13 Pull-up/Pull-down
GPIOC->PUPDR &= ~GPIO_PUPDR_PUPDR13_Msk;
// Enable SYSCFG clock
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
// Select Port C as interrupt source for EXTI line 13
SYSCFG->EXTICR[3] &= ~ SYSCFG_EXTICR4_EXTI13_Msk;
SYSCFG->EXTICR[3] |= SYSCFG_EXTICR4_EXTI13_PC;
// Enable EXTI line 13
EXTI->IMR |= EXTI_IMR_IM13;
// Disable Rising / Enable Falling trigger
EXTI->RTSR &= ~EXTI_RTSR_RT13;
EXTI->FTSR |= EXTI_FTSR_FT13;
}
The corresponding interrupt channel must be enabled at NVIC level. Note that several interrupts are already in use by FreeRTOS itself. Those system interrupts must be kept at higher priority level. Therefore, the user interrupts must use subsequent priority numbers. There's a symbol in FreeRTOSConfig.h that defines the reserved interrupt priorities boundary:
/* Interrupt nesting behavior configuration. */
#define configMAX_API_CALL_INTERRUPT_PRIORITY 5
It is recommended to use that symbol (+offset) to set the interrupt priority. Remember that unlike task priorities numbering, the higher the number here, the less the priority level. So giving a priority of configMAX_API_CALL_INTERRUPT_PRIORITY+0 provides the highest possible priority level for a user interrupt (i.e. after FreeRTOS interrupts).
The next one would be configMAX_API_CALL_INTERRUPT_PRIORITY+1, and so on...
/*
* BSP_NVIC_Init()
* Setup NVIC controller for desired interrupts
*/
void BSP_NVIC_Init()
{
// Set maximum priority for EXTI line 4 to 15 interrupts
NVIC_SetPriority(EXTI4_15_IRQn, configMAX_API_CALL_INTERRUPT_PRIORITY + 1);
// Enable EXTI line 4 to 15 (user button on line 13) interrupts
NVIC_EnableIRQ(EXTI4_15_IRQn);
}
Then you will need the corresponding ISR (a.k.a Interrupt Handler). As a start, let us do something idiot, just printing the '#' character upon interruption, to make sure everything is well configured. Remember that interrupt handlers (ISR) are usually grouped in the stm32f0xx_it.c file:
/******************************************************************************/
/* STM32F0xx Peripherals Interrupt Handlers */
/* Add here the Interrupt Handler for the used peripheral(s) (PPP), for the */
/* available peripheral interrupt handler's name please refer to the startup */
/* file (startup_stm32f0xx.s). */
/******************************************************************************/
/**
* This function handles EXTI line 13 interrupt request.
*/
void EXTI4_15_IRQHandler()
{
// Test for line 13 pending interrupt
if ((EXTI->PR & EXTI_PR_PR13_Msk) != 0)
{
// Clear pending bit 13 by writing a '1'
EXTI->PR = EXTI_PR_PR13;
// Do what you need
my_printf("#");
}
}
Finally, just try the following code in main.c. There are only two tasks. Task_1 toggles the LED every 200ms. Task_2 prints a '.' every 100ms. There are no inter-tasks objects involved, only delays.
/*
* main.c
*
* Created on: 28/02/2018
* Author: Laurent
*/
#include "main.h"
// Static functions
static void SystemClock_Config (void);
// FreeRTOS tasks
void vTask1 (void *pvParameters);
void vTask2 (void *pvParameters);
// Main function
int main()
{
// Configure System Clock
SystemClock_Config();
// Initialize LED pin
BSP_LED_Init();
// Initialize the user Push-Button
BSP_PB_Init();
// Initialize Debug Console
BSP_Console_Init();
// Initialize NVIC
BSP_NVIC_Init(); // <-- Configure NVIC here
// Start Trace Recording
vTraceEnable(TRC_START);
// Create Tasks
xTaskCreate(vTask1, "Task_1", 256, NULL, 1, NULL);
xTaskCreate(vTask2, "Task_2", 256, NULL, 2, NULL);
// Start the Scheduler
vTaskStartScheduler();
while(1)
{
// The program should never be here...
}
}
/*
* Task_1
*/
void vTask1 (void *pvParameters)
{
while(1)
{
// LED toggle
BSP_LED_Toggle();
// Wait for 200ms
vTaskDelay(200);
}
}
/*
* Task_2
*/
void vTask2 (void *pvParameters)
{
while(1)
{
my_printf(".");
// Wait for 100ms
vTaskDelay(100);
}
}
As a result, you should see a printed '#' every time you press the user button, in between dots coming from Task_2. If that's working, then everything is correctly set.
So far here, we're using the hardware interrupt just like we did before without interaction with FreeRTOS. Moreover, all the processing is done in the ISR. In this example, "all the processing" is just printing a '#' in the console. Most of the time, this is not what you want to do in the context of the RTOS. You would rather catch the interrupt event and use it to unblock a task.
That can be done with a simple binary semaphore. The semaphore would be given in the interrupt handler, and taken from the awaiting task. Let us do that!
2. Using a binary semaphore to synchronize a task
2.1. Basic setup
Declare a binary semaphore as a global variable:
// Kernel objects
xSemaphoreHandle xSem;
Then, create the semaphore before the scheduler is started:
...
// Create Semaphore object
xSem = xSemaphoreCreateBinary();
// Give a nice name to the Semaphore in the trace recorder
vTraceSetSemaphoreName(xSem, "xSEM");
...
Then edit the ISR so that the semaphore is released upon interrupt. Note that OS specific API functions MUST be used from within interrupt handlers. These function are well named with a 'fromISR' suffix. So instead of the regular xSemaphoreGive() function, we need to call a xSemaphoreGiveFromISR() function. Such '... fromISR()' functions exist for any kind of kernel object (semaphores, queues, event groups,...) when set from inside of an interrupt handler and must be used instead of the regular sister function.
/**
* This function handles EXTI line 13 interrupt request.
*/
extern xSemaphoreHandle xSem;
void EXTI4_15_IRQHandler()
{
// Test for line 13 pending interrupt
if ((EXTI->PR & EXTI_PR_PR13_Msk) != 0)
{
// Clear pending bit 13 by writing a '1'
EXTI->PR = EXTI_PR_PR13;
// Release the semaphore
xSemaphoreGiveFromISR(xSem, NULL);
}
}
The FreeRTOS prototype of the xSemaphoreGiveFromISR() function (which is an alias to the more generic xQueueGiveFromISR() function) is given below:
BaseType_t xQueueGiveFromISR( QueueHandle_t xQueue,
BaseType_t * const pxHigherPriorityTaskWoken )
The second argument is a pointer pxHigherPriorityTaskWoken that helps identifying the highest priority task waiting for the released object. We're not using it now, but we'll come back to this later on.
Finally, edit Task_2 so that it tries to take xSem for 100ms. In case of a success, we print '#'. Otherwise, if no semaphore was available for 100ms, we print '.':
/*
* Task_2
*/
void vTask2 (void *pvParameters)
{
portBASE_TYPE xStatus;
while(1)
{
// Wait here for Semaphore with 100ms timeout
xStatus = xSemaphoreTake(xSem, 100);
// Test the result of the take attempt
if (xStatus == pdPASS)
{
// The semaphore was taken as expected
// Display console message
my_printf("#");
}
else
{
// The 100ms timeout elapsed without Semaphore being taken
// Display another message
my_printf(".");
}
}
}
As a result, we get the same behavior as before. This is the right way implementing an interrupt-to-task synchronization mechanism.
- Commit name "Interrupt" - Push onto Gitlab |
2.2. Timing analysis
Take a look at the traces below. An additional lane appears for the ISR. ISR can be considered as 'above all' tasks in terms of priority. It cannot be preempted by anything other than another ISR of higher priority (in this case, it is not OS preemption that's involved but simply NVIC doing its job by the way).
This is an occurrence of Task_2 reaching its timeout waiting for the xSem semaphore. That's when a '.' is printed:
And this is what happens when we push the button. The ISR is first triggered, giving the xSem semaphore, and then, a little while later, Task_2 can successfully take xSem and print '#'. All is well... almost!
Why does it take so long to get Task_2 activated after the ISR returns? In the above screenshot, that about 200µs. Below is a full report of Task_2 waiting times along the course of our snapshot trace:
First, we notice that these waiting times are rather random... why?
Second, we get a worst case at about 900µs! That's a lot. You can double-click that point in the Actor Instance Graph, and confirm that in the Trace View:
Well, we need to remember that FreeRTOS scheduler is cadenced by OS ticks. The interrupt is fully asynchronous. This means that when we push the button, the interrupt handler is fired instantly, and so is (i) the release of the xSem semaphore, and (ii) the Task_2 switching to the ready state. But then, the real activation of Task_2 will have to wait for the scheduler to examine the full application situation. Task_2 will only be activated if no other task with higher priority level is ready. And that will only happen on next OS tick!
Therefore a worst case delay of 1ms should be expected between the task getting ready, and the task getting active.
Given that hardware interrupt processes are designed to process event quite 'instantly', such delay is not acceptable. Fortunately, FreeRTOS provides a solution!
2.3. Fast context switching
In order to address the above raised issue, FreeRTOS features a dedicated mechanism to force a scheduling operation and get the waiting task active as soon as the ISR returns. That's what the second argument of the pxHigherPriorityTaskWoken implements.
Try this in the interrupt handler:
/**
* This function handles EXTI line 13 interrupt request.
*/
extern xSemaphoreHandle xSem;
void EXTI4_15_IRQHandler()
{
portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
// Test for line 13 pending interrupt
if ((EXTI->PR & EXTI_PR_PR13_Msk) != 0)
{
// Clear pending bit 13 by writing a '1'
EXTI->PR = EXTI_PR_PR13;
// Release the semaphore
xSemaphoreGiveFromISR(xSem, &xHigherPriorityTaskWoken);
// Perform a context switch to the waiting task
portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);
}
}
The portEND_SWITCHING_ISR() function should be placed at the very end of the interrupt handler. It produces an instant context switch in order to give attention to the waiting task. See the result. Now Task_2 get consistently active right after ISR completes with waiting time reduced to about 35µs:
- Commit name "Interrupt with context switching" - Push onto Gitlab |
3. A potential issue when using interrupts
Well done, but we have still a potential issue in the above example. As it is now, the main() function enables the EXTI interrupt event and the corresponding NVIC channels BEFORE the scheduler is started. If an interrupt occurs in the meantime (small chance, but still), the ISR executes and the OS API functions xSemaphoreGiveFromISR() is called before the scheduler has been fired. That would lead to a crash.
The good practice is to configure and allow interrupts in the init part of the tasks, instead of in the main() function. When a task starts, you are 100% sure that scheduler has already been fired because it is the latter that actually get that task active. Doing so, it is no more possible to regroup NVIC settings in a one-and-only function, but that doesn't matter. You can use the code below as an example:
First remove NVIC channel init from the common BSP function:
/*
* BSP_NVIC_Init()
* Setup NVIC controller for desired interrupts
*/
void BSP_NVIC_Init()
{
// Remove interrupt channel settings from here
}
Then perform interrupt settings as a part of Task_2 initializations:
/*
* main.c
*
* Created on: 28/02/2018
* Author: Laurent
*/
#include "main.h"
// Static functions
static void SystemClock_Config (void);
// FreeRTOS tasks
void vTask1 (void *pvParameters);
void vTask2 (void *pvParameters);
// Kernel objects
xSemaphoreHandle xSem;
// Main function
int main()
{
// Configure System Clock
SystemClock_Config();
// Initialize LED pin
BSP_LED_Init();
// Initialize Debug Console
BSP_Console_Init();
// Start Trace Recording
vTraceEnable(TRC_START);
// Create Semaphore object
xSem = xSemaphoreCreateBinary();
// Give a nice name to the Semaphore in the trace recorder
vTraceSetSemaphoreName(xSem, "xSEM");
// Create Tasks
xTaskCreate(vTask1, "Task_1", 256, NULL, 1, NULL);
xTaskCreate(vTask2, "Task_2", 256, NULL, 2, NULL);
// Start the Scheduler
vTaskStartScheduler();
while(1)
{
// The program should never be here...
}
}
/*
* Task_1
*/
void vTask1 (void *pvParameters)
{
while(1)
{
// LED toggle
BSP_LED_Toggle();
// Wait for 200ms
vTaskDelay(200);
}
}
/*
* Task_2
*/
void vTask2 (void *pvParameters)
{
portBASE_TYPE xStatus;
// Initialize the user Push-Button
BSP_PB_Init();
// Set priority for EXTI line 4 to 15, and enable interrupt
NVIC_SetPriority(EXTI4_15_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY + 1);
NVIC_EnableIRQ(EXTI4_15_IRQn);
// Now enter the task loop
while(1)
{
// Wait here for Semaphore with 100ms timeout
xStatus = xSemaphoreTake(xSem, 100);
// Test the result of the take attempt
if (xStatus == pdPASS)
{
// The semaphore was taken as expected
// Display console message
my_printf("#");
}
else
{
// The 100ms timeout elapsed without Semaphore being taken
// Display another message
my_printf(".");
}
}
}
Make sure the project builds and works as before. Now you've got the fundamental ideas to correctly deal with interrupts within FreeRTOS context.
- Commit name "Interrupt init in task" - Push onto Gitlab |