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/Unlockare thin wrappers overport_lock/unlock(rt/include/chsys.h:371-398);chSysLockFromISR/UnlockFromISRoverport_lock_from_isr(:415-441);CH_IRQ_PROLOGUE/EPILOGUEwrapPORT_IRQ_PROLOGUE/EPILOGUEplus hooks (:112-131).ARMv7-M:
port_lock()raises BASEPRI toCORTEX_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_PRIORITYswitches to PRIMASK/cpsidand PendSV, dropping fast IRQs and the extraport_extctx(chcore.h:191-207).State machine for debug: each OS instance carries
isr_cntandlock_cnt(rt/include/chdebug.h:61-81). Every transition primitive asserts preconditions and halts withSV#1..SV#11: e.g.__dbg_check_lockrequiresisr_cnt==0 && lock_cnt==0then setslock_cnt=1;__dbg_check_enter_isrincrementsisr_cntunder 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 inchdebug.c:20-70.ISR exit:
__port_irq_epiloguelocks, checksSCB->ICSR.RETTOBASE, pushes a fake exception frame whose PC is__port_switch_from_isrifchSchIsPreemptionRequired()else__port_exit_from_isr, and returns still locked (chcore.c:227-274).__port_switch_from_isrcallschSchDoPreemptionthensvc #0;SVC_Handlerdiscards 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_tinside the per-coreready_list_t {pqueue, current}(rt/include/chobjects.h:373-383).ch_pqueue_insert_behindscans whileprio >= p->prio(FIFO among equals);insert_aheadscans whileprio > p->prio(LIFO among equals) (rt/include/chlists.h, pqueue section).ch_sch_prio_insertis inlined whenCH_CFG_OPTIMIZE_SPEEDelse 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)).chSchReadyIinserts behind and, in SMP, pokes the owning core (chschd.c:285-299);chSchGoSleepSpops the highest and switches (:310-341);chSchWakeupSswitches immediately if the woken thread is higher, placing the current thread ahead of its peers since its quantum is not spent (:401-451);chSchRescheduleSonly iffirstprio > current(:462-473).Round robin:
CH_CFG_TIME_QUANTUM(template default 0,rt/templates/chconf.h:145);chSysTimerHandlerIdecrementsticks(rt/src/chsys.c:262-283);chSchIsPreemptionRequireduses>while quantum remains,>=when exhausted (chschd.c:489-507);chSchDoPreemptionrequeues behind with a fresh quantum or ahead (:521-561);chSchDoYieldSyields to equal-or-higher (>=) and goes behind (:606-617).Idle: per-instance
idlethreadatIDLEPRIO, optional (CH_CFG_NO_IDLE_THREAD), with enter/leave hooks (rt/src/chinstances.c:61-73,174-191). SMP (RT 7/8): oneos_instance_tper core with its own rlist, vtlist, dbg, trace (chobjects.h:416-480),currcoreresolves viaport_get_core_id()(chsys.h:51-58), threads carryowner; cross-core wakeups usechSysNotifyInstanceand a single global spinlock inport_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_enqueuecomputesdelta = (now-lasttime) + delayand inserts by delta, reprogramming the alarm only if it becomes first (rt/src/chvt.c:218-262).vt_set_alarmclamps tolastdelta(min safe delta) and loops re-arming if “now” already passed the deadline, recordingCH_RFCU_VT_INSUFFICIENT_DELTAand raisinglastdeltaadaptively (:69-128); same invt_insert_first(:139-208).Callbacks run from the tick/alarm ISR between
chSysUnlockFromISR()/chSysLockFromISR()(:540-542,591-595): I-class only, never thread context.chVTDoTickIin tickless mode advanceslasttimelazily 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_TIMEDELTA0 = tick mode (decrement head delta,:525-549), >=2 = tickless withport_timer_set/start/stop_alarm(rt/include/chvt.h:42-52, template 2 atchconf.h:120).systime_tmay be 16/32/64 bits,sysinterval_tmust be >= resolution (rt/include/chtime.h:75-122);VT_MAX_DELAYmasks when intervals are wider than systime (chvt.c:36-39).chTimeAddX/DiffX/IsInRangeXare plain modular unsigned arithmetic (chtime.h, ~431-449);chVTGetTimeStampIextends to a 64-bit monotonic stamp (chvt.c:682-706).Thread timeouts:
chSchGoSleepTimeoutSarms a stack-allocatedvirtual_timer_twith__sch_wakeup, sleeps, then resets if still armed (chschd.c:363-382); the callback dequeues per wait state, setsMSG_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, loopwhile (tp->prio < currtp->prio): raisetp->prio; iftpisWTMTX, re-sort it in that mutex’s queue, settp = that mutex's ownerandcontinue(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 sleepWTMTX; ownership is handed over by the unlocker (asserts:256-257).chMtxUnlockSrequires LIFO unlock order (mtxlist == mp,:464), recomputesnewprio = 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 addscnt(:110-112,180-188);chThdSetPrioritypreserves 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_PRIORITYswaps FIFO insert for priority insert (rt/src/chsem.c:79-83,221-238);chSemSignalWaitis atomic; reset with message (:173-184).Events: per-thread
ependingmask;chEvtSignalIreadies on any (WTOREVT) or all (WTANDEVT) (rt/src/chevents.c:340-353); listeners are a linked list off the source, broadcast ORs flags, filters bywflags, then signals the listener thread’s event bits (:148-161,391-410). Messages: sender enqueues ontp->msgqueue(priority order optional,rt/include/chmsg.h:52-56), readies a receiver inWTMSG, sleepsSNDMSGQuntilchMsgReleaseS; receiver marks itSNDMSG(rt/src/chmsg.c:84-133): a rendezvous with zero copies.Exit/wait:
chThdExitSreadies all threads linked onwaiting, removes from registry ifrefs==0and nodispose, sleepsFINAL(chthreads.c:711-745);chThdWaitlinks ontp->waitingthenchThdReleases (:768-793);refs/disposedrive heap or pool reclamation (rt/src/chdynamic.c:53-62). Registry exports a ROMch_debugoffsets 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);chThdCreateIdoestp = &nil.threads[tdp->prio], so priority == table index and the last slot is idle (nil/src/ch.c:762-793,THD_TABLE_*atch.h:634-661). No ready list:chSchReadyIsets state andnil.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_tis ~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 andSV#state checker (ch.c:119-258), samechconf.hnames, the same ARMv7-M port (chcoreasm.S:45-53switchesCONTEXT_OFFSETfor 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_switchcompares PSP againstwabaseor, withPORT_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_THREADSpre-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_LEVEL0..3 withchSftAssert/chSftValidateDataPointerXembedded 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), andchchecks.herroring on any missingchconf.hsymbol 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 SMPchcoresmp.h(PORT_CORES_NUMBER,port_get_core_id, spinlock,port_notify_instance).
Part B: NuttX¶
B1 Scheduler¶
g_readytorunis adq_queue_twhose head is the running task in non-SMP (sched/init/nx_start.c:95,sched/sched/sched.h:72,391).nxsched_add_prioritizedinserts after all entries withprio >= new(FIFO among equals) and returns true when it becomes head (sched.h:477-528).nxsched_add_readytorun: if the running task holdslockcount>0and the newcomer is higher, it parks ing_pendingtasksasTSTATE_TASK_PENDING; otherwise insert, and a new head becomesRUNNING(sched_addreadytorun.c:69-121).sched_lockis a per-TCB counter (sched_lock.c:67-99);sched_unlockat count 1 merges pending tasks (sched_mergepending.c:64-152), replenishes an expired RR slice, handles sporadictimeslice == -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); oneg_idletcb[CONFIG_SMP_NCPUS](nx_start.c:198,328-420).SMP:
g_assignedtasks[cpu]holds each CPU’s running task,g_readytorunbecomes the global pool of unassigned tasks, plusTSTATE_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 setsg_delivertasks[cpu]and IPIs (nxsched_deliver_task,sched.h:563-585); the target runsnxsched_process_delivered->nxsched_switch_runningwhich scans the ready list for an affinity-compatible higher task (sched_process_delivered.c:63-120,sched_addreadytorun.c:152-216).TCB_FLAG_CPU_LOCKEDpins tasks.RR:
CONFIG_RR_INTERVALms,timeslicedecremented 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 viatimeslice=-1if the scheduler is locked) and time not consumed because the task was suspended is turned into a replenishment scheduled atrepl_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 enforcesbudget <= repl/2(:776-820). This is POSIXSCHED_SPORADICtemporal protection.
B2 Interrupts and context switch¶
exception_commonsaves 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 callsarm_doirq(irq, regs)(arch/arm/src/armv7-m/arm_exception.S:124-209).arm_doirqstoresregsinto the running TCB, dispatches, thentcb = 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 andbx r14(arm_exception.S:215-243): a switch is simply returning another task’s frame.Thread-side switches:
up_switch_contextissys_call0(SYS_switch_context)unless already in an ISR (arch/arm/include/irq.h:87-94), handled inarm_svcall.c:96-106;arm_fullcontextrestoreisSYS_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 intoarm_sigdeliverand laterSYS_restore_context(arm_sigdeliver.c:57-192).up_irq_savereturns the old BASEPRI and raises it toNVIC_SYSH_DISABLE_PRIORITY(arch/arm/include/armv7-m/irq.h:385-393);CONFIG_ARCH_HIPRI_INTERRUPTkeeps zero-latency IRQs above it.enter_critical_sectionisup_irq_saveunlessCONFIG_IRQCOUNT; non-SMP countsrtcb->irqcount(sched/irq/irq_csection.c:258-284); SMP takes the globalg_cpu_irqlockspinlock, marksg_cpu_irqset, and usesg_cpu_nestcount[cpu]for ISR nesting (:92-254,335-433);restore_critical_sectionre-acquires for the incoming task (include/nuttx/irq.h:407-419). NVIC nesting is allowed;CONFIG_ARCH_INTERRUPTSTACKgives 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_svcalldefault case validates the number and nesting depth (CONFIG_SYS_NNEST), saves return PC/EXC_RETURN/CONTROL inxcp.syscall[], sets PC toarm_dispatch_syscalland CONTROL to privileged, flagsTCB_FLAG_SYSCALL(arm_svcall.c:311-356);arm_dispatch_syscallindexesg_stublookup[nr]and returns viasvc 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 byCONFIG_BUILD_FLAT/PROTECTED/KERNELconditionals (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) andg_wdactivelistis sorted with wrap-safeclock_compare(sched/wdog/wd_start.c:189-226,include/nuttx/clock.h:540-559).wd_start_abstickreinserts, and if the new head changed (and we are not inside expiration) reprograms the one-shot (wd_start.c:268-330);wd_expirationpops expired entries and calls them under the critical section with a nested flag (:107-165);wd_cancelreprograms or stops when the head is removed (wd_cancel.c:61-115).Tick mode:
nxsched_process_timerbumps ticks, runs RR/sporadic per CPU, thenwd_timer(sched_processtick.c:169-202). Tickless: reads hardware ticks (up_timer_gettick), updates the scheduler clock, runswd_timer(sched_processtickless.c:342-377); RR/sporadic deadlines are themselves a wdogg_sched_eventwhose delay is the minimum remaining slice across CPUs (:135-154,286-319,448-460);nxsched_reassess_timerre-evaluates aftersched_unlock(:416-426). Alarm vs interval back-ends andg_oneshot_maxticksclamp (Kconfig:76-100,wdog.h:143-191).CONFIG_HRTIMERadds a nanosecond timer layer (list or rb-tree,sched/hrtimer/hrtimer.h,hrtimer_start.c:58-91) under whichwd_timerbecomes a callback (wd_start.c:365-373).clock_systime_tickssources from RTC_HIRES, hardware tick, or the software counter (clock_systime_ticks.c:66-86);CONFIG_SYSTEM_TIME64widensclock_t; default tick is 10 ms (include/nuttx/clock.h:153-156). All timed waits share onetcb->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-EINTRon unmasked pending signals, takes the fast path with atomics (mutex holder word with a BLOCKING bit, oratomic_subon the count), else recordswaitobj, adds the mutex owner as holder, withSEM_PRIO_INHERITdoessched_lock(); nxsem_boost_priority(), thennxsched_remove_self,TSTATE_WAIT_SEM, priority-ordered insert in the wait list, andup_switch_context.nxsem_post_slowuses cmpxchg, releases the holder, pops the first waiter, cancels itswaitdog, readies it, thennxsem_restore_baseprio(sem_post.c:72-274).Holders: one
semholder_sper (sem, task) with a count, drawn from a globalCONFIG_SEM_PREALLOCHOLDERSpool (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 recomputesmax(base, boost_priority, highest waiter on every held sem)(:391-434).Timeout race:
nxsem_tickwaitarmswaitdogwithnxsem_timeout, then waits and cancels (sem_tickwait.c:69-126); the callback checkstask_state == TSTATE_WAIT_SEMinside the critical section beforenxsem_wait_irq, which unlinks, repairs the count or mutex blocking bit, setserrcode, and readies (sem_timeout.c:60-83,sem_waitirq.c:71-133). Signals use the same path withEINTR(sig_dispatch.c:693-706).Message queues keep prioritized
waitfornotempty/notfulllists and counters (include/nuttx/mqueue.h:103-118); waiters block withwaitdogand are woken bynxmq_notify_send/receive, which also fires the registeredsigevent(mq_sndinternal.c:127-306,mq_waitirq.c:65-112). Signals:nxsig_tcbdispatchreadies aTSTATE_WAIT_SIGtask, queues actions and schedulesup_schedule_sigaction(cross-CPU vianxsched_smp_call_single), handles SIGCONT (sig_dispatch.c:489-751).Deletion:
nxtask_terminatemarksEXIT_PROCESSING, IPIs a remote running task, thennxtask_exithook->nxtask_recover: cancelswaitdog,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_satCONFIG_NUTTX_USERSPACEwith entry point, text/data/bss/heap bounds, andtask_startup/signal_handlertrampolines (include/nuttx/userspace.h:68,90-122).SYS_task_start/SYS_pthread_startdrop toCONTROL_NPRIVand jump into user space;SYS_signal_handler(_return)bounce through it (arm_svcall.c:169-304).Syscall plumbing:
syscall/syscall.csvgenerates userproxies/(issuesvc) and kernelstubs/plusg_stublookup(syscall/syscall_stublookup.c:45-57); numbering comes fromsyscall_lookup.hoffset byCONFIG_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 macrosmpu_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/) andkmm_map_userfor 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 insched_switchcontext.c:96-99,sched_lock.c:94,irq_csection.c:316,irq_dispatch.c:138-149,wd_start.c:60-79; selectable viaCONFIG_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:
_assertprints 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-405demand real-hardwareapps/ostestlogs (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_cntstate 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_debugoffsets 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_sectionon switch.Table-driven task lists (
g_tasklisttableattributes: prioritized/indexed/offset) so one add/remove routine serves all wait states.g_pendingtasksfor readying undersched_lockwith later merge; POSIXSCHED_SPORADICbudget/replenishment as a temporal-protection primitive.Absolute-tick wdog list with wrap-safe compare and a single
waitdogper TCB; tickless RR/sporadic implemented as an ordinary wdog.Critmonitor with caller addresses and panic thresholds;
sched_notetyped 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_recoverpath 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 toNVIC_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_yieldis implemented as a reprioritize-to-self; large Kconfig surface makes behavior configuration-dependent and hard to audit.