Skip to content

Tasks: Creation and Control

A task is a C function that never returns. It runs in an infinite loop, does its job, and then sleeps until it’s time to work again. Each task gets its own stack, its own priority, and the illusion of having the CPU all to itself.

void MyTask(void *pvParameters) {
// one-time setup (runs once when the task first executes)
for (;;) {
// do work
vTaskDelay(pdMS_TO_TICKS(50)); // sleep 50 ms — frees the CPU
}
// never reaches here
}

This is the function you’ll call most often in main() before starting the scheduler.

BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // function pointer: void (*)(void *)
const char * const pcName, // name (for debugging only — max 16 chars)
configSTACK_DEPTH_TYPE usStackDepth, // stack size in WORDS (not bytes!)
void * const pvParameters, // argument passed to the task function
UBaseType_t uxPriority, // priority: higher number = more important
TaskHandle_t * const pxCreatedTask // optional handle (NULL if you don't need it)
);

Returns pdPASS on success, errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY on failure.

xTaskCreate(
BlinkTask, // function
"Blink", // debug name
256, // stack: 256 words = 1024 bytes
NULL, // no parameter
tskIDLE_PRIORITY + 1, // one above idle
NULL // don't need a handle
);

You can pass a pointer to any data through the pvParameters argument. The task receives it as void * and casts it back:

typedef struct {
uint8_t led_pin;
uint32_t period_ms;
} BlinkCfg;
// IMPORTANT: this must outlive the task — use static or global storage
static BlinkCfg fast = { .led_pin = GPIO_PIN_0, .period_ms = 200 };
static BlinkCfg slow = { .led_pin = GPIO_PIN_1, .period_ms = 2000 };
void BlinkTask(void *pvParameters) {
BlinkCfg *cfg = (BlinkCfg *)pvParameters;
for (;;) {
GPIOPinWrite(GPIO_PORTN_BASE, cfg->led_pin, cfg->led_pin);
vTaskDelay(pdMS_TO_TICKS(cfg->period_ms));
GPIOPinWrite(GPIO_PORTN_BASE, cfg->led_pin, 0);
vTaskDelay(pdMS_TO_TICKS(cfg->period_ms));
}
}
// In main():
xTaskCreate(BlinkTask, "Fast", 256, &fast, 1, NULL);
xTaskCreate(BlinkTask, "Slow", 256, &slow, 1, NULL);

If you need to suspend, resume, or delete a task later, save its handle:

TaskHandle_t hSensor = NULL;
xTaskCreate(SensorTask, "Sensor", 512, NULL, 3, &hSensor);
// Later, from another task:
vTaskSuspend(hSensor); // pause it
vTaskResume(hSensor); // wake it back up

Every task gets its own stack — it holds local variables, function call frames, and saved CPU registers during context switches. Get this wrong and you’ll either waste RAM or crash.

Task typeRecommended startWhy
Lightweight (button scan, LED toggle)256 words (1 KB)Few local variables, no string ops
Medium (timekeeping, display, queues)512 words (2 KB)Some local buffers, moderate call depth
Heavy (sprintf, snprintf, large buffers)1024+ words (4+ KB)String formatting alone can use hundreds of bytes
  1. Start generous. Use 512 words for everything. Get your system working first.

  2. Measure under worst-case load. Inside each task, call:

    UBaseType_t freeWords = uxTaskGetStackHighWaterMark(NULL);
    // This returns the MINIMUM free stack words since the task started

    Run your system with all features active and rapid input — the high-water mark captures the peak usage.

  3. Trim with a safety margin. If a task uses 180 words out of 512, you could reduce to 256 (leaving ~30% margin). Don’t go below configMINIMAL_STACK_SIZE.

  4. Enable overflow detection during development:

    // In FreeRTOSConfig.h
    #define configCHECK_FOR_STACK_OVERFLOW 2

    And implement the hook:

    void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
    // Make the crash visible — LED, UART, breakpoint
    while (1) {}
    }
  • Large local arrays: char buf[256] inside a task eats 64 words on every call. Move large buffers to static storage.
  • Recursion: Each recursive call adds a full stack frame. Avoid it in tasks.
  • printf / sprintf: These can use 200+ bytes of stack internally. Use snprintf with small buffers, or move the buffer to static storage.
  • Deep call chains: If your task calls A() which calls B() which calls C(), all their local variables are on the stack simultaneously.

Both functions block the calling task (moving it to the Blocked state, using zero CPU). But they handle timing differently.

Sleeps for a duration starting from now:

for (;;) {
doWork(); // takes some time (say 12 ms)
vTaskDelay(pdMS_TO_TICKS(50)); // then sleep 50 ms from NOW
}
// Actual period = 12 + 50 = 62 ms — drifts over time

Use when: slight drift is acceptable (display refresh, status logging, one-time waits).

vTaskDelayUntil(&lastWake, ticks) — Absolute / Periodic

