FreeRTOS skill for Codex
Task tables, not priority guesswork. Codex starts every FreeRTOS job by tabulating tasks with their priority, stack, blocking call and deadline, which exposes most starvation and deadlock bugs at a glance. It enables asserts and overflow hooks, uses task notifications and stream buffers from ISRs, checks Cortex-M interrupt priorities against configMAX_SYSCALL_INTERRUPT_PRIORITY, and sizes memory from measured watermarks.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- freertos Installs to ~/.agents/skills/freertos/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install freertos The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project freertos Installs to .agents/skills/freertos/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/freertos.zipunzip -o freertos.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) freertos PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $freertos, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs and debugs FreeRTOS applications on Cortex-M, ESP32 and RISC-V - task priorities and stack sizing, queues, task notifications, mutexes versus semaphores, FromISR APIs and interrupt priority limits, software timers, heap schemes, and FreeRTOSConfig.h checks. Use when tasks starve or deadlock, a HardFault or configASSERT fires inside the kernel, stacks overflow, an ISR must wake a task, or when laying out the task structure for new firmware.
Use it when
- Tasks starve, deadlock or miss deadlines
- A HardFault or assert fires inside the FreeRTOS kernel
- An interrupt needs to hand work to a task
- Planning task structure and memory for new RTOS firmware
Use something else when
- The project is built on Zephyr's kernel: use zephyr-rtos
- The bug is in register access or shared-variable semantics without an RTOS: use embedded-c
How to check the result
Run the worst-case load with asserts and hooks enabled, then record each task's stack high-water mark and the minimum-ever free heap and confirm 20 % headroom.
The mistake it prevents
Calling a FromISR API from an interrupt whose priority is more urgent than configMAX_SYSCALL_INTERRUPT_PRIORITY corrupts kernel lists and crashes much later.
Done well means: every task has a written reason for its priority and a measured stack
high-water mark, ISRs only signal and never wait, shared data has exactly one owner or one
lock, and configASSERT plus overflow and malloc-failure hooks are on in every debug build.
Inspect first
- Read
AGENTS.md, thenFreeRTOSConfig.h(orsdkconfigon ESP-IDF, where it is generated) and note:configTICK_RATE_HZ,configMAX_PRIORITIES,configCHECK_FOR_STACK_OVERFLOW,configUSE_MALLOC_FAILED_HOOK,configASSERT,configSUPPORT_STATIC_ALLOCATION,configMAX_SYSCALL_INTERRUPT_PRIORITY. - Find which heap file is linked (
heap_1.c…heap_5.c) andconfigTOTAL_HEAP_SIZE. - List every
xTaskCreate*call: name, stack, priority, what it blocks on. Draw that table before changing anything; most bugs are visible in it.
Units trap
| Port | usStackDepth in xTaskCreate means |
|---|---|
| Vanilla FreeRTOS (STM32, NXP, RP2040 SDK port) | Words (StackType_t), so 256 = 1 KB on 32-bit |
| ESP-IDF | Bytes, so 256 is far too small |
Debug configuration (minimum)
#define configASSERT(x) do { if (!(x)) { taskDISABLE_INTERRUPTS(); for (;;); } } while (0)
#define configCHECK_FOR_STACK_OVERFLOW 2
#define configUSE_MALLOC_FAILED_HOOK 1
#define configUSE_TRACE_FACILITY 1
#define configRECORD_STACK_HIGH_ADDRESS 1
void vApplicationStackOverflowHook(TaskHandle_t task, char *name) {
(void)task;
log_panic("stack overflow: %s", name); // keep it minimal; the stack is already bad
for (;;) {}
}
void vApplicationMallocFailedHook(void) { log_panic("malloc failed"); for (;;) {} }
Halt-on-assert is right in development; in production log the location and reset through the watchdog instead.
Priority design
- Higher number means higher priority. Idle is 0. Keep
configMAX_PRIORITIESsmall (5-7) so the plan stays readable. - Assign by deadline, shortest deadline highest (rate monotonic), not by “importance”.
- Every task above idle must block (queue, notification, delay) or lower tasks starve.
- The timer daemon runs callbacks at
configTIMER_TASK_PRIORITY; slow callbacks delay all timers. Keep them short or forward work to a task.
Prio Task Blocks on Deadline
4 motor_ctl notification from TIM ISR 1 ms
3 comms_rx UART DMA idle notification 5 ms
2 sensor vTaskDelayUntil 100 ms 100 ms
1 logger queue (depth 16) best effort
0 idle - -
Choosing the primitive
| Need | Use | Not |
|---|---|---|
| ISR wakes one known task | Direct task notification (vTaskNotifyGiveFromISR / ulTaskNotifyTake) | Binary semaphore (slower, more RAM) |
| Pass data between tasks | Queue of small structs or pointers into a pool | Global variable plus flag |
| Byte stream from ISR to one task | Stream buffer | Queue of single bytes |
| Protect a shared resource | Mutex (priority inheritance) | Binary semaphore (no inheritance, priority inversion) |
| Wait for several conditions | Event group | Polling flags |
| Count resources | Counting semaphore |
ISR to task
static TaskHandle_t s_rx_task;
void USART2_IRQHandler(void) {
BaseType_t woken = pdFALSE;
clear_irq_flags();
vTaskNotifyGiveFromISR(s_rx_task, &woken);
portYIELD_FROM_ISR(woken); // switch now if the woken task is higher priority
}
static void rx_task(void *arg) {
for (;;) {
if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(100)) == 0) {
continue; // timeout: check link health
}
drain_rx_buffer();
}
}
Hard rules:
- Only
...FromISRAPIs inside interrupts. Never a blocking call, never a mutex. - On Cortex-M, any ISR that calls a FreeRTOS API must have a priority numerically equal to
or greater than
configMAX_SYSCALL_INTERRUPT_PRIORITY(lower urgency). A higher-urgency ISR calling the API corrupts the kernel;configASSERTinvPortValidateInterruptPrioritycatches it if enabled. On STM32, useNVIC_PRIORITYGROUP_4so all bits are preemption bits. - ISRs above that threshold are allowed but must not touch the kernel at all.
Periodic tasks
static void sensor_task(void *arg) {
TickType_t last = xTaskGetTickCount();
for (;;) {
sample();
xTaskDelayUntil(&last, pdMS_TO_TICKS(100)); // fixed period, no drift
}
}
vTaskDelay drifts by the execution time. pdMS_TO_TICKS rounds down; with a 100 Hz tick,
pdMS_TO_TICKS(5) is 0, which means “do not block”. Check tick rate before small delays.
Memory
- Prefer static allocation for tasks, queues and buffers created at boot
(
xTaskCreateStatic,xQueueCreateStatic); no heap failure possible after start. - If heap is used,
heap_4(coalescing) is the default choice;heap_5when RAM is in several regions. Never free and reallocate in steady state. - Measure, then size: run the worst-case path, then
uxTaskGetStackHighWaterMark(NULL)per task (words left, minimum ever) andxPortGetMinimumEverFreeHeapSize(). Keep at least 20-25 % stack headroom. printfandsnprintfwith floats can use 1-2 KB of stack on newlib; tasks that log need more than tasks that do not.
Deadlock and starvation checklist
- Two mutexes taken in different orders in different tasks? Fix the order globally
- Mutex held across a blocking call (queue wait, delay, I/O)? Restructure
-
portMAX_DELAYon a queue whose producer can die? Use a timeout and log it - A high-priority task polling without blocking? It starves everything below
- Deleted tasks never cleaned up because idle never runs? Idle needs CPU time to free
memory after
vTaskDelete
Tooling
vTaskList/vTaskGetRunTimeStats(needconfigUSE_STATS_FORMATTING_FUNCTIONSand a run-time counter) give a one-shot task table over UART.- SEGGER SystemView or Percepio Tracealyzer show context switches and priority inversion; use them when timing bugs do not reproduce with logs.
- Debugger RTOS awareness (OpenOCD
-rtos FreeRTOS, J-Link, STM32CubeIDE) shows every task’s stack; check it on a HardFault before reading code.
Verification
- Task table (name, priority, stack, blocks on, deadline) written in the code or docs
- All asserts and hooks enabled in debug build; long run without triggering them
- High-water marks recorded under worst-case load; headroom at least 20 %
- Minimum-ever free heap recorded; stays constant after boot
- Interrupt priorities checked against
configMAX_SYSCALL_INTERRUPT_PRIORITY
Report
**Kernel:** FreeRTOS V11.1 on STM32F407, 1 kHz tick, heap_4 32 KB
**Root cause:** UART ISR at NVIC prio 2 called xQueueSendFromISR (limit is 5) -> list corruption
**Change:** ISR prio 6; queue of bytes replaced by stream buffer; logger mutex now inherits priority
**Measured:** stack high-water min 31 % (logger), heap min-ever 9.8 KB constant over 6 h What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "FreeRTOS"
short_description: "Task tables, not priority guesswork"
default_prompt: "$freertos configASSERT fires in vPortValidateInterruptPriority, what is wrong?" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.