If you are searching for a reliable STM32 FreeRTOS Arduino xTaskCreate and vTaskStartScheduler example, you have likely outgrown standard delay() loops. Running a Real-Time Operating System on an STM32 microcontroller via the Arduino IDE allows you to handle concurrent sensor polling, motor control, and telemetry without blocking the main execution thread. However, ARM Cortex-M boards handle memory and interrupts differently than 8-bit AVRs, and a misconfigured scheduler will instantly trigger a HardFault.
This guide targets the WeAct Studio STM32F411CEU6 (Black Pill V2.0). We chose this board over the aging STM32F103C8T6 (Blue Pill) because the F411 features 128KB of SRAM and a Cortex-M4F core running at 100MHz. FreeRTOS requires RAM for task stacks; the Blue Pill's 20KB SRAM chokes quickly when you spawn more than two or three tasks, whereas the F411 gives you the headroom to actually use the RTOS as intended.
Time to Build: 20 minutes
Prerequisites: Arduino IDE 2.x, STM32duino core installed via Boards Manager, ST-Link V2 programmer.
Hardware Spec Sheet & Pin Mapping
Before writing code, we need to establish the physical layer. The Black Pill V2.0 uses 3.3V logic. Do not connect 5V sensors directly to the GPIO pins without a logic level shifter, or you will fry the silicon.
Parts List
- MCU: WeAct Studio STM32F411CEU6 (Black Pill V2.0)
- Programmer: Genuine ST-Link V2 (or high-quality clone with 3.3V SWDIO/SWCLK)
- Indicators: 2x 5mm LEDs (1x Red, 1x Green)
- Current Limiting: 2x 220Ω through-hole resistors
- Wiring: 22 AWG solid core jumper wires, 400-point breadboard
Pin Mapping Table
| Component | STM32F411 Pin | Arduino Alias | Notes |
|---|---|---|---|
| Onboard LED | PC13 | LED_BUILTIN | Active LOW (sink current to turn on) |
| External Red LED | PA0 | D2 / PA0 | Connect anode to PA0, cathode to GND via 220Ω |
| External Green LED | PA1 | D3 / PA1 | Connect anode to PA1, cathode to GND via 220Ω |
| ST-Link SWDIO | DIO (PA13) | N/A | Serial Wire Debug Data |
| ST-Link SWCLK | CLK (PA14) | N/A | Serial Wire Debug Clock |
The Complete STM32 FreeRTOS Arduino Example
Below is the complete, compilable C++ code. This example spawns two independent tasks: one that blinks the external red LED at 2Hz, and another that toggles the green LED while printing a heartbeat message over Serial. Notice that the loop() function is completely empty. Once the scheduler starts, the RTOS takes over thread management.
Note: Ensure you have installed the 'FreeRTOS' library by Richard Berry (or the STM32duino built-in wrapper) via the Arduino Library Manager before compiling.
#include <Arduino.h>
#include <FreeRTOS.h>
#include <task.h>
// --- Pin Definitions ---
const uint8_t PIN_LED_RED = PA0;
const uint8_t PIN_LED_GREEN = PA1;
const uint8_t PIN_LED_BUILTIN = PC13;
// --- Task Handles ---
TaskHandle_t redLedTaskHandle = NULL;
TaskHandle_t greenLedTaskHandle = NULL;
// --- Task 1: Red LED Blink (2Hz) ---
void redLedTask(void *pvParameters) {
// Initialize pin inside the task to avoid setup() race conditions
pinMode(PIN_LED_RED, OUTPUT);
for (;;) {
digitalWrite(PIN_LED_RED, HIGH);
vTaskDelay(pdMS_TO_TICKS(250)); // 250ms ON
digitalWrite(PIN_LED_RED, LOW);
vTaskDelay(pdMS_TO_TICKS(250)); // 250ms OFF
}
}
// --- Task 2: Green LED & Serial Telemetry ---
void greenLedTask(void *pvParameters) {
pinMode(PIN_LED_GREEN, OUTPUT);
uint32_t loopCount = 0;
for (;;) {
digitalWrite(PIN_LED_GREEN, !digitalRead(PIN_LED_GREEN));
loopCount++;
// Print heartbeat every 10 cycles
if (loopCount % 10 == 0) {
Serial.print("[GreenTask] Heartbeat: ");
Serial.println(loopCount);
// Check remaining stack watermark for debugging
Serial.print("[GreenTask] Min Free Stack Words: ");
Serial.println(uxTaskGetStackHighWaterMark(NULL));
}
vTaskDelay(pdMS_TO_TICKS(500)); // 500ms cycle
}
}
void setup() {
Serial.begin(115200);
while (!Serial && millis() < 3000) {
// Wait for serial monitor, max 3 seconds
}
Serial.println("System Boot: Initializing FreeRTOS Tasks...");
pinMode(PIN_LED_BUILTIN, OUTPUT);
digitalWrite(PIN_LED_BUILTIN, LOW); // Turn on built-in LED to show boot
// Create Red LED Task (Stack size in WORDS, not bytes. 128 words = 512 bytes)
BaseType_t xReturned1 = xTaskCreate(
redLedTask, // Task function
"RedLED", // Task name
128, // Stack depth (words)
NULL, // Parameters
1, // Priority (1 = low)
&redLedTaskHandle // Task handle
);
// Create Green LED Task (Higher priority, larger stack for Serial.print)
BaseType_t xReturned2 = xTaskCreate(
greenLedTask,
"GreenLED",
256, // 256 words = 1024 bytes (Serial.print is stack-heavy)
NULL,
2, // Priority (2 = higher than RedLED)
&greenLedTaskHandle
);
// Error Handling: Check if tasks were successfully allocated
if (xReturned1 != pdPASS || xReturned2 != pdPASS) {
Serial.println("FATAL: Failed to allocate memory for tasks. Halting.");
// Blink built-in LED rapidly to indicate fatal boot error
for(;;) {
digitalWrite(PIN_LED_BUILTIN, !digitalRead(PIN_LED_BUILTIN));
delay(50);
}
}
Serial.println("Starting Scheduler...");
// Start the RTOS scheduler. This function should NEVER return.
vTaskStartScheduler();
// If execution reaches here, the scheduler failed to start (usually heap exhaustion)
Serial.println("FATAL: Scheduler failed to start.");
}
void loop() {
// Empty. The RTOS scheduler handles execution from here on.
}
Debugging: When the Scheduler Crashes or Fails to Start
When moving from bare-metal Arduino to FreeRTOS on STM32, the most common point of failure is immediately after calling vTaskStartScheduler(). Instead of your LEDs blinking, the board locks up, or your serial monitor spits out a cryptic fault.
The First Three Things to Check
- Stack Depth Units: In
xTaskCreate, the stack size parameter is defined in words (4 bytes each on a 32-bit ARM), not bytes. If you pass1024thinking it's bytes, you are actually requesting 4096 bytes. Two tasks doing this will exhaust the F411's heap instantly. - Interrupt Priority Clashes: STM32duino configures the SysTick timer for the Arduino
millis()function. FreeRTOS requires strict interrupt nesting. EnsureconfigMAX_SYSCALL_INTERRUPT_PRIORITYin yourFreeRTOSConfig.hmatches the STM32 HAL expectations (usually priority 5 or 15, depending on the core version). - Serial.print in High-Priority Tasks: The Arduino
Serialobject uses interrupts and internal buffers. Calling it from a high-priority RTOS task while a lower-priority task holds the UART lock can cause a priority inversion deadlock.
Exact Error String: HardFault_Handler or errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
If your serial monitor outputs FreeRTOS error: xTaskCreate returned errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY, or if the board immediately drops into the HardFault_Handler upon calling the scheduler, follow this ranked cause list:
| Rank | Cause | Fix |
|---|---|---|
| 1 | Heap Exhaustion: configTOTAL_HEAP_SIZE in FreeRTOSConfig.h is too small for the requested task stacks. |
Increase heap size in the config header, or reduce the stack depth parameter in xTaskCreate. |
| 2 | Stack Overflow during execution: The task ran out of stack space (often caused by large local arrays or deep Serial.print calls), corrupting the heap. |
Use uxTaskGetStackHighWaterMark() to measure actual usage. Increase stack size by 20% above the watermark. |
| 3 | Missing Idle Task Memory: The scheduler requires a small amount of heap to create the hidden Idle and Timer tasks. | Ensure you have at least 512 bytes of free heap remaining after your user tasks are created. |
For deeper architectural guidelines on ARM Cortex-M memory management, refer to the official FreeRTOS task documentation and the STM32duino Core Wiki.
Extending and Simplifying Your FreeRTOS Build
Once you have the basic scheduler running, you will inevitably need to scale the project. Here is how to adapt the build for production environments.
How to Simplify: Use Static Allocation
Dynamic memory allocation (the heap) is a major source of fragmentation and non-deterministic behavior in embedded systems. You can simplify your build and eliminate the heap entirely by using Static Allocation. Instead of xTaskCreate, use xTaskCreateStatic. This requires you to pass pre-allocated arrays for the task stack and the StaticTask_t control block. The RTOS will use your global variables instead of asking the heap for memory, guaranteeing that memory allocation never fails at runtime.
How to Extend: Inter-Task Communication
Blinking LEDs is fine for testing, but real projects require tasks to share data. Do not use global variables protected by volatile flags; this defeats the purpose of the RTOS. Instead, extend your build using:
- Queues (
xQueueCreate): Best for passing sensor readings from an I2C polling task to a WiFi telemetry task. - Mutexes (
xSemaphoreCreateMutex): Best for protecting shared hardware resources, like the SPI bus or the UART serial port, ensuring two tasks don't write to the bus simultaneously. - Task Notifications: The lightest-weight method (using zero RAM overhead) to wake up a sleeping task from an Interrupt Service Routine (ISR).
Frequently Asked Questions
How much stack memory does xTaskCreate actually use on STM32?
The number you pass to xTaskCreate is in words. On the 32-bit STM32F411, one word equals 4 bytes. If you pass 128 as the stack depth, FreeRTOS allocates 512 bytes for the task stack, plus roughly 168 bytes for the Task Control Block (TCB). Always use uxTaskGetStackHighWaterMark(NULL) inside your task loop to see how close you are to the bottom of the stack. If the watermark returns a single-digit number, you are microseconds away from a HardFault crash.
Why does my Arduino STM32 code halt after vTaskStartScheduler?
It is supposed to halt. vTaskStartScheduler() contains an infinite loop that manages the context switching between your tasks. It will only return to the setup() function if it fails to allocate the memory required for the hidden Idle task. If your code 'halts' and does nothing (no LEDs blinking), it means the scheduler started, but your tasks are either blocked indefinitely, waiting on a semaphore that never triggers, or they crashed silently due to a stack overflow.
Can I mix standard Arduino delay() with FreeRTOS vTaskDelay()?
No. You must strictly use vTaskDelay() or vTaskDelayUntil() inside your RTOS tasks. The standard Arduino delay() function uses a busy-wait loop (or a SysTick blocking loop depending on the core implementation) that prevents the FreeRTOS scheduler from switching to other tasks. If you call delay(1000) inside a high-priority task, you will starve your lower-priority tasks and potentially trigger the RTOS Watchdog timer. The only exception is inside hardware initialization routines before the scheduler is started.