Section titled “vTaskDelayUntil(&lastWake, ticks) — Absolute / Periodic”

Sleeps until an absolute tick count, compensating for how long the work took:

TickType_t lastWake = xTaskGetTickCount();
const TickType_t period = pdMS_TO_TICKS(50);
for (;;) {
doWork(); // takes 12 ms
vTaskDelayUntil(&lastWake, period); // wakes at lastWake + 50
}
// Actual period = exactly 50 ms (as long as doWork < 50 ms)

Use when: the period must not drift (timekeeping, sampling, control loops).

vTaskDelayvTaskDelayUntil
TimingRelative to nowAbsolute (periodic)
DriftAccumulates over timeNone (if work < period)
Use caseDisplay, logging, one-shotsSampling, timekeeping, control
ComplexitySimpler — just one argumentNeeds a TickType_t variable

Sometimes you need to pause a task completely — not just delay it. vTaskSuspend() moves a task to the Suspended state, where the scheduler ignores it entirely until you call vTaskResume().

TaskHandle_t hSensor = NULL;
// Create with a handle
xTaskCreate(SensorTask, "Sensor", 512, NULL, 3, &hSensor);
// From another task — pause the sensor while reconfiguring the bus
vTaskSuspend(hSensor);
reconfigureI2C();
vTaskResume(hSensor);

A task can also suspend itself:

void CalibrationTask(void *pvParameters) {
runCalibration();
// Done — suspend myself (could also use vTaskDelete)
vTaskSuspend(NULL); // NULL = current task
}

vTaskDelete(NULL); // delete the calling task
vTaskDelete(hOther); // delete another task by handle

When you delete a task, FreeRTOS frees its stack and TCB memory (the idle task handles the cleanup). Use this for tasks with a finite job — like a one-shot calibration or initialization sequence.


TickType_t now = xTaskGetTickCount();

Returns the number of ticks since the scheduler started. At configTICK_RATE_HZ = 1000, this is effectively milliseconds.

To convert to milliseconds explicitly:

uint32_t ms = now; // at 1000 Hz, ticks == ms
// For other tick rates:
// uint32_t ms = (now * 1000) / configTICK_RATE_HZ;

Use this for timestamping events, measuring elapsed time, or initializing vTaskDelayUntil:

TickType_t start = xTaskGetTickCount();
doExpensiveWork();
TickType_t elapsed = xTaskGetTickCount() - start;
// elapsed is in ticks (= ms at 1000 Hz)

8. Complete Example: Two Tasks with Suspend/Resume

Section titled “8. Complete Example: Two Tasks with Suspend/Resume”

This example creates two tasks on the TM4C1294XL:

  • Blink toggles an LED every 200 ms
  • Controller periodically suspends and resumes Blink to demonstrate manual control
#include "FreeRTOS.h"
#include "task.h"
static TaskHandle_t hBlink = NULL;
// Task 1: Blink LED (PN0) every 200 ms
static void BlinkTask(void *pvParameters) {
(void)pvParameters;
TickType_t lastWake = xTaskGetTickCount();
for (;;) {
GPIOPinWrite(GPIO_PORTN_BASE, GPIO_PIN_0, GPIO_PIN_0);
vTaskDelayUntil(&lastWake, pdMS_TO_TICKS(200));
GPIOPinWrite(GPIO_PORTN_BASE, GPIO_PIN_0, 0);
vTaskDelayUntil(&lastWake, pdMS_TO_TICKS(200));
}
}
// Task 2: Suspend Blink for 1 second every 3 seconds
static void ControllerTask(void *pvParameters) {
(void)pvParameters;
for (;;) {
vTaskDelay(pdMS_TO_TICKS(3000)); // LED blinks normally for 3 s
vTaskSuspend(hBlink); // freeze the blink task
vTaskDelay(pdMS_TO_TICKS(1000)); // LED stays off for 1 s
vTaskResume(hBlink); // resume blinking
}
}
int main(void) {
// Hardware init: clocks, GPIO for PN0 ...
xTaskCreate(BlinkTask, "Blink", 256, NULL, 1, &hBlink);
xTaskCreate(ControllerTask, "Ctrl", 256, NULL, 2, NULL);
vTaskStartScheduler();
for (;;) {} // only reached if scheduler fails
}

MistakeWhat happensFix
Returning from a task functionUndefined behavior (usually a crash)Always use for (;;) or while (1)
Passing a local variable as parameterTask reads garbage after main() returnsUse static or global storage
Stack too smallSilent memory corruption, random crashesStart with 512, measure with uxTaskGetStackHighWaterMark
Busy-waiting instead of vTaskDelayStarves all lower-priority tasksAlways block — never spin
Calling xQueueSend from an ISRScheduler corruptionUse xQueueSendFromISR in interrupts
Using vTaskDelay for periodic timingPeriod drifts by the work timeUse vTaskDelayUntil for fixed periods