FreeRTOS on Xiao ESP32S3

by tech_nickk in Circuits > Microcontrollers

22 Views, 0 Favorites, 0 Comments

FreeRTOS on Xiao ESP32S3

FUKLR5ZM9484QY9.png

Introduction to FreeRTOS

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 Systems

FreeRTOS addresses critical challenges in embedded system development that simple sequential programming cannot effectively handle. Here's why it's necessary:




The Problem with Traditional Programming

In traditional "superloop" microcontroller programming, code runs sequentially in an infinite loop. This approach has several limitations:

  1. Timing Issues: When multiple tasks need different timing requirements, managing them becomes increasingly complex with delay functions.
  2. 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.
  3. Code Complexity: As projects grow, managing multiple device interactions in a single loop becomes unwieldy and error-prone.
  4. Resource Contention: Without proper synchronization, accessing shared resources can lead to data corruption or unpredictable behavior.




Benefits of Using FreeRTOS

FreeRTOS solves these problems through:




1. True Multitasking

Rather 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 Timing

FreeRTOS 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 Structure

Instead of complex state machines in a single loop, you can write:

  1. One task for handling connectivity
  2. Another for sensor readings
  3. A third for user interface elements Each task becomes simpler and more focused.




4. Resource Management

FreeRTOS provides semaphores, mutexes, and queues to safely share resources and communicate between tasks, preventing data corruption and race conditions.




5. Power Efficiency

FreeRTOS can put the processor into sleep modes when tasks are inactive, reducing power consumption—critical for battery-powered devices.




Real-World Example: IoT Sensor Node

Consider a device that:

  1. Reads multiple sensors
  2. Processes the data
  3. Connects to Wi-Fi
  4. Sends data to a server
  5. Manages a display
  6. 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:

  1. Create separate tasks for each function
  2. Assign appropriate priorities
  3. Let the scheduler handle the timing
  4. Use queues to pass data between components
  5. Implement power-saving strategies




When FreeRTOS is Essential

FreeRTOS becomes particularly necessary for:

  1. Time-sensitive applications: Industrial controls, medical devices, or automotive systems where precise timing is critical.
  2. Complex interactions: Systems managing multiple peripherals, communications, and user interfaces simultaneously.
  3. Resource-constrained devices: When you need to maximize efficiency on devices with limited processing power.
  4. 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

  1. IoT Devices: Managing sensors, connectivity, and data processing concurrently.
  2. Industrial Automation: Handling multiple control processes with timing guarantees.
  3. Consumer Electronics: Managing user interfaces while performing background operations.
  4. Medical Devices: Ensuring reliable operation with predictable timing for critical functions.
  5. Automotive Systems: Managing multiple control systems with different priorities.




Getting Started with Xiao ESP32-S3

The Seeed Studio Xiao ESP32-S3 is a compact yet powerful development board featuring:

  1. ESP32-S3 dual-core processor
  2. 8MB PSRAM and 8MB flash memory
  3. Wi-Fi and Bluetooth connectivity
  4. USB Type-C with native USB support
  5. Multiple GPIO pins in a small form factor


Supplies

  1. XIAO ESP32S3
  2. Two LEDS
  3. TWO 330 Ohms Resistors
  4. DHT 22 Sensor
  5. Jumper Wires Breadboard

Setting Up the Development Environment


  1. Install the Arduino IDE from arduino.cc

Add ESP32 board support:

  1. Open Arduino IDE
  2. Go to File > Preferences
  3. Add https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json to Additional Board Manager URLs
  4. Go to Tools > Board > Boards Manager
  5. Search for "esp32" and install the latest version
  6. 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:

  1. Go to Tools > Board > ESP32 Arduino
  2. Select "XIAO_ESP32S3"
  3. Select the correct board:Go to Tools > Board > ESP32 ArduinoSelect "XIAO_ESP32S3"

Install FreeRTOS library:

  1. FreeRTOS comes pre-installed with the ESP32 Arduino core


Example 1: Blinking Two LEDs Simultaneously

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 1




Includes 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
  1. The code includes the necessary FreeRTOS header files to access task creation and management functions.
  2. 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.




Task Handles

TaskHandle_t Task1Handle;
TaskHandle_t Task2Handle;
  1. 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).




Task 1: LED1 Blinking Function

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);
}
}
  1. This function configures LED1_PIN as an output pin.
  2. It enters an infinite loop (while(1)) which is standard for FreeRTOS tasks that need to run continuously.

