Skip to content
Home » Embedded Systems » RTOS » FreeRTOS Tutorial: Getting Started with Real-Time Operating Systems

FreeRTOS Tutorial: Getting Started with Real-Time Operating Systems

Introduction to FreeRTOS featured image with dark brown background, RTOS badge, FREE icon in orange circle, and Getting Started with Real-Time OS subtitle by nerdyelectronics.com
Embedded Systems Learning Path
Part 75 of 129View Full Path →

KEY TAKEAWAYS

  • FreeRTOS is a lightweight real-time operating system for microcontrollers with preemptive scheduling
  • Tasks are independent threads of execution, each with its own stack and priority level
  • Synchronization primitives (semaphores, mutexes, queues) coordinate communication between tasks
  • FreeRTOS enables concurrent operations that would be complex to implement in a bare-metal super-loop

What is FreeRTOS?

FreeRTOS is the most popular real-time operating system (RTOS) for microcontrollers. It is free, open-source (MIT license), and runs on nearly every microcontroller architecture: ARM Cortex-M, AVR, PIC, RISC-V, ESP32, and more.FreeRTOS provides:
  • Multitasking: Run multiple tasks concurrently on a single CPU
  • Task scheduling: Priority-based preemptive scheduling
  • Synchronization: Semaphores, mutexes, and event groups
  • Communication: Queues and stream buffers for inter-task data exchange
  • Timers: Software timers for periodic or one-shot events
  • Memory management: Multiple heap allocation schemes
FreeRTOS is now maintained by Amazon Web Services (AWS) and is the foundation for AWS IoT device development.

Why Use an RTOS?

Without an RTOS, embedded firmware typically runs in a super loop:
// Super loop (bare-metal) approach
int main(void) {
    system_init();

    while (1) {
        read_sensors();       // Takes 5ms
        update_display();     // Takes 20ms
        check_buttons();      // Takes 1ms
        send_data_to_cloud(); // Takes 200ms (blocks!)
        blink_led();          // Takes 1ms
    }
}
The problem: while send_data_to_cloud() blocks for 200ms, nothing else runs. The display freezes, buttons are unresponsive, and sensor readings are delayed.With FreeRTOS, each of these becomes an independent task:
// FreeRTOS approach - each task runs independently
void vSensorTask(void *params)   { while(1) { read_sensors();       vTaskDelay(100); } }
void vDisplayTask(void *params)  { while(1) { update_display();     vTaskDelay(50);  } }
void vButtonTask(void *params)   { while(1) { check_buttons();      vTaskDelay(10);  } }
void vCloudTask(void *params)    { while(1) { send_data_to_cloud(); vTaskDelay(5000); } }
void vLedTask(void *params)      { while(1) { blink_led();          vTaskDelay(500); } }
Now when the cloud task blocks waiting for a network response, the scheduler switches to other tasks. The system remains responsive.

Core FreeRTOS Concepts

Tasks

A task is a function that runs in an infinite loop with its own stack. You create tasks with xTaskCreate():
#include "FreeRTOS.h"
#include "task.h"

void vBlinkTask(void *pvParameters) {
    while (1) {
        gpio_toggle(LED_PIN);
        vTaskDelay(pdMS_TO_TICKS(500));  // Delay 500ms
    }
}

int main(void) {
    // Create a task with:
    //   - Function: vBlinkTask
    //   - Name: "Blink" (for debugging)
    //   - Stack size: 256 words
    //   - Parameters: NULL
    //   - Priority: 1
    //   - Handle: NULL
    xTaskCreate(vBlinkTask, "Blink", 256, NULL, 1, NULL);

    // Start the scheduler - this never returns
    vTaskStartScheduler();

    // Should never reach here
    while (1);
}

Task Priorities

Each task has a priority (0 = lowest). The scheduler always runs the highest-priority task that is ready. If two tasks have equal priority, they share CPU time using round-robin scheduling.
// Priority assignment
xTaskCreate(vMotorControl, "Motor",   256, NULL, 4, NULL);  // Highest
xTaskCreate(vSensorRead,   "Sensor",  256, NULL, 3, NULL);
xTaskCreate(vDisplay,      "Display", 256, NULL, 2, NULL);
xTaskCreate(vLogging,      "Log",     256, NULL, 1, NULL);  // Lowest

Delays: vTaskDelay vs vTaskDelayUntil

vTaskDelay() delays relative to when it is called. vTaskDelayUntil() delays until an absolute time, providing more precise periodic execution:
// Precise 100ms period regardless of task execution time
void vPeriodicTask(void *params) {
    TickType_t xLastWakeTime = xTaskGetTickCount();

    while (1) {
        // Do work here (takes variable time)
        read_and_process_sensor();

        // Wait until exactly 100ms from last wake
        vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(100));
    }
}

Queues

Queues are the primary way to send data between tasks safely:
#include "queue.h"

QueueHandle_t xSensorQueue;

