FreeRTOS Tasks skill for Claude Code

Tasks that block, ISRs that defer, stacks that fit. Claude designs FreeRTOS systems from a task table of triggers, deadlines and priorities, picks the right primitive per data path (task notifications, queues, stream buffers, mutexes), keeps ISRs to capture-and-notify, and sizes stacks from measured high-water marks with overflow hooks enabled. It diagnoses starvation, priority inversion, deadlocks and ISR priority asserts systematically.

Name
freertos-tasks
Size
206 lines · ≈2,450 tokens
Files
1
License
MIT
Updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Designs and debugs FreeRTOS applications — task decomposition, priority assignment, queues, stream and message buffers, mutexes vs semaphores, task notifications, event groups, ISR-safe FromISR APIs with deferred processing, software timers, stack sizing with high-water marks, and FreeRTOSConfig.h checks. Use when writing RTOS tasks on STM32, ESP32, RP2040, NXP or similar MCUs, when tasks starve, deadlock or miss deadlines, on stack overflow or hard faults, priority inversion, watchdog resets, or when an ISR needs to hand data to a task.

Try asking

  • My logger task never runs once the sensor task starts — what is wrong with the priorities?
  • Hand UART bytes from the RX interrupt to a parser task the right way
  • We get random hard faults after adding MQTT. Size the task stacks properly

Use it when

  • Splitting MCU firmware into RTOS tasks and choosing priorities
  • An ISR needs to pass data or events to a task
  • Tasks starve, deadlock, miss deadlines, or trip the watchdog
  • Hard faults or stack overflows appear after adding features

Skip it when

  • Building on Zephyr's kernel, devicetree and Kconfig — use zephyr-rtos
  • Async Rust executors on microcontrollers — use embedded-rust

How to check the result

Every task has a measured stack high-water mark with headroom, and an hour at maximum interrupt load shows zero overruns, no asserts, idle time above zero, and stable minimum free heap.

The common mistake it prevents

Protecting a shared resource with a binary semaphore instead of a mutex, which loses priority inheritance and allows unbounded priority inversion.

~/.claude/skills/freertos-tasks/SKILL.md

--- name: freertos-tasks description: Designs and debugs FreeRTOS applications — task decomposition, priority assignment, queues, stream and message buffers, mutexes vs… license: MIT ---

Done well means: every task has a stated job, period or trigger, priority and measured stack headroom; tasks block instead of polling; ISRs do the minimum and defer to a task; shared data has exactly one protection mechanism; and the config has overflow checking and asserts enabled in development.

Ground rules

  • A task that never blocks starves every lower-priority task. Every task loop must contain a blocking call (xQueueReceive, ulTaskNotifyTake, vTaskDelayUntil, …) with a finite or deliberate timeout.
  • From an ISR, call only ...FromISR APIs, and only from interrupts at or below configMAX_SYSCALL_INTERRUPT_PRIORITY (Cortex-M: numerically greater or equal priority value). Calling RTOS APIs from a higher-priority ISR corrupts the kernel.
  • Never vTaskDelay or take a mutex inside an ISR, and never take a mutex you might already hold unless it is recursive.
  • Use vTaskDelayUntil/xTaskDelayUntil for periodic work; vTaskDelay drifts.
  • Allocate RTOS objects at startup (static allocation for safety-critical builds). No creation/deletion in steady state.

Workflow

- [ ] 1. List activities: trigger (period / event / ISR), deadline, data in/out
- [ ] 2. Merge activities with the same trigger and rate into one task
- [ ] 3. Assign priorities by deadline (rate-monotonic: shorter period = higher)
- [ ] 4. Choose the IPC primitive per data path (table below)
- [ ] 5. Write ISRs as "capture + notify"; process in a task
- [ ] 6. Enable stack overflow check, asserts, runtime stats; size stacks from measurement
- [ ] 7. Stress test: max interrupt rates, all tasks busy, watch for missed deadlines

Task design table (write one per project)

