A Quick Start Guide to FreeRTOS

FreeRTOS is a widely used real-time operating system kernel for embedded systems.

1. What is FreeRTOS?

  • Essence: It is an open-source (MIT licensed), configurable real-time operating system (RTOS) kernel.

    Core Features:

    Characteristics:

    • Small resource footprint: The kernel itself typically compiles to only 6KB – 12KB of ROM and a few hundred bytes of RAM (depending on architecture and configuration).
    • Highly configurable: By using a configuration file (<span>FreeRTOSConfig.h</span>), only the necessary features can be included to minimize resource usage.
    • Strong portability: The code is primarily written in C and provides official ports for various microcontroller architectures (ARM Cortex-M, MIPS, PIC32, RX, RISC-V, etc.).
    • Mature ecosystem: It has a large user base, extensive documentation, tutorials, and community support. It is integrated and recommended by many chip manufacturers (such as STM32CubeMX, ESP-IDF, etc.).
    • Task scheduling: Create, manage, and schedule multiple independent “tasks” (threads).
    • Inter-task communication: Provides mechanisms such as queues, semaphores, mutexes, and event groups to allow tasks to safely exchange information and synchronize.
    • Inter-task synchronization: Coordinates the execution order of tasks.
    • Memory management: Provides optional dynamic memory allocation schemes (such as heap_1, heap_4, heap_5).
    • Time management: Provides system tick timers (<span>vTaskDelay</span>, <span>vTaskDelayUntil</span>), software timers.
    • Interrupt management: Provides safe methods for passing information from interrupt service routines to tasks (such as binary semaphores, counting semaphores, queues).

2. Why use FreeRTOS?

In embedded projects with high complexity, strict real-time requirements, and the need for reliable concurrency, using FreeRTOS (or other RTOS) offers significant advantages over bare-metal development (Super Loop):

  • Simplifies the development of complex applications: Breaks down complex functionalities into multiple independent tasks, making design, coding, debugging, and maintenance easier, in line with the “divide and conquer” philosophy.
  • Achieves true concurrency: Multiple tasks appear to run simultaneously, improving CPU utilization and allowing the system to handle multiple tasks at once (e.g., collecting data, displaying interfaces, and performing network communication simultaneously).
  • Meets real-time requirements: Provides a priority-based preemptive scheduler that ensures high-priority critical tasks (such as motor control, emergency response) can immediately gain CPU execution rights when needed, meeting hard real-time or soft real-time deadlines.
  • Handles blocking operations more efficiently: When tasks need to wait for external events (such as sensor data arrival, network packet arrival, timeout), they can actively block themselves to release CPU resources, allowing other ready tasks to run. The CPU will not waste resources in busy-wait loops.
  • Provides powerful synchronization and communication mechanisms: Built-in mechanisms such as queues and semaphores make synchronization and data exchange between tasks and between tasks and interrupts safe, simple, and reliable, greatly reducing the risk of resource conflicts (Race Conditions) and priority inversion.
  • Enhances code modularity and reusability: Independent tasks are easier to encapsulate into functional modules, improving code reusability.
  • Lowers the development threshold (to some extent): Once the basic concepts of RTOS are understood, using the provided abstraction layers (tasks, queues, etc.) may be more intuitive than manually implementing equivalent functionalities on bare metal using state machines and complex interrupt management.

3. What is the difference from bare-metal development? (Super Loop vs. RTOS)

