If you have ever tried to make your Arduino do two things at the same time — say, read a sensor while also updating a display and responding to button presses — you have run into the fundamental limitation of the standard loop() approach. The solution used by professional embedded engineers worldwide is a Real-Time Operating System (RTOS), and the most popular free option for Arduino is FreeRTOS.
This tutorial walks you through everything you need to know to get FreeRTOS running on your Arduino, create multiple tasks, share data safely between them, and build genuinely responsive embedded systems — all without spending a rupee on additional hardware beyond your existing board.
What Is an RTOS and Why Do You Need One?
A Real-Time Operating System (RTOS) is a lightweight operating system designed to execute tasks within guaranteed time constraints. Unlike a full OS such as Linux, an RTOS has an extremely small footprint — FreeRTOS can run in as little as 6–12 KB of Flash and under 1 KB of RAM — making it perfect for microcontrollers like the ATmega328P (Arduino Uno) or ATmega2560 (Arduino Mega).
The standard Arduino loop() is fundamentally cooperative single-threaded execution. Every line of code blocks everything else. If you call delay(1000), your entire program freezes for a second. This is fine for simple projects, but breaks down the moment you need:
- Concurrent sensor reading and display updates
- Tight timing for motor control alongside serial communication
- Responsive UI while running background data logging
- Network communication (Ethernet/Wi-Fi) with local I/O
FreeRTOS solves this by providing preemptive multitasking: the scheduler switches between tasks so rapidly (every 1 ms by default on Arduino) that all tasks appear to run simultaneously. Each task gets its own stack, priority, and execution context.
Installing FreeRTOS on Arduino
Getting FreeRTOS onto your Arduino takes about two minutes:
- Open the Arduino IDE and go to Sketch → Include Library → Manage Libraries
- Search for “FreeRTOS” — look for the library by Richard Barry / Real Time Engineers Ltd
- Click Install
Alternatively, install via the Arduino CLI:
arduino-cli lib install "FreeRTOS"
The library supports the following Arduino boards out of the box:
- Arduino Uno / Nano / Mini — ATmega328P, 32 KB Flash, 2 KB RAM (tight but workable)
- Arduino Mega 2560 — ATmega2560, 256 KB Flash, 8 KB RAM (recommended for serious projects)
- Arduino Leonardo — ATmega32U4, USB HID support
- Arduino Nano Every — ATmega4809, more RAM than Uno
After installation, include the library at the top of your sketch:
#include <Arduino_FreeRTOS.h>
#include <semphr.h> // for semaphores
#include <queue.h> // for queues
Creating Your First FreeRTOS Tasks
The most important FreeRTOS concept is the task. A task is simply a C function that runs in an infinite loop. Here is the minimal structure:
void TaskBlink(void *pvParameters) {
// one-time setup for this task
pinMode(LED_BUILTIN, OUTPUT);
for (;;) { // infinite loop — never return!
digitalWrite(LED_BUILTIN, HIGH);
vTaskDelay(500 / portTICK_PERIOD_MS); // non-blocking delay
digitalWrite(LED_BUILTIN, LOW);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
The critical difference from regular code: vTaskDelay() suspends only this task and lets other tasks run during the delay. It is non-blocking from the system’s perspective.
Create and start tasks in setup():
void setup() {
Serial.begin(9600);
xTaskCreate(
TaskBlink, // function name
"Blink", // task name (for debugging)
128, // stack size in words (not bytes)
NULL, // parameters to pass
1, // priority (1 = lowest)
NULL // task handle (optional)
);
xTaskCreate(
TaskSerial,
"Serial",
128,
NULL,
1,
NULL
);
// vTaskStartScheduler() is called automatically by the library
}
void loop() {
// Leave empty — FreeRTOS takes over
}
Task Priorities and the FreeRTOS Scheduler
FreeRTOS uses a preemptive priority-based scheduler. Every tick (1 ms on Arduino by default), the scheduler checks whether a higher-priority task is ready to run and switches to it if so.
Priority rules:
- Higher number = higher priority (opposite of what many expect)
- Tasks of equal priority share time in a round-robin fashion
- A high-priority task that is blocked (waiting) does not starve lower-priority tasks
- Priority 0 is reserved for the idle task (runs only when nothing else can)
Practical priority assignment example:
// Priority 3: safety-critical motor control
xTaskCreate(TaskMotorControl, "Motor", 256, NULL, 3, NULL);
// Priority 2: sensor reading (needs to be timely)
xTaskCreate(TaskSensorRead, "Sensor", 256, NULL, 2, NULL);
// Priority 1: display update (visual, can lag slightly)
xTaskCreate(TaskDisplay, "Display", 128, NULL, 1, NULL);
// Priority 1: serial logging (lowest urgency)
xTaskCreate(TaskLogging, "Log", 128, NULL, 1, NULL);
Use taskYIELD() inside a task to voluntarily give up the CPU to another task of equal priority without blocking.
Semaphores and Mutexes: Sharing Resources Safely
When two tasks share a resource — a global variable, a peripheral like SPI or I2C, a serial port — you must protect access. Without synchronisation, you get race conditions: corrupted data, garbled output, and hard-to-debug crashes.
Mutex (Mutual Exclusion Semaphore) — ensures only one task accesses a shared resource at a time:
SemaphoreHandle_t xI2CMutex;
void setup() {
xI2CMutex = xSemaphoreCreateMutex();
// create tasks...
}
void TaskSensor1(void *pvParameters) {
for (;;) {
if (xSemaphoreTake(xI2CMutex, portMAX_DELAY) == pdTRUE) {
// Safe to use I2C here
float temp = bmp280.readTemperature();
xSemaphoreGive(xI2CMutex); // Release!
}
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
Binary Semaphore — used for task synchronisation (signalling an event):
SemaphoreHandle_t xButtonSemaphore;
void setup() {
xButtonSemaphore = xSemaphoreCreateBinary();
attachInterrupt(digitalPinToInterrupt(2), buttonISR, FALLING);
}
void buttonISR() {
// From ISR context — use FromISR variant!
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xButtonSemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
void TaskHandleButton(void *pvParameters) {
for (;;) {
if (xSemaphoreTake(xButtonSemaphore, portMAX_DELAY) == pdTRUE) {
// Button was pressed — handle it
Serial.println("Button pressed!");
}
}
}
Queues: Passing Data Between Tasks
Queues are the standard FreeRTOS mechanism for passing data between tasks safely. Think of a queue as a thread-safe FIFO buffer.
QueueHandle_t xSensorQueue;
void setup() {
// Create a queue holding 10 float values
xSensorQueue = xQueueCreate(10, sizeof(float));
xTaskCreate(TaskReadSensor, "SensorRead", 256, NULL, 2, NULL);
xTaskCreate(TaskProcessData, "Process", 256, NULL, 1, NULL);
}
void TaskReadSensor(void *pvParameters) {
for (;;) {
float temperature = analogRead(A0) * 0.48828; // example
xQueueSend(xSensorQueue, &temperature, portMAX_DELAY);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
void TaskProcessData(void *pvParameters) {
float receivedTemp;
for (;;) {
if (xQueueReceive(xSensorQueue, &receivedTemp, portMAX_DELAY) == pdTRUE) {
Serial.print("Temp: ");
Serial.println(receivedTemp);
}
}
}
Queues can hold any data type, including structs. For complex sensor data, define a struct:
typedef struct {
float temperature;
float humidity;
uint32_t timestamp;
} SensorData_t;
QueueHandle_t xDataQueue = xQueueCreate(5, sizeof(SensorData_t));
Practical Project: Multitasking Weather Station
Let us put it all together with a real project: a weather station that simultaneously reads temperature/humidity, logs data over serial, and displays results — all three happening independently.
Hardware needed:
- Arduino Mega 2560 (recommended for RAM headroom)
- DHT11 or DHT20 sensor
- 16×2 LCD or TFT display
- Push button for display mode toggle
#include <Arduino_FreeRTOS.h>
#include <queue.h>
#include <semphr.h>
#include <DHT.h>
#define DHTPIN 4
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
typedef struct {
float temp;
float humidity;
} WeatherData_t;
QueueHandle_t xWeatherQueue;
SemaphoreHandle_t xSerialMutex;
void TaskReadDHT(void *pvParameters) {
WeatherData_t data;
for (;;) {
data.temp = dht.readTemperature();
data.humidity = dht.readHumidity();
if (!isnan(data.temp)) {
xQueueSend(xWeatherQueue, &data, 0); // don't block if full
}
vTaskDelay(2000 / portTICK_PERIOD_MS); // DHT needs 2s minimum
}
}
void TaskLogSerial(void *pvParameters) {
WeatherData_t data;
for (;;) {
if (xQueueReceive(xWeatherQueue, &data, portMAX_DELAY) == pdTRUE) {
if (xSemaphoreTake(xSerialMutex, portMAX_DELAY) == pdTRUE) {
Serial.print("Temp: "); Serial.print(data.temp);
Serial.print("C Humidity: "); Serial.print(data.humidity);
Serial.println("%");
xSemaphoreGive(xSerialMutex);
}
}
}
}
void TaskHeartbeat(void *pvParameters) {
for (;;) {
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(9600);
dht.begin();
pinMode(LED_BUILTIN, OUTPUT);
xWeatherQueue = xQueueCreate(3, sizeof(WeatherData_t));
xSerialMutex = xSemaphoreCreateMutex();
xTaskCreate(TaskReadDHT, "DHT", 256, NULL, 2, NULL);
xTaskCreate(TaskLogSerial, "Log", 200, NULL, 1, NULL);
xTaskCreate(TaskHeartbeat, "HB", 64, NULL, 1, NULL);
}
void loop() {} // Empty
Tips, Stack Sizes, and Troubleshooting
Stack overflow is the most common FreeRTOS beginner problem. If your Arduino randomly resets, a task has overflowed its stack. Solutions:
- Enable stack overflow detection: define
configCHECK_FOR_STACK_OVERFLOW 2and implementvApplicationStackOverflowHook() - Increase the stack size for the crashing task (try doubling it)
- Use
uxTaskGetStackHighWaterMark(NULL)to measure peak stack usage
Stack size guidance (words, not bytes — multiply by 2 for bytes on AVR):
- Simple GPIO task: 64–100 words
- Task with Serial or String usage: 200–300 words
- Task with floating-point math: 256–400 words
- Task using libraries (DHT, display): 300–512 words
Memory management tips for small AVR boards:
- Avoid
Stringobjects (heap fragmentation) — usechar[]arrays - Avoid
Serial.print(F("text"))— always use F() macro to keep strings in Flash - Monitor free heap with
xPortGetFreeHeapSize() - If total RAM usage (stack + queue + mutex) exceeds 1.5 KB on an Uno, move to a Mega
Timing: The default tick rate is 1 ms. vTaskDelay(1) delays exactly one tick (1 ms). Use pdMS_TO_TICKS(ms) macro for clarity. For periodic tasks with accurate timing, use vTaskDelayUntil() instead of vTaskDelay().
Frequently Asked Questions
Can I use FreeRTOS with Arduino Uno even with only 2 KB RAM?
Yes, but you are severely constrained. Each task needs at minimum 64–128 words (128–256 bytes) of stack plus the task control block (~100 bytes). Realistically you can run 2–3 simple tasks on an Uno. For anything more complex, use an Arduino Mega 2560 (8 KB RAM) or Nano Every (6 KB RAM). The Mega is the most popular FreeRTOS platform for beginners.
What is the difference between vTaskDelay() and delay()?
delay() is a busy-wait that blocks the entire CPU — no other task can run. vTaskDelay() suspends only the calling task and places it in a blocked state, freeing the scheduler to run other tasks during that time. Always use vTaskDelay() inside FreeRTOS tasks. Never use delay() in a FreeRTOS sketch.
Can I use interrupts with FreeRTOS?
Yes, but ISRs (Interrupt Service Routines) cannot call normal FreeRTOS API functions. Use the FromISR variants: xSemaphoreGiveFromISR(), xQueueSendFromISR(), xTaskNotifyFromISR(). Always pass a BaseType_t xHigherPriorityTaskWoken parameter and call portYIELD_FROM_ISR() at the end of the ISR if it returns pdTRUE.
Is FreeRTOS suitable for commercial or production projects?
Absolutely. FreeRTOS is MIT-licensed and is used in millions of commercial products worldwide, including medical devices, industrial controllers, and consumer electronics. It is maintained by Amazon (AWS FreeRTOS) and has a rigorous safety certification track record. Many Indian product companies use FreeRTOS in their embedded products.
How do I debug FreeRTOS task issues?
Use vTaskList() to print a table of all tasks, their states, stack high watermarks, and priorities to Serial. Call it periodically in a low-priority task. Also use vTaskGetRunTimeStats() to see CPU time distribution between tasks — this quickly reveals which task is hogging the CPU or starving others.
FreeRTOS transforms Arduino from a simple sequential executor into a capable embedded platform that can handle real-world complexity. Whether you are building an industrial sensor logger, a drone controller, or a smart home hub, task-based thinking and the FreeRTOS scheduler will make your code cleaner, more reliable, and genuinely concurrent.
Ready to start your RTOS journey? Browse our full range of Arduino boards and accessories at Zbotic.in — from the Mega 2560 for RAM-heavy FreeRTOS projects to compact Nano boards for space-constrained applications. All shipped fast across India.
Add comment