# 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 `chThdRelease`s (`: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.