Feature Bare-metal Development (Super Loop) FreeRTOS (RTOS)
Program Structure Single infinite loop (<span>while(1) { ... }</span>), sequentially calling various functions inside. Multiple independent tasks (<span>Task</span>), each with its own entry function and stack.
Concurrency Model Pseudo-concurrency (simulated): relies on state machines and rapid polling of functions. True concurrency (perceptible): the scheduler manages task execution based on priority.
Task Scheduling Developers manually manage execution order (function call order, state machines). Preemptive scheduler automatically manages: decides which task runs based on priority and time slices.
CPU Utilization Inefficient: The CPU may spin (busy wait) while waiting for events (e.g., delays, waiting for serial data). Efficient: Tasks can block while waiting, allowing the CPU to immediately switch to execute other ready tasks.
Blocking Operation Handling Very unintuitive and inefficient, usually using flags or polling. Core advantage!<span>vTaskDelay</span>, <span>xQueueReceive</span>, <span>xSemaphoreTake</span>, etc., tasks can “sleep” to yield the CPU.
Real-time Responsiveness Poor: Long execution times of low-priority functions in the loop can block critical event responses. Excellent (configurable): High-priority tasks can preempt low-priority tasks at any time, ensuring timely handling of critical events.
Communication and Synchronization Primarily relies on global variables, flags, and interrupts. Must be very careful to handle race conditions. Built-in safety mechanisms: queues, semaphores, mutexes, event groups, etc., greatly reduce risks.
Complexity Intuitive development for simple applications. The more complex the project, the more chaotic the code structure (spaghetti code), making logic harder to clarify. Steep learning curve (tasks, scheduling, synchronization primitives). But complex applications are clearer and simpler!.
Resource Usage Extremely low (only the application itself). No scheduling overhead. Requires additional overhead: The kernel itself occupies ROM/RAM (a few KB to a dozen KB). Each task requires independent stack space. There is scheduling overhead (context switching).
Debugging Difficulty Simple issues are easy to debug. Complex logic (timing dependencies, race conditions) is difficult to debug. Introduces new issues: task scheduling timing, synchronization issues (deadlocks, priority inversion). But common tools include trace views, debugging hooks, etc.
Typical Applications Very simple controls (e.g., running lights, button controls), resource-sensitive low-power sensor nodes. Devices requiring multitasking, real-time response, network connectivity, file systems, complex user interactions, background processing, etc. (e.g., industrial controllers, consumer electronics, network devices).

4. Comparison of Bare-metal Development and FreeRTOS

Let’s write a program to implement the blinking of two LEDs, one LED blinks every 100ms, and the other LED blinks every 500ms.

4.1 Bare-metal Development

#include "stm32f10x.h"  // STM32F10x series microcontroller standard peripheral library header file

// LED pin definitions
#define LED1_PIN     GPIO_Pin_5  // LED1 uses pin (PB5)
#define LED1_PORT    GPIOB       // LED1 uses port (GPIOB)
#define LED2_PIN     GPIO_Pin_5  // LED2 uses pin (PE5)
#define LED2_PORT    GPIOE       // LED2 uses port (GPIOE)

// Timer interrupt frequency (Hz)
#define TIM2_FREQ    2    // LED1 blink frequency: 2Hz (500ms period, on for 500ms, off for 500ms)
#define TIM3_FREQ    10   // LED2 blink frequency: 10Hz (100ms period, on for 100ms, off for 100ms)

// System clock frequency (set according to actual situation)
#define SYSTEM_CLOCK 72000000  // 72MHz (assuming system clock is 72MHz)

// Timer counter initialization value calculation
// Formula: period value = (system clock frequency) / (desired frequency * 1000) - 1
// Explanation: The timer takes 1/72MHz seconds to count once, and needs to count to a specific value to generate an interrupt
#define TIM2_PERIOD  (SYSTEM_CLOCK / (TIM2_FREQ * 1000) - 1)  // Timer 2 period value (for LED1)
#define TIM3_PERIOD  (SYSTEM_CLOCK / (TIM3_FREQ * 1000) - 1)  // Timer 3 period value (for LED2)

// Global variables to track LED states
volatile uint8_t led1_state = 0;  // Current state of LED1 (0=off, 1=on)
volatile uint8_t led2_state = 0;  // Current state of LED2 (0=off, 1=on)

// GPIO initialization function
void GPIO_Configuration(void) {
    GPIO_InitTypeDef GPIO_InitStructure;  // GPIO configuration structure
    
    // Enable APB2 bus clock for GPIOB and GPIOE
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOE, ENABLE);
    
    // Configure PB5 pin as push-pull output mode
    GPIO_InitStructure.GPIO_Pin = LED1_PIN;          // Select pin 5
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // Push-pull output mode (can directly drive LED)
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // Output speed 50MHz (high-speed output)
    GPIO_Init(LED1_PORT, &amp;GPIO_InitStructure);       // Apply configuration to GPIOB port
    
    // Configure PE5 pin as push-pull output mode
    GPIO_InitStructure.GPIO_Pin = LED2_PIN;          // Select pin 5
    GPIO_Init(LED2_PORT, &amp;GPIO_InitStructure);       // Apply configuration to GPIOE port
    
    // Initial state: both LEDs are off
    GPIO_SetBits(LED1_PORT, LED1_PIN);  // PB5 outputs high (LED1 off)
    GPIO_SetBits(LED2_PORT, LED2_PIN);  // PE5 outputs high (LED2 off)
}

