ROB-320 · Lesson 4 of 12 · 30 min
RTOS fundamentals: tasks, states and context switching
A real-time operating system, or RTOS, gives firmware the one thing bare-metal designs lack: the ability to write each activity as its own straight-line program that can wait for events, while a kernel decides which activity runs at each moment. A control task can be written as 'wait for the next tick, read the encoder, compute, write the output, repeat', and a communications task as 'wait for a message, parse it, reply', without either knowing about the other. The kernel switches between them in microseconds, always running the most urgent one that is ready. This lesson explains what an RTOS provides, how tasks move between states, what a context switch costs, and how the two most widely used open-source kernels for robot microcontrollers, FreeRTOS and Zephyr, present these ideas.
What an RTOS is, and is not
An RTOS is a small kernel that provides tasks (also called threads), a scheduler, timing services and communication and synchronization objects such as queues, semaphores, mutexes and event flags. Most are linked into the application as a library, so the whole firmware is one program. Unlike a general-purpose operating system such as Linux or Windows, an RTOS is designed so that the time taken by kernel operations is bounded and documented, the highest-priority ready task always runs, and memory use is small and often fixed at build time. A typical microcontroller RTOS kernel occupies 5 to 20 kB of flash and a few kilobytes of RAM.
What an RTOS does not do is make a system real-time by itself. A badly designed task set on an RTOS misses deadlines just as reliably as a badly designed super-loop. The RTOS supplies the mechanism (priority-based preemption with bounded overhead); the engineer supplies the policy (priorities, periods and the analysis in lesson 5 that shows they work).
Tasks and their states
Each task has its own stack, its own saved processor registers when it is not running, a priority and a state. The states are broadly the same in every RTOS:
- Running: the task is executing on the processor. On a single core, exactly one task is running at any time; if nothing else is ready, the kernel runs an idle task.
- Ready: the task could run but a higher- or equal-priority task is running.
- Blocked (or waiting, pending): the task is waiting for something, such as a time delay to expire, a message to arrive or a semaphore to be given. Blocked tasks use no processor time.
- Suspended: the task has been explicitly removed from scheduling until resumed. This is used rarely in well-designed systems.
Most of the time, most tasks are blocked. A well-designed robot controller might have ten tasks, of which at any instant one is running and the other nine are waiting for their next tick, message or event. This is what makes an RTOS efficient: waiting costs nothing, unlike the polling loops of bare-metal code.
Priority-based preemptive scheduling
The core rule of nearly every RTOS scheduler is simple: the highest-priority ready task runs. If a higher-priority task becomes ready, for example because the timer tick unblocks it or an interrupt gives it a semaphore, the kernel preempts the running task immediately, saving its context and switching to the higher-priority one. Tasks of equal priority either take turns at each tick (round-robin time slicing) or run until they block, depending on configuration. This fixed-priority preemptive scheduling is what lesson 5 examines, and its great advantage is that the timing of a high-priority task does not depend on lower-priority tasks at all, apart from shared resources and interrupts.
Priority numbering differs between kernels and is a classic source of bugs. In FreeRTOS a larger number means higher priority, from 0 (idle) up to configMAX_PRIORITIES − 1. In Zephyr a smaller number means higher priority, and negative priorities are cooperative threads that are not preempted by other threads. ARM Cortex-M interrupt priorities, meanwhile, use smaller numbers for more urgent interrupts. Always check the convention before porting code or reading a trace.
The tick and tickless operation
Most RTOSes keep time with a periodic tick interrupt, commonly 1 kHz, which advances the kernel clock, unblocks tasks whose delays have expired and performs time slicing. Delays and timeouts are therefore measured in ticks, and their resolution is one tick: a task that asks to wait 1 tick may wait anywhere from just over 0 to 1 full tick, depending on when in the tick period it made the request. For finer timing, control loops are released directly by a hardware timer interrupt rather than by tick-based delays.
Battery-powered robots and sensors often use tickless idle: when all tasks are blocked, the kernel stops the periodic tick, programs a timer for the next wake-up and lets the processor sleep. This saves considerable energy but requires care in timing, since time is reconstructed from the wake-up timer.
Worked problem
Delay resolution with a 1 kHz tick
A task calls a delay of 3 ticks at a random moment within a tick period, with a 1 kHz tick. What range of actual delays can it see, and what is the problem for a periodic loop?
Show solution
- The first tick may arrive anywhere from almost 0 to 1 ms after the call.
- The delay ends on the third tick boundary: between just over 2 ms and 3 ms.
Answer: The delay is between 2 and 3 ms. A loop that calls a relative delay after its work also drifts by its own execution time each cycle. Use an absolute-time periodic wait, or release the task from a hardware timer.
Periodic tasks done right
The most common RTOS bug in control code is the drifting periodic task. Writing 'do the work, then delay for 1 ms' gives a period of 1 ms plus the work's execution time plus any preemption, which varies every cycle. RTOSes provide absolute-time waits for exactly this reason: FreeRTOS's vTaskDelayUntil and the newer xTaskDelayUntil take a reference wake time and advance it by the period, and Zephyr offers k_timer periodic timers and absolute timeouts. The task wakes on a fixed grid regardless of how long each cycle's work took, as long as the work finishes within the period.
/* FreeRTOS: a 1 kHz joint control task with drift-free timing */
void joint_task(void *arg)
{
const TickType_t period = pdMS_TO_TICKS(1);
TickType_t last_wake = xTaskGetTickCount();
for (;;) {
BaseType_t on_time = xTaskDelayUntil(&last_wake, period);
if (on_time == pdFALSE) overrun_count++; /* missed a release */
read_encoder();
compute_control();
write_pwm();
}
}
/* created once at start-up, priority above communications tasks */
xTaskCreate(joint_task, "joint", 512, NULL, 5, NULL);xTaskDelayUntil returns pdFALSE when the wake time had already passed, which means the previous cycle overran its period; counting these is a cheap, permanent overrun detector that belongs in every periodic control task.
Context switching and its cost
A context switch saves the running task's registers to its stack, chooses the next task and restores that task's registers. On a Cortex-M4 or M7 the kernel triggers the switch through the PendSV exception, and the hardware automatically stacks eight registers on exception entry while the kernel saves the other eight, plus floating-point registers if the task used the FPU. A full switch typically takes a few hundred processor cycles, around 1 to 3 µs at 100 to 200 MHz. On larger application processors with memory management units, caches and translation lookaside buffers, switches cost more, both directly and through the cache misses that follow.
The overhead matters at high rates. Each periodic task costs at least two switches per period (in and out), and every preemption by an interrupt-driven task adds two more. The cost should be included in each task's execution time when doing the analysis of lesson 5.
# Context-switch overhead as a share of the processor
f_cpu = 168e6 # Cortex-M4 clock, Hz
switch_cycles = 300 # cycles per context switch (measured)
t_sw = switch_cycles / f_cpu
tasks = [("current", 20000), ("joint", 1000), ("imu", 500),
("comms", 200), ("battery", 10)] # name, rate in Hz
switches_per_s = sum(2 * r for n, r in tasks)
print("one switch: %.2f us" % (t_sw * 1e6))
print("switches per second: %d" % switches_per_s)
print("overhead: %.2f %% of the processor" % (100 * switches_per_s * t_sw))
print("without the 20 kHz task: %.2f %%" %
(100 * (switches_per_s - 40000) * t_sw))one switch: 1.79 us switches per second: 43420 overhead: 7.75 % of the processor without the 20 kHz task: 0.61 %
Running the 20 kHz current loop as an RTOS task would spend nearly 8 % of the processor just switching in and out of it. This is why the fastest loops normally run directly in interrupt routines or on dedicated hardware, and RTOS tasks handle work at a few kilohertz and below.
Inter-task communication
Tasks cooperate through kernel objects rather than shared global variables, because the kernel objects handle blocking and protection correctly.
| Object | Purpose | Typical robot use |
|---|---|---|
| Queue (message queue) | Pass copies of data items, first in first out, with blocking when empty or full | Commands from the communications task to the motion task |
| Binary semaphore | Signal that an event happened | An ADC interrupt waking the sampling task |
| Counting semaphore | Count available resources or pending events | Free buffers in a DMA pool |
| Mutex | Protect a shared resource, with priority inheritance | Exclusive access to an SPI bus shared by two sensors |
| Event flags (event group) | Wait for one or several conditions | Start motion only when all axes report homed |
| Task notification or direct signal | Lightweight signal to one specific task | Fast interrupt-to-task wake-up in FreeRTOS |
Interrupt routines may give semaphores and send to queues, using special interrupt-safe versions of the calls (FreeRTOS functions ending in FromISR, for example), but must never call functions that block. Lesson 6 covers mutexes and the priority-inversion problem they solve, and lesson 7 covers sharing data with interrupts.
Worked problem
Sizing a command queue
A communications task receives motion commands in bursts of up to 12 messages within 2 ms. The motion task removes one command every 1 ms. Each command is 32 bytes. How deep must the queue be to avoid losing commands, and how much RAM does it use?
Show solution
- During a 2 ms burst, 12 arrive and about 2 are removed, so the backlog peaks at about 10.
- Add margin for a late motion task: choose 16 entries.
- RAM: \( 16 \times 32 = 512 \) bytes plus a small control block.
Answer: A 16-entry queue of 32-byte items (512 bytes) absorbs the burst with margin. The sender should use a short timeout and report an error if the queue is ever full rather than blocking indefinitely.
FreeRTOS and Zephyr
FreeRTOS, maintained by Amazon Web Services under the MIT licence, is the most widely deployed microcontroller RTOS. Its kernel is small, ported to dozens of architectures, and distributed with long-term-support releases; the 202604 LTS release in April 2026 is the current one at the time of writing. It provides tasks, queues, semaphores, mutexes, event groups, software timers and stream buffers, and an SMP version for multicore microcontrollers. Zephyr, a Linux Foundation project under the Apache 2.0 licence, is larger in scope: a kernel plus device drivers, networking, Bluetooth, file systems, a build system based on CMake and devicetree hardware descriptions, released in a 4.x series roughly every four months with periodic long-term-support versions. micro-ROS, the microcontroller flavour of ROS 2, runs on both, as well as on NuttX, another POSIX-style RTOS.
Other kernels appear in robots too: commercial RTOSes such as VxWorks, QNX and INTEGRITY dominate safety-certified and aerospace systems; ThreadX (now Eclipse ThreadX, MIT-licensed since 2024) is common in consumer and industrial devices; and RTEMS is used in space systems. The concepts in this lesson carry across all of them.
Worked problem
Stack RAM for a task set
A controller has 9 tasks with stacks of 2 kB, 1 kB (four tasks), 512 bytes (three tasks) and an idle task of 256 bytes. Each task control block uses about 100 bytes. How much RAM do tasks use?
Show solution
- Stacks: \( 2048 + 4 \times 1024 + 3 \times 512 + 256 = 7936 \) bytes.
- Control blocks: \( 10 \times 100 = 1000 \) bytes (nine tasks plus idle).
- Total: about 8.9 kB.
Answer: About 8.9 kB, which is significant on a microcontroller with 64 kB of RAM. Lesson 8 shows how to measure actual stack use and trim oversized stacks safely.
Common mistakes
MistakeReleasing a periodic task with a relative delay after its work
InsteadUse an absolute-time wait such as xTaskDelayUntil, or a hardware timer.
MistakeAssuming larger numbers always mean higher priority
InsteadCheck each kernel's convention; FreeRTOS and Zephyr differ.
MistakeCalling a blocking RTOS function from an interrupt routine
InsteadUse the interrupt-safe variants and never block in an interrupt.
MistakeRunning a 20 kHz loop as an RTOS task
InsteadRun very fast loops in interrupts or hardware to avoid switching overhead.
MistakeSharing global variables between tasks without protection
InsteadUse queues, mutexes or other kernel objects.
Summary and key terms
- An RTOS provides tasks, a scheduler with bounded overhead, timing services and synchronization objects.
- Tasks move between running, ready, blocked and suspended states, and blocked tasks use no processor time.
- Fixed-priority preemptive scheduling always runs the highest-priority ready task.
- Tick-based delays have one-tick resolution, so periodic tasks need absolute-time waits.
- A context switch costs hundreds of cycles, so very fast loops belong in interrupts or hardware.
- Queues, semaphores, mutexes and event flags pass data and signals safely between tasks and interrupts.
- FreeRTOS and Zephyr are the main open-source microcontroller RTOSes, and both host micro-ROS.
- RTOS
- A real-time operating system with bounded, documented kernel timing.
- Task (thread)
- An independently scheduled activity with its own stack and priority.
- Ready state
- A task that could run but is waiting for the processor.
- Blocked state
- A task waiting for an event, message or time delay.
- Preemption
- Interrupting a running task to run a higher-priority one.
- Context switch
- Saving one task's registers and restoring another's.
- Tick
- The periodic kernel interrupt that keeps time and expires delays.
- Tickless idle
- Stopping the tick while all tasks are blocked to save power.
- Queue
- A kernel object that passes copies of data items between tasks.
- Absolute-time wait
- Blocking until a fixed point in time, used for drift-free periodic tasks.
Practice problems
Work each problem on paper first, then reveal the answer to check.
Name the four common task states.
Show answer
Running, ready, blocked, suspended.
In FreeRTOS, is priority 0 high or low?
Show answer
Lowest (idle).
In Zephyr, what does a negative priority mean?
Show answer
A cooperative thread not preempted by other threads.
A switch takes 250 cycles at 200 MHz. How long?
Show answer
1.25 µs.
A 2 kHz task causes how many switches per second?
Show answer
At least 4,000.
Which object suits waking a task from an ADC interrupt?
Show answer
A binary semaphore or task notification.
Which object protects a shared SPI bus?
Show answer
A mutex.
What does xTaskDelayUntil returning pdFALSE indicate?
Show answer
The previous cycle overran its period.
Why do blocked tasks cost nothing?
Show answer
The scheduler does not run them until their event occurs.
Under which licence is FreeRTOS distributed?
Show answer
MIT.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.