// Producer task: reads sensor and sends to queue
void vSensorTask(void *params) {
    xSensorQueue = xQueueCreate(10, sizeof(float));

    while (1) {
        float temperature = read_temperature();
        xQueueSend(xSensorQueue, &temperature, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

// Consumer task: receives from queue and displays
void vDisplayTask(void *params) {
    float received_temp;

    while (1) {
        // Block until data is available
        if (xQueueReceive(xSensorQueue, &received_temp, portMAX_DELAY)) {
            display_temperature(received_temp);
        }
    }
}

Semaphores and Mutexes

Binary Semaphore: Used for task synchronization, typically to signal a task from an ISR:
SemaphoreHandle_t xButtonSemaphore;

// ISR: signal that button was pressed
void EXTI_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xButtonSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// Task: wait for button press
void vButtonTask(void *params) {
    xButtonSemaphore = xSemaphoreCreateBinary();

    while (1) {
        // Block until semaphore is given (button pressed)
        xSemaphoreTake(xButtonSemaphore, portMAX_DELAY);
        handle_button_press();
    }
}
Mutex: Used to protect shared resources from simultaneous access by multiple tasks:
SemaphoreHandle_t xUartMutex;

void vTask1(void *params) {
    while (1) {
        xSemaphoreTake(xUartMutex, portMAX_DELAY);
        uart_send_string("Message from Task 1rn");
        xSemaphoreGive(xUartMutex);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void vTask2(void *params) {
    while (1) {
        xSemaphoreTake(xUartMutex, portMAX_DELAY);
        uart_send_string("Message from Task 2rn");
        xSemaphoreGive(xUartMutex);
        vTaskDelay(pdMS_TO_TICKS(1500));
    }
}

A Complete Example: Temperature Monitor

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"

QueueHandle_t xTempQueue;
SemaphoreHandle_t xUartMutex;

void vSensorTask(void *params) {
    TickType_t xLastWake = xTaskGetTickCount();
    while (1) {
        float temp = read_temperature_sensor();
        xQueueSend(xTempQueue, &temp, 0);
        vTaskDelayUntil(&xLastWake, pdMS_TO_TICKS(2000));
    }
}

void vDisplayTask(void *params) {
    float temp;
    while (1) {
        if (xQueueReceive(xTempQueue, &temp, portMAX_DELAY)) {
            xSemaphoreTake(xUartMutex, portMAX_DELAY);
            printf("Temperature: %.1f Crn", temp);
            xSemaphoreGive(xUartMutex);

            display_show_value(temp);
        }
    }
}

void vAlarmTask(void *params) {
    while (1) {
        float temp;
        if (xQueuePeek(xTempQueue, &temp, portMAX_DELAY)) {
            if (temp > 40.0f) {
                buzzer_on();
                led_set(LED_RED, ON);
            } else {
                buzzer_off();
                led_set(LED_RED, OFF);
            }
        }
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

int main(void) {
    hardware_init();

    xTempQueue = xQueueCreate(5, sizeof(float));
    xUartMutex = xSemaphoreCreateMutex();

    xTaskCreate(vSensorTask,  "Sensor",  256, NULL, 3, NULL);
    xTaskCreate(vDisplayTask, "Display", 256, NULL, 2, NULL);
    xTaskCreate(vAlarmTask,   "Alarm",   256, NULL, 4, NULL);

    vTaskStartScheduler();
    while (1);
}

FreeRTOS Configuration

FreeRTOS is configured through a file called FreeRTOSConfig.h. Key settings include:
// FreeRTOSConfig.h (key entries)
#define configUSE_PREEMPTION            1     // Enable preemptive scheduling
#define configCPU_CLOCK_HZ              72000000  // MCU clock speed
#define configTICK_RATE_HZ              1000  // 1ms tick (1000 Hz)
#define configMAX_PRIORITIES            5     // Priority levels (0-4)
#define configMINIMAL_STACK_SIZE        128   // Minimum task stack (words)
#define configTOTAL_HEAP_SIZE           (8 * 1024)  // 8KB heap for tasks
#define configUSE_MUTEXES               1
#define configUSE_COUNTING_SEMAPHORES   1
#define configUSE_QUEUE_SETS            1

Common Mistakes

  1. Stack overflow: Tasks have fixed-size stacks. If a task uses more stack than allocated, it corrupts memory. Enable configCHECK_FOR_STACK_OVERFLOW during development.
  2. Calling blocking functions from ISR: Never use xQueueSend() or xSemaphoreTake() in an ISR. Use their FromISR variants: xQueueSendFromISR(), xSemaphoreGiveFromISR().
  3. Priority inversion: A low-priority task holds a mutex that a high-priority task needs. Use xSemaphoreCreateMutex() (which has built-in priority inheritance) instead of binary semaphores for resource protection.
  4. Forgetting to start the scheduler: Always call vTaskStartScheduler() after creating tasks.

Summary

FreeRTOS transforms how you structure embedded firmware. Instead of a single super loop trying to do everything, you break your system into independent tasks that the scheduler manages. Queues pass data between tasks safely. Semaphores and mutexes handle synchronization. The result is cleaner code, better responsiveness, and easier maintenance. Start with simple tasks and delays, then add queues and semaphores as your project grows in complexity.

Leave a Reply

Your email address will not be published. Required fields are marked *