// Configure Timer 2 function (for controlling LED1)
void TIM2_Configuration(void) {
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;  // Timer base configuration structure
    NVIC_InitTypeDef NVIC_InitStructure;           // Interrupt controller configuration structure
    
    // Enable APB1 bus clock for TIM2 (Timer 2 is on APB1 bus)
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    
    // Timer base settings
    TIM_TimeBaseStructure.TIM_Period = TIM2_PERIOD;       // Auto-reload value (determines interrupt frequency)
    TIM_TimeBaseStructure.TIM_Prescaler = 0;              // Prescaler (0 means no prescaling)
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;          // Clock division (no division)
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // Up counting mode
    TIM_TimeBaseInit(TIM2, &amp;TIM_TimeBaseStructure);       // Apply configuration to TIM2
    
    // Enable TIM2 update interrupt (interrupt generated when counter overflows)
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
    
    // Configure TIM2 interrupt channel
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;       // Timer 2 interrupt channel
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // Preemption priority 0 (highest)
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;     // Sub-priority 1
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;        // Enable this interrupt channel
    NVIC_Init(&amp;NVIC_InitStructure);                        // Apply interrupt configuration
    
    // Start Timer 2 (start counting)
    TIM_Cmd(TIM2, ENABLE);
}

// Configure Timer 3 function (for controlling LED2)
void TIM3_Configuration(void) {
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;  // Timer base configuration structure
    NVIC_InitTypeDef NVIC_InitStructure;           // Interrupt controller configuration structure
    
    // Enable APB1 bus clock for TIM3 (Timer 3 is on APB1 bus)
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    
    // Timer base settings
    TIM_TimeBaseStructure.TIM_Period = TIM3_PERIOD;       // Auto-reload value
    TIM_TimeBaseStructure.TIM_Prescaler = 0;              // Prescaler (no prescaling)
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;          // Clock division
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // Up counting mode
    TIM_TimeBaseInit(TIM3, &amp;TIM_TimeBaseStructure);       // Apply configuration to TIM3
    
    // Enable TIM3 update interrupt
    TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
    
    // Configure TIM3 interrupt channel
    NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;       // Timer 3 interrupt channel
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // Preemption priority 0
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;     // Sub-priority 2
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;        // Enable interrupt channel
    NVIC_Init(&amp;NVIC_InitStructure);                        // Apply interrupt configuration
    
    // Start Timer 3
    TIM_Cmd(TIM3, ENABLE);
}

// System clock configuration function
void SystemClock_Config(void) {
    // Use the default configuration from the standard library (set system clock to 72MHz)
    // Note: In actual applications, clock configuration may need to be adjusted based on hardware
    SystemInit();
}

// Main function - program entry point
int main(void) {
    // System clock configuration (set to 72MHz)
    SystemClock_Config();
    
    // Initialize GPIO (configure LED pins as outputs)
    GPIO_Configuration();
    
    // Configure timers
    TIM2_Configuration();  // Configure TIM2 for LED1 (500ms blink)
    TIM3_Configuration();  // Configure TIM3 for LED2 (100ms blink)
    
    // Main loop - Timer interrupt handles all LED control work
    while (1) {
        // The main loop can execute other low-priority tasks
        // Or enter low-power mode (reduce power consumption)
        __WFI();  // Wait for interrupt instruction (enter low-power mode, wake up when interrupt occurs)
    }
}

// TIM2 interrupt service function (handles LED1 control)
void TIM2_IRQHandler(void) {
    // Check if TIM2 update interrupt flag is set
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
        // Clear interrupt flag (important! Otherwise, it will keep entering the interrupt)
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
        
        // Toggle LED1 state
        if (led1_state) {
            GPIO_SetBits(LED1_PORT, LED1_PIN);    // Output high (LED off)
        } else {
            GPIO_ResetBits(LED1_PORT, LED1_PIN);  // Output low (LED on)
        }
        led1_state = !led1_state;  // Update LED state flag
    }
}

// TIM3 interrupt service function (handles LED2 control)
void TIM3_IRQHandler(void) {
    // Check if TIM3 update interrupt flag is set
    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
        // Clear interrupt flag
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
        
        // Toggle LED2 state
        if (led2_state) {
            GPIO_SetBits(LED2_PORT, LED2_PIN);    // Output high (LED off)
        } else {
            GPIO_ResetBits(LED2_PORT, LED2_PIN);  // Output low (LED on)
        }
        led2_state = !led2_state;  // Update LED state flag
    }
}

4.2 FreeRTOS Development

#include "stm32f10x.h"        // STM32F10x series microcontroller standard peripheral library
#include "FreeRTOS.h"         // FreeRTOS real-time operating system core header file
#include "task.h"             // FreeRTOS task management related functions and macros

// Define task handle variables (for task control)
TaskHandle_t myTask1Handler;  // Task 1 handle pointer
TaskHandle_t myTask2Handler;  // Task 2 handle pointer

