Embedded

FreeRTOS tasks and scheduling

By CourseTron Team ·  7 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    🖨  Download / Print this guide (PDF)

    In short: FreeRTOS tasks are independent functions scheduled by a small real-time kernel that uses fixed priorities and preemption to decide which one runs next. Understanding task creation, priority assignment, and the synchronization primitives (queues and semaphores) is the foundation of writing responsive, deterministic embedded firmware.

    What Are freertos tasks?

    In FreeRTOS, a task is simply a C function that the kernel treats as an independent thread of execution. Each task has its own stack, its own copy of processor register context, and a priority. On a single-core microcontroller only one task actually executes at any instant, but the FreeRTOS scheduler switches between them so quickly that they appear to run concurrently. This model lets you decompose a complex embedded application into small, cooperating units of work rather than one monolithic super-loop.

    A task function has a fixed signature: it takes a single void * parameter and, by convention, never returns. Instead it runs an infinite loop. If a task ever needs to finish, it must delete itself explicitly with vTaskDelete(NULL) rather than falling off the end of the function.

    How Task Creation Works

    You create a task with xTaskCreate() (dynamic allocation) or xTaskCreateStatic() (you supply the stack and task control block buffers yourself). The call registers the task with the kernel and places it in the Ready state. The key arguments are:

    • Task function — the entry point the kernel will call.
    • Stack depth — the size of the task's private stack, specified in words, not bytes. Undersizing this is the most common cause of hard faults.
    • Parameter — the void * passed to the task, useful for reusing one function across several task instances.
    • Priority — an integer from 0 (idle) up to configMAX_PRIORITIES - 1.
    • Task handle — an optional output used later to suspend, resume, or delete the task.

    Nothing runs until you call vTaskStartScheduler(), which hands control of the processor to the kernel. From that point on, the scheduler — not main() — decides what executes.

    Key fact: FreeRTOS uses a fixed-priority preemptive scheduler by default. The highest-priority task that is able to run always runs, and it preempts any lower-priority task immediately — the lower task does not get to finish first.

    Priorities and the Preemptive Scheduler

    Every task sits in one of several states: Running, Ready (able to run but not currently scheduled), Blocked (waiting for an event or timeout), or Suspended. The scheduler's job is to pick, at every scheduling point, the highest-priority task that is in the Ready state.

    Scheduling points occur on every tick interrupt (driven by a periodic timer, often 1 ms) and whenever an API call changes task readiness — for example when data arrives on a queue a blocked task was waiting on. If that event unblocks a task of higher priority than the one currently running, FreeRTOS switches immediately. This is what makes the system real-time: a high-priority task responds predictably, bounded only by interrupt latency and context-switch time.

    Preemption by a higher-priority taskHigh priorityLow priorityRunningBlockedRunningReadyRunningBlockedt1t2t1: an event unblocks the high-priority task, which preempts the low one immediatelyt2: the high-priority task blocks again, so the low-priority task resumesCourseTron · coursetron.com
    Timing of preemption: when an event unblocks a higher-priority task, the scheduler switches to it immediately; when it blocks again, the lower-priority task resumes.

    When two or more Ready tasks share the same priority, the scheduler time-slices between them on each tick using round-robin, provided configUSE_TIME_SLICING is enabled. A critical design rule follows: a high-priority task must block (via a delay, a queue, or a semaphore) rather than spin in a busy loop, otherwise it starves every lower-priority task and the idle task never runs.

    FreeRTOS task statesReadyRunningBlockedSuspendeddispatchpreemptwaitevent / timeout → ReadyCourseTron · coursetron.com
    FreeRTOS task states: the scheduler dispatches the highest-priority Ready task to Running, a higher-priority task preempts it, and a Blocked task returns to Ready on its event or timeout.

    Queues: Passing Data Between Tasks

    Tasks need to communicate without corrupting shared data. The primary FreeRTOS mechanism is the queue — a thread-safe FIFO buffer created with xQueueCreate(). A producer task calls xQueueSend() to copy an item into the queue; a consumer calls xQueueReceive() to copy it out.

    Queues matter because they handle both data transfer and synchronization at once. If a task tries to receive from an empty queue, it can block for a specified timeout; the kernel removes it from the Ready list so it consumes no CPU, then unblocks it the instant an item arrives. Crucially, FreeRTOS queues pass data by copy, not by reference, which avoids many ownership and lifetime bugs — though for large payloads you typically enqueue a pointer instead.

    Queue: passing data between tasks by copyProducer taskxQueueSend()QueueFIFO, by copyConsumer taskxQueueReceive()Empty queue: the consumer blocks, using no CPU, until an item arrivesCourseTron · coursetron.com
    A producer copies an item into the FIFO queue with xQueueSend(); the blocked consumer wakes when the item arrives and copies it out with xQueueReceive().

    Semaphores and Mutexes

    Where queues move data, semaphores coordinate access and signal events. FreeRTOS offers several flavours:

    • Binary semaphores — a single token, ideal for signalling from an ISR to a task ("deferred interrupt handling"). The ISR gives the semaphore; a high-priority task waiting on it unblocks and does the heavy work outside interrupt context.
    • Counting semaphores — hold up to N tokens, used to manage a pool of identical resources or to count events.
    • Mutexes — a special binary semaphore for mutual exclusion that includes priority inheritance. If a low-priority task holds a mutex that a high-priority task needs, the holder is temporarily boosted to the higher priority, limiting priority inversion.

    A mutex must always be taken and given by the same task, whereas a binary semaphore can be given and taken by different contexts — this distinction trips up many newcomers. From an interrupt you must use the ...FromISR() variants, which never block and signal whether a context switch should follow.

    Priority inversion: binary semaphore vs mutexBinary semaphore (no inheritance)Mutex (priority inheritance)High: blockedwaits on a resource the low task holdsMedium: runspreempts the low-priority holderLow: holds the resourcepreempted, cannot release itHigh: blockedwaits for the mutexLow: temporarily raisedto the waiting task's priorityResultholder finishes, releases the lock soonerCourseTron · coursetron.com
    Priority inversion: with a binary semaphore a medium-priority task can preempt the low-priority holder; a mutex raises the holder to the waiting task's priority so it releases the lock sooner.
    Learn this hands-on

    Real projects and industry tool flows, with certification.

    Explore CourseTron Courses →

    freertos tasks — Interview FAQ

    What happens if a FreeRTOS task function returns?

    It is undefined behaviour and usually causes a crash, because the kernel expects task functions to run forever. A task that must terminate should call vTaskDelete(NULL) to delete itself cleanly, which frees its TCB and stack (when dynamically allocated) via the idle task.

    What is priority inversion and how does FreeRTOS reduce it?

    Priority inversion occurs when a high-priority task is blocked waiting on a resource held by a low-priority task, which itself gets preempted by medium-priority tasks. FreeRTOS mutexes implement priority inheritance: the low-priority holder is temporarily raised to the waiting task's priority so it finishes and releases the lock sooner. Note that binary semaphores do not provide this.

    Why must you use the FromISR API variants inside an interrupt?

    Regular API calls can block the caller, which is illegal in interrupt context. The FromISR variants never block and instead return a pxHigherPriorityTaskWoken flag; you then call portYIELD_FROM_ISR() so the scheduler switches to the newly unblocked higher-priority task immediately on exit.

    How do you choose the task stack size?

    Estimate the deepest call chain plus local variables and interrupt frame overhead, then size generously and verify empirically. Use uxTaskGetStackHighWaterMark() during testing to see how close a task came to overflowing, and trim from there. Stack depth is specified in words, so multiply by the word size to reason in bytes.

    Related: RTOS vs bare-metal firmware · the VLSI Glossary · all courses.

    Advertisement