Back to Blog
Blog/Internet of Things (IoT)

STM32 FreeRTOS Implementation: Building Multi-Tasking Firmware from Register Level

Updated: January 30, 2026

Learn to implement FreeRTOS on STM32 microcontrollers with register-level programming. Master task scheduling, memory management, and real-time patterns.

#stm32#freertos#embedded-systems#arm-cortex-m#firmware#real-time-os#register-level#iot
Advertisement

Building multi-tasking firmware on STM32 microcontrollers requires understanding both the real-time operating system fundamentals and the underlying hardware architecture. FreeRTOS has become a de facto standard for embedded systems due to its minimal footprint, predictable behavior, and extensive community support. When combined with register-level programming on STM32 devices, developers gain precise control over hardware resources while benefiting from structured task scheduling.

FreeRTOS and ARM Cortex-M Architecture

The ARM Cortex-M processor family includes specific features designed to support real-time operating systems efficiently. These features reduce the overhead of context switching and enable deterministic interrupt handling that real-time applications require. Understanding these architectural elements is essential for implementing FreeRTOS at the register level.

Advertisement

The Nested Vectored Interrupt Controller manages all exception and interrupt handling with hardware-supported priority levels. This controller supports interrupt nesting, preemption, and automatic context saving for a subset of registers. The NVIC also handles the PendSV exception, which FreeRTOS uses explicitly for context switching requests. PendSV has programmable priority and can be pended to trigger at an appropriate time without disturbing higher-priority interrupts.

ARM Cortex-M processors include a hardware stack that supports automatic stacking of registers when entering exception handlers. The processor pushes specific registers including R0-R3, R12, return address, and the program status register automatically when an exception occurs. This hardware-assisted stacking reduces the software overhead of saving processor state during context switches.

The SysTick timer, integrated into every Cortex-M core, provides a consistent time base across all devices in the family. This 24-bit countdown timer generates periodic interrupts that FreeRTOS uses as the system tick. The SysTick timer is clocked from the core clock and requires minimal configuration to generate the tick interrupt at the desired frequency.

In industrial automation systems, precise timing is critical for coordinating multiple actuators and sensors. A factory floor controller running at 72 MHz requires a 1 millisecond system tick to synchronize position updates across servo motors while maintaining communication with supervisory systems. The developer configures SysTick to generate interrupts at exactly 1000 Hz, ensuring that control loops execute with deterministic timing and that watchdog timers receive regular kick signals.

C
#include "stm32f10x.h"

void SysTick_Init(void) {
    // Reload Value = (SystemCoreClock / 1000) - 1
    // For 72MHz: (72000000 / 1000) - 1 = 71999
    SysTick->LOAD = 71999;
    
    // Clear current value
    SysTick->VAL = 0;
    
    // Configure Control and Status Register
    // Bit 2: CLKSOURCE = 1 (Processor Clock)
    // Bit 1: TICKINT = 1 (Enable Interrupt)
    // Bit 0: ENABLE = 1 (Enable Counter)
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                    SysTick_CTRL_TICKINT_Msk   |
                    SysTick_CTRL_ENABLE_Msk;
}

Execute the code with caution.

FreeRTOS Kernel Architecture on STM32

FreeRTOS implements a preemptive, priority-based scheduler with cooperative task yielding options. Tasks execute in a round-robin fashion within each priority level when time-slicing is enabled. The kernel maintains ready, blocked, suspended, and deleted task lists to track task states. At each tick interrupt, the scheduler determines if a context switch is necessary based on task priorities and time slices.

The FreeRTOS port for ARM Cortex-M is intentionally minimal because the processor architecture provides substantial RTOS support. The port layer consists primarily of the PendSV exception handler for context switching, SysTick interrupt handler for the system tick, and configuration definitions for processor-specific constants. This minimal port reduces code size and execution overhead compared to ports for architectures without hardware RTOS support.

Task control blocks contain the task state, stack pointer, priority, and other kernel-managed information. Each task maintains its own stack region that holds the task's context when not running. The stack pointer within the task control block points to the saved context. When the scheduler switches tasks, it updates the process stack pointer and restores the new task's context from its stack.