// Simple software delay function (Note: not recommended in RTOS, will block the entire system)
void Delay(u32 count)         // count: delay loop count
{
    u32 i=0;                  // Loop counter
    for(;i<count;i++);        // Empty loop for delay (occupies CPU)
}

// Task 1 function: control PB5 pin LED blinking
void myTask1( void * arg)     // arg: task parameter (not used)
{
    while(1)                  // Task infinite loop body
    {
        GPIO_ResetBits(GPIOB,GPIO_Pin_5);  // PB5 outputs low (LED on)
        vTaskDelay(500);                    // Block delay for 500 system ticks
        GPIO_SetBits(GPIOB,GPIO_Pin_5);     // PB5 outputs high (LED off)
        vTaskDelay(500);                    // Block delay for 500 system ticks
    }
}

// Task 2 function: control PE5 pin LED blinking
void myTask2( void * arg)     // arg: task parameter (not used)
{
    while(1)                  // Task infinite loop body
    {
        GPIO_ResetBits(GPIOE,GPIO_Pin_5);  // PE5 outputs low (LED on)
        vTaskDelay(100);                    // Block delay for 100 system ticks
        GPIO_SetBits(GPIOE,GPIO_Pin_5);     // PE5 outputs high (LED off)
        vTaskDelay(100);                    // Block delay for 100 system ticks
    }
}

// Main function - program entry point
int main(void)
{ 
    GPIO_InitTypeDef  GPIO_InitStructure;  // GPIO initialization structure
     
    // Initialize GPIO peripheral clock and configuration
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|
    RCC_APB2Periph_GPIOE, ENABLE);         // Enable APB2 bus clock for GPIOB and GPIOE
    
    // Configure PB5 pin (LED1)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;          // Select pin 5
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;    // Push-pull output mode
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;   // Output speed 50MHz
    GPIO_Init(GPIOB, &amp;GPIO_InitStructure);              // Apply configuration to GPIOB port
    GPIO_SetBits(GPIOB,GPIO_Pin_5);                     // PB5 initialized to high (LED off)
    
    // Configure PE5 pin (LED2)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;           // Select pin 5
    GPIO_Init(GPIOE, &amp;GPIO_InitStructure);              // Apply configuration to GPIOE port
    GPIO_SetBits(GPIOE,GPIO_Pin_5);                     // PE5 initialized to high (LED off)
    
    // Create FreeRTOS tasks
    xTaskCreate(myTask1,"myTask1",512,NULL,2,&amp;myTask1Handler);  // Create task 1
    xTaskCreate(myTask2,"myTask2",512,NULL,2,&amp;myTask2Handler);  // Create task 2
    
    // Start FreeRTOS scheduler (never returns)
    vTaskStartScheduler();  // Start kernel scheduler, begin executing tasks
    
    // After the scheduler starts, it should not reach here (safe fallback loop)
    while(1)
    {
        // If the scheduler fails to start, it will enter here
        // In actual applications, error handling mechanisms should be added
    }
}

5. What are the benefits?

In summary, the core benefits of using FreeRTOS include:

  1. Improved responsiveness: Ensures high-priority tasks are executed immediately when events occur.
  2. Optimized resource utilization: By blocking tasks, it avoids CPU spinning, ensuring the CPU is always doing useful work.
  3. Modular design: Clear functional divisions improve code readability, maintainability, and reusability.
  4. Simplified concurrency: Built-in safe communication and synchronization mechanisms reduce the complexity and error risks of manual concurrency management.
  5. Complexity management: An effective means of building large, complex embedded software systems.
  6. Robust ecosystem: Extensive support, rich resources, and community knowledge bases accelerate development and troubleshooting.
  7. Cost-effective and reliable: No licensing fees, validated by numerous commercial products, stable and reliable.
  8. Scalability: Supports adding rich middleware (such as FreeRTOS+ TCP/IP, FAT FS, AWS IoT, etc.).

6. Conclusion

Bare-metal development is suitable for simple, highly deterministic projects with extremely stringent resource requirements. It is very efficient when the scale is small and the logic is clear.

FreeRTOS is suitable for projects with increased complexity, requiring the handling of multiple independent activities (especially those involving blocking operations), and having event response time requirements. It greatly simplifies the development difficulty of such applications, improves system real-time performance, resource utilization, and code structure clarity through the task scheduling, synchronization, and communication mechanisms provided by the operating system kernel. The choice of whether to use an RTOS is a key decision in embedded system design and needs to be weighed against the specific needs and resource constraints of the project. For most modern non-trivial embedded applications, using an RTOS like FreeRTOS is often the better choice.

Leave a Comment