The scheduler decides which task runs. Today you'll understand priorities, preemption, task states, and how to use FreeRTOS task management APIs.
A FreeRTOS task is in one of four states: Running: currently executing on the CPU (only one at a time in single-core). Ready: can run, waiting for CPU. Blocked: waiting for an event (timer, queue, semaphore). Suspended: explicitly suspended, not scheduled. The scheduler always runs the highest-priority ready task. Among equal-priority tasks, round-robin scheduling with a configurable time slice (default 1 tick = 1ms).
FreeRTOS priorities: 0 = lowest (idle task), configMAX_PRIORITIES-1 = highest. When a higher-priority task becomes ready, it immediately preempts the running task. This is preemptive scheduling — it happens mid-instruction-stream, controlled by the PendSV exception. Set priorities by function: safety-critical ISR handlers at highest, sensor reading at medium, logging at low. The idle task (priority 0) runs when nothing else is ready — put garbage collection or power saving here.
Each task has a dedicated stack. Stack overflow = corruption of adjacent memory = hard fault or silent data corruption (worse). Configure stack size: estimate maximum call depth × bytes per stack frame + local variables. Use uxTaskGetStackHighWaterMark(NULL) to find the minimum free stack space ever recorded. Enable stack overflow checking in FreeRTOSConfig.h: configCHECK_FOR_STACK_OVERFLOW 2 uses a canary pattern to detect overflow.
// Task priorities, states, and stack monitoring
#include "FreeRTOS.h"
#include "task.h"
TaskHandle_t xSensorHandle = NULL;
TaskHandle_t xLogHandle = NULL;
// High priority: read sensor every 10ms
void vSensorTask(void *pv) {
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xPeriod = pdMS_TO_TICKS(10);
while (1) {
// vTaskDelayUntil gives precise periodic execution
// Unlike vTaskDelay, it accounts for execution time
vTaskDelayUntil(&xLastWakeTime, xPeriod);
float sensor_val = readSensor(); // your sensor code
// Process sensor_val, post to queue, etc.
}
}
// Low priority: log stack usage every 5 seconds
void vMonitorTask(void *pv) {
while (1) {
vTaskDelay(pdMS_TO_TICKS(5000));
// Stack high water mark: min free stack ever
UBaseType_t sensorHWM = uxTaskGetStackHighWaterMark(xSensorHandle);
UBaseType_t logHWM = uxTaskGetStackHighWaterMark(xLogHandle);
UBaseType_t monitorHWM = uxTaskGetStackHighWaterMark(NULL); // this task
printf("Stack HWM (words): sensor=%u log=%u monitor=%u\n",
sensorHWM, logHWM, monitorHWM);
printf("Free heap: %u bytes\n", (unsigned)xPortGetFreeHeapSize());
}
}
// Stack overflow hook (configCHECK_FOR_STACK_OVERFLOW = 2)
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
printf("STACK OVERFLOW: task '%s'\n", pcTaskName);
// Log to persistent storage if available, then reset
NVIC_SystemReset();
}
void vApplicationMallocFailedHook(void) {
printf("MALLOC FAILED\n");
NVIC_SystemReset();
}
int main(void) {
// Higher priority for sensor task (time-critical)
xTaskCreate(vSensorTask, "SENSOR", 256, NULL, 3, &xSensorHandle);
xTaskCreate(vMonitorTask, "MON", 512, NULL, 1, NULL);
vTaskStartScheduler();
while (1);
}
vTaskDelayUntil() instead of vTaskDelay() for periodic tasks. vTaskDelay(10ms) means '10ms after this line'. vTaskDelayUntil() means '10ms after the last wake — regardless of how long execution took'. This prevents drift accumulation over thousands of iterations.Implement a rate-monotonic scheduling analysis for your tasks. Rate-monotonic theory says a set of periodic tasks is schedulable if the sum of (execution_time/period) ≤ n*(2^(1/n) - 1). For 3 tasks: measure actual execution time of each task with DWT cycle counter. Compute utilization. Is your system schedulable? What happens when you add a 4th task?