Register-Level FreeRTOS Port Implementation

Implementing FreeRTOS at the register level requires direct manipulation of Cortex-M core registers and STM32 peripheral registers. The configuration header file defines critical constants including the system tick frequency, total heap size, and processor-specific settings. These definitions control the behavior of the RTOS and must match the hardware configuration.

The SysTick timer requires configuration at the register level to generate the tick interrupt. The SysTick Reload Value Register determines the tick period based on the system clock. The SysTick Control and Status Register enables the timer, selects the clock source, and enables the tick interrupt. Writing the appropriate values to these registers configures the system tick without using library functions.

The PendSV exception handler performs the actual context switching between tasks. This handler executes at the lowest priority to ensure it runs only when no other exception or interrupt requires service. The context switch routine saves the current task's context, updates the current task control block pointer, and restores the new task's context. The handler manipulates the process stack pointer directly to switch between task stacks.

Interrupt priority configuration requires writing to the NVIC priority registers. Each interrupt has an associated priority value that determines if it can preempt other interrupts. FreeRTOS requires a specific priority configuration to ensure the RTOS interrupt handlers behave correctly. The API priority group configuration divides the priority bits between preemption and subpriority, which must match the FreeRTOS configuration.

Medical device firmware must guarantee that critical safety interrupts take absolute precedence over routine processing tasks. A portable infusion pump uses NVIC priority grouping with 4 bits for preemption priority and 0 bits for subpriority, ensuring that an emergency stop interrupt can immediately preempt any lower-priority activity including the FreeRTOS tick handler. The developer configures the Application Interrupt and Reset Control Register to set the appropriate priority group, then programs each peripheral interrupt with priority values that respect the FreeRTOS maximum syscall interrupt threshold.

C
#include <stdint.h>
#include "core_cm4.h" // CMSIS Core header for NVIC access

/**
 * @brief Configures NVIC Priority Grouping and Interrupt Priority for FreeRTOS.
 * 
 * FreeRTOS on ARM Cortex-M requires the Priority Grouping to be set to all preemption
 * priority and no sub-priority (Priority Group 4).
 * 
 * @param IRQn The interrupt request number (e.g., TIM2_IRQn).
 * @param priority The priority value to set. Valid range depends on implementation 
 *                 (usually 0-15 for 4 bits). Lower numeric value means higher priority.
 *                 For FreeRTOS API calls in ISR, this must be numerically equal to or 
 *                 greater than configMAX_SYSCALL_INTERRUPT_PRIORITY.
 */
void configure_nvic_freertos(IRQn_Type IRQn, uint32_t priority) {
    // Set Priority Grouping to 4 (0 bits for sub-priority, 4 bits for pre-emption)
    // Reference: ARM Cortex-M Technical Reference Manual & FreeRTOS Documentation
    NVIC_SetPriorityGrouping(0x03);

    // Set the interrupt priority
    NVIC_SetPriority(IRQn, priority);
}

Execute the code with caution.

Task Creation and Scheduling

Creating a task in FreeRTOS involves allocating a task control block, allocating a stack, and initializing the task context. At the register level, this process requires understanding the Cortex-M exception frame layout. The initial stack frame for a task must contain values that will be popped into registers when the task first runs. This includes the program counter pointing to the task function, the program status register with appropriate flags, and initial values for general-purpose registers.

The task scheduler maintains a ready list sorted by task priority. The highest-priority ready task always executes. When a higher-priority task becomes ready, it preempts the currently running task immediately. This preemption occurs through the PendSV exception, which the scheduler pends to request a context switch. The context switch saves the current task's context, updates the current task pointer, and loads the new task's context.

Task priorities range from zero to the maximum configured priority value, with zero typically representing the idle task priority. Higher numeric values represent higher priorities. Time-slicing within a priority level occurs when multiple tasks share the same priority and the preemption flag is enabled. The tick interrupt triggers the scheduler to check if the current time slice has expired and if another task at the same priority should run.

The idle task runs when no application task is ready. FreeRTOS creates this task automatically during kernel initialization. The idle task has the lowest priority and can optionally call a hook function where developers place low-priority background work or put the processor into sleep mode to save power.

