Ground Embedded KO Subscribe

Notes / RTOS

Tasks and the scheduler

Work is split into tasks, each with its own endless loop, and the scheduler shares out CPU time. Task states and the idea of a tick.

GROUND EMBEDDED · NOTERTOS 02Tasks and the schedulerBUFFERhead →tail →write · read

Tasks

A task is an endless-loop function with its own stack.

void imu_task(void *arg) {
    for (;;) {
        read_imu();
        vTaskDelay(pdMS_TO_TICKS(1));   // give up the CPU for 1 ms
    }
}
xTaskCreate(imu_task, "imu", 256, NULL, 3, NULL);

Task states

Blockedasleep, waitingReadywaiting its turnRunningusing the CPUBlockedwaiting again
The states a task moves between. Most of its time should be spent Blocked.
State Meaning
Running Using the CPU right now (one per core)
Ready Able to run but waiting its turn
Blocked Asleep, waiting for time, a queue or a semaphore
Suspended Explicitly stopped

A task should be Blocked most of the time. A task that holds the CPU and spins when there is nothing to do starves lower-priority tasks.

Ticks and context switches

TickTask ATask B
On every tick interrupt the scheduler decides again who runs. Tasks of equal priority take turns.

SysTick interrupts every 1 ms (the tick) and the scheduler picks the task to run next. To switch tasks, the current registers are saved on that task's stack and the next task's registers are restored. On Cortex-M the PendSV exception does this.

Splitting into tasks

Split by work with different periods and deadlines: sensor reading, control, communication, display, logging. Split too finely and the communication between tasks becomes the complicated part.

The four task states

State Meaning
Running Using the CPU now. With one core, one at a time
Ready Able to run, but waiting behind a more urgent task
Blocked Waiting for time or an event (queue, semaphore). Uses no CPU
Suspended Explicitly put on hold

The scheduler runs the highest-priority task among those that are ready. In a well-built system most tasks spend most of their time blocked.

In a picture: preemption

Control (high)Comm (medium)Idle (lowest)wakescomm resumeswakes
When the higher-priority control task wakes, the communication task stops where it is and continues after the control task finishes.

A task is an endless loop

void sensor_task(void *arg) {
    TickType_t last = xTaskGetTickCount();
    for (;;) {
        read_sensor();
        vTaskDelayUntil(&last, pdMS_TO_TICKS(10));   // exactly a 10 ms period
    }
}

xTaskCreate(sensor_task, "sensor", 256, NULL, 3, NULL);   // 256-word stack, priority 3

vTaskDelay(10) means "10 ms from now"; vTaskDelayUntil means "10 ms from the last wake-up time". Use the latter for work that needs an exact period. With the former, the period slips by the processing time.

3D labRun what you have read so far→

When a task never lets go of the CPU

void bad_task(void *arg) {
    for (;;) { if (flag) do_work(); }     // keeps checking without waiting
}

This task is always ready, so lower-priority tasks never run. Every task's loop needs a point where it gives up the CPU, such as a delay or a wait on a queue or semaphore.

Criteria for setting priorities

  • Shorter period, higher priority: a 1 ms control loop must be above a 100 ms log output.
  • Stricter deadline, higher priority: separate work that must not be late from work that can be a little late.
  • Long-running work goes low: file writes, display updates.
  • Do not use too many levels: three or four are enough in most cases.

Stack size

Each task has its own stack, and its size is fixed at creation. How much is needed varies a lot with local variables, call depth and whether printf is used.

UBaseType_t left = uxTaskGetStackHighWaterMark(NULL);   // the smallest margin seen so far

After a good long run, increase the stack if this value is below 20% of the total. Keep stack-overflow detection (configCHECK_FOR_STACK_OVERFLOW) on throughout development.

Ticks and time resolution

The scheduler wakes on each periodic tick interrupt (usually 1 ms) and decides the next task. Delays can only be given in ticks, so timing finer than 1 ms is handled with a hardware timer.

Problems that come up in practice

  • A task function ends with return: a task must not end. To finish, it has to delete itself.
  • Calling ordinary APIs from an interrupt: use only functions with the FromISR suffix.
  • A high-priority task doing a long calculation: every task below stops meanwhile. Split the work or lower the priority.
  • The idle task never gets to run: memory of deleted tasks is cleaned up in the idle task.
3D LABRTOS Scheduler SimulationRun what this note explains and watch it respond.Open the lab →FREE PDFGet the 3 free ebooksIf you want to keep reading this topic as one book: BLDC motor control, serial communication and sensors at book length. PDFs are in Korean.Get them free →