Understanding Task State Machine in FreeRTOS

Understanding Task State Machine in FreeRTOS

In the previous article, we used FreeRTOS to light up an LED, which essentially got FreeRTOS running. To effectively use an RTOS, it is necessary to understand how the scheduler works from a black-box perspective. Of course, if you want to study its internal implementation, you can read the source code. However, I feel that viewing the kernel as a black box and organizing the concepts from a user perspective is also very useful.

Therefore, this article will not delve into the kernel code but will instead learn about the concepts related to the task state machine from the user’s perspective, as well as the functions of the corresponding APIs.

The Role of RTOS Core

In the previous analysis of the FreeRTOS framework, I provided an understanding diagram:

Understanding Task State Machine in FreeRTOS

For microcontrollers, there is generally only one core. The main role of an RTOS is to manage user multi-tasking and schedule management on the physical CPU core. To facilitate understanding, we can think of the RTOS scheduler as a software mechanism that virtually creates a soft core for each application task from the hardware CPU core. This makes each task appear to have its own CPU core, creating an illusion of parallelism in terms of time, although this parallelism is actually pseudo-parallelism.

Typically, a microcontroller has only one hardware core, so at any given time, only one task can be running. However, this understanding is still not comprehensive; from the perspective of application programming, another key player that cannot be ignored is the interrupt handler.

Understanding Task State Machine in FreeRTOS

Task States

State Concepts

It is necessary to understand the state concepts of FreeRTOS thoroughly to use the APIs correctly and know what behaviors to expect when calling a particular API.

<<Mastering the FreeRTOS Real Time Kernel>> provides a top-level state machine view of tasks in the task management chapter:

Understanding Task State Machine in FreeRTOS

For single-core chips, any task can either be in a running state or a non-running state. However, only one task can be in the running state at any given time. This is why the task boxes in the first part of the diagram are stacked, while the second part shows only one task box. In fact, the non-running state is further divided into several sub-states:

Understanding Task State Machine in FreeRTOS

  • Suspended: Suspended State – What does it mean to be suspended? Simply put, once a task enters the suspended state, the scheduler will not schedule it anymore, meaning it will not be loaded into the CPU core for execution. The task’s state remains at the snapshot of the moment it entered the suspended state.

    It’s like watching a fantasy drama where the kernel scheduler is a master of spells, casting a time-stopping spell. With a snap of the fingers, the task is frozen and cannot move. But the task still exists, it just doesn’t move. Until the spell is lifted. The so-called snapshot refers to the task’s TCB (Task Control Block), which saves the physical CPU-related registers at the moment of suspension.

  • Ready: Ready State – This means that the task can be loaded into the CPU core for execution by the scheduler, but it has not yet been loaded. Why is there a ready state? As mentioned earlier, the main role of the RTOS is to manage multi-task scheduling. Therefore, there can be multiple tasks in the ready state at the same time. The scheduler’s scheduling algorithm is responsible for determining which task will run first, based on its internal scheduling algorithm.

    FreeRTOS supports the following scheduling algorithms:

    • Time-Slice Scheduling Policy: Also known as Round Robin scheduling algorithm, this algorithm does not guarantee equal time allocation among tasks of the same priority. It only ensures that ready state tasks of the same priority will enter the running state in turn.

      This may be confusing. First, Time Slice refers to the interval between two Tick interrupts. Each time a new Tick interrupt occurs, the scheduler checks whether there are any tasks in the task queue with the same priority as the currently running task. If there are, the currently running task is swapped out of the CPU, and the new task is swapped in. Therefore, this mechanism does not guarantee equal CPU time slices for tasks in the ready state with the same priority.

    • Fixed Priority Preemptive Scheduling: This scheduling algorithm selects tasks for loading based on their priority. In other words, high-priority tasks always get CPU time before low-priority tasks. A low-priority task can only execute when there are no higher-priority tasks in the ready state.

      To understand this more accurately: if a task with a higher priority than the currently running task enters the ready state, the preemptive scheduling algorithm will immediately “preempt” the running low-priority task. Being preempted means that the low-priority task is immediately swapped out of the running state by the scheduler and enters the ready state, while the high-priority task is loaded into the CPU for execution. It is important to note that the low-priority task enters the ready state rather than the suspended state. When the high-priority task completes its execution and enters the blocked state, the original low-priority task will have a chance to be scheduled for execution.

  • Blocked: Blocked State – The blocked state can be simply understood as the task being stuck somewhere. The task will not continue to run until the blocking is removed and it is transitioned to the ready state, then scheduled to the running state. It is important to distinguish between blocked state and suspended state; suspended means being removed from the scheduling list unless manually restored to the task scheduling list. In contrast, when the blocking event is resolved, the blocked state automatically transitions to the ready state, thus having the opportunity to be swapped into the CPU for execution.

    Blocking events can generally be divided into two categories:

    • Time Events: For example, calling vTaskDelay causes the task to delay for a certain amount of time. Once this function is called, the task is blocked until the delay time ends, at which point it will transition to the ready state.
    • Synchronization Events: For instance, waiting for a message queue, acquiring a semaphore, or obtaining a mutex, etc.

