# Research note: Embassy and RIOT **Kind:** Read-only source analysis, 2026-10-07. Embassy from a sparse shallow clone of embassy-rs/embassy at commit `b3e27baf6c20` (embassy-executor, embassy-time, embassy-time-driver, embassy-time-queue-utils, embassy-sync); paths relative to the repository root. RIOT from RIOT-OS/RIOT at commit `f9e38576567b`, read from the git objects (`git show HEAD:`) because the sparse checkout was empty; paths relative to the repository root as stored in git. Line numbers drift; paths and symbols are the durable reference. Synthesized in `R-001`. --- ## Part A: Embassy ### A1 Execution model - The run queue is an intrusive lock-free stack (`cordyceps::TransferStack`): enqueue sets `task.next = old head` and atomically swaps the head; dequeue takes the whole batch with `take_all` and iterates it (reverse order, justified for fairness) (embassy-executor/src/raw/run_queue.rs:45-58, 99-111). Link storage is the `run_queue_item` field inside every `TaskHeader` (raw/mod.rs:109-120, run_queue.rs:18, 39-42); targets without pointer atomics fall back to a critical-section-guarded stack (run_queue.rs:195-235). - A waker is (pointer to TaskHeader, static VTABLE); clone copies, drop is a no-op, so waking needs no allocation (raw/waker.rs:5-21). `wake_task` does `state.run_enqueue`: atomically sets RUN_QUEUED and only if it was clear pushes to the owning executor and calls the pender (raw/mod.rs:630-639, 462-469). `task_from_waker` recovers the task by comparing vtable addresses so wait queues can store a one-word task pointer (waker.rs:34-49; turbo variant waker_turbo.rs). - State bits: `STATE_SPAWNED` and `STATE_RUN_QUEUED` (raw/state_atomics.rs:25-27; the ARM variant packs AtomicBools into one u32, state_atomics_arm.rs:14-25, 68-74; critical-section fallback state_critical_section.rs). Spawn is a CAS `0 -> SPAWNED|RUN_QUEUED` (state_atomics.rs:42-46); lifecycle diagram at mod.rs:70-108. There is no TIMER_QUEUED bit in this version: timer-queue membership lives in `TaskHeader.timer_queue_item` (mod.rs:116-117) and "in queue" is encoded as `waker.is_some()` (embassy-time-queue-utils/src/queue_integrated.rs:21-22, 146-149). - Task storage: `TaskStorage { raw: TaskHeader, future: UninitCell }` is `repr(C)` so the header is at offset 0 (mod.rs:218-224); `TaskPool` is `[TaskStorage; N]`; spawn claims the first free slot or returns `SpawnError::Busy` (mod.rs:383-400, spawner.rs:60-67). The `#[task]` macro emits a `static` sized at compile time (src/lib.rs:151-193; README.md:5-6); the future is built in place via a closure (mod.rs:250-256, 330). On `Poll::Ready` the future is dropped, `poll_fn` replaced by `poll_exited`, then `despawn` (mod.rs:276-291). - Poll loop: `SyncExecutor::poll` is `run_queue.dequeue_all(|p| (task.poll_fn)(p))` (mod.rs:487-506). The thread-mode Cortex-M executor is `loop { poll(); wfe }` with pender `sev` (src/platform/cortex_m.rs:107-117, 16-21). `InterruptExecutor`: the pender pends an NVIC IRQ (STIR on v7, cortex_m.rs:23-47), the ISR calls `executor.poll()` (179-182), `start()` unmasks the IRQ and returns a `SendSpawner` (207-223). Preemptive priority levels are several executors on software interrupts at different NVIC priorities; a higher-priority ISR executor preempts a lower one mid-poll, while tasks inside one executor stay cooperative (cortex_m.rs:134-147; README.md:12). The pender may be called from any context and must never call `poll` re-entrantly (src/pender.rs:10-28, mod.rs:530-535). - Optional `scheduler-priority` and `scheduler-deadline` sort each dequeued batch into a `SortedList` (run_queue.rs:127-176; Cargo.toml:210-215; src/metadata.rs:60-94), still with no preemption within an executor. - The "context switch" cost is one indirect call `poll_fn(p)` (mod.rs:498); the waker is `mem::forget`-ed to avoid a virtual drop (299-301). Stack: one per executor (the thread executor on the main stack, interrupt executors on the exception stack); future state is in the static `TaskStorage`, not on any stack. ### A2 Time - Driver trait: `fn now(&self) -> u64; fn schedule_wake(&self, at: u64, waker: &Waker)` (embassy-time-driver/src/lib.rs:118-139). `now()` must be monotonic and effectively never overflow; 16- and 32-bit hardware must be extended to 64 bits in software (lib.rs:120-128). Binding is by the linker symbols `_embassy_time_now` and `_embassy_time_schedule_wake` from `time_driver_impl!`, exactly one driver per image (lib.rs:141-177). - `TICK_HZ` is a compile-time const chosen by `tick-hz-*` features (lib.rs:112-115, src/tick.rs:3-40). `Instant` and `Duration` are `u64` ticks (embassy-time/src/instant.rs:9-10, duration.rs:10-11); ms and µs conversion uses GCD-reduced constant multiply and divide (duration.rs:31-37, 56-66; src/lib.rs:43-45); `Instant + Duration` is `checked_add` and panics on overflow, `try_*` variants exist (instant.rs:178-211, timer.rs:162-174). No wrap handling at all: 64 bits at 1 MHz is about 584,000 years (embassy-time/README.md:11-13). - Timer queue API: `schedule_wake(at, waker) -> bool` ("re-arm the alarm") and `next_expiration(now) -> u64` (wakes expired, returns the minimum) (embassy-time-queue-utils/src/lib.rs:37-48). The integrated queue is an intrusive singly linked list through the per-task `TimerQueueItem`; re-scheduling an already queued task only lowers `expires_at` (queue_integrated.rs:35-37, 76-108, 114-132). The generic queue is a `heapless::Vec<(at, Waker)>`, default 64 slots; when full it pops and wakes a victim (queue_generic.rs:38-40, 70-77, 103-120). - One hardware alarm multiplexing: the recommended driver loop is `if queue.schedule_wake(..) { loop { next = queue.next_expiration(now); if set_alarm(next) break } }` under a critical section (embassy-time-driver/src/lib.rs:73-84). The CMSDK driver shows it concretely: a 32-bit free-running "timekeeper" whose overflow IRQ bumps `periods` (embassy-time/src/driver_cmsdk/mod.rs:197-199), one alarm timer, `trigger_alarm` re-arms from the queue (227-243), `set_alarm` returns false if the target already passed (245-260). No per-tick interrupt: only overflow and next-deadline IRQs. - `Timer::at` and `after` store `expires_at`; `poll` returns Ready only if `yielded_once && expired`, otherwise `schedule_wake(expires_at)` and Pending, so it always yields at least once (timer.rs:128-165, 304-315). `block_for` busy-waits on `Instant::now()` (delay.rs:14-17). - Ticker is drift-free: `next()` does `expires_at += duration` from the previous expiry, not from `now` (timer.rs:359-375, 470-481; README.md:34). Consequence: if several periods are missed it fires back to back until caught up (no skip); `reset*` methods re-anchor to now (402-460). ### A3 Synchronization - `RawMutex { const INIT; fn lock(&self, f) -> R }` only guarantees no concurrent access and is reentrant-tolerant (embassy-sync/src/blocking_mutex/raw.rs:17-38). `CriticalSectionRawMutex` is `critical_section::with` (the portable CS crate, 59-65); `NoopRawMutex` is `f()` and is `!Sync` so it cannot cross executors (120-138); `ThreadModeRawMutex` asserts `ICSR.VECTACTIVE == 0` (167-198); `PanicRawMutex` detects misuse (88-110). - Async `Mutex`: `locked: bool` plus one `WakerRegistration` behind a blocking mutex (mutex.rs:19-47); `lock` registers the waker if locked (77-96); guard drop clears and wakes (203-215). Not fair or FIFO: a single waker slot; registering a second waiter wakes the first so they re-register in turn (waitqueue/waker_registration.rs:21-43). No ownership or priority concept. - `Channel`: `Deque` plus `receiver_waker` and `senders_waker` (both single-slot) (channel.rs:687-691); send wakes the receiver (773-786), receive wakes senders when it frees a full queue (731-744). MPMC in API but multi-waiter contention resolves via the wake-the-other-waiter trick. - `Signal`: `enum State { None, Waiting(Waker), Signaled(T) }`; `signal` overwrites and wakes (signal.rs:44-49, 77-84, 92-112), a latest-value notification for one consumer. - `PubSubChannel`: `Deque<(T, remaining_readers)>`, u64 message ids, `MultiWakerRegistration` and `` (pubsub/mod.rs:304-319); slow subscribers get `Lagged(n)` (364-369); `publish_immediate` drops the oldest (354-362); an entry is freed when the last reader consumed it (383-387). - Wait queues: `WakerRegistration` single slot (waker_registration.rs:10-50); `AtomicWaker` lock-free 2-bit state machine WAITING, REGISTERING, WAKING with wake hand-off to an in-flight register (waitqueue/atomic_waker.rs:62-77, 146-268), aliasing `CriticalSectionWaker` where no 32-bit CAS exists (282-283); `MultiWakerRegistration` is a heapless Vec that wakes everyone when full (multi_waker.rs:9-51). - `zerocopy_channel`: SPSC over a caller-provided `&mut [T]`; `SendSlot` hands out `&mut T` into the ring and `send_done()` commits (zerocopy_channel.rs:39-66, 125-172, 193-217). `Pipe`: byte `RingBuffer` with read and write `WakerRegistration`, implements `embedded_io_async` (pipe.rs:216-218, 273-275, 550-574). ### Embassy: ideas to borrow - Static task pools with compile-time sizing and `Busy` on exhaustion, no heap (raw/mod.rs:383-400, lib.rs:151-193). - Waker-style completion: a one-word task pointer plus "enqueue only if RUN_QUEUED was clear" makes wakes idempotent and ISR-safe (mod.rs:630-639; state_atomics_arm.rs:68-74); intrusive run-queue links inside the TCB (run_queue.rs:18). - A batch-dequeue run queue (take all then iterate) for O(1) ISR enqueue (run_queue.rs:99-111). - One-alarm multiplexing via `schedule_wake -> bool` plus `next_expiration(now)`, with the "set alarm, verify not in past, retry" loop (embassy-time-driver/src/lib.rs:73-84; driver_cmsdk/mod.rs:227-260); a 64-bit tick timebase so wrap never matters (lib.rs:120-128). - Ticker semantics `expires_at += period` (timer.rs:470-481). - The `RawMutex` abstraction selecting a critical section, a thread-mode assert, or a no-op lock per data-sharing scope, plus the portable `critical_section` crate (blocking_mutex/raw.rs:30-38, 59-65, 167-198). - Lagged-aware broadcast with per-message reader counts (pubsub/mod.rs:306, 364-369); the zero-copy slot channel (zerocopy_channel.rs). ### Embassy: weaknesses and limitations - No preemption inside an executor; a long poll blocks all peers; priorities only via separate interrupt executors on single-core bare metal (cortex_m.rs:134-147; Cargo.toml:160-165). - No priority inheritance or ownership in the async `Mutex`; single-slot wakers cause waiter thrashing (mutex.rs:19-23; waker_registration.rs:30-38). - No isolation: everything is privileged, with `unsafe` task and pointer casts (mod.rs:218-224); `SpawnToken` drop panics (spawner.rs:51-56); `Instant` arithmetic panics on overflow (instant.rs:202-211). - Timer queue capacity: the generic queue silently wakes a victim when full (queue_generic.rs:70-77); the integrated queue requires Embassy wakers only (waker.rs:34-38). - Debuggability of async state machines relies on an optional `trace` feature (lib.rs:7-14); Ticker catch-up bursts after long stalls (timer.rs:472-475). --- ## Part B: RIOT ### B1 Scheduler - `sched_runqueues[SCHED_PRIO_LEVELS]` (circular singly linked `clist`, O(1) rpush, lpop, lpoprpush, core/lib/include/clist.h:21-40) plus `runqueue_bitcache` u32 (core/sched.c:76-77); 16 levels by default, maximum 32 (core/include/sched.h:188-189, sched.c:60). Bit order flips on CLZ availability so selection is one `clz` or `ctz` (sched.c:84-116). - `sched_run`: with no idle thread and an empty bitcache it loops `sched_arch_idle()` (sched.c:149-158); clears `sched_context_switch_request`, selects the highest-priority queue and its head `sched_runqueues[nextrq].next->next` (160-164), marks RUNNING, updates `sched_active_pid` and `thread`, TLS, the MPU stack guard (177-219). - Priority 0 is highest ("nice" values, sched.h:22-39); `THREAD_PRIORITY_MIN = SCHED_PRIO_LEVELS-1 = IDLE` (core/lib/include/thread_config.h:118-131). Equal priorities are semi-cooperative (sched.h:36-39); `thread_yield` does `sched_runq_advance`, which is `clist_lpoprpush` (core/thread.c:151-162, sched.h:318-321). Optional `sched_round_robin`: a ztimer (10 ms on ZTIMER_MSEC) advances the queue and yields, driven by `sched_runq_callback` on runqueue changes (sys/sched_round_robin/sched_round_robin.c:41-107; sys/include/sched_round_robin.h:42-71; hooks in sched.c:166-168, 237-242, 256-258). - States: STOPPED, ZOMBIE, SLEEPING, MUTEX, RECEIVE, SEND, REPLY_BLOCKED, FLAG_BLOCKED_ANY and ALL, MBOX_BLOCKED, COND_BLOCKED, RUNNING, PENDING (sched.h:159-172); `STATUS_ON_RUNQUEUE == STATUS_RUNNING`, so `status >= RUNNING` means runnable (sched.h:180). `sched_set_status` pushes or pops the runqueue when crossing that threshold and clears the bit when a queue empties (sched.c:227-276). - Switch request: `sched_switch(other_prio)` yields immediately via `thread_yield_higher()` in thread context, or sets `sched_context_switch_request = 1` in ISR context (sched.c:278-302, sched.h:210-233). On Cortex-M the switch is PendSV (`SCB_ICSR_PENDSVSET`, cpu/cortexm_common/thread_arch.c:549) whose handler calls `sched_run` with IRQs disabled and skips the switch if the same thread wins (thread_arch.c:318-343). - IRQ API: `irq_disable()` returns the old PRIMASK, `irq_restore(state)` (core/lib/include/irq.h:46-72; cpu/cortexm_common/include/irq_arch.h:52-83); `irq_is_in()` is `IPSR & 0xFF` (irq_arch.h:119-121). Blocking from an ISR is refused, not asserted everywhere: `thread_sleep` returns early (thread.c:109-113), `ztimer_sleep` asserts (sys/ztimer/util.c:45), mutex `_block` asserts an active thread (core/mutex.c:62-64), `msg_send` redirects to `msg_send_int` (core/msg.c:65-67). - Idle: the `core_idle_thread` module runs `pm_set_lowest()` in a loop at THREAD_PRIORITY_IDLE (core/lib/init.c:92-98, 116-120); otherwise `sched_arch_idle()` (`pm_set_lowest()` or WFI, then briefly re-enable IRQs) runs inside the scheduler (thread_arch.c:553-566; sched.h:268-281). - `MAXTHREADS` defaults to 32 (sched.h:94-96); `sched_threads[KERNEL_PID_LAST+1]` is a static `thread_t*` array (sched.c:57), PIDs `KERNEL_PID_FIRST=1 .. FIRST+MAXTHREADS-1` (sched.h:105-115); `thread_create` linearly scans for a free PID else `-EOVERFLOW` (thread.c:313-326), places the TCB at the top of the caller's stack (259-272), paints the stack for `thread_measure_stack_free` (298-308, 207-234). `thread_t` fields: sp, status, priority, pid, flags, rq_entry, wait_data, msg_waiters, msg_queue (cib), msg_array (core/include/thread.h:167-208). ### B2 IPC and synchronization - `msg_t` is sender_pid, u16 type, union {u32 value, pointer} (core/include/msg.h:194-200). `_msg_send`: if the target is not RECEIVE_BLOCKED, try to copy into the target's per-thread `cib` ring (msg.c:43-61, 105-122); no queue and blocking leads to SEND_BLOCKED, priority-sorted into `target->msg_waiters` (134-154); `msg_try_send` returns 0 instead (124-129); if the target is receive-blocked, copy directly into `target->wait_data` and make it PENDING (159-170). `_msg_receive` pops the ring or blocks RECEIVE_BLOCKED; a blocked sender's message is pulled in and the sender released (360-444). `msg_send_receive` and `msg_reply` rendezvous via REPLY_BLOCKED (260-326). `msg_init_queue` sets the per-thread array (504-510); docs: synchronous by default, asynchronous only with a queue, `try_send` drops (msg.h:87-110). - `thread_flags_t` is u16 in the TCB (core/include/thread_flags.h:118, thread.h:175). `thread_flags_set` ORs the mask and `_thread_flags_wake` compares against the mask parked in `wait_data` for ANY or ALL, sets PENDING and `sched_context_switch_request` (core/thread_flags.c:25-51, 143-157); `wait_any`, `wait_all`, `wait_one` block in STATUS_FLAG_BLOCKED_* and atomically clear what they return (53-62, 87-135). Predefined `THREAD_FLAG_MSG_WAITING (1<<15)` and `THREAD_FLAG_TIMEOUT (1<<14)` (thread_flags.h:93-99); message queueing and blocked sends set MSG_WAITING via `thread_flags_set_internal` so one wait covers messages, timeouts, and events (msg.c:57-59, 149-151). `sys/event` builds on this: `event_post` pushes a sender-allocated clist node and sets `THREAD_FLAG_EVENT` (sys/event/event.c:34-49), `event_wait_multi` loops pop then `thread_flags_wait_any` (85-110), timeouts via THREAD_FLAG_TIMEOUT (113-129, 162-177; sys/include/event.h:33, 45). - Mutex: `mutex_t.queue` is NULL, `MUTEX_LOCKED ((list_node_t*)-1)`, or the head of a priority-sorted waiter list (core/include/mutex.h:36-72, 130; core/mutex.doc.md:17-24); waiters are inserted by `thread_add_to_list` (mutex.c:67-74, thread.c:187-205); unlock wakes the head, the highest priority (204-210). Priority inheritance is opt-in (`core_mutex_priority_inheritance`): raise the owner on block, restore on unlock (mutex.c:76-85, 216-224); the default is none (mutex.doc.md:5-9). `mutex_cancel`: `mutex_cancel_t {mutex, thread, cancelled}` (mutex.h:103-107, 167-169), `mutex_lock_cancelable` returns `-ECANCELED` (mutex.c:135-182), `mutex_cancel` removes the thread from the waiter list and wakes it, ISR-safe (263-293), used for `ztimer_mutex_lock_timeout` (util.c:221-243). `mutex_unlock_and_sleep` (233-261). - `cond_wait` unlocks, blocks COND_BLOCKED on a sorted list, relocks on return (core/cond.c:32-48); signal and broadcast then `sched_switch(min_prio)` (50-75). `rmutex_t {mutex, refcount, owner}` with an atomic owner read on relock (core/lib/include/rmutex.h:34-56, core/lib/rmutex.c:40-120). `mbox`: a cib ring of `msg_t` plus readers and writers lists; put hands directly to a waiting reader, else queues or blocks the writer (core/mbox.c:57-124). ### B3 ztimer - Clock tree: `ztimer_clock_t {list, ops, last, adjust_set, adjust_sleep, adjust_clock_start, users, max_value, lower_last, checkpoint, block_pm_mode}` (sys/include/ztimer.h:365-388), `ztimer_ops_t {set, now, cancel, start, stop}` (335-360). A convert clock owns `lower` and a `lower_entry` timer whose callback is `ztimer_handler(self)` (sys/ztimer/convert.c:52-68); frac scales set and now with `frac_scale` (convert_frac.c:48-71, 108-116), shift uses `>>` and `<<` (convert_shift.c:32-75), muldiv64 uses 64-bit ceiling division (convert_muldiv64.c:34-74). `ZTIMER_USEC` is the base timer directly at 1 MHz, shift for 250 or 500 kHz, else frac; `ZTIMER_MSEC` prefers an lptimer or RTT, else frac on the base timer (sys/ztimer/init.c:117-146, 212-247; design text ztimer.h:90-124). - Extension: a clock with `max_value < UINT32_MAX` arms at most `max_value/2` and keeps intermediate callbacks even with no timers (core.c:398-422, 434-457); `_ztimer_now_extend` checkpoints `checkpoint += (lower_now - lower_last) mod max_value` (core.c:263-283, ztimer.h:161-177, 680-696); `ztimer_extend` is always pulled in (Makefile.dep:23-26). - Ondemand: `ztimer_acquire` and `release` count users and call `ops->start` and `stop` (core.c:53-113); `ztimer_set` acquires and subtracts `adjust_clock_start` (163-174); `ztimer_handler` releases per fired timer and stops re-arming when nobody needs the clock (474-492); `periph_timer` stops the hardware right after init (periph_timer.c:83-97, 131-135); converters forward start and stop downwards (convert.c:38-50). Without ondemand, `pm_block` and `pm_unblock` bracket the first and last timer (core.c:222-228, 361-367). - Timer list: entries store the delta to the predecessor, `list.offset` is the absolute base (ztimer.h:127-134). `_add_entry_to_list` walks summing deltas and rebases the successor (core.c:216-253); `_ztimer_update_head_offset` folds `now - list.offset` into the head entries (286-327); removal gives the offset back to the successor (329-370). `ztimer_handler` pops offset-0 entries, fires callbacks, re-checks for expiries that happened during callbacks, then re-arms and yields if not in an ISR (424-502). - Wrap: `ztimer_now_t` is u32 and only differences are meaningful (ztimer.h:304-308, 575-577); the backend set is always relative and clamped to a `min` tick count (periph_timer.c:44-48; `CONFIG_ZTIMER_USEC_MIN 10`, sys/include/ztimer/config.h:76-77) to avoid underflow (ztimer.h:180-188). - Periodic: `ztimer_periodic_t.last`; reset computes `target = last + interval; offset = target - now; if (offset > interval) offset = 0` and `last = target` (periodic.c:29-41), drift-free with an immediate catch-up tick when late. `ztimer_periodic_wakeup` instead resets `*last_wakeup = now` on a miss, dropping phase (util.c:65-82). - Sleep and flags: `ztimer_sleep` uses a stack `MUTEX_INIT_LOCKED` unlocked by the callback and subtracts `adjust_sleep` (util.c:36-63); `ztimer_set_timeout_flag` sets THREAD_FLAG_TIMEOUT (177-191); `ztimer_set_msg` and `ztimer_msg_receive_timeout` (102-130); `ztimer_mutex_lock_timeout` via `mutex_cancel` (221-243). - Adjust: `adjust_set` subtracted in every `ztimer_set` (core.c:186-192), `adjust_sleep` in sleep, `adjust_clock_start` on cold start; `ztimer_overhead_set` and `sleep` measure `after - pre - base` (overhead.c:38-63); `ztimer_auto_adjust` takes the minimum over 100 samples at init (init.c:272-291, 386-405); defaults 0 (config.h:158-184). xtimer remains as a legacy API with `ztimer_xtimer_compat` (Makefile.dep:14-20); ztimer's relative-offset list plus clock chain replaces xtimer's single 64-bit clock. ### RIOT: ideas to borrow - thread_flags: a 16-bit per-TCB word with ANY, ALL, ONE waits, and reserved bits so IPC and timers compose into one wait (thread_flags.c:25-51; msg.c:57-59; util.c:177-191). - The ztimer clock tree: a hardware clock plus converter layers, each a full clock (ztimer.h:90-124; convert.c:52-68); a relative-offset list that uses the full 32-bit range (core.c:216-253); max_value/2 extension with checkpointing (core.c:398-422, 263-283). - Overhead compensation `adjust_set`, `adjust_sleep`, `adjust_clock_start` with boot-time auto-calibration (core.c:186-192; init.c:279-291). - Ondemand clocks with user counting so timers themselves gate low power (core.c:53-113, 163-174). - `mutex_cancel` as the generic timeout primitive for any blocking lock (mutex.c:263-293; util.c:221-243). - A bitcache runqueue with CLZ-ordered bits, `STATUS_ON_RUNQUEUE` threshold encoding, TCB-in-stack placement, and stack painting (sched.c:84-116, 262-276; thread.c:259-308). - Sender-allocated event queues that never drop (event.c:34-49; event.h:33). ### RIOT: weaknesses - No priority inheritance by default; an opt-in module only, and only for `mutex_t` (mutex.doc.md:5-9; mutex.c:76-85). - A static `MAXTHREADS` array and linear PID scan (sched.h:94-96; thread.c:313-326); a 32-level cap tied to a u32 bitcache (sched.c:60). - msg semantics are easy to misuse: blocking sends can deadlock without a receiver queue, `msg_try_send` silently drops, and queue sizing is per thread (msg.h:87-110; msg.c:124-129); `msg_send_receive` abuses the reply buffer for sending (msg.c:275-279). - Same-priority threads are cooperative unless `sched_round_robin` (itself a ztimer client) is enabled (sched.h:36-39; sched_round_robin.c:28-30). - 32-bit `ztimer_now_t` demands wrap discipline from users and continuous clock activity for comparisons (ztimer.h:575-600); ondemand forgets time while the clock is off. - Several ISR-misuse paths return silently instead of asserting (thread.c:109-113, 66-70).