Zephyr: What `__ASSERT` Means, Interrupt Context, and Why You Must Not Create Threads in ISRs.

TL;DR

  • __ASSERT(cond, "msg") is a Zephyr macro (not C syntax) that checks cond at runtime when assertions are enabled via Kconfig and halts with a message if it fails.
  • Interrupt context = the CPU is running an Interrupt Service Routine (ISR); it preempts threads and must be short and deterministic.
  • Never create threads in an ISR. Thread creation touches memory and scheduler structures—too slow and unsafe for ISR context. Use deferred execution instead (work queues, semaphores, message queues).

The Code You Saw

__ASSERT(!arch_is_in_isr(), "Threads may not be created in ISRs");

This asserts that we are not currently in interrupt context. If we are in an ISR, the assertion will fire in debug builds so you catch the misuse immediately.


What __ASSERT Actually Is

__ASSERT is a preprocessor macro provided by Zephyr to help catch bugs early during development/testing. It typically expands to: check a condition → print a diagnostic → halt/panic when the check fails. When assertions are disabled (via Kconfig for release builds), the macro compiles out to nothing (zero runtime cost).

  • Enabled/disabled by Kconfig options (e.g., CONFIG_ASSERT, or similar debug/assert settings depending on Zephyr version).
  • The message string helps pinpoint what failed and why.
  • Use it to guard context requirements, preconditions, invariants, and “must not happen” states.

Public API note: Zephyr exposes a public helper like k_is_in_isr(); the kernel also has lower‑level arch_is_in_isr() used internally by architectures/ports.


What Is “Interrupt Context” (ISR Context)?

When a hardware event fires (timer, GPIO, UART, etc.), the CPU:

  1. Saves the current thread’s CPU state (registers/PC…),
  2. Jumps to an Interrupt Service Routine,
  3. Executes quick, critical work,
  4. Returns to whatever was running before.

Properties of ISR context:

  • Highest urgency/priority: ISR preempts threads.
  • Non‑schedulable: You don’t context‑switch from an ISR into another thread arbitrarily.
  • Time‑boxed: Staying too long blocks other interrupts and breaks real‑time guarantees.

Why Thread Creation Is Forbidden in ISRs

Creating a thread (e.g., k_thread_create) is heavyweight and non‑deterministic relative to ISR timing constraints:

  1. Memory + Stack Setup
    Allocates/initializes a stack and TCB; may touch system allocators—too slow for an ISR.

  2. Scheduler Data Structures
    Adds the new thread into ready queues, computes priorities, potentially triggers rescheduling. Scheduler state may be locked or in a fragile state during ISR handling.

  3. Potential for Deadlocks
    Kernel primitives used during creation often assume sleepable context. ISRs cannot block—waiting on a lock from an ISR would deadlock the system.

  4. Latency Explosion
    Long ISRs delay other interrupts, increase jitter, and can even cause missed deadlines/events.

Bottom line: ISRs must be tiny, predictable, and non‑blocking. Thread creation is none of those.


What Else You Should Not Do in an ISR

Avoid anything blocking or slow:

  • k_sleep(), waiting on semaphores/mutexes, or any API that may pend.
  • ❌ Long loops, busy work, or big copies.
  • ❌ Unbounded dynamic allocation (malloc, large k_malloc calls).
  • ❌ Verbose logging floods (printing a few bytes is okay; large dumps are not).
  • ❌ Operations that might lock the scheduler for a long time.

In Zephyr’s API reference, look for the “Context” section on each API. Many functions explicitly say whether they are safe from ISRs.


Do This Instead: ISR → Thread Handoff Patterns

The right approach is to do the minimum in the ISR, then defer the heavy work to a normal thread:

/* Define a work item and its handler (runs in thread context) */
static void my_work_handler(struct k_work *work)
{
    /* heavy processing here, safe to block if needed */
}

K_WORK_DEFINE(my_work, my_work_handler);

void my_isr(void *arg)
{
    /* Minimal ISR work: capture/acknowledge and defer */
    k_work_submit(&my_work);  /* API marked ISR-safe in docs */
}

Variants you’ll commonly use:

  • k_work_submit() – submit now to the system workqueue.
  • k_work_submit_to_queue() – target a custom workqueue you created.
  • k_work_schedule() / k_work_reschedule() – delayable work for timed deferral.

2) Semaphore “Kick”

K_SEM_DEFINE(evt_sem, 0, 1);

void my_isr(void *arg)
{
    /* Signal the worker thread; returns immediately */
    k_sem_give(&evt_sem);     /* ISR-safe */
}

void worker(void *p1, void *p2, void *p3)
{
    while (true) {
        k_sem_take(&evt_sem, K_FOREVER);  /* blocks in thread context */
        /* do the actual work */
    }
}

3) Message Queue / FIFO

K_MSGQ_DEFINE(rx_q, sizeof(uint32_t), 16, 4);

void my_isr(void *arg)
{
    uint32_t sample = read_reg();
    (void)k_msgq_put(&rx_q, &sample, K_NO_WAIT);  /* non-blocking, ISR-safe */
}

void consumer(void *a, void *b, void *c)
{
    uint32_t v;
    while (k_msgq_get(&rx_q, &v, K_FOREVER) == 0) {
        /* process v */
    }
}

Choose the primitive that best matches your data/flow. The pattern is always: tiny ISR → signal → real work in a thread.


Common Gotchas

  • “But I’ll only create the thread once!”
    Still wrong place. Do one‑time setup in main() or an init hook, not in an ISR.

  • Logging from ISR is slow.
    Keep it short; consider deferring logs via work or queues if you need volume.

  • API context rules differ.
    Some functions have *_from_isr variants in other RTOSes; in Zephyr, consult the docs. Use only APIs marked ISR‑callable.


Quick Checklist

  • ISR does the minimum (ack, snapshot data, post a signal).
  • No blocking, no sleeps, no heavy memory work in ISR.
  • Heavy work runs in a thread via workqueue/queue/semaphore handoff.
  • Protect shared data with proper synchronization (in the thread, not the ISR).

FAQ

Is __ASSERT the same as C’s assert()?
Similar intent, different implementation and control knobs. assert() comes from <assert.h> in libc; __ASSERT is Zephyr‑specific and governed by Kconfig.

How do I check if I’m in an ISR?
Use Zephyr’s public helper (e.g., k_is_in_isr()), or rely on kernel guards that assert for you, like the example shown.

What if I must react immediately?
Do the minimum (ack/capture) and schedule high‑priority work on a dedicated workqueue so it runs ASAP—still outside the ISR.


Recap

  • __ASSERT → Zephyr’s assertion macro; catches misuse early.
  • Interrupt context → ISR time: preemptive, non‑schedulable, time‑critical.
  • Creating threads in an ISR → verboten due to latency, scheduler integrity, and blocking hazards.
  • Solution → Defer work to threads via work queues, semaphores, or message queues.

Happy hacking—and keep your ISRs lean. :)