As mentioned above regarding the preemptive scheduling algorithm, the following diagram illustrates this well. At the time points shown in the diagram, a high-priority task will immediately preempt a low-priority task once it becomes ready.

Understanding Task State Machine in FreeRTOS

State Transitions

Having gone through the state concepts, understanding the state machine requires looking at it from two dimensions: 1. What states are there, and what is the physical meaning of each state; 2. What are the conditions for state transitions, and what conditions trigger state changes.

The task state diagram above describes this quite clearly. Here is a summary of how these states actually transition:

  • Entering Suspended State: In any state of the task, once the application program calls the vTaskSuspend API, the specified task will be set to the suspended state.
void vTaskSuspend( TaskHandle_t pxTaskToSuspend ); 
void vTaskSuspendAll( void ); 

Both of the above tasks can be used to set a task to the suspended state. vTaskSuspend is used to set a specified task to the suspended state, where pxTaskToSuspend is the specified task descriptor, while vTaskSuspendAll sets all tasks to the suspended state.

  • Exiting Suspended State: When a task is already in the suspended state and needs to be restored, the application must call vTaskResume or xTaskResumeAll to restore a specific task or all tasks to the ready state. Note that this is the ready state, not the running state; entering the running state is implemented by the scheduler.
void vTaskResume( TaskHandle_t pxTaskToResume ); 
BaseType_t xTaskResumeAll( void ); 

To allow a task to resume running, the above two APIs must be called from a non-suspended task; otherwise, it is impossible to resume because suspended tasks have no opportunity to gain CPU usage rights and run.

Regarding the application scenarios for the suspended state, for example, if an application detects a fault, it may need to handle the fault by suspending a specific task or all tasks until the fault is resolved.

  • Entering Blocked State: The concept of blocking is relative to running; that is, a currently running task is blocked from proceeding due to an OS API call, so in the state diagram, the running state is blocked. This means that the task was running but will be swapped out of the CPU after this call.

What APIs can cause a running task to block?

1. Time Event APIs:

void vTaskDelay( TickType_t xTicksToDelay ); 
void vTaskDelayUntil( TickType_t *pxPreviousWakeTime, TickType_t xTimeIncrement ); 

These two APIs are used when a task wishes to voluntarily yield the CPU. Once called, the task is set to the blocked state until the waiting time expires, at which point the scheduler sets the corresponding task to the ready state. The scheduler then decides whether to load it into the CPU for execution based on the scheduling algorithm.

For example, if a task needs to execute at fixed intervals, it can call this delay function after executing its code to yield the CPU, allowing other tasks to have a chance to run.

