FreeRTOS is a real-time operating system (RTOS) designed specifically for microcontrollers and small microprocessors. It provides a reliable foundation for developing applications that require precise timing and task management on resource-constrained devices.
All the Example Codes in this Guide can be found on this repo: FreeRTOS on Xiao ESP32S3
What is FreeRTOS?FreeRTOS is a lightweight operating system that enables multitasking on microcontrollers. It allows developers to create applications that can execute multiple tasks concurrently while managing system resources efficiently. The "Free" in FreeRTOS refers to its open-source nature, available under the MIT license, making it accessible for both commercial and personal projects.
The Need for FreeRTOS in Embedded SystemsFreeRTOS addresses critical challenges in embedded system development that simple sequential programming cannot effectively handle. Here's why it's necessary:
The Problem with Traditional ProgrammingIn traditional "superloop" microcontroller programming, code runs sequentially in an infinite loop. This approach has several limitations:
- Timing Issues: When multiple tasks need different timing requirements, managing them becomes increasingly complex with delay functions.
- Responsiveness Problems: Long-running tasks block everything else. For example, if you're reading a sensor that takes 1 second, the system can't respond to other events during that time.
- Code Complexity: As projects grow, managing multiple device interactions in a single loop becomes unwieldy and error-prone.
- Resource Contention: Without proper synchronization, accessing shared resources can lead to data corruption or unpredictable behavior.
FreeRTOS solves these problems through:
1. True MultitaskingRather than one task blocking others, FreeRTOS allows multiple tasks to run concurrently. While one task waits for something (like a sensor reading), others can continue executing.
2. Deterministic TimingFreeRTOS provides mechanisms to ensure time-critical operations happen when they should. Task priorities ensure high-priority operations get CPU time when needed.
3. Simplified Program StructureInstead of complex state machines in a single loop, you can write:
- One task for handling connectivity
- Another for sensor readings
- A third for user interface elements Each task becomes simpler and more focused.
FreeRTOS provides semaphores, mutexes, and queues to safely share resources and communicate between tasks, preventing data corruption and race conditions.
5. Power EfficiencyFreeRTOS can put the processor into sleep modes when tasks are inactive, reducing power consumption—critical for battery-powered devices.
Real-World Example: IoT Sensor NodeConsider a device that:
- Reads multiple sensors
- Processes the data
- Connects to Wi-Fi
- Sends data to a server
- Manages a display
- Handles user inputs
Without an RTOS, timing becomes a nightmare—you'd need complex state machines, careful timing calculations, and would still face responsiveness issues.
With FreeRTOS, you can:
- Create separate tasks for each function
- Assign appropriate priorities
- Let the scheduler handle the timing
- Use queues to pass data between components
- Implement power-saving strategies
FreeRTOS becomes particularly necessary for:
- Time-sensitive applications: Industrial controls, medical devices, or automotive systems where precise timing is critical.
- Complex interactions: Systems managing multiple peripherals, communications, and user interfaces simultaneously.
- Resource-constrained devices: When you need to maximize efficiency on devices with limited processing power.
- Reliable operation: Applications where system stability and predictable behavior are non-negotiable.
For the Xiao ESP32-S3 specifically, FreeRTOS is valuable because the board's dual-core processor and connectivity features (Wi-Fi, Bluetooth) are perfectly complemented by an RTOS that can effectively distribute tasks across cores and manage complex communication stacks.
Common Applications- IoT Devices: Managing sensors, connectivity, and data processing concurrently.
- Industrial Automation: Handling multiple control processes with timing guarantees.
- Consumer Electronics: Managing user interfaces while performing background operations.
- Medical Devices: Ensuring reliable operation with predictable timing for critical functions.
- Automotive Systems: Managing multiple control systems with different priorities.
The Seeed Studio Xiao ESP32-S3 is a compact yet powerful development board featuring:
- ESP32-S3 dual-core processor
- 8MB PSRAM and 8MB flash memory
- Wi-Fi and Bluetooth connectivity
- USB Type-C with native USB support
- Multiple GPIO pins in a small form factor
- Install the Arduino IDE from arduino.cc
Add ESP32 board support:
- Open Arduino IDE
- Go to File > Preferences
- Add https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json to Additional Board Manager URLs
- Go to Tools > Board > Boards Manager
- Search for "esp32" and install the latest version
- Add ESP32 board support:Open Arduino IDEGo to File > PreferencesAdd
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
to Additional Board Manager URLsGo to Tools > Board > Boards ManagerSearch for "esp32" and install the latest version
Select the correct board:
- Go to Tools > Board > ESP32 Arduino
- Select "XIAO_ESP32S3"
- Select the correct board:Go to Tools > Board > ESP32 ArduinoSelect "XIAO_ESP32S3"
Install FreeRTOS library:
- FreeRTOS comes pre-installed with the ESP32 Arduino core
This example demonstrates how to create two independent tasks, each controlling an LED with different blinking patterns.
Schematic
Code
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// Define LED pins (adjust these to match your hardware setup)
#define LED1_PIN 2 // Built-in LED on ESP32-S3
#define LED2_PIN 3 // Example external LED pin
// Task handles
TaskHandle_t Task1Handle;
TaskHandle_t Task2Handle;
// Task 1: Blink LED1 every 500ms
void blinkLED1Task(void *parameter) {
pinMode(LED1_PIN, OUTPUT);
while(1) {
digitalWrite(LED1_PIN, HIGH);
vTaskDelay(500 / portTICK_PERIOD_MS);
digitalWrite(LED1_PIN, LOW);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
// Task 2: Blink LED2 every 1000ms
void blinkLED2Task(void *parameter) {
pinMode(LED2_PIN, OUTPUT);
while(1) {
digitalWrite(LED2_PIN, HIGH);
vTaskDelay(1000 / portTICK_PERIOD_MS);
digitalWrite(LED2_PIN, LOW);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("Creating tasks...");
// Create tasks
xTaskCreate(
blinkLED1Task, // Task function
"BlinkLED1", // Task name
2048, // Stack size (bytes)
NULL, // Task parameters
1, // Task priority (0-24, higher number = higher priority)
&Task1Handle // Task handle
);
xTaskCreate(
blinkLED2Task, // Task function
"BlinkLED2", // Task name
2048, // Stack size (bytes)
NULL, // Task parameters
1, // Task priority
&Task2Handle // Task handle
);
Serial.println("Tasks created successfully");
}
void loop() {
// Empty loop - tasks are handled by FreeRTOS scheduler
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
Check out the WOKWI Simulation here: WOKWI Simulation
You can create a copy of and practice with it, Like blinking the two LEDs at different rates or even adding more LEDs
Key Concepts in Example 1Includes and Definitions#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// Define LED pins (adjust these to match your hardware setup)
#define LED1_PIN 2 // Built-in LED on ESP32-S3
#define LED2_PIN 3 // Example external LED pin
- The code includes the necessary FreeRTOS header files to access task creation and management functions.
- LED pins are defined as constants, with LED1_PIN using the built-in LED on the ESP32-S3 (pin 2) and LED2_PIN representing an external LED connected to pin 3.
TaskHandle_t Task1Handle;
TaskHandle_t Task2Handle;
- These variables store references to the created tasks. They're used to identify and control the tasks if needed later (e.g., for suspending, resuming, or deleting tasks).
void blinkLED1Task(void *parameter) {
pinMode(LED1_PIN, OUTPUT);
while(1) {
digitalWrite(LED1_PIN, HIGH);
vTaskDelay(500 / portTICK_PERIOD_MS);
digitalWrite(LED1_PIN, LOW);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
- This function configures LED1_PIN as an output pin.
- It enters an infinite loop (
while(1)
) which is standard for FreeRTOS tasks that need to run continuously.
Inside the loop:
- It turns the LED on with
digitalWrite(LED1_PIN, HIGH)
- Waits for 500 milliseconds using
vTaskDelay(500 / portTICK_PERIOD_MS)
- Turns the LED off with
digitalWrite(LED1_PIN, LOW)
- Waits for another 500 milliseconds
- Inside the loop:It turns the LED on with
digitalWrite(LED1_PIN, HIGH)
Waits for 500 milliseconds usingvTaskDelay(500 / portTICK_PERIOD_MS)
Turns the LED off withdigitalWrite(LED1_PIN, LOW)
Waits for another 500 milliseconds - The
vTaskDelay
function is crucial here. It yields control back to the FreeRTOS scheduler, allowing other tasks to run during the delay. The parameterportTICK_PERIOD_MS
is a constant that converts milliseconds to FreeRTOS tick periods.
void blinkLED2Task(void *parameter) {
pinMode(LED2_PIN, OUTPUT);
while(1) {
digitalWrite(LED2_PIN, HIGH);
vTaskDelay(1000 / portTICK_PERIOD_MS);
digitalWrite(LED2_PIN, LOW);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
This function is structurally identical to Task 1, but:
- It controls LED2_PIN instead
- It uses a longer delay of 1000 milliseconds (1 second), creating a different blinking pattern
- This function is structurally identical to Task 1, but:It controls LED2_PIN insteadIt uses a longer delay of 1000 milliseconds (1 second), creating a different blinking pattern
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("Creating tasks...");
// Create tasks
xTaskCreate(
blinkLED1Task, // Task function
"BlinkLED1", // Task name
2048, // Stack size (bytes)
NULL, // Task parameters
1, // Task priority (0-24, higher number = higher priority)
&Task1Handle // Task handle
);
xTaskCreate(
blinkLED2Task, // Task function
"BlinkLED2", // Task name
2048, // Stack size (bytes)
NULL, // Task parameters
1, // Task priority
&Task2Handle // Task handle
);
Serial.println("Tasks created successfully");
}
- The setup function initializes Serial communication at 115200 baud rate and waits for 1 second.
It then creates two tasks using xTaskCreate()
, which has several parameters:
- Task Function: The function to be executed by the task (blinkLED1Task or blinkLED2Task)
- Task Name: A descriptive name for debugging purposes
- Stack Size: The amount of memory (in bytes) allocated for the task's stack (2048 bytes here)
- Task Parameters: Data passed to the task (NULL means no parameters)
- Task Priority: A number from 0-24 where higher numbers mean higher priority; both tasks have the same priority (1)
- Task Handle: A pointer to store the task's handle, used for later management
- It then creates two tasks using
xTaskCreate()
, which has several parameters:Task Function: The function to be executed by the task (blinkLED1Task or blinkLED2Task)Task Name: A descriptive name for debugging purposesStack Size: The amount of memory (in bytes) allocated for the task's stack (2048 bytes here)Task Parameters: Data passed to the task (NULL means no parameters)Task Priority: A number from 0-24 where higher numbers mean higher priority; both tasks have the same priority (1)Task Handle: A pointer to store the task's handle, used for later management
cpp
Copy
void loop() {
// Empty loop - tasks are handled by FreeRTOS scheduler
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
- The loop function is largely empty because the FreeRTOS scheduler is now in control of executing tasks.
- Including
vTaskDelay()
in the loop prevents the CPU from wasting cycles in an empty loop.
- When the ESP32-S3 starts, it runs the
setup()
function which creates the two LED blinking tasks. - The FreeRTOS scheduler takes over and starts executing both tasks concurrently.
- Task 1 runs until it hits
vTaskDelay()
, then the scheduler suspends it temporarily. - The scheduler then runs Task 2 until it hits its own
vTaskDelay()
. - As tasks enter and exit their delay states, the scheduler intelligently switches between them.
- From the user's perspective, both LEDs appear to blink independently and simultaneously.
This is a perfect example of how FreeRTOS enables multitasking on a single-core or multi-core microcontroller, allowing multiple operations to run concurrently without complex manual timing management.
Example 2: Continuous Data Collection with Internet ReconnectionThis example demonstrates collecting sensor data continuously, storing it locally when internet connection is lost, and sending it once the connection is restored.
Schematic
Code
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_wifi.h"
#include "WiFi.h"
#include <HTTPClient.h>
#include <DHT.h>
// WiFi credentials
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
// Server details
const char* serverUrl = "http://your-api-endpoint.com/data";
// DHT11 sensor configuration
#define DHTPIN D2 // DHT11 data pin (GPIO4)
#define DHTTYPE DHT22 // DHT sensor type
DHT dht(DHTPIN, DHTTYPE);
// Task handles
TaskHandle_t DataCollectionTask;
TaskHandle_t ConnectionManagerTask;
TaskHandle_t DataSenderTask;
// Structure to hold DHT22 readings
typedef struct {
float temperature;
float humidity;
unsigned long timestamp;
} SensorReading;
// Queue for passing sensor data between tasks
QueueHandle_t dataQueue;
// Storage for readings when offline
#define MAX_STORED_READINGS 100
SensorReading storedReadings[MAX_STORED_READINGS];
int storedReadingCount = 0;
bool isConnected = false;
// Mutex for protecting the stored readings array
SemaphoreHandle_t storageMutex;
// Task to collect sensor data
void dataCollectionTask(void *parameter) {
while(1) {
// Create a reading structure
SensorReading reading;
// Read temperature and humidity from DHT11
reading.temperature = dht.readTemperature();
reading.humidity = dht.readHumidity();
reading.timestamp = millis();
// Check if reading is valid
if (isnan(reading.temperature) || isnan(reading.humidity)) {
Serial.println("Failed to read from DHT sensor!");
} else {
Serial.print("Temperature: ");
Serial.print(reading.temperature);
Serial.print("°C, Humidity: ");
Serial.print(reading.humidity);
Serial.println("%");
// Try to send to queue first (for immediate processing if online)
if (xQueueSend(dataQueue, &reading, 0) != pdTRUE) {
// Queue full or not available, store locally
if (xSemaphoreTake(storageMutex, portMAX_DELAY) == pdTRUE) {
if (storedReadingCount < MAX_STORED_READINGS) {
storedReadings[storedReadingCount++] = reading;
Serial.println("Reading stored locally");
} else {
Serial.println("Local storage full!");
}
xSemaphoreGive(storageMutex);
}
}
}
// Collect data every 5 seconds
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
}
// Task to manage WiFi connection
void connectionManagerTask(void *parameter) {
while(1) {
if (WiFi.status() != WL_CONNECTED) {
isConnected = false;
Serial.println("WiFi disconnected, attempting to reconnect...");
WiFi.begin(ssid, password);
// Try for 10 seconds to connect
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
vTaskDelay(500 / portTICK_PERIOD_MS);
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("WiFi reconnected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
isConnected = true;
}
} else {
isConnected = true;
}
// Check connection every 30 seconds
vTaskDelay(30000 / portTICK_PERIOD_MS);
}
}
// Task to send data to server
void dataSenderTask(void *parameter) {
SensorReading receivedReading;
HTTPClient http;
while(1) {
if (isConnected) {
// First try to send any stored readings
if (xSemaphoreTake(storageMutex, portMAX_DELAY) == pdTRUE) {
if (storedReadingCount > 0) {
Serial.println("Sending stored readings...");
for (int i = 0; i < storedReadingCount; i++) {
// Create JSON payload with both temperature and humidity
String payload = "{\"temperature\":" + String(storedReadings[i].temperature) +
",\"humidity\":" + String(storedReadings[i].humidity) +
",\"timestamp\":" + String(storedReadings[i].timestamp) + "}";
http.begin(serverUrl);
http.addHeader("Content-Type", "application/json");
int httpResponseCode = http.POST(payload);
if (httpResponseCode > 0) {
Serial.println("Stored reading sent successfully");
} else {
Serial.println("Error sending stored reading: " + http.errorToString(httpResponseCode));
// Put unsent readings back in front of the queue
for (int j = i; j < storedReadingCount; j++) {
storedReadings[j-i] = storedReadings[j];
}
storedReadingCount -= i;
break; // Stop trying if server is not responding
}
http.end();
// Short delay between sends to not overwhelm the server
vTaskDelay(100 / portTICK_PERIOD_MS);
}
// All stored readings sent successfully
if (storedReadingCount == 0) {
Serial.println("All stored readings sent!");
}
}
xSemaphoreGive(storageMutex);
}
// Then check for new readings in the queue
if (xQueueReceive(dataQueue, &receivedReading, 0) == pdTRUE) {
// Create JSON payload with both temperature and humidity
String payload = "{\"temperature\":" + String(receivedReading.temperature) +
",\"humidity\":" + String(receivedReading.humidity) +
",\"timestamp\":" + String(receivedReading.timestamp) + "}";
http.begin(serverUrl);
http.addHeader("Content-Type", "application/json");
int httpResponseCode = http.POST(payload);
if (httpResponseCode > 0) {
Serial.println("Data sent successfully");
} else {
Serial.println("Error sending data: " + http.errorToString(httpResponseCode));
// Store the reading that failed to send
if (xSemaphoreTake(storageMutex, portMAX_DELAY) == pdTRUE) {
if (storedReadingCount < MAX_STORED_READINGS) {
storedReadings[storedReadingCount++] = receivedReading;
Serial.println("Reading stored after send failure");
}
xSemaphoreGive(storageMutex);
}
}
http.end();
}
}
// Check for new data to send every second
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(115200);
delay(1000);
// Initialize DHT11 sensor
dht.begin();
Serial.println("DHT11 Initialized");
// Initialize WiFi in station mode
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("Connecting to WiFi...");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
isConnected = true;
// Create the queue
dataQueue = xQueueCreate(10, sizeof(SensorReading));
// Create mutex
storageMutex = xSemaphoreCreateMutex();
// Create tasks
xTaskCreate(
dataCollectionTask,
"DataCollection",
4096,
NULL,
1,
&DataCollectionTask
);
xTaskCreate(
connectionManagerTask,
"ConnectionManager",
4096,
NULL,
2, // Higher priority for connection management
&ConnectionManagerTask
);
xTaskCreate(
dataSenderTask,
"DataSender",
4096,
NULL,
1,
&DataSenderTask
);
Serial.println("All tasks created successfully");
}
void loop() {
// Empty loop - FreeRTOS scheduler handles the tasks
vTaskDelay(1000 / portTICK_PERIOD_MS);
Check out the WOKWI Simulation here: WOKWI Simulation
You can create a copy of and practice with it
Key Concepts in Example 2- Task Synchronization: Using a mutex (
SemaphoreHandle_t
) to protect shared resources. - Inter-task Communication: Using a queue (
QueueHandle_t
) to pass data between tasks. - Resource Management: Managing local storage for data when connection is unavailable.
- Multiple Task Priorities: Giving connection management a higher priority than data collection and sending.
- Running: The task is currently executing.
- Ready: The task is ready to run but waiting for CPU time.
- Blocked: The task is waiting for an event (e.g., delay timeout, semaphore).
- Suspended: The task is not available for scheduling.
- Deleted: The task has been deleted but not removed from memory.
FreeRTOS provides memory allocation functions that are designed to be deterministic and avoid fragmentation:
pvPortMalloc()
: Allocate memoryvPortFree()
: Free allocated memory
- Use Static Allocation: When possible, use static allocation instead of dynamic allocation to avoid fragmentation and allocation failures.
- Choose Task Priorities Carefully: Assign higher priorities to time-critical tasks, but be mindful of priority inversion issues.
- Avoid Blocking in High-Priority Tasks: High-priority tasks should not block for long periods as they prevent lower-priority tasks from running.
- Use Appropriate Stack Sizes: Allocate enough stack space to prevent overflow but not so much that you waste RAM.
- Leverage Event-Driven Programming: Use FreeRTOS notification events instead of polling when possible.
- Consider Tick Rates: Configure the FreeRTOS tick rate appropriately for your application's timing requirements.
- Monitor CPU Usage: Use FreeRTOS's built-in statistics gathering to identify bottlenecks and optimize task performance.
If you like this guide, give it a thumbs up, follow me for more fun projects and guides, and drop a comment if you successfully build a project with FreeRTOS and XIAO, I’d love to see it!
Comments
Please log in or sign up to comment.