8. Dealing with Interrupts

 

This tutorial addresses hardware interrupts. 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
 */

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 boudary (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY). It is recommanded 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 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY+1 provides the highest possible priority level for a user interrupt (i.e. after FreeRTOS interrupts). The next one would be configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY+2, 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, configLIBRARY_MAX_SYSCALL_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 program
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 '#' everytime you press the user button, in between dots coming from Task_2. If that's working, then eveyting is correctly set.

image_000.png
 
 

2. Using a binary semaphore to synchonize a task

 
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. The second argument (&xHigherPriorityTaskWoken) permits to identify the highest priority task waiting for the released object. More on this later...

/**
  * 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);
	}
}

 

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
 
 
Take a look at the trace below. An additional lane appears for the ISR. ISR can be considered as an 'above all' task in terms of priority. It cannot be pre-empted by anything other than another ISR of higher priority (in this case, it is not OS pre-emption that's involved but simply NVIC doing its job by the way).
We observe a significant delay between ISR execution and Task_2 getting active (about 700µs at first glance). This is because whenever the interrupt occurs, the information is processed by the scheduler on next OS tick event (i.e. every 1ms here). Therefore a worst case delay of 1ms is expected between the task getting ready, and the task getting active. Given that hadware interrupt processes are designed to process event quite 'instantly', such delay is barely acceptable!
 
image_001.png
 
 

In order to address the above raised issue, FreeRTOS features a dedicated mechanism to shortcut scheduler operation and get the waiting taks active as soon as the ISR returns. Try this:

/**
  * 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, bypassing the scheduler in order to give attention to the waiting task. See the result. Now Task_2 get active right after ISR completes.

image_002.png
 
 

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 sarted. 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 regroup NVIC settings in a one-and-only function. You can use the code below as an exemple:
 
Let-us 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 program
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 maximum priority for EXTI line 4 to 15 interrupts
	NVIC_SetPriority(EXTI4_15_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY + 1);

	// Enable EXTI line 4 to 15 (user button on line 13) interrupts
	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 everything you need to correctly deal with interrupts within FreeRTOS context.