vTaskDelayUntil generally first obtains the current Tick count and then delays to a certain increment.

2. Synchronization Event APIs:

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait ); 
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, 
                uint32_t ulBitsToClearOnExit, 
                uint32_t *pulNotificationValue, 
                TickType_t xTicksToWait ); 

// Message queue related
BaseType_t xQueueReceive( QueueHandle_t xQueue,  
               void *pvBuffer,  
               TickType_t xTicksToWait ); 
BaseType_t xQueueReceiveFromISR(  QueueHandle_t xQueue,  
                   void *pvBuffer,  
                   BaseType_t *pxHigherPriorityTaskWoken ); 
BaseType_t xQueuePeek( QueueHandle_t xQueue,  
             void *pvBuffer, TickType_t  
             xTicksToWait ); 
BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue, void *pvBuffer ); 

// Semaphore related
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait ); 
BaseType_t xSemaphoreTakeFromISR( SemaphoreHandle_t xSemaphore,  
                   signed BaseType_t *pxHigherPriorityTaskWoken ); 
BaseType_t xSemaphoreTakeRecursive( SemaphoreHandle_t xMutex,  
                    TickType_t xTicksToWait ); 

// Stream related
size_t xStreamBufferReceive( StreamBufferHandle_t xStreamBuffer, 
                void *pvRxData, 
                size_t xBufferLengthBytes, 
                TickType_t xTicksToWait ); 
size_t xStreamBufferReceiveFromISR( StreamBufferHandle_t xStreamBuffer, 
                    void *pvRxData, 
                    size_t xBufferLengthBytes, 
                    BaseType_t *pxHigherPriorityTaskWoken ); 
// Event related
EventBits_t xEventGroupWaitBits( const EventGroupHandle_t xEventGroup, 
                  const EventBits_t uxBitsToWaitFor, 
                  const BaseType_t xClearOnExit, 
                  const BaseType_t xWaitForAllBits, 
                  TickType_t xTicksToWait ); 
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup, 
                const EventBits_t uxBitsToSet, 
                const EventBits_t uxBitsToWaitFor, 
                TickType_t xTicksToWait ); 

// Message related
size_t xMessageBufferReceive( MessageBufferHandle_t xMessageBuffer, 
                 void *pvRxData, 
                 size_t xBufferLengthBytes, 
                 TickType_t xTicksToWait ); 
size_t xMessageBufferReceiveFromISR( MessageBufferHandle_t xMessageBuffer, 
                    void *pvRxData, 
                    size_t xBufferLengthBytes, 
                    BaseType_t *pxHigherPriorityTaskWoken ); 

This type of task is mainly used for synchronization or communication between tasks or between tasks and interrupts. Instead of busy-waiting, blocking the task while waiting for a message or event essentially improves CPU utilization.

It is important to note that some APIs cannot be used to wait for messages or events from interrupts. If synchronization or communication with interrupt handlers is needed, the corresponding interrupt version of the API must be used.

Summary

To summarize the task-related states of FreeRTOS, other RTOSs are generally similar, although the implementation details may vary slightly. To use an RTOS correctly, it is essential to clearly understand the concepts related to task states. There is no need to memorize the related APIs; just understanding the concepts is sufficient. You can look them up when needed.

Understanding Task State Machine in FreeRTOS

1. None)

2. Why are schematic diagrams always poorly drawn? These tips are essential to know.

3. How do embedded devices display IP locality?

4. RISC-V for MCU/MPU, RTOS, but facing challenges…

5. A method for self-updating firmware on microcontrollers!

6. The Xuantie Cup RISC-V Application Innovation Competition has officially started, and registration is now open!

Understanding Task State Machine in FreeRTOS

Disclaimer: This article is a network repost, and the copyright belongs to the original author. If there are any copyright issues, please contact us, and we will confirm the copyright based on the materials you provide and pay for the manuscript or delete the content.

Leave a Comment