freertos-patterns — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited freertos-patterns (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Comprehensive patterns for multitasking on ESP32 (FreeRTOS) and RP2040 (dual-core with mutex synchronization).
#include <Arduino.h>
// Task function prototype
void taskBlink(void* parameter);
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
// Create task with priority 1, 2KB stack
xTaskCreate(
taskBlink, // Task function
"Blink", // Task name
2048, // Stack size (bytes)
NULL, // Parameters
1, // Priority (0 = lowest)
NULL // Task handle
);
}
void taskBlink(void* parameter) {
while(true) {
digitalWrite(LED_BUILTIN, HIGH);
vTaskDelay(pdMS_TO_TICKS(500));
digitalWrite(LED_BUILTIN, LOW);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void loop() {
// Empty - FreeRTOS scheduler manages tasks
}Use FreeRTOS patterns when:
Use RP2040 multicore patterns when:
Platform Comparison:
ESP32: Core0 (Protocol) + Core1 (App) = 240MHz dual-core, 520KB SRAM
RP2040: Core0 (loop0) + Core1 (loop1) = 133MHz dual-core, 264KB SRAMDon't use threading when:
while(true) loop#include <Arduino.h>
// Task handles for monitoring
TaskHandle_t taskBlinkHandle;
TaskHandle_t taskSensorHandle;
// Shared data (protected by mutex)
SemaphoreHandle_t dataMutex;
float sensorValue = 0.0;
void taskBlink(void* parameter) {
pinMode(LED_BUILTIN, OUTPUT);
while(true) {
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void taskReadSensor(void* parameter) {
pinMode(A0, INPUT);
while(true) {
int raw = analogRead(A0);
float voltage = raw * (3.3 / 4095.0);
// Protect shared data with mutex
if (xSemaphoreTake(dataMutex, pdMS_TO_TICKS(100))) {
sensorValue = voltage;
xSemaphoreGive(dataMutex);
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void taskPrintStats(void* parameter) {
while(true) {
// Read shared data safely
float localValue;
if (xSemaphoreTake(dataMutex, pdMS_TO_TICKS(100))) {
localValue = sensorValue;
xSemaphoreGive(dataMutex);
}
Serial.print("Sensor: ");
Serial.print(localValue, 2);
Serial.println(" V");
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
void setup() {
Serial.begin(115200);
// Create mutex before tasks
dataMutex = xSemaphoreCreateMutex();
// Create tasks with different priorities
xTaskCreate(taskBlink, "Blink", 2048, NULL, 1, &taskBlinkHandle);
xTaskCreate(taskReadSensor, "Sensor", 4096, NULL, 2, &taskSensorHandle);
xTaskCreate(taskPrintStats, "Print", 4096, NULL, 1, NULL);
}
void loop() {
// Monitor tasks every 5 seconds
vTaskDelay(pdMS_TO_TICKS(5000));
Serial.println("\n=== Task Statistics ===");
Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap());
// Get stack high water marks
UBaseType_t blinkStack = uxTaskGetStackHighWaterMark(taskBlinkHandle);
UBaseType_t sensorStack = uxTaskGetStackHighWaterMark(taskSensorHandle);
Serial.printf("Blink task free stack: %d bytes\n", blinkStack * 4);
Serial.printf("Sensor task free stack: %d bytes\n", sensorStack * 4);
}#include <Arduino.h>
// Queue for sensor data
QueueHandle_t sensorQueue;
struct SensorData {
uint32_t timestamp;
float temperature;
float humidity;
int lightLevel;
};
void taskReadSensors(void* parameter) {
SensorData data;
while(true) {
data.timestamp = millis();
data.temperature = random(200, 300) / 10.0; // Mock: 20-30°C
data.humidity = random(400, 700) / 10.0; // Mock: 40-70%
data.lightLevel = analogRead(A0);
// Send to queue (wait max 10ms)
if (xQueueSend(sensorQueue, &data, pdMS_TO_TICKS(10)) != pdPASS) {
Serial.println("Queue full, dropping data!");
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void taskLogData(void* parameter) {
SensorData data;
while(true) {
// Wait indefinitely for data
if (xQueueReceive(sensorQueue, &data, portMAX_DELAY) == pdPASS) {
Serial.printf("[%lu] T=%.1f°C H=%.1f%% L=%d\n",
data.timestamp,
data.temperature,
data.humidity,
data.lightLevel);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(A0, INPUT);
// Create queue: 10 items, size of SensorData struct
sensorQueue = xQueueCreate(10, sizeof(SensorData));
if (sensorQueue == NULL) {
Serial.println("Queue creation failed!");
while(1);
}
xTaskCreate(taskReadSensors, "Producer", 4096, NULL, 2, NULL);
xTaskCreate(taskLogData, "Consumer", 4096, NULL, 1, NULL);
}
void loop() {
vTaskDelay(portMAX_DELAY);
}#include <Arduino.h>
// Mutex with priority inheritance
SemaphoreHandle_t resourceMutex;
void taskHighPriority(void* parameter) {
while(true) {
Serial.println("HIGH: Requesting resource...");
if (xSemaphoreTake(resourceMutex, pdMS_TO_TICKS(1000))) {
Serial.println("HIGH: Got resource");
vTaskDelay(pdMS_TO_TICKS(100));
xSemaphoreGive(resourceMutex);
} else {
Serial.println("HIGH: Timeout!");
}
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
void taskLowPriority(void* parameter) {
while(true) {
Serial.println("LOW: Requesting resource...");
if (xSemaphoreTake(resourceMutex, pdMS_TO_TICKS(1000))) {
Serial.println("LOW: Got resource, holding 500ms...");
vTaskDelay(pdMS_TO_TICKS(500)); // Simulate long operation
xSemaphoreGive(resourceMutex);
}
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
void setup() {
Serial.begin(115200);
// Create mutex with priority inheritance
resourceMutex = xSemaphoreCreateMutex();
// Low priority task gets resource first
xTaskCreate(taskLowPriority, "Low", 2048, NULL, 1, NULL);
vTaskDelay(pdMS_TO_TICKS(100));
// High priority task will boost low priority when waiting
xTaskCreate(taskHighPriority, "High", 2048, NULL, 3, NULL);
}
void loop() {
vTaskDelay(portMAX_DELAY);
}#include <Arduino.h>
#define BUTTON_PIN 0 // Boot button on ESP32
SemaphoreHandle_t buttonSemaphore;
volatile uint32_t isrCount = 0;
// ISR handler
void IRAM_ATTR buttonISR() {
isrCount++;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// Signal semaphore from ISR
xSemaphoreGiveFromISR(buttonSemaphore, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
void taskButtonHandler(void* parameter) {
uint32_t eventCount = 0;
while(true) {
// Wait indefinitely for semaphore
if (xSemaphoreTake(buttonSemaphore, portMAX_DELAY) == pdPASS) {
eventCount++;
Serial.printf("Button event #%lu (ISR count: %lu)\n",
eventCount, isrCount);
// Debounce delay
vTaskDelay(pdMS_TO_TICKS(200));
}
}
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Create binary semaphore (initially empty)
buttonSemaphore = xSemaphoreCreateBinary();
xTaskCreate(taskButtonHandler, "Button", 2048, NULL, 3, NULL);
// Attach interrupt
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
Serial.println("Press button (GPIO 0) to trigger event");
}
void loop() {
vTaskDelay(portMAX_DELAY);
}#include <Arduino.h>
TaskHandle_t workerTaskHandle;
void taskWorker(void* parameter) {
uint32_t notificationValue;
while(true) {
// Wait for notification (blocks indefinitely)
notificationValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
Serial.printf("Received notification: %lu\n", notificationValue);
// Process work based on notification count
for (uint32_t i = 0; i < notificationValue; i++) {
Serial.printf("Processing task %lu...\n", i + 1);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
}
void taskController(void* parameter) {
uint32_t workCount = 1;
while(true) {
vTaskDelay(pdMS_TO_TICKS(2000));
Serial.printf("Sending %lu work items\n", workCount);
// Notify worker task (increment notification count)
xTaskNotifyGive(workerTaskHandle);
workCount = (workCount % 5) + 1;
}
}
void setup() {
Serial.begin(115200);
xTaskCreate(taskWorker, "Worker", 2048, NULL, 2, &workerTaskHandle);
xTaskCreate(taskController, "Controller", 2048, NULL, 1, NULL);
}
void loop() {
vTaskDelay(portMAX_DELAY);
}Note: RP2040 uses setup1()/loop1() for Core1, not FreeRTOS tasks.
#include <Arduino.h>
#include <pico/mutex.h>
// Shared data protected by mutex
mutex_t dataMutex;
float sharedSensorValue = 0.0;
volatile uint32_t core0Count = 0;
volatile uint32_t core1Count = 0;
// Core 0: Default Arduino setup() and loop()
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
pinMode(A0, INPUT);
// Initialize mutex before Core1 starts
mutex_init(&dataMutex);
Serial.println("Core0 setup complete");
}
void loop() {
// Core 0: Read sensor with mutex protection
int raw = analogRead(A0);
float voltage = raw * (3.3 / 4095.0);
mutex_enter_blocking(&dataMutex);
sharedSensorValue = voltage;
core0Count++;
mutex_exit(&dataMutex);
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
delay(500);
}
// Core 1: Runs in parallel on second core
void setup1() {
Serial.println("Core1 setup complete");
}
void loop1() {
// Core 1: Print sensor data with mutex protection
float localValue;
uint32_t localCore0Count, localCore1Count;
mutex_enter_blocking(&dataMutex);
localValue = sharedSensorValue;
localCore0Count = core0Count;
localCore1Count = core1Count;
core1Count++;
mutex_exit(&dataMutex);
Serial.printf("Sensor: %.2fV | Core0: %lu | Core1: %lu\n",
localValue, localCore0Count, localCore1Count);
delay(1000);
}Key Differences from ESP32:
xTaskCreate() - use setup1()/loop1() for Core1mutex_enter_blocking() instead of xSemaphoreTake()setup() before Core1 starts// Expected output:
[HIGH] Priority 3 task running
[MED] Priority 2 task running
[LOW] Priority 1 task running
Free heap: ~280000 bytesPass criteria:
// Expected output:
[1234] T=25.3°C H=55.2% L=2048
[2234] T=26.1°C H=53.8% L=2051
Queue spaces available: 7Pass criteria:
// Expected output:
Task A: value = 0
Task B: value = 1
Task A: value = 2
// No race conditions or torn readsPass criteria:
// Expected output:
Task 'Blink' free stack: 1456 bytes (72% free)
Task 'Sensor' free stack: 3012 bytes (73% free)
WARNING: Task 'Logger' only 200 bytes free!Pass criteria:
// WRONG: Task stack too small
xTaskCreate(taskBigArray, "Task", 1024, NULL, 1, NULL);
void taskBigArray(void* parameter) {
char buffer[2048]; // Stack overflow!
// ...
}Fix:
✅ // Calculate stack: function locals + call stack + margin
xTaskCreate(taskBigArray, "Task", 4096, NULL, 1, NULL);
// Monitor at runtime
UBaseType_t freeStack = uxTaskGetStackHighWaterMark(NULL);
if (freeStack < 512) {
Serial.println("WARNING: Low stack!");
}// WRONG: Blocks entire CPU
void taskBlink(void* parameter) {
while(true) {
digitalWrite(LED, HIGH);
delay(1000); // Other tasks can't run!
digitalWrite(LED, LOW);
delay(1000);
}
}Fix:
✅ void taskBlink(void* parameter) {
while(true) {
digitalWrite(LED, HIGH);
vTaskDelay(pdMS_TO_TICKS(1000)); // Yields to other tasks
digitalWrite(LED, LOW);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}// WRONG: Race condition
int sharedCounter = 0;
void taskA(void* p) {
while(true) {
sharedCounter++; // Not atomic!
vTaskDelay(10);
}
}
void taskB(void* p) {
while(true) {
Serial.println(sharedCounter); // May read torn value
vTaskDelay(10);
}
}Fix:
✅ SemaphoreHandle_t counterMutex;
int sharedCounter = 0;
void taskA(void* p) {
while(true) {
xSemaphoreTake(counterMutex, portMAX_DELAY);
sharedCounter++;
xSemaphoreGive(counterMutex);
vTaskDelay(10);
}
}
void taskB(void* p) {
while(true) {
xSemaphoreTake(counterMutex, portMAX_DELAY);
int local = sharedCounter;
xSemaphoreGive(counterMutex);
Serial.println(local);
vTaskDelay(10);
}
}// WRONG: ISR may crash if code not in IRAM
void buttonISR() {
xSemaphoreGiveFromISR(sem, NULL);
}Fix:
✅ void IRAM_ATTR buttonISR() {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(sem, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}// WRONG: Task A locks mutex1 then mutex2
// Task B locks mutex2 then mutex1
// = DEADLOCK!
void taskA(void* p) {
xSemaphoreTake(mutex1, portMAX_DELAY);
xSemaphoreTake(mutex2, portMAX_DELAY);
// Critical section
xSemaphoreGive(mutex2);
xSemaphoreGive(mutex1);
}
void taskB(void* p) {
xSemaphoreTake(mutex2, portMAX_DELAY); // Wrong order!
xSemaphoreTake(mutex1, portMAX_DELAY);
// Critical section
xSemaphoreGive(mutex1);
xSemaphoreGive(mutex2);
}Fix:
✅ // ALWAYS acquire locks in same order
void taskA(void* p) {
xSemaphoreTake(mutex1, portMAX_DELAY);
xSemaphoreTake(mutex2, portMAX_DELAY);
// Critical section
xSemaphoreGive(mutex2);
xSemaphoreGive(mutex1);
}
void taskB(void* p) {
xSemaphoreTake(mutex1, portMAX_DELAY); // Same order!
xSemaphoreTake(mutex2, portMAX_DELAY);
// Critical section
xSemaphoreGive(mutex2);
xSemaphoreGive(mutex1);
}// WRONG: Core1 may start before mutex is ready
void setup() {
Serial.begin(115200);
// Core1's setup1() may run here!
}
void setup1() {
mutex_init(&myMutex); // Too late! Core0 may access mutex
}Fix:
✅ // Initialize mutex in setup() before Core1 starts
mutex_t myMutex;
void setup() {
Serial.begin(115200);
mutex_init(&myMutex); // Core1 guaranteed to see initialized mutex
}
void setup1() {
// Mutex already ready
}ESP32 FreeRTOS:
RP2040 Multicore:
When to Choose Which:
Platform SRAM Flash Cores Clock Cost
ESP32 520KB 4MB 2 240MHz $8
RP2040 264KB 2MB 2 133MHz $4
Arduino UNO 2KB 32KB 1 16MHz $25ESP32 Task Model:
xTaskCreatePinnedToCore(task1, "T1", 4096, NULL, 2, NULL, 0); // Core 0
xTaskCreatePinnedToCore(task2, "T2", 4096, NULL, 1, NULL, 1); // Core 1
xTaskCreatePinnedToCore(task3, "T3", 4096, NULL, 3, NULL, 0); // Core 0 (high priority)
// Scheduler manages 3+ tasks across 2 coresRP2040 Core Model:
void setup() { /* Core 0 init */ }
void loop() { /* Core 0 work */ }
void setup1() { /* Core 1 init */ }
void loop1() { /* Core 1 work */ }
// Exactly 2 parallel infinite loopsESP32 FreeRTOS Mutex:
xSemaphoreTakeFromISR()RP2040 Pico SDK Mutex:
mutex_enter_timeout_us())| Priority | Use Case | Example |
|---|---|---|
| 0 | Idle task (auto-created) | Cleanup, power management |
| 1-2 | Background logging, LED blink | Data logging, status updates |
| 3-5 | Sensor reading, network | WiFi, I2C/SPI communication |
| 6-10 | Real-time control, safety | Motor control, emergency stop |
| 11-24 | Reserved for system | WiFi stack, Bluetooth |
Rule: Keep user tasks in 1-10 range, leave 11+ for ESP-IDF internals.
Stack Size = Function Locals + Call Chain + Margin
Example:
- Function has 1KB local array
- Calls 3 functions (256 bytes each)
- Margin: 1KB
Total: 1024 + 768 + 1024 = 2816 → Round to 3072 (3KB)Monitoring:
UBaseType_t freeWords = uxTaskGetStackHighWaterMark(NULL);
uint32_t freeBytes = freeWords * sizeof(StackType_t);| Primitive | Use Case | Max Count | Priority Inheritance |
|---|---|---|---|
| Mutex | Exclusive resource access | 1 | ✅ Yes |
| Binary Semaphore | Event signaling | 1 | ❌ No |
| Counting Semaphore | Resource pool (e.g., 5 buffers) | N | ❌ No |
| Queue | Data passing | N items | ❌ No |
When to use each:
FreeRTOS Heap (configTOTAL_HEAP_SIZE):
Task Stack (per-task):
ESP32 Default:
totalHeap - (numTasks * avgStackSize) - systemOverhead#include <esp_task_wdt.h>
void taskCritical(void* parameter) {
// Register with watchdog (3 second timeout)
esp_task_wdt_add(NULL);
while(true) {
// Do critical work
readSafetySensors();
updateMotorControl();
// Reset watchdog timer
esp_task_wdt_reset();
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void setup() {
// Enable watchdog
esp_task_wdt_init(3, true); // 3 second timeout, panic on trigger
xTaskCreate(taskCritical, "Safety", 4096, NULL, 10, NULL);
}#include <Arduino.h>
EventGroupHandle_t systemEvents;
// Event bits
#define BIT_WIFI_CONNECTED (1 << 0)
#define BIT_SENSOR_READY (1 << 1)
#define BIT_SD_MOUNTED (1 << 2)
#define BIT_ALL_READY (BIT_WIFI_CONNECTED | BIT_SENSOR_READY | BIT_SD_MOUNTED)
void taskStartup(void* parameter) {
// Wait for all subsystems
EventBits_t bits = xEventGroupWaitBits(
systemEvents,
BIT_ALL_READY,
pdFALSE, // Don't clear bits
pdTRUE, // Wait for ALL bits
portMAX_DELAY
);
if ((bits & BIT_ALL_READY) == BIT_ALL_READY) {
Serial.println("All subsystems ready, starting main task");
startMainApplication();
}
vTaskDelete(NULL);
}
void taskInitWiFi(void* parameter) {
// Initialize WiFi...
Serial.println("WiFi connected");
xEventGroupSetBits(systemEvents, BIT_WIFI_CONNECTED);
vTaskDelete(NULL);
}
void taskInitSensor(void* parameter) {
// Initialize sensor...
Serial.println("Sensor ready");
xEventGroupSetBits(systemEvents, BIT_SENSOR_READY);
vTaskDelete(NULL);
}
void taskInitSD(void* parameter) {
// Initialize SD card...
Serial.println("SD mounted");
xEventGroupSetBits(systemEvents, BIT_SD_MOUNTED);
vTaskDelete(NULL);
}
void setup() {
Serial.begin(115200);
systemEvents = xEventGroupCreate();
xTaskCreate(taskInitWiFi, "InitWiFi", 4096, NULL, 1, NULL);
xTaskCreate(taskInitSensor, "InitSensor", 2048, NULL, 1, NULL);
xTaskCreate(taskInitSD, "InitSD", 2048, NULL, 1, NULL);
xTaskCreate(taskStartup, "Startup", 2048, NULL, 2, NULL);
}#include <Arduino.h>
TimerHandle_t periodicTimer;
uint32_t timerCount = 0;
void timerCallback(TimerHandle_t xTimer) {
timerCount++;
Serial.printf("Timer fired: %lu\n", timerCount);
// Don't call blocking functions here!
// Max execution time: ~10ms
}
void setup() {
Serial.begin(115200);
// Create timer: 1 second period, auto-reload
periodicTimer = xTimerCreate(
"Periodic", // Name
pdMS_TO_TICKS(1000), // Period (1 second)
pdTRUE, // Auto-reload
NULL, // Timer ID
timerCallback // Callback
);
if (periodicTimer != NULL) {
xTimerStart(periodicTimer, 0);
}
}
void loop() {
vTaskDelay(portMAX_DELAY);
}// Pin tasks to specific cores
void taskWiFi(void* parameter) {
while(true) {
// WiFi operations (run on Core 0 with protocol stack)
handleWiFiPackets();
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void taskApplication(void* parameter) {
while(true) {
// Application logic (run on Core 1)
processUserInput();
updateDisplay();
vTaskDelay(pdMS_TO_TICKS(50));
}
}
void setup() {
// Core 0: Protocol stack (WiFi, BLE)
xTaskCreatePinnedToCore(taskWiFi, "WiFi", 4096, NULL, 2, NULL, 0);
// Core 1: Application logic
xTaskCreatePinnedToCore(taskApplication, "App", 4096, NULL, 1, NULL, 1);
}TaskHandle_t workerHandle;
volatile bool shouldExit = false;
void taskWorker(void* parameter) {
while(!shouldExit) {
// Do work
processData();
vTaskDelay(pdMS_TO_TICKS(100));
}
// Cleanup before exit
Serial.println("Worker task exiting");
freeResources();
vTaskDelete(NULL);
}
void stopWorker() {
shouldExit = true;
// Wait for task to exit gracefully
while(eTaskGetState(workerHandle) != eDeleted) {
vTaskDelay(pdMS_TO_TICKS(10));
}
Serial.println("Worker stopped");
}#ifdef ESP32 and #ifdef ARDUINO_ARCH_RP2040 to select patternPre-flight checks:
Runtime validation:
Performance criteria:
Code quality:
Progression for students:
Common misconceptions:
Debugging tips:
configCHECK_FOR_STACK_OVERFLOW 2 in sdkconfig (ESP32)vTaskList() to print all task states (ESP32)ESP.getFreeHeap() every 5 seconds (ESP32)Serial.printf() to log core execution order~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.