FreeRTOS - A few notes

Here are some (rather random) notes from learning how to use FreeRTOS. These are specific to the port I'm using (ARM® Cortex™-M4 in Keil), but I thought I should still write them down somewhere just in case somebody else finds them helpful.

Task Creation

The first thing in creating a task is to allocate the memory for the Task Control Block (TCB) and the stack for the task:

pxNewTCB = prvAllocateTCBAndStack( usStackDepth, puxStackBuffer );

If the call is successful, the TCB will get initialized:

prvInitialiseTCBVariables( pxNewTCB, pcName, uxPriority, xRegions, usStackDepth );

This will store the task name, set the priority and add the TCB itself to a couple of lists which be walked through by the scheduler.

Next is to initialize the task stack. It is done such that it looks as if the task was already running, but had been interrupted by the scheduler:

portSTACK_TYPE *pxPortInitialiseStack( portSTACK_TYPE *pxTopOfStack, pdTASK_CODE pxCode, void *pvParameters )
{
	/* Simulate the stack frame as it would be created by a context switch
	interrupt. */

	/* Offset added to account for the way the MCU uses the stack on entry/exit
	of interrupts, and to ensure alignment. */
	pxTopOfStack--;

	*pxTopOfStack = portINITIAL_XPSR;	/* xPSR */
	pxTopOfStack--;
	*pxTopOfStack = ( portSTACK_TYPE ) pxCode;	/* PC */
	pxTopOfStack--;
	*pxTopOfStack = ( portSTACK_TYPE ) prvTaskExitError;	/* LR */

	/* Save code space by skipping register initialisation. */
	pxTopOfStack -= 5;	/* R12, R3, R2 and R1. */
	*pxTopOfStack = ( portSTACK_TYPE ) pvParameters;	/* R0 */

	/* A save method is being used that requires each task to maintain its
	own exec return value. */
	pxTopOfStack--;
	*pxTopOfStack = portINITIAL_EXEC_RETURN;

	pxTopOfStack -= 8;	/* R11, R10, R9, R8, R7, R6, R5 and R4. */

	return pxTopOfStack;
}

This is to manipulate the stack pointer to emulate a "switched out" situation. The purpose of doing it this way is to make the process of running the task for the first time no different from that of resuming it from a blocked state - the register values will always get popped from the stack before the task starts/resumes running.

The next important thing is to decide when this task should run. This depends on the priority of the task and when the task is created. The key is the pxCurrentTCB global variable. In the simple scenario that all tasks are created before the scheduler is started, pxCurrentTCB will point to the first of the highest priority task created:

if( pxCurrentTCB == NULL )
{
	/* There are no other tasks, or all the other tasks are in
	the suspended state - make this the current task. */
	pxCurrentTCB =  pxNewTCB;

	if( uxCurrentNumberOfTasks == ( unsigned portBASE_TYPE ) 1 )
	{
		/* This is the first task to be created so do the preliminary
		initialisation required.  We will not recover if this call
		fails, but we will report the failure. */
		prvInitialiseTaskLists();
	}
}
else
{
	/* If the scheduler is not already running, make this task the
	current task if it is the highest priority task to be created
	so far. */
	if( xSchedulerRunning == pdFALSE )
	{
		if( pxCurrentTCB->uxPriority <= uxPriority )
		{
			pxCurrentTCB = pxNewTCB;
		}
	}
}

For example if we have a sequence like this:

  1. Create Task A with priority 1
  2. Create Task B with priority 2
  3. Start scheduler

First pxCurrentTCB will point to the TCB of Task A since it is the first task created thus pxCurrentTCB would have been NULL. When Task B is created, however, pxCurrentTCB will point to the TCB of Task B as it has a higher priority than Task A. When the scheduler starts, the higher priority Task B will run first.

Another scenario is when a task is created by another task, i.e. the scheduler is already running. If the newly created task's priority is not higher than the task that created it, then it will just stay in the ready list. If that's not the case, then preemption will occur:

if( xSchedulerRunning != pdFALSE )
{
	/* If the created task is of a higher priority than the current task
	then it should run now. */
	if( pxCurrentTCB->uxPriority < uxPriority )
	{
		taskYIELD_IF_USING_PREEMPTION();
	}
}
For example, if somewhere in Task A xTaskCreate is called to create Task B, and if Task B has a priority higher than that of Task A, then a context switch will occur and Task B will start running right away.

Context Switching

Context switching is triggered by setting the PendSV exception bit. This happens when functions (macros really) like taskYIELD(), taskYIELD_IF_USING_PREEMPTION(), portYIELD() or portYIELD_FROM_ISR( x ) are called. These will all end up the xPortPendSVHandler() being called:

__asm void xPortPendSVHandler( void )
{
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;

	PRESERVE8

	mrs r0, psp

	/* Get the location of the current TCB. */
	ldr	r3, =pxCurrentTCB
	ldr	r2, [r3]

	/* Is the task using the FPU context?  If so, push high vfp registers. */
	tst r14, #0x10
	it eq
	vstmdbeq r0!, {s16-s31}

	/* Save the core registers. */
	stmdb r0!, {r4-r11, r14}

	/* Save the new top of stack into the first member of the TCB. */
	str r0, [r2]

	stmdb sp!, {r3}
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0
	bl vTaskSwitchContext
	mov r0, #0
	msr basepri, r0
	ldmia sp!, {r3}

	/* The first item in pxCurrentTCB is the task top of stack. */
	ldr r1, [r3]
	ldr r0, [r1]

	/* Pop the core registers. */
	ldmia r0!, {r4-r11, r14}

	/* Is the task using the FPU context?  If so, pop the high vfp registers
	too. */
	tst r14, #0x10
	it eq
	vldmiaeq r0!, {s16-s31}

	msr psp, r0

	#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
		#if WORKAROUND_PMU_CM001 == 1
			push { r14 }
			pop { pc }
		#endif
	#endif

	bx r14
	nop
}

You can go through this line by line if you like. But the key thing there is that, after saving the current context, the function vTaskSwitchContext is called:

void vTaskSwitchContext( void )
{
	if( uxSchedulerSuspended != ( unsigned portBASE_TYPE ) pdFALSE )
	{
		/* The scheduler is currently suspended - do not allow a context
		switch. */
		xYieldPending = pdTRUE;
	}
	else
	{
		xYieldPending = pdFALSE;
		traceTASK_SWITCHED_OUT();

		#if ( configGENERATE_RUN_TIME_STATS == 1 )
		{
				#ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
					portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
				#else
					ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
				#endif

				/* Add the amount of time the task has been running to the
				accumulated	time so far.  The time the task started running was
				stored in ulTaskSwitchedInTime.  Note that there is no overflow
				protection here	so count values are only valid until the timer
				overflows.  The guard against negative values is to protect
				against suspect run time stat counter implementations - which
				are provided by the application, not the kernel. */
				if( ulTotalRunTime > ulTaskSwitchedInTime )
				{
					pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
				}
				ulTaskSwitchedInTime = ulTotalRunTime;
		}
		#endif /* configGENERATE_RUN_TIME_STATS */

		taskFIRST_CHECK_FOR_STACK_OVERFLOW();
		taskSECOND_CHECK_FOR_STACK_OVERFLOW();

		taskSELECT_HIGHEST_PRIORITY_TASK();

		traceTASK_SWITCHED_IN();

		#if ( configUSE_NEWLIB_REENTRANT == 1 )
		{
			/* Switch Newlib's _impure_ptr variable to point to the _reent
			structure specific to this task. */
			_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
		}
		#endif /* configUSE_NEWLIB_REENTRANT */
	}
}	

Once again, a bunch of stuff. But the absolute heart of this function is the call to taskSELECT_HIGHEST_PRIORITY_TASK():