A wearable health monitor creates multiple tasks to handle distinct responsibilities while maintaining real-time responsiveness. The sensor acquisition task runs at priority 3 and reads accelerometer and heart rate data via I2C at 100 Hz, the data processing task at priority 2 performs signal filtering and step counting algorithms, and the Bluetooth transmission task at priority 1 sends aggregated health data to a mobile device. The developer creates each task with appropriate stack sizes based on worst-case stack usage analysis and registers them with the scheduler before starting the kernel.

C
#include "FreeRTOS.h"
#include "task.h"

void vTaskFunction(void *pvParameters) {
    int paramValue = *((int *)pvParameters);
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void vCreateCustomTask() {
    int taskParam = 10;
    TaskHandle_t xHandle = NULL;

    BaseType_t xReturned = xTaskCreate(
        vTaskFunction,          /* Task entry point */
        "CustomTask",           /* Task name */
        2048,                   /* Stack size in words */
        &taskParam,             /* Parameter passed to task */
        tskIDLE_PRIORITY + 2,   /* Task priority */
        &xHandle                /* Task handle */
    );

    if (xReturned == pdPASS) {
        // Task created successfully
    }
}

Execute the code with caution.

Memory Management in FreeRTOS

FreeRTOS provides multiple heap management schemes to accommodate different application requirements and memory constraints. Heap_1 allocates memory but never frees it, suitable for static allocation patterns where tasks are created once and never deleted. Heap_2 supports allocation and deallocation but suffers from memory fragmentation, making it unsuitable for long-running applications.

Heap_3 wraps the standard C library malloc and free functions, requiring the compiler to provide these functions with appropriate reentrancy protection. This scheme leverages the standard library's memory management but may not have predictable timing behavior. Heap_4 uses a first-fit allocation algorithm with coalescing of adjacent free blocks, reducing fragmentation over time. Heap_5 allows the heap to span multiple non-contiguous memory regions, useful when the application has scattered RAM areas.

At the register level, understanding the STM32 memory map is essential for configuring the heap correctly. Different STM32 series have different RAM sizes and layouts. The linker script must reserve the appropriate region for the FreeRTOS heap. The heap size configuration in FreeRTOSConfig.h must match the reserved region size to prevent overflow into other data sections.

Battery-powered IoT devices require careful memory layout to maximize available RAM for application buffers and FreeRTOS heap. An environmental sensor node based on STM32L4 with 64KB SRAM needs to reserve 32KB for the FreeRTOS heap while allocating remaining memory for network buffers, sensor data structures, and application state. The developer modifies the GNU ARM linker script to define a dedicated ucHeap section placed at a specific memory region, then configures FreeRTOSConfig.h to use this exact memory address and size for heap_5 allocator.

LD
MEMORY {
  RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS {
  .heap (NOLOAD) : {
    . = ALIGN(8);
    __HeapBase = .;
    __HeapLimit = ORIGIN(RAM) + LENGTH(RAM);
    *(.heap)
    . = ALIGN(8);
  } > RAM
}

Execute the code with caution.

Memory protection units available on some Cortex-M4 and Cortex-M7 devices provide hardware isolation between memory regions. FreeRTOS can optionally integrate with the MPU to enforce task memory isolation and protect critical kernel data from accidental corruption. This integration requires configuring MPU regions through the memory protection unit registers.

Safety-critical automotive firmware often requires memory protection to prevent untrusted tasks from corrupting critical system data. An engine control unit uses the Cortex-M4 MPU to isolate the FreeRTOS kernel data structures from application tasks, configure read-only access to calibration parameters stored in flash, and restrict each task's access to only its own stack and designated RAM regions. The developer programs MPU region registers with base addresses, sizes, and access permissions during system initialization, then enables the MPU before starting the scheduler.

C
#include "FreeRTOS.h"
#include "task.h"

/* Task function to be protected by MPU */
void vProtectedTask(void *pvParameters) {
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

/* Function to configure MPU regions and create the task */
void vConfigureMPUTask(void) {
    TaskHandle_t xHandle;
    /* Allocate stack statically to ensure address alignment and persistence */
    static uint8_t ucTaskStack[configMINIMAL_STACK_SIZE];
    
    /* Structure to define MPU regions */
    /* configNUM_MPU_REGIONS is typically defined in FreeRTOSConfig.h */
    xMemoryRegion xRegions[ configNUM_MPU_REGIONS ];

    /* Initialize unused regions to prevent accidental access */
    for(int i = 0; i < configNUM_MPU_REGIONS; i++) {
        xRegions[i].pvBaseAddress = NULL;
        xRegions[i].ulLengthInBytes = 0;
        xRegions[i].ulParameters = 0;
    }

    /* 1. Configure Task Stack Region (Read/Write, Privileged Access usually required for context switching) */
    xRegions[0].pvBaseAddress = ucTaskStack;
    xRegions[0].ulLengthInBytes = sizeof(ucTaskStack);
    xRegions[0].ulParameters = portMPU_REGION_READ_WRITE;

    /* 2. Configure Flash/Code Region (Read Only) */
    xRegions[1].pvBaseAddress = (void *) 0x00000000; /* Example Flash Start Address */
    xRegions[1].ulLengthInBytes = 0x40000;            /* Example Size (256KB) */
    xRegions[1].ulParameters = portMPU_REGION_READ_ONLY;

    /* 3. Configure Peripherals Region (Read/Write) */
    xRegions[2].pvBaseAddress = (void *) 0x40000000; /* Example Peripheral Base */
    xRegions[2].ulLengthInBytes = 0x20000;            /* Example Size */
    xRegions[2].ulParameters = portMPU_REGION_READ_WRITE;

    /* Create the task with restricted access (MPU enabled) */
    BaseType_t xReturned = xTaskCreateRestricted(
        vProtectedTask,         /* Task entry point */
        "MPUTask",              /* Task name */
        configMINIMAL_STACK_SIZE, /* Stack size */
        NULL,                   /* Parameters */
        tskIDLE_PRIORITY + 1,   /* Priority */
        xRegions,               /* MPU Regions configuration */
        &xHandle                /* Task Handle */
    );

    if(xReturned != pdPASS) {
        /* Handle task creation failure */
        for(;;);
    }
}

Execute the code with caution.

Synchronization and Inter-Task Communication

FreeRTOS provides multiple mechanisms for task synchronization and data exchange. Queues allow safe passing of data between tasks and between tasks and interrupts. A queue holds a fixed number of items of a specified size. Tasks can write to and read from queues with optional blocking timeouts. When a task attempts to read from an empty queue or write to a full queue, the task enters the blocked state until space or data becomes available or the timeout expires.

Semaphores provide resource counting and event signaling. Binary semaphores signal events between tasks or between interrupts and tasks. A task waiting on a binary semaphore blocks until another task or interrupt gives the semaphore. Counting semaphores track the availability of multiple identical resources. Each successful take operation decrements the count, and each give operation increments it.

Mutexes provide mutual exclusion for protecting shared resources. Unlike semaphores, mutexes include priority inheritance to prevent priority inversion. When a high-priority task attempts to take a mutex held by a low-priority task, the kernel temporarily boosts the low-priority task's priority to its own priority. This ensures the low-priority task releases the mutex promptly, avoiding indefinite blocking of the high-priority task.

Task notifications provide a lightweight alternative to queues and semaphores for simple signaling scenarios. Each task has a 32-bit notification value that another task or interrupt can set. Tasks can wait for notifications with optional timeout. Notifications are faster and use less memory than other communication primitives but offer less functionality.

Smart metering infrastructure uses queues to buffer measurement data between collection and transmission tasks. An electricity meter samples voltage and current at 4 kHz in an interrupt-driven routine, sends sample packets to a processing task via a high-priority queue, and the processing task aggregates readings into 15-second intervals before passing them to a communication task via a second queue. The developer creates queues with sufficient depth to handle worst-case burst scenarios and uses blocking operations to efficiently synchronize producer-consumer timing.

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

QueueHandle_t xQueue;

void vSenderTask(void *pvParameters) {
    int lValueToSend = (int)pvParameters;
    for (;;) {
        xQueueSend(xQueue, &lValueToSend, 0);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void vReceiverTask(void *pvParameters) {
    int lReceivedValue;
    for (;;) {
        if (xQueueReceive(xQueue, &lReceivedValue, pdMS_TO_TICKS(200)) == pdPASS) {
            // Data processed here
        }
    }
}

int main(void) {
    xQueue = xQueueCreate(5, sizeof(int));
    if (xQueue != NULL) {
        xTaskCreate(vSenderTask, "Sender", 1000, (void*)100, 1, NULL);
        xTaskCreate(vReceiverTask, "Receiver", 1000, NULL, 2, NULL);
        vTaskStartScheduler();
    }
    for (;;);
    return 0;
}

Execute the code with caution.

Interrupt Integration with FreeRTOS

Proper interrupt configuration is critical for FreeRTOS applications. The RTOS kernel uses interrupts for the system tick and for interrupt-driven inter-task communication. Interrupt service routines must follow specific conventions to interact correctly with FreeRTOS.

The FreeRTOS API provides special functions designed for use in interrupt context. These functions have the "FromISR" suffix and disable the scheduler while executing. Standard FreeRTOS API functions must not be called from interrupt service routines because they assume they are running in task context.

Interrupt priority configuration determines the interaction between interrupts and FreeRTOS. The RTOS requires that interrupts that use FreeRTOS API functions have a priority lower than the maximum system call priority. This configuration ensures that critical RTOS operations cannot be preempted by interrupts that call RTOS functions. The configMAX_SYSCALL_INTERRUPT_PRIORITY constant defines this threshold.

Nesting interrupts requires careful priority assignment. Higher-priority interrupts can preempt lower-priority interrupt handlers, including the PendSV exception handler used for context switching. FreeRTOS disables interrupts briefly during critical sections to protect shared data structures. Understanding the interaction between interrupt priorities and critical sections is essential for real-time performance.

Industrial PLC communication modules process incoming protocol frames via UART interrupts and must safely transfer data to FreeRTOS tasks without blocking the interrupt handler. A Modbus RTU slave device receives variable-length messages at 115200 baud, extracts complete frames in the UART interrupt handler, and uses queue send from ISR functions to pass frames to a protocol parser task. The developer configures the UART interrupt priority below the configMAX_SYSCALL_INTERRUPT_PRIORITY threshold and uses the BaseType_t parameter to determine if a context switch is needed.

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

/* The queue used to hold received characters. */
extern QueueHandle_t xUARTQueue;

/* UART Interrupt Service Routine. */
void UART_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    char cReceivedChar;
    uint32_t ulStatus;

    /* Read the interrupt status from the hardware registers. */
    ulStatus = UART->ISR;

    /* Check if the Receive Not Empty interrupt flag is set. */
    if ((ulStatus & UART_FLAG_RXNE) != 0) {
        /* Read the received byte. This usually clears the interrupt flag. */
        cReceivedChar = (char)(UART->RDR);

        /*
         * Post the character onto the queue using the FromISR API.
         * Ref: https://www.freertos.org/xQueueSendToBackFromISR.html
         */
        if (xQueueSendToBackFromISR(xUARTQueue, &cReceivedChar, &xHigherPriorityTaskWoken) != pdPASS) {
            /* The queue was full - optional error handling here. */
        }
    }

    /*
     * If sending to the queue caused a task to unblock, and the unblocked task
     * has a priority higher than the currently running task, then
     * xHigherPriorityTaskWoken will have been set to pdTRUE internally within
     * xQueueSendToBackFromISR().
     */
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

Execute the code with caution.

Advanced FreeRTOS Patterns

Software timers enable deferred processing with timer-based scheduling. The timer service task manages all software timers and executes their callback functions when timers expire. Software timers provide a convenient way to perform periodic or one-shot actions without creating dedicated tasks. The timer service task runs at a configurable priority that determines the relative priority of timer callbacks.

Event groups allow multiple tasks to synchronize based on multiple event bits. Each event group contains a set of bits that tasks can set or clear. Tasks can wait for any combination of bits using conjunction or disjunction logic. Event groups are efficient for tasks that need to wait on multiple conditions before proceeding.

Stream buffers and message buffers provide efficient data transfer mechanisms for streams of data. Stream buffers are optimized for single producer, single consumer scenarios with byte-oriented data. Message buffers add variable-length message framing to stream buffers. These primitives are efficient for sending continuous data streams between tasks or from interrupts to tasks.

Low-power modes integrate with FreeRTOS through tickless idle functionality. When the idle task runs and no task is ready before the next tick, the RTOS can enter a low-power mode and reconfigure the tick interrupt to wake the processor at the appropriate time. This capability significantly reduces power consumption in battery-powered applications.

Building automation controllers use software timers to manage periodic status indicators and maintenance routines without dedicating tasks to these functions. An HVAC controller creates a one-shot timer for a compressor cooldown delay after shutoff, a periodic timer for blinking the status LED every 500 milliseconds to indicate normal operation, and another periodic timer for scheduling filter maintenance reminders every 30 days. The developer creates the timer service task with appropriate priority and defines callback functions that interact safely with other RTOS objects.

C
#include "FreeRTOS.h"
#include "timers.h"

/* Timer handle declaration */
TimerHandle_t xAutoReloadTimer;
TimerHandle_t xOneShotTimer;

/* Callback function prototypes */
void vAutoReloadTimerCallback( TimerHandle_t xTimer );
void vOneShotTimerCallback( TimerHandle_t xTimer );

void vCreateAndConfigureTimers( void ) {
    /* Create an auto-reload timer that expires every 1000ms */
    xAutoReloadTimer = xTimerCreate(
        "AutoReload",           /* Text name for debugging (not used by kernel) */
        pdMS_TO_TICKS( 1000 ),  /* Timer period in ticks */
        pdTRUE,                 /* Auto-reload is set to pdTRUE */
        ( void * ) 0,           /* Timer ID is not used */
        vAutoReloadTimerCallback/* The callback function */
    );

    /* Create a one-shot timer that expires once after 2000ms */
    xOneShotTimer = xTimerCreate(
        "OneShot",
        pdMS_TO_TICKS( 2000 ),
        pdFALSE,                /* Auto-reload is set to pdFALSE for one-shot */
        ( void * ) 0,
        vOneShotTimerCallback
    );

    /* Check if timers were created successfully */
    if( ( xAutoReloadTimer != NULL ) && ( xOneShotTimer != NULL ) ) {
        /* Start the timers. The block time is set to 0 to avoid blocking 
         * if the timer command queue is full. */
        xTimerStart( xAutoReloadTimer, 0 );
        xTimerStart( xOneShotTimer, 0 );
    }
}

/* Auto-reload timer callback implementation */
void vAutoReloadTimerCallback( TimerHandle_t xTimer ) {
    /* Code to execute periodically */
}

/* One-shot timer callback implementation */
void vOneShotTimerCallback( TimerHandle_t xTimer ) {
    /* Code to execute once */
}

Execute the code with caution.

Wireless sensor nodes deployed in remote locations must maximize battery life through aggressive low-power operation. An agricultural soil moisture monitor spends most of its time in deep sleep mode, waking only every 15 minutes to take measurements and transmit data via LoRaWAN. The developer implements the FreeRTOS tickless idle mode by configuring the low-power timer as an external wake source, calculating the maximum sleep duration based on the next scheduled task or timer, and programming the STM32 stop mode with appropriate wakeup sources while preserving volatile application state.

C
/**
 * Configure tickless idle mode for STM32 low-power sleep operation
 * Based on STMicroelectronics Low Power API and FreeRTOS Tickless Idle
 * Reference: https://www.st.com/resource/en/application_note/dm00282246-stm32-microcontroller-lowpower-mode-consequences-on-peripheral-operation-stmicroelectronics.pdf
 */

#include "stm32xxxx_hal.h"
#include "FreeRTOS.h"
#include "task.h"

/* Low power timer handle */
LPTIM_HandleTypeDef hlptim1;

/**
 * @brief Initialize Low Power Timer for tickless idle
 */
void LPTIM_Init(void)
{
    hlptim1.Instance = LPTIM1;
    hlptim1.Init.Clock.Source = LPTIM_CLOCKSOURCE_APBCLOCK_LPOSC;
    hlptim1.Init.Clock.Prescaler = LPTIM_PRESCALER_DIV1;
    hlptim1.Init.UltraLowPowerClock.Polarity = LPTIM_CLOCKPOLARITY_RISING;
    hlptim1.Init.UltraLowPowerClock.SampleTime = LPTIM_CLOCKSAMPLETIME_DIRECTTRANSITION;
    hlptim1.Init.Trigger.Source = LPTIM_TRIGSOURCE_SOFTWARE;
    hlptim1.Init.OutputPolarity = LPTIM_OUTPUTPOLARITY_HIGH;
    hlptim1.Init.UpdateMode = LPTIM_UPDATE_IMMEDIATE;
    hlptim1.Init.CounterSource = LPTIM_COUNTERSOURCE_INTERNAL;
    
    HAL_LPTIM_Init(&hlptim1);
}

/**
 * @brief Enter tickless idle sleep mode
 * @param xExpectedIdleTime Expected idle time in ticks
 */
void vPortSuppressTicksAndSleep(TickType_t xExpectedIdleTime)
{
    eSleepModeStatus eSleepStatus;
    uint32_t ulReloadValue, ulCompleteTickPeriods;
    
    /* Enter critical section to prevent context switch */
    taskENTER_CRITICAL();
    
    /* Calculate reload value for low-power timer */
    ulReloadValue = (uint32_t)(xExpectedIdleTime * configTICK_RATE_HZ / 1000);
    
    /* Check if there are tasks ready to run */
    eSleepStatus = eTaskConfirmSleepModeStatus();
    
    if (eSleepStatus == eAbortSleep)
    {
        taskEXIT_CRITICAL();
    }
    else
    {
        /* Configure LPTIM for the next tick */
        __HAL_LPTIM_DISABLE(&hlptim1);
        __HAL_LPTIM_CLEAR_FLAG(&hlptim1, LPTIM_FLAG_CMPM);
        __HAL_LPTIM_SET_COMPARE(&hlptim1, ulReloadValue);
        
        /* Start LPTIM in interrupt mode */
        HAL_LPTIM_Start_IT(&hlptim1);
        
        /* Enter low-power mode - STOP2 for lower power consumption */
        taskEXIT_CRITICAL();
        HAL_PWR_EnterSTOP2Mode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
        
        /* Wake up - reconfigure system clock */
        SystemClock_Config();
        
        taskENTER_CRITICAL();
        
        /* Disable low-power timer */
        HAL_LPTIM_Stop_IT(&hlptim1);
        
        /* Calculate complete tick periods */
        ulCompleteTickPeriods = __HAL_LPTIM_GET_COUNTER(&hlptim1);
        
        /* Adjust the system tick */
        vTaskStepTick(ulCompleteTickPeriods);
        
        taskEXIT_CRITICAL();
    }
}

/**
 * @brief Low Power Timer interrupt callback
 */
void HAL_LPTIM_CompareMatchCallback(LPTIM_HandleTypeDef *hlptim)
{
    /* Tick interrupt handler - wake up the MCU */
}

Execute the code with caution.

Industry-Level Project Examples

Multi-Sensor Data Logger

A multi-sensor data logger demonstrates FreeRTOS basics with practical application requirements. The system reads temperature, humidity, and pressure sensors at different rates, buffers data, and periodically writes to non-volatile storage. This project uses separate tasks for sensor reading, data processing, and storage operations, demonstrating real-world task separation and priority management.

The sensor reading task runs at the highest priority to ensure data is captured at the required sampling rates. This task communicates via queues with the data processing task, which performs calculations and formatting. The storage task runs at lower priority and writes processed data to external flash or an SD card. A watchdog timer task monitors system health and triggers recovery if tasks fail to report status.

This project requires configuring multiple UART or SPI peripherals for sensor communication, DMA for efficient data transfer, and proper interrupt priority management. The FreeRTOS queue mechanism buffers sensor data between tasks, accommodating variations in processing time. Memory management using heap_4 prevents fragmentation over long-term operation.

Industrial Motor Controller

An industrial motor controller showcases advanced FreeRTOS patterns for real-time control applications. The system controls motor speed and position using feedback from encoders, manages safety interlocks, implements communication protocols for external commands, and performs diagnostic monitoring. This application requires deterministic timing and strict priority assignments.

The motor control task runs at the highest priority and executes a control loop with precise timing requirements. This task uses FreeRTOS stream buffers to receive encoder data from high-speed interrupt routines and send motor commands to PWM generation hardware. The communication task handles Modbus or CAN bus messages at a lower priority, decoding commands and updating shared parameters protected by mutexes.

Safety monitoring runs at a dedicated priority and checks various fault conditions. The safety task can immediately stop the motor if critical faults are detected, overriding normal operation. A diagnostic task periodically reads status information and transmits it to a supervisory system. This architecture demonstrates how FreeRTOS manages multiple concurrent real-time requirements in safety-critical applications.

IoT Edge Gateway

An IoT edge gateway demonstrates complex FreeRTOS integration with multiple communication protocols and edge processing capabilities. The gateway connects to local sensors via various interfaces, processes data locally, and transmits summarized information to cloud services. This project combines network protocols, data serialization, and edge analytics.

The data collection task manages multiple sensor interfaces using UART, SPI, and I2C. This task buffers raw data and passes it to the processing task via queues. The processing task applies filtering, aggregation, and feature extraction to reduce data volume. Results are sent to the cloud communication task, which manages MQTT or HTTP connections to cloud services.

FreeRTOS event groups coordinate startup sequencing and reconnection logic. The gateway waits for network availability before initiating cloud connections. Stream buffers efficiently transfer data between the network stack and application tasks. The RTOS tickless idle mode reduces power consumption when the gateway operates on battery backup during power outages.

Debugging and Optimization

Debugging FreeRTOS applications requires specialized tools to visualize task behavior and timing. Segger SystemView and Tracealyzer provide tracing capabilities that record kernel events, task switches, and interrupt activity. These tools help identify priority inversion problems, missed deadlines, and unexpected blocking behavior.

Analyzing stack usage is critical for ensuring task stacks are sized appropriately. FreeRTOS provides a stack high-water mark that tracks the minimum remaining stack space. Developers should periodically check this value during development and adjust stack sizes to provide adequate headroom. Stack overflow debugging is enabled through configuration options and causes the RTOS to trigger a breakpoint when overflow is detected.

Measuring CPU utilization helps optimize task scheduling and identify inefficiencies. FreeRTOS includes a runtime statistics feature that tracks task execution time. This data reveals which tasks consume the most CPU time and whether tasks are ready for execution but not receiving CPU cycles. Optimizing task execution time or adjusting priorities can improve overall system responsiveness.

Interrupt latency measurements validate that the system meets real-time timing requirements. The latency from an interrupt request to the execution of the first instruction in the interrupt handler depends on interrupt priority, critical section duration, and processor clock frequency. FreeRTOS critical sections should be kept short to minimize interrupt latency.

Sources

  1. FreeRTOS Official Documentation - https://www.freertos.org/Documentation/RTOS_book.html
  2. STMicroelectronics STM32 Reference Manuals - https://www.st.com/en/microcontrollers-microprocessors/stm32-32-bit-arm-cortex-mcus.html
  3. ARM Developer Cortex-M Processor Documentation - https://developer.arm.com/documentation/
  4. ARM Cortex-M Technical Reference Manual - https://developer.arm.com/documentation/dui0553/latest/
  5. FreeRTOS Porting Guide - https://www.freertos.org/RTOS-Cortex-M3-M4.html
  6. AWS FreeRTOS Kernel Quick Start Guide - https://docs.aws.amazon.com/freertos/
  7. GNU Arm Embedded Toolchain - https://developer.arm.com/downloads/-/gnu-rm
  8. Segger SystemView - https://www.segger.com/products/development-tools/systemview/
  9. STMicroelectronics STM32CubeHAL Reference - https://www.st.com/resource/en/user_manual/dm00105879.pdf
  10. Embedded Artistry - https://embeddedartistry.com/blog/
Advertisement

Related Articles

Thanks for reading! Share this article with someone who might find it helpful.