Inside the loop:

  1. It turns the LED on with digitalWrite(LED1_PIN, HIGH)
  2. Waits for 500 milliseconds using vTaskDelay(500 / portTICK_PERIOD_MS)
  3. Turns the LED off with digitalWrite(LED1_PIN, LOW)
  4. Waits for another 500 milliseconds
  5. Inside the loop:It turns the LED on with digitalWrite(LED1_PIN, HIGH)
  6. Waits for 500 milliseconds using vTaskDelay(500 / portTICK_PERIOD_MS)
  7. Turns the LED off with digitalWrite(LED1_PIN, LOW)
  8. Waits for another 500 milliseconds
  9. The vTaskDelay function is crucial here. It yields control back to the FreeRTOS scheduler, allowing other tasks to run during the delay. The parameter portTICK_PERIOD_MS is a constant that converts milliseconds to FreeRTOS tick periods.




Task 2: LED2 Blinking Function

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:

  1. It controls LED2_PIN instead
  2. It uses a longer delay of 1000 milliseconds (1 second), creating a different blinking pattern
  3. 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




Setup Function

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");
}
  1. 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:

  1. Task Function: The function to be executed by the task (blinkLED1Task or blinkLED2Task)
  2. Task Name: A descriptive name for debugging purposes
  3. Stack Size: The amount of memory (in bytes) allocated for the task's stack (2048 bytes here)
  4. Task Parameters: Data passed to the task (NULL means no parameters)
  5. Task Priority: A number from 0-24 where higher numbers mean higher priority; both tasks have the same priority (1)
  6. Task Handle: A pointer to store the task's handle, used for later management
  7. 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




Loop Function

cpp

Copy

void loop() {
// Empty loop - tasks are handled by FreeRTOS scheduler
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
  1. The loop function is largely empty because the FreeRTOS scheduler is now in control of executing tasks.
  2. Including vTaskDelay() in the loop prevents the CPU from wasting cycles in an empty loop.




How the Multitasking Works:

  1. When the ESP32-S3 starts, it runs the setup() function which creates the two LED blinking tasks.
  2. The FreeRTOS scheduler takes over and starts executing both tasks concurrently.
  3. Task 1 runs until it hits vTaskDelay(), then the scheduler suspends it temporarily.
  4. The scheduler then runs Task 2 until it hits its own vTaskDelay().
  5. As tasks enter and exit their delay states, the scheduler intelligently switches between them.
  6. 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 Reconnection

This 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

  1. Task Synchronization: Using a mutex (SemaphoreHandle_t) to protect shared resources.
  2. Inter-task Communication: Using a queue (QueueHandle_t) to pass data between tasks.
  3. Resource Management: Managing local storage for data when connection is unavailable.
  4. Multiple Task Priorities: Giving connection management a higher priority than data collection and sending.


Advanced FreeRTOS Concepts

Task States in FreeRTOS

  1. Running: The task is currently executing.
  2. Ready: The task is ready to run but waiting for CPU time.
  3. Blocked: The task is waiting for an event (e.g., delay timeout, semaphore).
  4. Suspended: The task is not available for scheduling.
  5. Deleted: The task has been deleted but not removed from memory.




Memory Management

FreeRTOS provides memory allocation functions that are designed to be deterministic and avoid fragmentation:

  1. pvPortMalloc(): Allocate memory
  2. vPortFree(): Free allocated memory




Tips for Efficient FreeRTOS Applications

  1. Use Static Allocation: When possible, use static allocation instead of dynamic allocation to avoid fragmentation and allocation failures.
  2. Choose Task Priorities Carefully: Assign higher priorities to time-critical tasks, but be mindful of priority inversion issues.
  3. Avoid Blocking in High-Priority Tasks: High-priority tasks should not block for long periods as they prevent lower-priority tasks from running.
  4. Use Appropriate Stack Sizes: Allocate enough stack space to prevent overflow but not so much that you waste RAM.
  5. Leverage Event-Driven Programming: Use FreeRTOS notification events instead of polling when possible.
  6. Consider Tick Rates: Configure the FreeRTOS tick rate appropriately for your application's timing requirements.
  7. Monitor CPU Usage: Use FreeRTOS's built-in statistics gathering to identify bottlenecks and optimize task performance.


Conclusion


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!