Research note: ChibiOS (RT and NIL) and Apache NuttX

Kind: Read-only source analysis, 2026-10-07. ChibiOS/ChibiOS at commit fd2e59c34878 (RT 8.0.0, NIL 4.2.0; paths relative to os/). Apache NuttX at commit b463a4bf7a48 (paths relative to the repo root; the clone was blob:none, missing blobs were pulled with git’s promisor lazy fetch and read with git show, working tree untouched). Line numbers drift; paths and symbols are the durable reference. Synthesized in R-001.


Part A: ChibiOS

A1 API classes, locking, fast/slow IRQs, state checker, ISR-exit reschedule

  • Suffix contract is normative: no suffix = thread context, X = any context, S = system-locked, I = ISR-or-locked (AGENTS.md:19). chSysLock/Unlock are thin wrappers over port_lock/unlock (rt/include/chsys.h:371-398); chSysLockFromISR/UnlockFromISR over port_lock_from_isr (:415-441); CH_IRQ_PROLOGUE/EPILOGUE wrap PORT_IRQ_PROLOGUE/EPILOGUE plus hooks (:112-131).

  • ARMv7-M: port_lock() raises BASEPRI to CORTEX_BASEPRI_KERNEL (and takes the SMP spinlock) instead of disabling IRQs (common/ports/ARMv7-M/chcore.h:856-874). CORTEX_FAST_PRIORITIES (default 2) reserves priorities 0..1 as “fast” IRQs that may never call OS APIs; CORTEX_PRIORITY_SVCALL = MAX + FAST, CORTEX_MAX_KERNEL_PRIORITY = SVCALL+1 (chcore.h:92,174-175,508-519). CORTEX_SIMPLIFIED_PRIORITY switches to PRIMASK/cpsid and PendSV, dropping fast IRQs and the extra port_extctx (chcore.h:191-207).

  • State machine for debug: each OS instance carries isr_cnt and lock_cnt (rt/include/chdebug.h:61-81). Every transition primitive asserts preconditions and halts with SV#1..SV#11: e.g. __dbg_check_lock requires isr_cnt==0 && lock_cnt==0 then sets lock_cnt=1; __dbg_check_enter_isr increments isr_cnt under lock (rt/src/chdebug.c:111-240). chDbgCheckClassI = lock_cnt>0 (any ISR depth), chDbgCheckClassS = lock_cnt>0 && isr_cnt==0 (:250-274); the SV table is documented in chdebug.c:20-70.

  • ISR exit: __port_irq_epilogue locks, checks SCB->ICSR.RETTOBASE, pushes a fake exception frame whose PC is __port_switch_from_isr if chSchIsPreemptionRequired() else __port_exit_from_isr, and returns still locked (chcore.c:227-274). __port_switch_from_isr calls chSchDoPreemption then svc #0; SVC_Handler discards the fake frame and unlocks atomically (compilers/GCC/chcoreasm.S:144-171, chcore.c:77-95); simplified mode pends PendSV instead (chcore.c:106-121).

A2 Scheduler

  • Ready list is a circular priority-ordered doubly linked list ch_priority_queue_t inside the per-core ready_list_t {pqueue, current} (rt/include/chobjects.h:373-383). ch_pqueue_insert_behind scans while prio >= p->prio (FIFO among equals); insert_ahead scans while prio > p->prio (LIFO among equals) (rt/include/chlists.h, pqueue section). ch_sch_prio_insert is inlined when CH_CFG_OPTIMIZE_SPEED else out-of-line (rt/include/chschd.h:166-185, rt/src/chschd.c:249-266); link-back safety asserts are embedded (chSftAssert(2, next->prev == cp)).

  • chSchReadyI inserts behind and, in SMP, pokes the owning core (chschd.c:285-299); chSchGoSleepS pops the highest and switches (:310-341); chSchWakeupS switches immediately if the woken thread is higher, placing the current thread ahead of its peers since its quantum is not spent (:401-451); chSchRescheduleS only if firstprio > current (:462-473).

  • Round robin: CH_CFG_TIME_QUANTUM (template default 0, rt/templates/chconf.h:145); chSysTimerHandlerI decrements ticks (rt/src/chsys.c:262-283); chSchIsPreemptionRequired uses > while quantum remains, >= when exhausted (chschd.c:489-507); chSchDoPreemption requeues behind with a fresh quantum or ahead (:521-561); chSchDoYieldS yields to equal-or-higher (>=) and goes behind (:606-617).

  • Idle: per-instance idlethread at IDLEPRIO, optional (CH_CFG_NO_IDLE_THREAD), with enter/leave hooks (rt/src/chinstances.c:61-73,174-191). SMP (RT 7/8): one os_instance_t per core with its own rlist, vtlist, dbg, trace (chobjects.h:416-480), currcore resolves via port_get_core_id() (chsys.h:51-58), threads carry owner; cross-core wakeups use chSysNotifyInstance and a single global spinlock in port_lock (chcore.h:873-886). The ahead/behind variants are now the static __sch_reschedule_ahead/behind (chschd.c:126-182).

A3 Virtual timers

  • Delta list anchored at vtlist.lasttime; vt_enqueue computes delta = (now-lasttime) + delay and inserts by delta, reprogramming the alarm only if it becomes first (rt/src/chvt.c:218-262). vt_set_alarm clamps to lastdelta (min safe delta) and loops re-arming if “now” already passed the deadline, recording CH_RFCU_VT_INSUFFICIENT_DELTA and raising lastdelta adaptively (:69-128); same in vt_insert_first (:139-208).

  • Callbacks run from the tick/alarm ISR between chSysUnlockFromISR()/chSysLockFromISR() (:540-542,591-595): I-class only, never thread context. chVTDoTickI in tickless mode advances lasttime lazily only by the expired delta (:576-577,658-659), detects skipped deadlines for continuous timers (:606-616), and stops the alarm when empty.

  • CH_CFG_ST_TIMEDELTA 0 = tick mode (decrement head delta, :525-549), >=2 = tickless with port_timer_set/start/stop_alarm (rt/include/chvt.h:42-52, template 2 at chconf.h:120). systime_t may be 16/32/64 bits, sysinterval_t must be >= resolution (rt/include/chtime.h:75-122); VT_MAX_DELAY masks when intervals are wider than systime (chvt.c:36-39). chTimeAddX/DiffX/IsInRangeX are plain modular unsigned arithmetic (chtime.h, ~431-449); chVTGetTimeStampI extends to a 64-bit monotonic stamp (chvt.c:682-706).

  • Thread timeouts: chSchGoSleepTimeoutS arms a stack-allocated virtual_timer_t with __sch_wakeup, sleeps, then resets if still armed (chschd.c:363-382); the callback dequeues per wait state, sets MSG_TIMEOUT, and for semaphores restores the count (:187-232).

A4 Mutexes, PI, condvars, semaphores, events, messages, exit/registry

  • chMtxLockS (rt/src/chmtx.c:172-274): if owned, loop while (tp->prio < currtp->prio): raise tp->prio; if tp is WTMTX, re-sort it in that mutex’s queue, set tp = that mutex's owner and continue (transitive chain walk); if WTCOND / priority semaphore / priority message queue, re-sort and stop; if READY, requeue in ready list; then insert self priority-ordered and sleep WTMTX; ownership is handed over by the unlocker (asserts :256-257).

  • chMtxUnlockS requires LIFO unlock order (mtxlist == mp, :464), recomputes newprio = max(realprio, first waiter of every still-owned mutex) (:477-492), gives the mutex to the FIFO first waiter and readies it (:499-503). Recursive mode adds cnt (:110-112,180-188); chThdSetPriority preserves inherited boosts (rt/src/chthreads.c:808-830).

  • Condvars release only the most recently taken mutex, queue priority-ordered, relock unless timeout; broadcast wakes with MSG_RESET (rt/src/chcond.c:235-250,323-345,179-190). Semaphores: negative count = waiters; CH_CFG_USE_SEMAPHORES_PRIORITY swaps FIFO insert for priority insert (rt/src/chsem.c:79-83,221-238); chSemSignalWait is atomic; reset with message (:173-184).

  • Events: per-thread epending mask; chEvtSignalI readies on any (WTOREVT) or all (WTANDEVT) (rt/src/chevents.c:340-353); listeners are a linked list off the source, broadcast ORs flags, filters by wflags, then signals the listener thread’s event bits (:148-161,391-410). Messages: sender enqueues on tp->msgqueue (priority order optional, rt/include/chmsg.h:52-56), readies a receiver in WTMSG, sleeps SNDMSGQ until chMsgReleaseS; receiver marks it SNDMSG (rt/src/chmsg.c:84-133): a rendezvous with zero copies.

  • Exit/wait: chThdExitS readies all threads linked on waiting, removes from registry if refs==0 and no dispose, sleeps FINAL (chthreads.c:711-745); chThdWait links on tp->waiting then chThdReleases (:768-793); refs/dispose drive heap or pool reclamation (rt/src/chdynamic.c:53-62). Registry exports a ROM ch_debug offsets table for debugger plugins (rt/src/chregistry.c:76-135).

A5 NIL kernel

  • Threads live in a fixed array nil.threads[CH_CFG_MAX_THREADS+1] (nil/include/ch.h:610), max 16 (:330-336); chThdCreateI does tp = &nil.threads[tdp->prio], so priority == table index and the last slot is idle (nil/src/ch.c:762-793, THD_TABLE_* at ch.h:634-661). No ready list: chSchReadyI sets state and nil.next = min(next, tp) (ch.c:583-596); sleeping scans the table for the first READY (:712-727). Timeouts are per-thread counters walked by the tick handler, with a tickless variant computing the next alarm (:354-444).

  • thread_t is ~ctx + state + one union word + timeout (+ event/msg fields) (ch.h:521-552); mutexes and registry are forced off (:177-190); no dynamic creation. Shares with RT: identical suffix contract and SV# state checker (ch.c:119-258), same chconf.h names, the same ARMv7-M port (chcoreasm.S:45-53 switches CONTEXT_OFFSET for RT/NIL), and OSLIB (mailboxes, pools, factory) runs on both (oslib/readme.txt). API surface: chSem*, chEvt*, chMsg*, chThdSuspend/Resume/Enqueue/Dequeue*, chSys*.

  • No numeric footprint claims in these headers; the claim is structural. Note the license tiering: basic tier caps tick at 1 kHz and disables tickless and messages (ch.h:301-319).

A6 Debug and quality

  • Checks/asserts selectable (chdebug.h:116-152); stack checking is in the port: port_switch compares PSP against wabase or, with PORT_ENABLE_GUARD_PAGES, moves an MPU no-access region to the new thread’s stack base (chcore.h:735-757, chcore.c:192-222); CH_DBG_FILL_THREADS pre-fills stacks (chthreads.c:96-102).

  • Trace: 128-entry ring of packed events (type:3, state:5, rtstamp:24, time, union) for ready/switch/isr/halt/user, mask-selectable (rt/include/chtrace.h:51-66,107-216). Statistics: IRQ and context-switch counters plus worst/best critical-zone times via TM (rt/include/chstats.h:55-61).

  • 8.0 additions: CH_CFG_HARDENING_LEVEL 0..3 with chSftAssert/chSftValidateDataPointerX embedded in list walks (rt/include/chsafety.h:83-151, chlists.h:444,483), a Runtime Faults Collection Unit mask + hook (rt/include/chrfcu.h:38-45), and chchecks.h erroring on any missing chconf.h symbol and wrong _CHIBIOS_RT_CONF_VER_8_0_ (:46-120). MISRA posture is visible only as PC-lint rule annotations (/*lint -e9075 [8.4]*/, chcore.c:76, chtime.h:440); no formal claim in headers.

  • Port contract (chcore.h): port_init, port_lock/unlock(_from_isr), port_disable/suspend/enable, port_get_lock_status/is_locked, port_is_isr_context, port_switch, PORT_SETUP_CONTEXT (:655-661), PORT_IRQ_PROLOGUE/EPILOGUE, port_wait_for_interrupt, port_timer_* (chcore_timer.h), and SMP chcoresmp.h (PORT_CORES_NUMBER, port_get_core_id, spinlock, port_notify_instance).

Part B: NuttX

B1 Scheduler

  • g_readytorun is a dq_queue_t whose head is the running task in non-SMP (sched/init/nx_start.c:95, sched/sched/sched.h:72,391). nxsched_add_prioritized inserts after all entries with prio >= new (FIFO among equals) and returns true when it becomes head (sched.h:477-528). nxsched_add_readytorun: if the running task holds lockcount>0 and the newcomer is higher, it parks in g_pendingtasks as TSTATE_TASK_PENDING; otherwise insert, and a new head becomes RUNNING (sched_addreadytorun.c:69-121).

  • sched_lock is a per-TCB counter (sched_lock.c:67-99); sched_unlock at count 1 merges pending tasks (sched_mergepending.c:64-152), replenishes an expired RR slice, handles sporadic timeslice == -1, and context-switches if needed (sched_unlock.c:57-180).

  • Priority is uint8_t, higher = more urgent, idle tasks are pid < NCPUS (include/nuttx/sched.h:619, sched.h:75); one g_idletcb[CONFIG_SMP_NCPUS] (nx_start.c:198,328-420).

  • SMP: g_assignedtasks[cpu] holds each CPU’s running task, g_readytorun becomes the global pool of unassigned tasks, plus TSTATE_TASK_ASSIGNED (sched.h:171-200, include/nuttx/sched.h:252). Adding a task picks the CPU running the lowest priority within affinity (nxsched_select_cpu, sched.h:588) and either switches locally or sets g_delivertasks[cpu] and IPIs (nxsched_deliver_task, sched.h:563-585); the target runs nxsched_process_delivered -> nxsched_switch_running which scans the ready list for an affinity-compatible higher task (sched_process_delivered.c:63-120, sched_addreadytorun.c:152-216). TCB_FLAG_CPU_LOCKED pins tasks.

  • RR: CONFIG_RR_INTERVAL ms, timeslice decremented per tick; at 0 it is refilled and the task is moved behind equals or an IPI handler does it on the owning CPU (sched_roundrobin.c:127-230). Sporadic (CONFIG_SCHED_SPORADIC): struct sporadic_s {hi_priority, low_priority, max_repl, nrepls, repl_period, budget, eventtime, replenishments[MAXREPL]} each with its own wdog (include/nuttx/sched.h:331-348). Budget start raises to high priority and arms a budget timer (sched_sporadic.c:250-275); on expiry the task drops to low priority (deferred via timeslice=-1 if the scheduler is locked) and time not consumed because the task was suspended is turned into a replenishment scheduled at repl_period/2 - unrealized (:439-525); the interval timer restarts the budget (:552-566); context switches call suspend/resume to account unrealized budget (sched_switchcontext.c:65-77, sched_sporadic.c:1057-1199). Validation enforces budget <= repl/2 (:776-820). This is POSIX SCHED_SPORADIC temporal protection.

B2 Interrupts and context switch

  • exception_common saves r4-r11, BASEPRI, CONTROL, EXC_RETURN (+s16-s31) onto the interrupted stack (MSP or PSP per EXC_RETURN), optionally moves to the interrupt stack, and calls arm_doirq(irq, regs) (arch/arm/src/armv7-m/arm_exception.S:124-209). arm_doirq stores regs into the running TCB, dispatches, then tcb = this_task(); nxsched_switch_context(running, tcb); return tcb->xcp.regs (arm_doirq.c:59-133). The assembly restores from the returned pointer, selects MSP/PSP from its EXC_RETURN, restores BASEPRI/CONTROL and bx r14 (arm_exception.S:215-243): a switch is simply returning another task’s frame.

  • Thread-side switches: up_switch_context is sys_call0(SYS_switch_context) unless already in an ISR (arch/arm/include/irq.h:87-94), handled in arm_svcall.c:96-106; arm_fullcontextrestore is SYS_restore_context (arm_internal.h:144-145). PendSV is used for direct vectors and signal delivery (arm_doirq.c:46-57, arm_schedulesigaction.c:85-140), which rewrites the saved frame so the task returns into arm_sigdeliver and later SYS_restore_context (arm_sigdeliver.c:57-192).

  • up_irq_save returns the old BASEPRI and raises it to NVIC_SYSH_DISABLE_PRIORITY (arch/arm/include/armv7-m/irq.h:385-393); CONFIG_ARCH_HIPRI_INTERRUPT keeps zero-latency IRQs above it. enter_critical_section is up_irq_save unless CONFIG_IRQCOUNT; non-SMP counts rtcb->irqcount (sched/irq/irq_csection.c:258-284); SMP takes the global g_cpu_irqlock spinlock, marks g_cpu_irqset, and uses g_cpu_nestcount[cpu] for ISR nesting (:92-254,335-433); restore_critical_section re-acquires for the incoming task (include/nuttx/irq.h:407-419). NVIC nesting is allowed; CONFIG_ARCH_INTERRUPTSTACK gives a dedicated MSP (arm_exception.S:180-203,256-265, arm_initialstate.c:179-207) and SMP requires it (sched/Kconfig:343-366).

  • Syscalls: arm_svcall default case validates the number and nesting depth (CONFIG_SYS_NNEST), saves return PC/EXC_RETURN/CONTROL in xcp.syscall[], sets PC to arm_dispatch_syscall and CONTROL to privileged, flags TCB_FLAG_SYSCALL (arm_svcall.c:311-356); arm_dispatch_syscall indexes g_stublookup[nr] and returns via svc SYS_syscall_return, which restores unprivileged state and unmasks signals (arm_dispatch_syscall.S:78-102, arm_svcall.c:121-152). Build modes are selected by CONFIG_BUILD_FLAT/PROTECTED/KERNEL conditionals (arm_svcall.c:168,206, arm_task_start.c:36, arm_createstack.c:119-147).

B3 Time

  • Watchdogs are not delta-encoded anymore: struct wdog_s {node, arg, func, expired} holds an absolute tick (include/nuttx/wdog.h:85-93) and g_wdactivelist is sorted with wrap-safe clock_compare (sched/wdog/wd_start.c:189-226, include/nuttx/clock.h:540-559). wd_start_abstick reinserts, and if the new head changed (and we are not inside expiration) reprograms the one-shot (wd_start.c:268-330); wd_expiration pops expired entries and calls them under the critical section with a nested flag (:107-165); wd_cancel reprograms or stops when the head is removed (wd_cancel.c:61-115).

  • Tick mode: nxsched_process_timer bumps ticks, runs RR/sporadic per CPU, then wd_timer (sched_processtick.c:169-202). Tickless: reads hardware ticks (up_timer_gettick), updates the scheduler clock, runs wd_timer (sched_processtickless.c:342-377); RR/sporadic deadlines are themselves a wdog g_sched_event whose delay is the minimum remaining slice across CPUs (:135-154,286-319,448-460); nxsched_reassess_timer re-evaluates after sched_unlock (:416-426). Alarm vs interval back-ends and g_oneshot_maxticks clamp (Kconfig:76-100, wdog.h:143-191).

  • CONFIG_HRTIMER adds a nanosecond timer layer (list or rb-tree, sched/hrtimer/hrtimer.h, hrtimer_start.c:58-91) under which wd_timer becomes a callback (wd_start.c:365-373). clock_systime_ticks sources from RTC_HIRES, hardware tick, or the software counter (clock_systime_ticks.c:66-86); CONFIG_SYSTEM_TIME64 widens clock_t; default tick is 10 ms (include/nuttx/clock.h:153-156). All timed waits share one tcb->waitdog (include/nuttx/sched.h:656).

B4 Waiting and PI

  • nxsem_wait_slow (sched/semaphore/sem_wait.c:73-316): under the critical section, returns -EINTR on unmasked pending signals, takes the fast path with atomics (mutex holder word with a BLOCKING bit, or atomic_sub on the count), else records waitobj, adds the mutex owner as holder, with SEM_PRIO_INHERIT does sched_lock(); nxsem_boost_priority(), then nxsched_remove_self, TSTATE_WAIT_SEM, priority-ordered insert in the wait list, and up_switch_context. nxsem_post_slow uses cmpxchg, releases the holder, pops the first waiter, cancels its waitdog, readies it, then nxsem_restore_baseprio (sem_post.c:72-274).

  • Holders: one semholder_s per (sem, task) with a count, drawn from a global CONFIG_SEM_PREALLOCHOLDERS pool (PANIC when exhausted) or one inline holder (sem_holder.c:58-115). Boost: for each holder, nxsched_set_priority(holder, waiter_prio) if higher (:315-338,687-701). It is not transitive: a holder blocked on another semaphore does not propagate the boost to that semaphore’s holders. Restore recomputes max(base, boost_priority, highest waiter on every held sem) (:391-434).

  • Timeout race: nxsem_tickwait arms waitdog with nxsem_timeout, then waits and cancels (sem_tickwait.c:69-126); the callback checks task_state == TSTATE_WAIT_SEM inside the critical section before nxsem_wait_irq, which unlinks, repairs the count or mutex blocking bit, sets errcode, and readies (sem_timeout.c:60-83, sem_waitirq.c:71-133). Signals use the same path with EINTR (sig_dispatch.c:693-706).

  • Message queues keep prioritized waitfornotempty/notfull lists and counters (include/nuttx/mqueue.h:103-118); waiters block with waitdog and are woken by nxmq_notify_send/receive, which also fires the registered sigevent (mq_sndinternal.c:127-306, mq_waitirq.c:65-112). Signals: nxsig_tcbdispatch readies a TSTATE_WAIT_SIG task, queues actions and schedules up_schedule_sigaction (cross-CPU via nxsched_smp_call_single), handles SIGCONT (sig_dispatch.c:489-751).

  • Deletion: nxtask_terminate marks EXIT_PROCESSING, IPIs a remote running task, then nxtask_exithook -> nxtask_recover: cancels waitdog, nxsem_recover (unlinks from wait list, fixes count, releases all held holders bumping counting-semaphore counts), nxmq_recover, sporadic reset, robust-mutex marking (task_terminate.c:114-186, task_recover.c:65-92, sem_recover.c:68-135, sem_holder.c:969-988).

B5 Isolation and build modes

  • PROTECTED: one privileged kernel blob and one user blob; the user image exports a struct userspace_s at CONFIG_NUTTX_USERSPACE with entry point, text/data/bss/heap bounds, and task_startup/signal_handler trampolines (include/nuttx/userspace.h:68,90-122). SYS_task_start/SYS_pthread_start drop to CONTROL_NPRIV and jump into user space; SYS_signal_handler(_return) bounce through it (arm_svcall.c:169-304).

  • Syscall plumbing: syscall/syscall.csv generates user proxies/ (issue svc) and kernel stubs/ plus g_stublookup (syscall/syscall_stublookup.c:45-57); numbering comes from syscall_lookup.h offset by CONFIG_SYS_RESERVED (include/sys/syscall.h:38-66). Kernel re-privileges via CONTROL on entry and demotes on return.

  • MPU: region allocator and power-of-two regions with subregion-disable masks (arm_mpu.c:252-258,368-407,472-513), mpu_control(enable, hfnmiena, privdefena) (:429-444), convenience macros mpu_user_flash/intsram, mpu_priv_*, mpu_peripheral (mpu.h:452-578); boards configure the user regions. KERNEL mode adds per-process address environments (CONFIG_ARCH_ADDRENV, sched/addrenv/) and kmm_map_user for user pointers inside the kernel (sem_wait.c:197-199); stacks come from the kernel or user heap by thread type (arm_createstack.c:119-147).

  • What is checked: syscall number range and nesting depth, signal delivery deferred while TCB_FLAG_SYSCALL (sig_dispatch.c:566), privilege bit on every transition. Pointer arguments are not validated by the stubs in PROTECTED mode (only MPU faults catch bad accesses); PI holders panic on pool exhaustion.

B6 Observability and quality

  • sched_note_* emits a typed event stream (start/stop/suspend/resume, CPU, preempt lock, csection, spinlock, syscall, IRQ, wdog, heap, dump/printf) with runtime filter modes (include/nuttx/sched_note.h:70-75,239-271) from hooks in sched_switchcontext.c:96-99, sched_lock.c:94, irq_csection.c:316, irq_dispatch.c:138-149, wd_start.c:60-79; selectable via CONFIG_SCHED_INSTRUMENTATION_* (sched/Kconfig:1176-1335).

  • Critmonitor records per-thread worst critical-section, preemption-off, busy-wait and run times with the caller address and can panic over thresholds (sched_critmonitor.c:203-383, include/nuttx/sched.h:708-736, Kconfig:921-1000); IRQ monitor counts and max times per vector, exposed in procfs (sched/irq/irq.h:50-58, irq_procfs.c); CPU load (sched_cpuload.c:141-166).

  • Stacks: colored at creation (arm_createstack.c:187-194), high-water scan (arm_checkstack.c:94-150,228-261), SP range or margin check on every switch (nxsched_checkstackoverflow.c:71-94), interrupt-stack margin assert (irq_dispatch.c:160-164), compiler-instrumented r10 limit check (arm_stackcheck.c, CONFIG_ARMV7M_STACKCHECK), deepest-call recording (sched/instrument/stack_monitor.c:61-93).

  • Assert: _assert prints version, file:line, registers, IRQ/kernel/user stacks, backtraces, all tasks with state names, optional deadlock detection, pauses other CPUs and captures their registers, then coredump/panic notifiers/board crashdump (sched/misc/assert.c:249-772). Quality rules: INVIOLABLES.md (strict POSIX, coding standard, no copyleft, all platforms); CONTRIBUTING.md:226-236,333-339,397-405 demand real-hardware apps/ostest logs (QEMU does not count), zero-trust review of test claims, tools/checkpatch.sh. Everything is Kconfig-driven (sched/Kconfig ~1800 lines).

ChibiOS: ideas to borrow

  • The S/I/X suffix contract backed by a runtime lock_cnt/isr_cnt state checker (SV#1-11) catches context misuse early and cheaply.

  • Fake-exception-frame ISR epilogue that defers rescheduling to a single tail (__port_switch_from_isr + svc) and the BASEPRI “kernel mask” leaving fast IRQs untouched.

  • Stack-allocated timeout timers (chSchGoSleepTimeoutS) and lazy-update delta list with adaptive minimum delta plus RFCU fault logging instead of silent misses.

  • Transitive PI loop that re-sorts the boosted thread in whatever queue it sits in, and LIFO unlock recomputation from the owned-mutex stack.

  • Hardening levels with link-back validation on list walks; ROM ch_debug offsets table for debugger plugins; per-core OS instance struct for SMP.

  • NIL’s “priority == table index” scheduler for tiny targets, sharing the same API, port and OSLIB.

ChibiOS: weaknesses

  • One global spinlock for SMP (port_lock), no per-core scheduler locks; cross-core wakeups rely on notifications.

  • Mutexes must be unlocked in LIFO order; no priority ceiling protocol; PI only across mutexes, condvars, and priority semaphores/messages.

  • Timer callbacks always run in ISR context; no thread-context timer option.

  • No memory isolation model beyond MPU guard pages; license tiering (NIL) gates features; MISRA conformance is only implied by lint comments.

  • Events are per-thread bitmasks (32 flags) and listeners are intrusive structs the user must own.

NuttX: ideas to borrow

  • “Return a different regs pointer” context switch keeps the port tiny and uniform across IRQ and SVC paths; restore_critical_section on switch.

  • Table-driven task lists (g_tasklisttable attributes: prioritized/indexed/offset) so one add/remove routine serves all wait states.

  • g_pendingtasks for readying under sched_lock with later merge; POSIX SCHED_SPORADIC budget/replenishment as a temporal-protection primitive.

  • Absolute-tick wdog list with wrap-safe compare and a single waitdog per TCB; tickless RR/sporadic implemented as an ordinary wdog.

  • Critmonitor with caller addresses and panic thresholds; sched_note typed event stream with runtime filters; rich assert/crash dump with all-task state and SMP pause-and-capture.

  • Generated syscall proxies/stubs from a CSV plus nested syscall save area; task_recover path that repairs semaphore counts and PI holders on kill.

NuttX: weaknesses

  • Priority inheritance is single-level (not transitive) and holder bookkeeping panics when the preallocated pool is exhausted.

  • SMP critical section is one global spinlock (g_cpu_irqlock) with bitmask bookkeeping; scheduler state spans several globals and IPI deliver flags, which is intricate to reason about.

  • Most kernel paths run with interrupts masked via enter_critical_section (BASEPRI to NVIC_SYSH_DISABLE_PRIORITY), so critical sections are long by construction; HIPRI interrupts are the only escape.

  • PROTECTED-mode stubs do not validate user pointers; isolation leans on MPU faults; pthread_start/signal trampolines live in a fixed-address userspace header.

  • Round-robin needs an IPI round-trip in SMP; sched_yield is implemented as a reprioritize-to-self; large Kconfig surface makes behavior configuration-dependent and hard to audit.