Skip to content

Understanding RTOS Scheduling Mechanisms with FreeRTOS

This article provides an in-depth technical guide to how Real-Time Operating Systems (RTOS), specifically FreeRTOS, manage task execution. When developing multi-threaded embedded applications, understanding the schedulerβ€”the component responsible for allocating CPU timeβ€”is crucial for predictable and reliable system behavior. We will explore the concepts of time slicing, priorities, task states, and context switching using a practical example implementation flow.

Core Concepts of RTOS Scheduling

In a bare-metal environment, tasks execute sequentially. An RTOS abstracts this by allowing multiple tasks to appear to run concurrently on a single core by rapidly switching execution between them.

Time Slicing and the Tick Interrupt

Most RTOS implementations utilize time slicing. This mechanism involves a periodic hardware timer that interrupts the processor at fixed intervals. This interval is known as the tick (often 1 millisecond in FreeRTOS).

The operating system's scheduler task is responsible for running every single tick. Its primary function is to assess all tasks and determine which one should run next based on pre-defined rules, primarily task priority.

Task Prioritization

Tasks are assigned a priority level. The scheduler must adhere to the following logic:

  1. When the scheduler runs (i.e., on every tick), it examines all tasks that are in the Ready state.
  2. It immediately selects and chooses the task with the highest numerical priority that is ready to run.
  3. Once a task is running, other tasks must wait their turn unless an interrupt occurs.

When a task runs, it consumes CPU time for the duration of the time slice until either: * It voluntarily yields the processor (by calling a delay API). * Its time slice expires. * A higher-priority task becomes ready.

[!WARNING] Hardware vs. Software Interrupts Hardware interrupts (like pin changes or timer overflows) always possess a higher priority level than any purely software-scheduled task. The scheduler will temporarily pause all task execution to service a hardware Interrupt Service Routine (ISR). This rule only changes if the code explicitly disables hardware interrupts.

Preemptive Scheduling

The mechanism describedβ€”where the CPU time of one task can be forcibly taken away to allow a higher-priority task to runβ€”is called preemptive scheduling. This ensures that critical, time-sensitive tasks are never starved by lower-priority, longer-running tasks.

Understanding Task States

FreeRTOS meticulously tracks the operating state of every managed task. Understanding these states dictates when and how a task can gain or lose control of the CPU.

Task States Defined

State Description Condition to Re-enter Run State
Ready The task is fully initialized and waiting for the scheduler to assign it CPU time. Scheduler selection due to high priority or time slice expiry.
Running The task currently has control of the CPU and is executing its instructions. Noneβ€”it remains running until interrupted or it yields.
Blocked The task cannot run because it is waiting for an external event (e.g., a delay expiring, a queue semaphore being released). The external event finally reports success (e.g., delay timer fires).
Suspended The task has been intentionally paused by another task or external code call using specific API functions (e.g., vTaskSuspend). Explicitly resumed by the caller using the corresponding API function (e.g., vTaskResume).

State Transitions

  1. Creation: A newly created task automatically enters the Ready state.
  2. Execution: The scheduler moves a task from Ready $\rightarrow$ Running.
  3. Waiting: A task calls an API (like vTaskDelay), moving it from Running $\rightarrow$ Blocked.
  4. Resuming: When the waiting condition is met, the event triggers the scheduler, which moves it from Blocked $\rightarrow$ Ready.
  5. Manual Control: Tasks can be moved to Suspended and only return to Ready via an explicit resume call.

Context Switching

Context switching is the fundamental mechanism that enables concurrency. It is the process of saving the full execution state of the currently running task and restoring the saved state of the next scheduled task.

[!NOTE] Components of Context The "context" encompasses everything necessary to resume the task exactly where it left off. This includes: 1. Program Counter (PC): The address of the next instruction to execute. 2. CPU Registers: All general-purpose CPU registers used by the task. 3. Stack Pointer (SP): The current position within the task's allocated stack memory. 4. Working Variables: Any local variables stored on the stack.

The underlying stack allocated for the task is used to store much of this context, which is why allocating a minimum, appropriate stack size is vital during task creation.

Implementation Example: Utilizing FreeRTOS Concepts

This section outlines the logical flow for setting up and managing tasks, based on common RTOS patterns.

1. Task Initialization Structure

When creating a task, you define its entry point, necessary parameters, and initial stack size.

2. Task Logic Loop

Each task must run in an infinite loop, periodically checking conditions or waiting on events.

Example Pattern:

void MyTaskFunction(void *pvParameters) {
    for (;;) {
        // Check for external events or timers
        if (xSemaphoreTake(xQueueHandle, portMAX_DELAY) == pdPASS) {
            // Process event data received
        }
        vTaskDelay(pdMS_TO_TICKS(100)); // Wait for 100ms
    }
}

3. System Management

Uses RTOS functions to manage the task lifecycle: * vTaskCreate(): Creates and starts a new task. * vTaskDelay(): Suspends the current task for a specified time. * ulTaskGetStackHighWaterMark(): Checks memory usage to prevent stack overflow.


Hands-On Guide: Simulating a Simple Semaphore Wait

If you were to implement a resource lock using semaphores:

  1. Create Semaphore: Initialize the semaphore with a count of 1 (Binary Semaphore).
    SemaphoreHandle_t xMutex = xSemaphoreCreateBinary();
    
  2. Acquire Lock: Before accessing shared resource data, attempt to take the semaphore.
    if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
        // CRITICAL SECTION: Access shared hardware/data
        printf("Resource accessed successfully.\n");
    
        // Release the lock when done
        xSemaphoreGive(xMutex);
    }
    
  3. Wait: If the semaphore cannot be acquired immediately, the task will block at xSemaphoreTake(), consuming no CPU resources until the resource is freed.

This pattern guarantees mutual exclusion, preventing race conditions between concurrent tasks.