If you want to run a real-time operating system on an 8-bit AVR board, you need to run FreeRTOS on Arduino hardware with at least 8KB of SRAM. While it is technically possible to squeeze the kernel onto an Arduino Uno (2KB SRAM), you will run out of memory before your third task finishes compiling. The Arduino Mega 2560 is the practical baseline for AVR-based FreeRTOS projects, offering 8KB of SRAM and 256KB of Flash.
This guide covers the exact hardware limits, provides a fully compilable 3-task build, and details the specific debugging steps for the memory and timing faults that inevitably occur when moving from bare-metal loop() to a preemptive scheduler.
AVR Memory Limits: Why Board Selection Matters
Before writing a single line of task code, you must understand the SRAM budget. The FreeRTOS kernel itself requires roughly 300 to 500 bytes of SRAM just to exist. Every task you create demands its own stack (allocated in SRAM). If you exceed the physical SRAM, the compiler won't always catch it; instead, the microcontroller will silently overwrite heap variables, leading to random reboots or locked I2C buses.
| Board Variant | MCU Core | Total SRAM | Kernel Overhead | Usable Task Memory | FreeRTOS Verdict |
|---|---|---|---|---|---|
| Arduino Uno R3 | ATmega328P (8-bit) | 2 KB | ~400 Bytes | ~1.6 KB | Avoid. Only supports 2 trivial tasks. |
| Arduino Mega 2560 | ATmega2560 (8-bit) | 8 KB | ~400 Bytes | ~7.6 KB | Ideal for AVR. Supports 5-8 moderate tasks. |
| Arduino Nano 33 IoT | SAMD21 (32-bit ARM) | 32 KB | ~1 KB | ~31 KB | Good. But native CMSIS-RTOS is often preferred. |
| ESP32 DevKit V1 | Xtensa LX6 (32-bit) | 520 KB | Built-in | Massive | Native. FreeRTOS is baked into ESP-IDF. |
Note: The usable task memory assumes you are also using standard Arduino libraries like Wire or SPI, which allocate their own hidden buffers in SRAM.
Project Build: 3-Task Sensor & Indicator Controller
We will build a system that reads an analog sensor, blinks a fast status LED, and blinks a slow heartbeat LED, all running as independent preemptive tasks. This demonstrates how FreeRTOS handles timing without relying on the blocking delay() function.
Parts List
- 1x Arduino Mega 2560 Rev3 (Genuine or high-quality clone with ATmega16U2 USB chip)
- 2x 5mm LEDs (1x Red, 1x Green)
- 2x 220Ω or 330Ω through-hole resistors
- 1x 10kΩ Linear Potentiometer (for analog sensor simulation)
- Breadboard and male-to-male jumper wires
Pin Mapping Table
| Component | Mega 2560 Pin | GPIO Mode | Task Assignment |
|---|---|---|---|
| Fast Status LED (Red) | Digital 8 | OUTPUT | TaskBlinkFast |
| Slow Heartbeat LED (Green) | Digital 9 | OUTPUT | TaskBlinkSlow |
| Potentiometer Wiper | Analog A0 | INPUT | TaskReadSensor |
| Potentiometer VCC | 5V | POWER | N/A |
| Potentiometer GND | GND | GROUND | N/A |
The Code: Compilable FreeRTOS Implementation
To compile this, install the FreeRTOS library by Richard Barry (often listed as Arduino_FreeRTOS in the Library Manager). Ensure you select the Arduino Mega 2560 in your board manager.
#include <Arduino_FreeRTOS.h>
// --- Pin Definitions ---
const int PIN_LED_FAST = 8;
const int PIN_LED_SLOW = 9;
const int PIN_SENSOR = A0;
// --- Task Handles ---
TaskHandle_t TaskFastHandle = NULL;
TaskHandle_t TaskSlowHandle = NULL;
TaskHandle_t TaskSensorHandle = NULL;
void setup() {
// Initialize Serial for debugging
Serial.begin(115200);
while (!Serial) { ; } // Wait for serial port on native USB boards
// Configure Pins
pinMode(PIN_LED_FAST, OUTPUT);
pinMode(PIN_LED_SLOW, OUTPUT);
pinMode(PIN_SENSOR, INPUT);
// Create Task 1: Fast Blink (Stack size: 128 words = 256 bytes)
BaseType_t xReturnedFast = xTaskCreate(
TaskBlinkFast,
"FastBlink",
128, // Stack depth in words
NULL, // Parameters
1, // Priority (1 is lowest, 2 is higher)
&TaskFastHandle
);
// Create Task 2: Slow Blink
BaseType_t xReturnedSlow = xTaskCreate(
TaskBlinkSlow,
"SlowBlink",
128,
NULL,
1,
&TaskSlowHandle
);
// Create Task 3: Sensor Read & Serial Print (Needs more stack for Serial)
BaseType_t xReturnedSensor = xTaskCreate(
TaskReadSensor,
"SensorRead",
256, // Larger stack for Serial.print overhead
NULL,
2, // Higher priority to ensure sensor reads aren't delayed
&TaskSensorHandle
);
// Error Handling: Check if tasks were created successfully
if (xReturnedFast != pdPASS || xReturnedSlow != pdPASS || xReturnedSensor != pdPASS) {
Serial.println(F("FATAL: Failed to allocate memory for tasks. Check SRAM."));
// Blink built-in LED rapidly to indicate fatal kernel failure
pinMode(LED_BUILTIN, OUTPUT);
while(1) {
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
delay(50);
}
}
// Start the scheduler. This should never return.
vTaskStartScheduler();
}
void loop() {
// Empty. The FreeRTOS scheduler takes over in setup().
// If execution reaches here, the scheduler failed to start (lack of heap).
}
// --- Task Implementations ---
void TaskBlinkFast(void *pvParameters) {
(void) pvParameters;
for (;;) {
digitalWrite(PIN_LED_FAST, HIGH);
vTaskDelay(100 / portTICK_PERIOD_MS); // 100ms ON
digitalWrite(PIN_LED_FAST, LOW);
vTaskDelay(100 / portTICK_PERIOD_MS); // 100ms OFF
}
}
void TaskBlinkSlow(void *pvParameters) {
(void) pvParameters;
for (;;) {
digitalWrite(PIN_LED_SLOW, HIGH);
vTaskDelay(500 / portTICK_PERIOD_MS); // 500ms ON
digitalWrite(PIN_LED_SLOW, LOW);
vTaskDelay(500 / portTICK_PERIOD_MS); // 500ms OFF
}
}
void TaskReadSensor(void *pvParameters) {
(void) pvParameters;
int sensorValue = 0;
for (;;) {
sensorValue = analogRead(PIN_SENSOR);
// Serial.print consumes significant stack space.
// If this task crashes, increase its stack depth in xTaskCreate.
Serial.print("Sensor Raw: ");
Serial.println(sensorValue);
vTaskDelay(1000 / portTICK_PERIOD_MS); // Read every 1 second
}
}
Debugging: First Three Things to Check When It Fails
Transitioning from bare-metal Arduino to FreeRTOS introduces memory and timing bugs that the standard Arduino IDE doesn't catch. If your board freezes, reboots, or fails to schedule tasks, check these three failure modes in order.
1. The Silent Reboot (Stack Overflow)
Symptom: The board runs for a few seconds, then randomly restarts or locks up completely. No serial output is printed.
Cause: You allocated too little stack memory in xTaskCreate. When Serial.println() or a complex math function executes, it pushes variables onto the task's stack. If the stack exceeds the allocated words, it overwrites the kernel's heap control structures, triggering a hard fault or watchdog reset.
Fix: Increase the usStackDepth parameter. The stack depth is defined in words (2 bytes on AVR), not bytes. If you passed 128, that is 256 bytes. Bump it to 256 (512 bytes) for any task using Serial or Wire. You can also use the uxTaskGetStackHighWaterMark() function to measure exactly how much stack a task is actually using at runtime.
2. Task Creation Fails (Heap Exhaustion)
Symptom: The serial monitor prints FATAL: Failed to allocate memory for tasks (from our error handling block), or xTaskCreate returns pdFAIL.
Cause: The FreeRTOS heap (configTOTAL_HEAP_SIZE defined in FreeRTOSConfig.h) is full. On the Mega 2560, the default heap is usually set to around 1.5KB to 2KB by the library authors to leave room for standard Arduino libraries.
Fix: You must edit the library's configuration file. Navigate to your Arduino libraries folder, open Arduino_FreeRTOS_Library/src/FreeRTOSConfig.h, and increase configTOTAL_HEAP_SIZE. Alternatively, reduce the stack sizes of your tasks. Never allocate more heap than your physical SRAM minus the expected usage of Wire/SPI buffers.
3. Task Starvation and Timing Jitter
Symptom: One task runs perfectly, but another task (like the slow blink) stutters, delays, or never runs at all.
Cause: You forgot vTaskDelay() or used the bare-metal delay() function inside a task. In FreeRTOS, if a task enters a while(1) loop without yielding, it will consume 100% of the CPU time at its priority level, starving all tasks of equal or lower priority.
Fix: Ensure every task has a yield point. Use vTaskDelay(1 / portTICK_PERIOD_MS) to yield to the scheduler, or use blocking FreeRTOS primitives like xQueueReceive() with a timeout. Never use the standard Arduino delay() inside a FreeRTOS task.
error: 'xTaskCreate' was not declared in this scope, you have either forgotten to #include <Arduino_FreeRTOS.h> at the very top of your sketch, or you have a naming conflict with the ESP32 core libraries. Ensure you are compiling for the AVR Mega, not an ESP32 board, when using this specific library syntax.
Extending and Simplifying the Build
Once you have the baseline scheduler running reliably, you will inevitably need to pass data between tasks or protect shared hardware resources.
How to Simplify (Stripping it Down)
If you are porting this to an Arduino Uno and hitting the 2KB SRAM wall, strip the build down to the absolute minimum:
- Delete the
TaskReadSensorentirely.Serial.printis the biggest memory hog in this sketch. - Reduce stack depths to
64words (128 bytes) for simple GPIO toggling tasks. - Disable the idle task hook in
FreeRTOSConfig.hby settingconfigUSE_IDLE_HOOKto 0 to save a few dozen bytes.
How to Extend (Scaling Up)
To make this a production-ready embedded system, you must implement Inter-Task Communication (ITC):
- Use Queues for Data: Instead of having the sensor task print to Serial, have it push the
analogReadinteger into anxQueueCreate()buffer. Create a dedicated 'Logger' task that pulls from this queue and handles the Serial printing. This isolates the slow I/O operation from the fast sensor sampling. - Use Mutexes for I2C: If you swap the potentiometer for a BME280 I2C sensor, and you have two tasks trying to read it, the I2C bus will lock up. Wrap your
Wire.beginTransmission()blocks in a Mutex usingxSemaphoreCreateMutex(). Task A takes the semaphore, reads the sensor, gives it back, and Task B waits its turn.
For deeper architectural guidance on queue management and semaphore patterns, consult the official FreeRTOS Reference Manual and review the AVR-specific memory notes in the Arduino_FreeRTOS GitHub Repository. Running an RTOS on an 8-bit chip is an exercise in strict resource discipline, but mastering it here makes transitioning to 32-bit ARM or ESP32 architectures significantly easier.






