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.
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
...FromISRAPIs, and only from interrupts at or belowconfigMAX_SYSCALL_INTERRUPT_PRIORITY(Cortex-M: numerically greater or equal priority value). Calling RTOS APIs from a higher-priority ISR corrupts the kernel. - Never
vTaskDelayor take a mutex inside an ISR, and never take a mutex you might already hold unless it is recursive. - Use
vTaskDelayUntil/xTaskDelayUntilfor periodic work;vTaskDelaydrifts. - 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_PRIORITIESof 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_SLICINGis 1 (default).
Choosing the IPC primitive
| Need | Use | Why |
|---|---|---|
| ISR wakes one task, optional count/value | Direct task notification (vTaskNotifyGiveFromISR / ulTaskNotifyTake) | Fastest, no object |
| Pass data items between tasks | Queue (copies by value) | Decoupled, multi-producer |
| Byte stream from ISR to one task (UART RX) | Stream buffer | Single writer/reader, efficient |
| Variable-length messages, one writer/reader | Message buffer | Length-prefixed |
| Protect a shared resource | Mutex (xSemaphoreCreateMutex) | Has priority inheritance |
| Signal an event / count events | Binary or counting semaphore, or notification | No ownership |
| Wait for several conditions | Event group | Bitwise AND/OR waits |
| Large buffers | Queue of pointers + ownership rule | Avoid 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 aroundprintf(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
xTaskCreateis 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/snprintfwith%fcan need 1–2 KB of stack. Large local buffers belong in static storage.- Check heap:
xPortGetFreeHeapSize()andxPortGetMinimumEverFreeHeapSize(). Use heap_4 (coalescing) unless you have a reason; heap_1 if nothing is ever freed.
Debugging
| Symptom | Likely cause | Action |
|---|---|---|
| Low-priority task never runs | Higher task never blocks | Find the polling loop; add blocking wait |
| Hard fault after adding a feature | Stack overflow | Overflow hook, HWM per task |
configASSERT in vPortValidateInterruptPriority | ISR priority too high for RTOS calls | Lower the NVIC priority (higher number) |
| Deadline missed sporadically | Priority inversion or long critical sections | Use mutex (inheritance), shorten taskENTER_CRITICAL regions |
| System freezes, no fault | Deadlock (two mutexes), or waiting with portMAX_DELAY | Timeouts everywhere; lock order; task state dump |
| Watchdog resets | A task stuck; watchdog fed from wrong place | Feed 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 (xQueueOverwriteon 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_CRITICALdisables 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
- Every task row in the design table has a measured high-water mark with headroom.
- 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.
- Runtime stats show the idle task gets CPU time (e.g. ≥ 20%) under max load.
- Inject faults: unplug a sensor, flood the UART; the system reports and recovers instead of freezing.
- 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.