Tasks: Creation and Control
1. What Is a Task?
Section titled “1. What Is a Task?”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}2. Creating a Task: xTaskCreate()
Section titled “2. Creating a Task: xTaskCreate()”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.
A Simple Example
Section titled “A Simple Example”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);Passing Parameters to Tasks
Section titled “Passing Parameters to Tasks”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 storagestatic 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);Using Task Handles
Section titled “Using Task Handles”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 itvTaskResume(hSensor); // wake it back up3. Choosing a Stack Size
Section titled “3. Choosing a Stack Size”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.
Starting Points
Section titled “Starting Points”| Task type | Recommended start | Why |
|---|---|---|
| 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 |
How to Right-Size Your Stacks
Section titled “How to Right-Size Your Stacks”-
Start generous. Use 512 words for everything. Get your system working first.
-
Measure under worst-case load. Inside each task, call:
UBaseType_t freeWords = uxTaskGetStackHighWaterMark(NULL);// This returns the MINIMUM free stack words since the task startedRun your system with all features active and rapid input — the high-water mark captures the peak usage.
-
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. -
Enable overflow detection during development:
// In FreeRTOSConfig.h#define configCHECK_FOR_STACK_OVERFLOW 2And implement the hook:
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {// Make the crash visible — LED, UART, breakpointwhile (1) {}}
Things That Eat Stack Fast
Section titled “Things That Eat Stack Fast”- Large local arrays:
char buf[256]inside a task eats 64 words on every call. Move large buffers tostaticstorage. - Recursion: Each recursive call adds a full stack frame. Avoid it in tasks.
printf/sprintf: These can use 200+ bytes of stack internally. Usesnprintfwith small buffers, or move the buffer to static storage.- Deep call chains: If your task calls
A()which callsB()which callsC(), all their local variables are on the stack simultaneously.
4. Delays: vTaskDelay vs vTaskDelayUntil
Section titled “4. Delays: vTaskDelay vs vTaskDelayUntil”Both functions block the calling task (moving it to the Blocked state, using zero CPU). But they handle timing differently.
vTaskDelay(ticks) — Relative Delay
Section titled “vTaskDelay(ticks) — Relative Delay”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 timeUse 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).
Quick Reference
Section titled “Quick Reference”vTaskDelay | vTaskDelayUntil | |
|---|---|---|
| Timing | Relative to now | Absolute (periodic) |
| Drift | Accumulates over time | None (if work < period) |
| Use case | Display, logging, one-shots | Sampling, timekeeping, control |
| Complexity | Simpler — just one argument | Needs a TickType_t variable |
5. Suspend and Resume
Section titled “5. Suspend and Resume”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 handlexTaskCreate(SensorTask, "Sensor", 512, NULL, 3, &hSensor);
// From another task — pause the sensor while reconfiguring the busvTaskSuspend(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}6. Deleting Tasks
Section titled “6. Deleting Tasks”vTaskDelete(NULL); // delete the calling taskvTaskDelete(hOther); // delete another task by handleWhen 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.
7. Reading the Tick Count
Section titled “7. Reading the Tick Count”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 msstatic 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 secondsstatic 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}9. Common Mistakes
Section titled “9. Common Mistakes”| Mistake | What happens | Fix |
|---|---|---|
| Returning from a task function | Undefined behavior (usually a crash) | Always use for (;;) or while (1) |
| Passing a local variable as parameter | Task reads garbage after main() returns | Use static or global storage |
| Stack too small | Silent memory corruption, random crashes | Start with 512, measure with uxTaskGetStackHighWaterMark |
Busy-waiting instead of vTaskDelay | Starves all lower-priority tasks | Always block — never spin |
Calling xQueueSend from an ISR | Scheduler corruption | Use xQueueSendFromISR in interrupts |
Using vTaskDelay for periodic timing | Period drifts by the work time | Use vTaskDelayUntil for fixed periods |