| Task | Trigger | Deadline | Priority | Stack (words) | Measured HWM | Blocks on |
|---|---|---|---|---|---|---|
| imu_read | DRDY ISR @1 kHz | 1 ms | 5 | 256 | 98 | ulTaskNotifyTake |
| control | 100 Hz periodic | 10 ms | 4 | 512 | 210 | xTaskDelayUntil |
| comms | queue msgs | 50 ms | 2 | 1024 | 300 | xQueueReceive |
| logger | queue msgs | best effort | 1 | 768 | 400 | xQueueReceive |

Fewer tasks are better. Each task costs a stack, a TCB, and context switches. Six to ten tasks is typical for a mid-size MCU product.

Priorities

  • Idle task is priority 0. Keep application tasks between 1 and configMAX_PRIORITIES-1; configMAX_PRIORITIES of 5–8 is enough for most systems.
  • Hard-deadline, short tasks highest; comms mid; logging/UI lowest.
  • Timer service task (configTIMER_TASK_PRIORITY) runs software timer callbacks; set it above tasks those callbacks must preempt, and keep callbacks short and non-blocking.
  • Equal-priority tasks time-slice only if configUSE_TIME_SLICING is 1 (default).

Choosing the IPC primitive

NeedUseWhy
ISR wakes one task, optional count/valueDirect task notification (vTaskNotifyGiveFromISR / ulTaskNotifyTake)Fastest, no object
Pass data items between tasksQueue (copies by value)Decoupled, multi-producer
Byte stream from ISR to one task (UART RX)Stream bufferSingle writer/reader, efficient
Variable-length messages, one writer/readerMessage bufferLength-prefixed
Protect a shared resourceMutex (xSemaphoreCreateMutex)Has priority inheritance
Signal an event / count eventsBinary or counting semaphore, or notificationNo ownership
Wait for several conditionsEvent groupBitwise AND/OR waits
Large buffersQueue of pointers + ownership ruleAvoid copying KBs

Never use a binary semaphore to protect a resource: it has no priority inheritance, so priority inversion is unbounded.

ISR pattern

static TaskHandle_t s_imu_task;

void EXTI4_IRQHandler(void) {                 // priority below configMAX_SYSCALL_INTERRUPT_PRIORITY
    BaseType_t woken = pdFALSE;
    clear_drdy_flag();
    vTaskNotifyGiveFromISR(s_imu_task, &woken);
    portYIELD_FROM_ISR(woken);               // switch now if the woken task is higher priority
}

static void imu_task(void *arg) {
    for (;;) {
        if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(5)) == 0) {
            report_fault(FAULT_IMU_TIMEOUT);  // missed data-ready: sensor or wiring problem
            continue;
        }
        imu_sample_t s;
        imu_read(&s);                         // SPI/I2C transfer happens in task context
        xQueueSend(s_control_q, &s, 0);       // never block the producer on a full queue silently
    }
}
  • The yield macro’s exact form varies by port (some, including ESP-IDF, also offer an argument-less variant); follow the port’s documented form.
  • If data must come from the ISR (e.g. UART bytes), push into a stream buffer with xStreamBufferSendFromISR, sized for the worst-case burst between task runs.

Periodic task

static void control_task(void *arg) {
    TickType_t last = xTaskGetTickCount();
    const TickType_t period = pdMS_TO_TICKS(10);
    for (;;) {
        if (xTaskDelayUntil(&last, period) == pdFALSE) {
            overruns++;                       // we were late; the deadline was missed
        }
        run_controller();
    }
}

pdMS_TO_TICKS truncates. With configTICK_RATE_HZ 100, pdMS_TO_TICKS(5) is 0 ticks. Use 1000 Hz ticks when you need millisecond delays, or a hardware timer for sub-tick work.

Mutex rules

  • Hold for the shortest time; copy data out, release, then process.
  • Always take with a timeout and handle failure (xSemaphoreTake(m, pdMS_TO_TICKS(50))).
  • If a task needs two mutexes, define a global lock order. Better: give the resource to one “owner” task and send it requests through a queue (gatekeeper pattern), which also works for shared peripherals like a UART or I2C bus.
  • printf/logging from several tasks: use a logger task with a queue, not a mutex around printf (which may allocate and block).

