/*
* FreeRTOS Scheduling Demo — STM32 Nucleo (Arduino framework + STM32FreeRTOS)
* ---------------------------------------------------------------------------
* GOAL: show the difference between WHEN a task becomes READY (set by its
* wake-up schedule) and WHEN it actually RUNS (decided by PRIORITY + PREEMPTION).
*
* KEY TERMS (what the scheduler is doing at any instant):
* RUNNING - the single task currently executing on the CPU.
* READY - a task that COULD run but isn't, because something of equal or
* higher priority holds the CPU. It is eligible, just not chosen.
* BLOCKED - a task waiting for time to pass (vTaskDelay/vTaskDelayUntil) or
* for an event. It is NOT eligible to run and costs no CPU.
* PREEMPTION - the moment a higher-priority task becomes READY, FreeRTOS
* immediately stops the lower-priority task mid-work and switches.
* PRIORITY - higher number = higher priority. The scheduler always runs the
* highest-priority READY task. Among EQUAL priority, it time-slices
* (round-robin, one tick each) when configUSE_TIME_SLICING is on.
*
* vTaskDelay() vs vTaskDelayUntil():
* vTaskDelay(N) - block for N ticks measured from NOW (from the moment
* you call it). If your work took a while first, the
* period DRIFTS. Good for "wait roughly N."
* vTaskDelayUntil(&t, N) - block until the ABSOLUTE tick t+N, then set t=t+N.
* The period is exact and does NOT accumulate drift,
* no matter how long the task's work took. Good for a
* true fixed-rate task (our 250 ms task).
*
* NOTE ON SINGLE CORE: the Nucleo is single-core, so there is exactly one CPU to
* fight over. That is IDEAL for this demo — no second core can hide the effect.
*
* LIMITATION (printing the exact Blocked->Ready instant):
* FreeRTOS moves a delayed task from Blocked to Ready inside the tick ISR; it
* does not hand the application a callback at that instant (that needs trace
* hooks). BUT a vTaskDelayUntil task is special: its wake tick IS known in
* advance (it's the absolute time it unblocks). So the 250 ms task below can
* print its scheduled-READY time and its actual-RUNNING time, and the gap
* between them is exactly the delay caused by a higher-priority task.
*/
#include <STM32FreeRTOS.h>
// ---- LED pins (adjust to pins broken out on YOUR board; avoid PA2/PA3 = Serial)
const int LED_HIGH = PB1; // Red -> High Priority Task
const int LED_250 = PB13; // Green -> 250 ms Periodic Task
const int LED_EQUAL = PB14; // Blue -> Equal Priority Task
const int LED_LOW = PB2; // Yellow -> Low Priority Task
// ---- Task priorities (named constants). Higher number = higher priority.
#define PRIO_LOW (tskIDLE_PRIORITY + 1) // 1 - background
#define PRIO_MEDIUM (tskIDLE_PRIORITY + 2) // 2 - the 250 ms task AND the equal task
#define PRIO_HIGH (tskIDLE_PRIORITY + 3) // 3 - preempts the medium tasks
#define STACK_WORDS 256 // stack in WORDS (STM32FreeRTOS). Lower if RAM-limited.
// Timestamp in ms, taken from the FreeRTOS tick (consistent with the scheduler).
static inline uint32_t nowMs() {
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
}
// Bounded CPU-BOUND work: spins (does NOT block) for d ms, holding the CPU at the
// caller's priority. Spinning is deliberate — it models real work and is what
// makes preemption visible: a higher-priority task will interrupt this spin, and
// this spin will keep lower/equal tasks from running while it lasts.
// It is bounded (never infinite), so the RTOS keeps functioning.
static void busyWorkMs(uint32_t d) {
TickType_t start = xTaskGetTickCount();
while ((xTaskGetTickCount() - start) < pdMS_TO_TICKS(d)) { /* spin */ }
}
// ---------------------------------------------------------------------------
// TASK 1 — 250 ms PERIODIC (medium priority, GREEN)
// Uses vTaskDelayUntil for a true 250 ms cadence with no drift. Prints the tick
// it became READY (its scheduled wake) and the tick it actually RAN — the gap is
// the higher-priority delay.
// ---------------------------------------------------------------------------
void task250(void *pv) {
TickType_t lastWake = xTaskGetTickCount();
char buf[96];
for (;;) {
// BLOCKED here until the absolute wake tick. On return, lastWake == the tick
// at which this task became READY.
vTaskDelayUntil(&lastWake, pdMS_TO_TICKS(250));
uint32_t readyMs = (uint32_t)(lastWake * portTICK_PERIOD_MS); // when READY
uint32_t runMs = nowMs(); // when RUNNING
uint32_t delayed = runMs - readyMs; // the gap
digitalWrite(LED_250, HIGH);
snprintf(buf, sizeof buf,
"250ms Task READY@%lu ms RUNNING@%lu ms (delayed %lu ms)",
(unsigned long)readyMs, (unsigned long)runMs, (unsigned long)delayed);
Serial.println(buf);
busyWorkMs(60); // a little visible work
digitalWrite(LED_250, LOW); // work done -> LED off, task loops and BLOCKS
}
}
// ---------------------------------------------------------------------------
// TASK 2 — HIGH PRIORITY (RED)
// Wakes periodically and does 40 ms of CPU-bound work. It uses vTaskDelay
// (relative), so its phase DRIFTS against the 250 ms schedule — guaranteeing
// that over time its 40 ms active window sometimes covers a 250 ms boundary.
// When it does, the 250 ms task becomes READY on time but must WAIT (its
// "delayed" number jumps up) until this task finishes. That is the whole point.
// ---------------------------------------------------------------------------
void taskHigh(void *pv) {
char buf[64];
for (;;) {
vTaskDelay(pdMS_TO_TICKS(200)); // BLOCKED ~200 ms (drifts vs 250)
digitalWrite(LED_HIGH, HIGH);
snprintf(buf, sizeof buf, "%lu ms - HIGH Priority START", (unsigned long)nowMs());
Serial.println(buf);
busyWorkMs(40); // RUNNING, CPU-bound -> preempts all lower
snprintf(buf, sizeof buf, "%lu ms - HIGH Priority END", (unsigned long)nowMs());
Serial.println(buf);
digitalWrite(LED_HIGH, LOW);
}
}
// ---------------------------------------------------------------------------
// TASK 3 — EQUAL PRIORITY (same as 250 ms task, BLUE)
// Periodically does 20 ms of work at PRIO_MEDIUM. When it and the 250 ms task
// are READY at the same time, they share the CPU by TIME-SLICING (round-robin,
// one tick each) — equal priority means "take turns," not "one wins."
// ---------------------------------------------------------------------------
void taskEqual(void *pv) {
char buf[64];
for (;;) {
vTaskDelay(pdMS_TO_TICKS(300));
digitalWrite(LED_EQUAL, HIGH);
snprintf(buf, sizeof buf, "%lu ms - EQUAL Priority working", (unsigned long)nowMs());
Serial.println(buf);
busyWorkMs(20);
digitalWrite(LED_EQUAL, LOW);
}
}
// ---------------------------------------------------------------------------
// TASK 4 — LOW PRIORITY (YELLOW)
// Continuous background work in short bounded chunks, then a small delay so it
// yields. It runs ONLY when nothing higher is READY, and gets PREEMPTED the
// instant any higher task wakes (watch its LED stay on while it's stalled,
// mid-work, waiting for the CPU back).
// ---------------------------------------------------------------------------
void taskLow(void *pv) {
for (;;) {
digitalWrite(LED_LOW, HIGH);
busyWorkMs(15); // may be preempted partway through
digitalWrite(LED_LOW, LOW);
vTaskDelay(pdMS_TO_TICKS(50)); // BLOCKED briefly so higher tasks own the CPU
}
}
void setup() {
Serial.begin(115200);
//delay(1000);
//Serial.println("--- start ---");
pinMode(LED_HIGH, OUTPUT); pinMode(LED_250, OUTPUT);
pinMode(LED_EQUAL, OUTPUT); pinMode(LED_LOW, OUTPUT);
Serial.println("--- FreeRTOS scheduling demo ---");
// Create tasks. Note two tasks at PRIO_MEDIUM (the 250 ms task and the equal
// task) to demonstrate equal-priority time-slicing.
xTaskCreate(taskHigh, "HIGH", STACK_WORDS, NULL, PRIO_HIGH, NULL);
xTaskCreate(task250, "250ms", STACK_WORDS, NULL, PRIO_MEDIUM, NULL);
xTaskCreate(taskEqual, "EQUAL", STACK_WORDS, NULL, PRIO_MEDIUM, NULL);
xTaskCreate(taskLow, "LOW", STACK_WORDS, NULL, PRIO_LOW, NULL);
vTaskStartScheduler(); // hand control to FreeRTOS; never returns
}
void loop() {}