#define taskSELECT_HIGHEST_PRIORITY_TASK()																			\
{																													\
	/* Find the highest priority queue that contains ready tasks. */												\
	while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopReadyPriority ] ) ) )										\
	{																												\
		configASSERT( uxTopReadyPriority );																			\
		--uxTopReadyPriority;																						\
	}																												\
																													\
	/* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of										\
	the	same priority get an equal share of the processor time. */													\
	listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopReadyPriority ] ) );						\
} /* taskSELECT_HIGHEST_PRIORITY_TASK */

This is a macro, not a real function. The call to listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopReadyPriority ] ) ) will eventually make pxCurrentTCB point to the TCB of the next highest priority task.

Now back to xPortPendSVHandler() above, after the call to vTaskSwitchContext(), pxCurrentTCB is now pointing to the TCB of a different task. The context of this different task will be switched in. And so completes the context switching.

Critical Region

We enter critical region by calling taskENTER_CRITICAL() which ends up calling function ulPortSetInterruptMask():

__asm unsigned long ulPortSetInterruptMask( void )
{
	PRESERVE8

	mrs r0, basepri
	mov r1, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r1
	bx r14
}

This simply disables all interrupts with priority same or lower than set by configMAX_SYSCALL_INTERRUPT_PRIORITY in FreeRTOSConfig.h.

The Scheduler

Once the tasks have been created, the scheduler will take over:

vTaskStartScheduler();

If everything goes well, this function will never return. The first thing the scheduler does is create the idle task:

		
xReturn = xTaskCreate( prvIdleTask, ( signed char * ) "IDLE", tskIDLE_STACK_SIZE, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), NULL );

Then it goes on to set the interrupt priority of PendSV (for context switching) and SysTick (for the tick) to the lowest (for STM32 the number is 15):

		
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

The next thing is to set up the SysTick timer to generate the tick interrupt at the required frequency:

			
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;;
portNVIC_SYSTICK_CTRL_REG = portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT;		

In my case the system clock configSYSTICK_CLOCK_HZ is 168000000 (168 MHz), and I just made the tick rate 1 KHz (1 ms tick). So the system timer reload value is 168000 - 1 = 167999.

When everything is ready, the scheduler will start the first task:

			
__asm void prvStartFirstTask( void )
{
	PRESERVE8

	/* Use the NVIC offset register to locate the stack. */
	ldr r0, =0xE000ED08
	ldr r0, [r0]
	ldr r0, [r0]
	/* Set the msp back to the start of the stack. */
	msr msp, r0
	/* Globally enable interrupts. */
	cpsie i
	/* Call SVC to start the first task. */
	svc 0
	nop
}		

This results in the SVCall (supervisor call) exception handler getting executed:

			
__asm void vPortSVCHandler( void )
{
	PRESERVE8

	/* Get the location of the current TCB. */
	ldr	r3, =pxCurrentTCB
	ldr r1, [r3]
	ldr r0, [r1]
	/* Pop the core registers. */
	ldmia r0!, {r4-r11, r14}
	msr psp, r0
	mov r0, #0
	msr	basepri, r0
	bx r14
}	

Remember above in the Task Creation section, pxCurrentTCB will always points to the highest-priority task before the scheduler gets started, to make sure that when the scheduler does start the highest-priority task will run first. Also remember in that section we talked about emulating pushing registers to the task's stack, so even when the task runs for the first time some dummy register values will get popped off the stack.

The Tick

On every tick the SysTick handler will fire up. It increments the tick count then decides whether a context switch is required (by setting the PendSV interrupt bit):

			
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
	/* A context switch is required.  Context switching is performed in
	the PendSV interrupt.  Pend the PendSV interrupt. */
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}

xTaskIncrementTick() will return true if a context switch is required, false otherwise. The reason for context switch due to tick increment could be tasks unblocked on timeout, or simply for time sliced execution of equal priority tasks.