Electronics - FreeRTOS - Scheduler, task and queue on STM32F3Discovery

When it comes to divide the time to share tasks, a simple microcontroller needs sometimes a friend.

This friend is called FreeRTOS and will help our dear STM32 to dispatch tasks as if each one was written for the CPU entirely.

Let's see this.

Shopping list

  • Logic Analyzer for I2C and UART bus communication

 

First of all

We are going to run three concurrent tasks on the STM32F3Discovery using FreeRTOS.

Each task has a dedicated role, a dedicated priority, and communicates with the others through a queue.

The goal is to understand how a real-time operating system structures a firmware that would otherwise be a single monolithic loop.
 

The problem

In a classic scheme, the main loop read the sensor, formatted the output, and drove the LEDs sequentially.

This works, but it has a fundamental limitation: Everything blocks everything else.

If the UART transmission is slow, the sensor read is delayed.

If you add a third operation, the timing of the first two degrades.
 

The solution

FreeRTOS solves this by giving each concern its own execution context.

The scheduler decides which task runs at any given moment based on priority.

 

The scheduler

In FreeRTOS, the scheduler is preemptive.

This means it can interrupt a running task at any tick interrupt and switch to a higher-priority task that just became ready.

With a tick rate of 1000 Hz, this decision happens every millisecond.

The three tasks in this project have different priorities.

When sensor_task wakes up from its 100ms sleep, the scheduler immediately preempts whatever was running and gives the CPU to sensor_task because it has the highest priority.

Once sensor_task sends its data to the queue and goes back to sleep, the scheduler picks the next highest-priority ready task.

Simple and efficient.

 

The queue

The queue is the backbone of this design.

It decouples sensor_task from display_task.

The sensor task does not need to know anything about UART formatting.

The display task does not need to know anything about I2C.

They share only the data structure that travels through the queue.

The kernel copies the data into the queue on send and copies it out on receive.

So in our example, there are no shared pointers, no global variables, no need for a mutex to protect the data in transit.

This is what makes queues the safest inter-task communication mechanism in FreeRTOS.

When display_task calls xQueueReceive with portMAX_DELAY, it blocks indefinitely.

The CPU is immediately given to another task.

No cycles are wasted polling.

When sensor_task sends data, the scheduler wakes display_task up at the next opportunity.

 

Task priorities and preemption in practice

led_task runs at the lowest priority.

It only gets CPU time when both sensor_task and display_task are blocked.

You can observe this directly on the board: The LED rotation pauses briefly every 100ms when sensor_task wakes up and takes the CPU.

This is how FreeRTOS preemption works and what we made visible with LEDs.

There is no cooperative yield in this code (tasks don't need to explicitly yield the CPU with taskYIELD).

The scheduler enforces the priority ordering at every tick and whenever a higher priority task becomes ready.

 

The FreeRTOS heap

Every task stack, every queue buffer, and every internal kernel object is allocated from the FreeRTOS heap at creation time.

We use heap_4, which supports both allocation and deallocation but also merge adjacent free blocks to reduce fragmentation.

The total heap size is configured in FreeRTOSConfig.h.

On the STM32F303VCT6 with 40 KB of SRAM, we reserve 10 KB for FreeRTOS.

If the heap is exhausted during a xTaskCreate or xQueueCreate call, vApplicationMallocFailedHook is called and the error LED lights up.

This is why checking return values at startup matters.

 

Reading signals

Link the following GPIO wires for your logic analyzer:

  • I2C:
    • PB6: SCL
    • PB7: SDA
  • UART
    • PA9: TX

 

Compiling, flashing, analyzing

$ cd firmware/stm32f3discovery/

$ chmod +x run.sh

$ ./run.sh all

If everything went well, you should see all LEDs rotate in clockwise and retrieving signals for I2C and UART on the logic analyzer software.
 

 

Conclusion

With this exercise, the firmware has crossed a significant threshold.

The code is no longer a sequential loop where every operation waits for the previous one to finish.

Three independent concerns now run concurrently, each at its own pace, each unaware of the others.

The sensor reads at 100ms, the display reacts to data as it arrives, and the LEDs rotate freely in the background.

None of them block each other.

This is the foundation every embedded system is built on.

From here, adding new features means adding new tasks, not rewriting existing ones.

Good job, you did it smiley

 

Add new comment

Plain text

  • No HTML tags allowed.
  • Lines and paragraphs break automatically.