Skip to main content

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.

image_000.png

 

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.

image_000.png

 

gitlab- commit Commit name "Interrupt"
- push 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:

 

image_002.png

 

gitlab- commit Commit name "Interrupt with context switching"
- push 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.
 

gitlab- commit Commit name "Interrupt init in task"
- push Push onto Gitlab