Stack sizing

// FreeRTOSConfig.h (development)
#define configCHECK_FOR_STACK_OVERFLOW   2
#define configUSE_MALLOC_FAILED_HOOK     1
#define configASSERT(x) if ((x) == 0) { taskDISABLE_INTERRUPTS(); for (;;); }
#define configGENERATE_RUN_TIME_STATS    1
#define configUSE_TRACE_FACILITY         1
#define INCLUDE_uxTaskGetStackHighWaterMark 1

void vApplicationStackOverflowHook(TaskHandle_t t, char *name) { /* log name, halt */ }
void vApplicationMallocFailedHook(void) { /* log, halt */ }
  • Stack depth argument to xTaskCreate is in words on vanilla FreeRTOS (4 bytes on 32-bit), but in bytes on ESP-IDF. Check the port before copying numbers.
  • Start generous, run every code path (including error paths, printf with floats, deepest call chains), then read uxTaskGetStackHighWaterMark(NULL) (minimum free ever). Keep at least 25% or ~128 words headroom.
  • printf/snprintf with %f can need 1–2 KB of stack. Large local buffers belong in static storage.
  • Check heap: xPortGetFreeHeapSize() and xPortGetMinimumEverFreeHeapSize(). Use heap_4 (coalescing) unless you have a reason; heap_1 if nothing is ever freed.

Debugging

SymptomLikely causeAction
Low-priority task never runsHigher task never blocksFind the polling loop; add blocking wait
Hard fault after adding a featureStack overflowOverflow hook, HWM per task
configASSERT in vPortValidateInterruptPriorityISR priority too high for RTOS callsLower the NVIC priority (higher number)
Deadline missed sporadicallyPriority inversion or long critical sectionsUse mutex (inheritance), shorten taskENTER_CRITICAL regions
System freezes, no faultDeadlock (two mutexes), or waiting with portMAX_DELAYTimeouts everywhere; lock order; task state dump
Watchdog resetsA task stuck; watchdog fed from wrong placeFeed only when all critical tasks check in

Task state dump: vTaskList() (needs trace facility) and vTaskGetRunTimeStats() for CPU share per task. A trace tool (Percepio Tracealyzer, SEGGER SystemView) shows priority inversions and blocking chains directly; use it for timing bugs.

Watchdog pattern

Have each critical task set its bit in an event group each cycle; a supervisor task waits for all bits with a timeout and only then feeds the hardware watchdog. Feeding the watchdog from a timer or the idle hook hides stuck tasks.

Gotchas

  • xQueueSend(q, &item, portMAX_DELAY) in a producer turns a slow consumer into a system-wide stall. Decide: drop, overwrite (xQueueOverwrite on length-1 queues), or count and report.
  • Deleting a task does not free resources it holds (mutexes, malloc’d memory).
  • Software timer callbacks run in the timer task; blocking inside them stalls every timer.
  • Floating point in tasks on Cortex-M4F/M7 needs the port’s FPU context saving; it is automatic on modern ports, but ISRs using floats add latency.
  • taskENTER_CRITICAL disables interrupts — keep it to a few microseconds.
  • SMP ports (ESP32 dual core, RP2040 SMP) can run two tasks truly in parallel; critical sections need spinlock variants (portENTER_CRITICAL(&mux) on ESP-IDF), and “higher priority runs alone” assumptions break.

Verification

  1. Every task row in the design table has a measured high-water mark with headroom.
  2. Run the max-load scenario (all interrupts at max rate) for at least an hour: overrun counters stay zero, no assert, minimum-ever free heap stable.
  3. Runtime stats show the idle task gets CPU time (e.g. ≥ 20%) under max load.
  4. Inject faults: unplug a sensor, flood the UART; the system reports and recovers instead of freezing.
  5. With a trace tool or GPIO toggles on a scope, confirm ISR-to-task latency meets the deadline.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc