The "Holy Bible" for embedded engineers
Understanding FreeRTOS fundamentals through concepts, not just code. Learn why RTOS matters and how to think about real-time systems.
Concept: FreeRTOS is a real-time operating system that manages multiple tasks by giving each one a slice of CPU time, ensuring critical operations happen when they need to happen.
Why it matters: Without an RTOS, you’d have to manually manage timing, priorities, and resource sharing between different parts of your program. This becomes impossible to maintain as complexity grows, and critical operations might miss their deadlines.
Minimal example: A simple system with two tasks - one blinking an LED every 100ms, another reading a sensor every 500ms. FreeRTOS ensures both happen reliably without interfering with each other.
Try it: Start with a single task that blinks an LED, then add a second task that reads a sensor. Observe how FreeRTOS manages both automatically.
Takeaways: FreeRTOS provides predictable timing and resource management, allowing you to focus on what your system should do rather than how to coordinate multiple operations.
Real-time doesn’t mean “fast” - it means predictable. A real-time system guarantees that operations complete within their specified time limits.
┌─────────────────────────────────────────────────────────────┐
│ Real-Time vs Non-Real-Time │
├─────────────────────────────────────────────────────────────┤
│ Real-Time System │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Task A │ │ Task B │ │ Task A │ │ Task B │ │
│ │ 100ms │ │ 500ms │ │ 100ms │ │ 500ms │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ ↑ ↑ ↑ ↑ │
│ 0ms 100ms 200ms 300ms │
│ │
│ Non-Real-Time System │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Task A │ │ Task B │ │ Task A │ │ Task B │ │
│ │ 100ms │ │ 500ms │ │ 150ms │ │ 600ms │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ ↑ ↑ ↑ ↑ │
│ 0ms 100ms 250ms 350ms │
│ │
│ ❌ Timing varies - unpredictable! │
└─────────────────────────────────────────────────────────────┘
Bare Metal Approach:
┌─────────────────────────────────────────────────────────────┐
│ Bare Metal Programming │
├─────────────────────────────────────────────────────────────┤
│ while(1) { │
│ // Check if it's time to blink LED │
│ if (timer_elapsed(100ms)) { │
│ toggle_led(); │
│ reset_timer(); │
│ } │
│ │
│ // Check if it's time to read sensor │
│ if (timer_elapsed(500ms)) { │
│ sensor_value = read_sensor(); │
│ reset_timer(); │
│ } │
│ │
│ // What if we need to add more tasks? │
│ // What if priorities change? │
│ // What if timing requirements change? │
│ } │
│ │
│ ❌ Becomes unmanageable quickly! │
└─────────────────────────────────────────────────────────────┘
FreeRTOS Approach:
┌─────────────────────────────────────────────────────────────┐
│ FreeRTOS Approach │
├─────────────────────────────────────────────────────────────┤
│ // Task 1: Blink LED every 100ms │
│ void vBlinkTask(void *pvParameters) { │
│ while(1) { │
│ toggle_led(); │
│ vTaskDelay(pdMS_TO_TICKS(100)); │
│ } │
│ } │
│ │
│ // Task 2: Read sensor every 500ms │
│ void vSensorTask(void *pvParameters) { │
│ while(1) { │
│ sensor_value = read_sensor(); │
│ vTaskDelay(pdMS_TO_TICKS(500)); │
│ } │
│ } │
│ │
│ // FreeRTOS handles the rest automatically! │
│ ✅ Clean, maintainable, scalable │
└─────────────────────────────────────────────────────────────┘
FreeRTOS sits between your application and the hardware, managing resources and timing:
┌─────────────────────────────────────────────────────────────┐
│ FreeRTOS Architecture │
├─────────────────────────────────────────────────────────────┤
│ Application Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Task 1 │ │ Task 2 │ │ Task 3 │ │
│ │ (Blink LED) │ │ (Read Sens) │ │ (Send Data) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ FreeRTOS Kernel │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Scheduler │ │ Memory │ │ Timing │ │
│ │ │ │ Manager │ │ Services │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Queues │ │ Semaphores │ │ Mutexes │ │
│ │ │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Hardware Abstraction Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Port │ │ Memory │ │ Interrupt │ │
│ │ Layer │ │ Model │ │ Handler │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Hardware (MCU) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ CPU │ │ RAM │ │ Peripherals│ │
│ │ │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
Scheduler: The brain that decides which task runs when Memory Manager: Handles stack allocation and memory pools Timing Services: Provides delays, timeouts, and periodic execution Communication: Queues, semaphores, and mutexes for task coordination
A task is like a separate program that runs independently. Think of it as a worker with a specific job to do.
┌─────────────────────────────────────────────────────────────┐
│ Task Lifecycle │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Created │───▶│ Ready │───▶│ Running │ │
│ └─────────┘ └─────────┘ └────┬────┘ │
│ │ │
│ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Deleted │◄───│ Blocked │◄───│ │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ • Created: Task exists but not yet scheduled │
│ • Ready: Task is ready to run, waiting for CPU │
│ • Running: Task is currently executing │
│ • Blocked: Task is waiting for something (delay, data) │
│ • Deleted: Task has been removed from system │
└─────────────────────────────────────────────────────────────┘
Tasks have priorities - higher priority tasks get CPU time first:
┌─────────────────────────────────────────────────────────────┐
│ Task Priority System │
├─────────────────────────────────────────────────────────────┤
│ │
│ Priority 5: Emergency Stop (highest) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ██████████████████████████████████████████████████ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Priority 4: Safety Monitoring │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ██████████████████████████████████████████████████ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Priority 3: Control Loop │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ██████████████████████████████████████████████████ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Priority 2: Data Logging │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ██████████████████████████████████████████████████ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Priority 1: Status Updates (lowest) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ██████████████████████████████████████████████████ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ⚠️ Higher priority tasks can interrupt lower ones! │
└─────────────────────────────────────────────────────────────┘
Here’s the minimal code to create a simple task:
#include "FreeRTOS.h"
#include "task.h"
// Task function - this is what the task will do
void vBlinkTask(void *pvParameters) {
while (1) {
// Toggle LED (your hardware-specific code here)
toggle_led();
// Wait for 500ms - FreeRTOS handles the timing
vTaskDelay(pdMS_TO_TICKS(500));
}
}
// In your main function
int main(void) {
// Create the task
xTaskCreate(
vBlinkTask, // Function to run
"BlinkTask", // Task name (for debugging)
128, // Stack size in words
NULL, // Parameters (none in this case)
1, // Priority (1 = lowest)
NULL // Task handle (not needed here)
);
// Start the FreeRTOS scheduler
vTaskStartScheduler();
// Should never reach here
while (1);
}
Key Points:
vTaskDelay()
doesn’t block the CPU - it lets other tasks runwhile(1)
loopWhen multiple tasks share resources (like a sensor or communication bus), they need to coordinate:
┌─────────────────────────────────────────────────────────────┐
│ Problem: Resource Conflict │
├─────────────────────────────────────────────────────────────┤
│ │
│ Task A: "I want to read the temperature sensor" │
│ Task B: "I want to read the temperature sensor" │
│ │
│ ❌ Both try to read at the same time → corrupted data! │
│ │
│ Solution: Use a mutex (mutual exclusion) │
│ │
│ Task A: "I'll take the mutex first" │
│ Task B: "I'll wait for the mutex" │
│ │
│ ✅ Only one task can access the sensor at a time │
└─────────────────────────────────────────────────────────────┘
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
// Mutex to protect the sensor
SemaphoreHandle_t xSensorMutex;
// Task that reads sensor
void vSensorTask(void *pvParameters) {
while (1) {
// Wait for mutex (wait forever if needed)
if (xSemaphoreTake(xSensorMutex, portMAX_DELAY) == pdTRUE) {
// We have the mutex - safe to read sensor
float temperature = read_temperature_sensor();
// Process temperature data
process_temperature(temperature);
// Give back the mutex so other tasks can use it
xSemaphoreGive(xSensorMutex);
}
// Wait before next reading
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// Initialize mutex
void vInitializeSystem(void) {
// Create the mutex
xSensorMutex = xSemaphoreCreateMutex();
// Create the task
xTaskCreate(vSensorTask, "Sensor", 128, NULL, 2, NULL);
}
FreeRTOS is highly configurable. Here are the key settings you need to understand:
// FreeRTOSConfig.h - Key configuration options
// Enable preemptive scheduling (tasks can interrupt each other)
#define configUSE_PREEMPTION 1
// System tick frequency (how often FreeRTOS checks for task switches)
#define configTICK_RATE_HZ 1000
// Maximum number of task priorities
#define configMAX_PRIORITIES 32
// Minimum stack size for tasks
#define configMINIMAL_STACK_SIZE 128
// Enable mutex support
#define configUSE_MUTEXES 1
// Enable queue support
#define configUSE_QUEUES 1
// Enable semaphore support
#define configUSE_COUNTING_SEMAPHORES 1
// Check for stack overflow (important for debugging)
#define configCHECK_FOR_STACK_OVERFLOW 2
What These Mean:
Objective: Understand basic task creation and timing.
Setup: Create a single task that blinks an LED at a specific frequency.
Steps:
vTaskDelay()
to control timingExpected Outcome: Understanding that FreeRTOS provides predictable timing.
Objective: Learn how tasks can work together.
Setup: Create two tasks - one blinks LED A every 200ms, another blinks LED B every 300ms.
Steps:
Expected Outcome: Understanding that multiple tasks can run concurrently without interference.
Objective: Learn about synchronization and resource protection.
Setup: Create two tasks that need to share a resource (like a UART or sensor).
Steps:
Expected Outcome: Understanding why synchronization is necessary and how to implement it.