The source code of the task function contains the following parts:
1. Priority handling
2. Task creation
3. Starting the first task
4. Task switching
5. Task blocking/waking
The previous article focused on the first three points, while this article will start from the scheduler to discuss the task switching (scheduling) mechanism.
1. Scheduling Mechanism of FreeRTOS Scheduler
FreeRTOS has two scheduling mechanisms: preemptive scheduling and cooperative scheduling.Preemptive scheduling: High-priority tasks can directly preempt low-priority tasks.Cooperative scheduling: Tasks cannot be preempted unless they finish themselves.These two modes are selected by setting the macro taskYIELD_IF_USING_PREEMPTION.
# if(configUSE_PREEMPTION == 0)# define taskYIELD_IF_USING_PREEMPTION() // Empty implementation# else# define taskYIELD_IF_USING_PREEMPTION() portYIELD_WITHIN_API()# endif
| Scheduling Mode | <span>configUSE_PREEMPTION=0</span> (Cooperative) |
<span>configUSE_PREEMPTION=1</span> (Preemptive) |
|---|---|---|
| Macro Expansion Result | Empty macro (no code generated) | Expands to <span>portYIELD_WITHIN_API()</span> |
| Triggering Task Switch Timing | Must explicitly call <span>taskYIELD()</span> |
Automatically triggered when a high-priority task is ready |
| Typical Calling Scenario | Detected pending tasks in <span>xTaskResumeAll()</span> |
When an interrupt wakes a high-priority task |
This article will mainly explain the more commonly used preemptive scheduling.
2. Priority Mechanism of FreeRTOS Scheduler
Fixed PriorityEach task is assigned a priority when created (0 is the lowest, <span><span>configMAX_PRIORITIES</span></span> is the highest).Priority Inversion HandlingCan be alleviated through priority inheritance of mutexes.
3. Functions of FreeRTOS Scheduler
Through the scheduler, tasks switch between the following states: (Specific function implementations will be analyzed in the next article)
ReadyReady to run, waiting for the scheduler to allocate CPU.RunningThe task currently being executed.BlockedWaiting for events (such as delays, semaphores, queues, etc.).SuspendedExplicitly paused (<span><span>vTaskSuspend()</span></span><span>) and does not participate in scheduling.</span><span><span>4. Implementation Principles of FreeRTOS Scheduling</span></span>
- Save the current task context (registers, stack pointer) to its Task Control Block (TCB).
- Select the highest priority task from the ready queue.
- Restore the new task’s context and execute it.
Diagram (Workflow of the Scheduler)
The good news is that the core functions of saving the current task context and restoring the new task’s context can be accomplished through an interrupt called Pendsv.
__asm void xPortPendSVHandler( void ){ extern uxCriticalNesting; extern pxCurrentTCB; extern vTaskSwitchContext;/* *INDENT-OFF* */ PRESERVE8 mrs r0, psp isb ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */ ldr r2, [ r3 ] stmdb r0 !, { r4 - r11 } /* Save the remaining registers. */ str r0, [ r2 ] /* Save the new top of stack into the first member of the TCB. */ stmdb sp !, { r3, r14 } mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY msr basepri, r0 dsb isb bl vTaskSwitchContext mov r0, #0 msr basepri, r0 ldmia sp !, { r3, r14 } ldr r1, [ r3 ] ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */ ldmia r0 !, { r4 - r11 } /* Pop the registers and the critical nesting count. */ msr psp, r0 isb bx r14 nop/* *INDENT-ON* */}
Pendsv is a system interrupt, and the above code is its interrupt service function.Let’s explain this function:First, it associates with the global pointer variable pxCurrentTCB to read the current task stack location through CurrentTCB, then saves the register values to the stack, calls vTaskSwitchContext to get newTCB, and finally restores the values saved in the new task stack to the registers and exits.Overall, this interrupt function saves the current task and starts the new task.Choosing the highest priority task from the ready queue is accomplished through vTaskSwitchContext.
void vTaskSwitchContext( void ){ if( uxSchedulerSuspended != ( UBaseType_t ) 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 ); } else { mtCOVERAGE_TEST_MARKER(); } ulTaskSwitchedInTime = ulTotalRunTime; } #endif /* configGENERATE_RUN_TIME_STATS */ /* Check for stack overflow, if configured. */ taskCHECK_FOR_STACK_OVERFLOW(); /* Before the currently running task is switched out, save its errno. */ #if ( configUSE_POSIX_ERRNO == 1 ) { pxCurrentTCB->iTaskErrno = FreeRTOS_errno; } #endif /* Select a new task to run using either the generic C or port * optimised asm code. */ taskSELECT_HIGHEST_PRIORITY_TASK(); /*lint !e9079 void * is used as this macro is used with timers and co-routines too. Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */ traceTASK_SWITCHED_IN(); /* After the new task is switched in, update the global errno. */ #if ( configUSE_POSIX_ERRNO == 1 ) { FreeRTOS_errno = pxCurrentTCB->iTaskErrno; } #endif #if ( configUSE_NEWLIB_REENTRANT == 1 ) { /* Switch Newlib's _impure_ptr variable to point to the _reent * structure specific to this task. * See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html * for additional information. */ _impure_ptr = &( pxCurrentTCB->xNewLib_reent ); } #endif /* configUSE_NEWLIB_REENTRANT */ }}
This function is the core function of the scheduler to select the next task.
If the scheduler is not suspended and the stack is not overflowing, it will call taskSELECT_HIGHEST_PRIORITY_TASK() to find the highest priority task that is ready and has been waiting the longest.
Pendsv will call TaskSwitchContext to determine the target for switching tasks.<span><span>5. Conditions for Starting the FreeRTOS Scheduler</span></span>Tasks actively yield CPUCall <span><span>taskYIELD()</span></span> or enter a blocked state (such as <span><span>vTaskDelay()</span></span><span>).</span>Interrupt Service Routines (ISR)Call portYIELD_FROM_ISR() after sending semaphores, queues, etc.System Tick (Tick) InterruptCheck for time slice rotation or delay expiration.In summary, tasks can switch in two ways: direct switching and delayed switching, and their mechanisms for starting the task scheduler are different.1. Immediate SchedulingImmediate scheduling can be triggered by a task calling taskYIELD() to directly trigger Pendsv (scheduling), or by some processing functions of the task calling taskYIELD() to trigger Pendsv.Here are two examples:
if( xYieldRequired != pdFALSE ) { taskYIELD_IF_USING_PREEMPTION(); } else { mtCOVERAGE_TEST_MARKER(); }
This code comes from <span><span>vTaskPrioritySet()</span></span><span>, which modifies the task's priority. If the priority is higher than the currently executing task, it will call <span>taskYIELD_IF_USING_PREEMPTION</span>() for direct scheduling.</span>
if( xSchedulerRunning != pdFALSE ) { /* If the created task is of a higher priority than the current task * then it should run now. */ if( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority ) { taskYIELD_IF_USING_PREEMPTION(); } else { mtCOVERAGE_TEST_MARKER(); } }
This code comes from prvAddNewTaskToReadyList(), if the new ready task’s priority is higher than the current task, it will call taskYIELD_IF_USING_PREEMPTION() for direct scheduling.
These two examples illustrate that in preemptive scheduling, when a higher priority task is ready, it will directly call taskYIELD_IF_USING_PREEMPTION() for scheduling.2. Delayed Scheduling:Delayed scheduling also calls Pendsv for scheduling, but it may be delayed due to interrupts being disabled or other interrupts being executed, especially timer interrupts.The core interrupt for delayed scheduling: system tick
void xPortSysTickHandler( void ){ /* The SysTick runs at the lowest interrupt priority, so when this interrupt * executes all interrupts must be unmasked. There is therefore no need to * save and then restore the interrupt mask value as its value is already * known - therefore the slightly faster vPortRaiseBASEPRI() function is used * in place of portSET_INTERRUPT_MASK_FROM_ISR(). */ vPortRaiseBASEPRI(); { /* 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; } } vPortClearBASEPRIFromISR();}
(System tick’s interrupt service function)
This interrupt will trigger Pendsv and switch tasks when xTaskIncrementTick() returns true. xTaskIncrementTick() will return true when a higher priority task’s delay expires, prompting the scheduler to trigger scheduling.
<span><span><span>xTaskIncrementTick</span></span><span><span>()</span></span></span>
BaseType_t xTaskIncrementTick( void ){ TCB_t * pxTCB; TickType_t xItemValue; BaseType_t xSwitchRequired = pdFALSE; /* Called by the portable layer each time a tick interrupt occurs. * Increments the tick then checks to see if the new tick value will cause any * tasks to be unblocked. */ traceTASK_INCREMENT_TICK( xTickCount ); if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) { /* Minor optimisation. The tick count cannot change in this * block. */ const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1; /* Increment the RTOS tick, switching the delayed and overflowed * delayed lists if it wraps to 0. */ xTickCount = xConstTickCount; if( xConstTickCount == ( TickType_t ) 0U ) /*lint !e774 'if' does not always evaluate to false as it is looking for an overflow. */ { taskSWITCH_DELAYED_LISTS(); } else { mtCOVERAGE_TEST_MARKER(); } /* See if this tick has made a timeout expire. Tasks are stored in * the queue in the order of their wake time - meaning once one task * has been found whose block time has not expired there is no need to * look any further down the list. */ if( xConstTickCount >= xNextTaskUnblockTime ) { for( ; ; ) { if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE ) { /* The delayed list is empty. Set xNextTaskUnblockTime * to the maximum possible value so it is extremely * unlikely that the * if( xTickCount >= xNextTaskUnblockTime ) test will pass * next time through. */ xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */ break; } else { /* The delayed list is not empty, get the value of the * item at the head of the delayed list. This is the time * at which the task at the head of the delayed list must * be removed from the Blocked state. */ pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too. Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */ xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) ); if( xConstTickCount < xItemValue ) { /* It is not time to unblock this item yet, but the * item value is the time at which the task at the head * of the blocked list must be removed from the Blocked * state - so record the item value in * xNextTaskUnblockTime. */ xNextTaskUnblockTime = xItemValue; break; /*lint !e9011 Code structure here is deemed easier to understand with multiple breaks. */ } else { mtCOVERAGE_TEST_MARKER(); } /* It is time to remove the item from the Blocked state. */ ( void ) uxListRemove( &( pxTCB->xStateListItem ) ); /* Is the task waiting on an event also? If so remove * it from the event list. */ if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ) { ( void ) uxListRemove( &( pxTCB->xEventListItem ) ); } else { mtCOVERAGE_TEST_MARKER(); } /* Place the unblocked task into the appropriate ready * list. */ prvAddTaskToReadyList( pxTCB ); /* A task being unblocked cannot cause an immediate * context switch if preemption is turned off. */ #if ( configUSE_PREEMPTION == 1 ) { /* Preemption is on, but a context switch should * only be performed if the unblocked task has a * priority that is equal to or higher than the * currently executing task. */ if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) { xSwitchRequired = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* configUSE_PREEMPTION */ } } } /* Tasks of equal priority to the currently running task will share * processing time (time slice) if preemption is on, and the application * writer has not explicitly turned time slicing off. */ #if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) { if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 ) { xSwitchRequired = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */ #if ( configUSE_TICK_HOOK == 1 ) { /* Guard against the tick hook being called when the pended tick * count is being unwound (when the scheduler is being unlocked). */ if( xPendedTicks == ( TickType_t ) 0 ) { vApplicationTickHook(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* configUSE_TICK_HOOK */ #if ( configUSE_PREEMPTION == 1 ) { if( xYieldPending != pdFALSE ) { xSwitchRequired = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* configUSE_PREEMPTION */ } else { ++xPendedTicks; /* The tick hook gets called at regular intervals, even if the * scheduler is locked. */ #if ( configUSE_TICK_HOOK == 1 ) { vApplicationTickHook(); } #endif } return xSwitchRequired;}
This function continuously receives hardware interrupts from the clock module to increment the system clock and check all TCBs in the delay list. If a task’s delay expires, it will place it into the ready list, and if this task’s priority is higher than the current task’s priority, it will return true to let systemtick() schedule this task.
We all know that the task delay function taskdelay() can make a task delay for a period of time. In fact, it writes the task’s wake-up time into the TCB and waits for xTaskIncrementTick() to compare and wake the task at the appropriate time.
Of course, delayed scheduling is not necessarily caused by a delay call; it can also be due to task blocking. The following two task functions <span><span>vTaskSuspend()</span><span> and </span></span><strong><code><span><span>vTaskResume()</span><span> are responsible for blocking and resuming a task, respectively.</span></span><strong><code><span><strong><code><span>vTaskSuspend()</span>
void vTaskSuspend( TaskHandle_t xTaskToSuspend ) { TCB_t * pxTCB; taskENTER_CRITICAL(); { /* If null is passed in here then it is the running task that is * being suspended. */ pxTCB = prvGetTCBFromHandle( xTaskToSuspend ); traceTASK_SUSPEND( pxTCB ); /* Remove task from the ready/delayed list and place in the * suspended list. */ if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ) { taskRESET_READY_PRIORITY( pxTCB->uxPriority ); } else { mtCOVERAGE_TEST_MARKER(); } /* Is the task waiting on an event also? */ if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ) { ( void ) uxListRemove( &( pxTCB->xEventListItem ) ); } else { mtCOVERAGE_TEST_MARKER(); } vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) ); #if ( configUSE_TASK_NOTIFICATIONS == 1 ) { BaseType_t x; for( x = 0; x < configTASK_NOTIFICATION_ARRAY_ENTRIES; x++ ) { if( pxTCB->ucNotifyState[ x ] == taskWAITING_NOTIFICATION ) { /* The task was blocked to wait for a notification, but is * now suspended, so no notification was received. */ pxTCB->ucNotifyState[ x ] = taskNOT_WAITING_NOTIFICATION; } } } #endif /* if ( configUSE_TASK_NOTIFICATIONS == 1 ) */ } taskEXIT_CRITICAL(); if( xSchedulerRunning != pdFALSE ) { /* Reset the next expected unblock time in case it referred to the * task that is now in the Suspended state. */ taskENTER_CRITICAL(); { prvResetNextTaskUnblockTime(); } taskEXIT_CRITICAL(); } else { mtCOVERAGE_TEST_MARKER(); } if( pxTCB == pxCurrentTCB ) { if( xSchedulerRunning != pdFALSE ) { /* The current task has just been suspended. */ configASSERT( uxSchedulerSuspended == 0 ); portYIELD_WITHIN_API(); } else { /* The scheduler is not running, but the task that was pointed * to by pxCurrentTCB has just been suspended and pxCurrentTCB * must be adjusted to point to a different task. */ if( listCURRENT_LIST_LENGTH( &xSuspendedTaskList ) == uxCurrentNumberOfTasks ) /*lint !e931 Right has no side effect, just volatile. */ { /* No other tasks are ready, so set pxCurrentTCB back to * NULL so when the next task is created pxCurrentTCB will * be set to point to it no matter what its relative priority * is. */ pxCurrentTCB = NULL; } else { vTaskSwitchContext(); } } } else { mtCOVERAGE_TEST_MARKER(); } }
<span><strong><code><strong><code><span><strong><code><span><strong><code><strong><code><span>vTaskResume()</span><span> is used to resume a suspended task.</span>
void vTaskResume( TaskHandle_t xTaskToResume ) { TCB_t * const pxTCB = xTaskToResume; /* It does not make sense to resume the calling task. */ configASSERT( xTaskToResume ); /* The parameter cannot be NULL as it is impossible to resume the * currently executing task. */ if( ( pxTCB != pxCurrentTCB ) &&& ( pxTCB != NULL ) ) { taskENTER_CRITICAL(); { if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE ) { traceTASK_RESUME( pxTCB ); /* The ready list can be accessed even if the scheduler is * suspended because this is inside a critical section. */ ( void ) uxListRemove( &( pxTCB->xStateListItem ) ); prvAddTaskToReadyList( pxTCB ); /* A higher priority task may have just been resumed. */ if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) { /* This yield may not cause the task just resumed to run, * but will leave the lists in the correct state for the * next yield. */ taskYIELD_IF_USING_PREEMPTION(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } taskEXIT_CRITICAL(); } else { mtCOVERAGE_TEST_MARKER(); } }
<strong><code><span>These two functions are used together, driven by events, to block/resume a task as needed. It is worth mentioning that in </span><strong><code><span><span>vTaskResume()</span>, if the resumed task has a higher priority, it will call Pendsv for preemption.</span><strong><code><span><span>6. Scheduler Suspension Mechanism</span></span><strong><code><span><span>The scheduler can also be turned on and off.</span></span><strong><code><span><span><span>When multiple tasks or interrupts need to access shared resources (such as global variables, queues, peripheral registers), if not protected, it may lead to </span></span></span><strong><span><span>data races</span></span></strong><span><span><span> (Race Condition). The traditional solution is to </span></span></span><strong><span><span>disable interrupts</span></span></strong><span><span><span> (</span></span></span><code><span><span>taskENTER_CRITICAL()</span></span><span>) which blocks all interrupt responses and disrupts real-time performance.</span><strong><code><span><span><span>TaskSuspendAll() and </span></span><span><span><span>xTaskResumeAll() are used to suspend/resume the scheduler (can be nested). These two tasks will not be interrupted and will not disable interrupts. They control the value of the macro uxSchedulerSuspended to inform the scheduler whether it is running.</span></span></span></span><strong><code><span><span><span>After the scheduler is suspended, if PendSV is triggered (such as calling taskYIELD()), the scheduler will check the value of uxSchedulerSuspended: if </span></span></span><span><span>uxSchedulerSuspended > 0</span></span>, then skip context switching and only mark <span><span>xYieldPending = pdTRUE</span></span> (deferred processing).After resuming the scheduler (<span><span>uxSchedulerSuspended == 0</span></span>), the pending switch requests will be processed.In summary, this article explains the core principles of the scheduler, its startup, triggering, suspension, recovery, and its various handling of tasks. A table summarizes: