Research note: Micrium uC/OS-III

Kind: Read-only source analysis, 2026-10-07, on a sparse shallow clone of weston-embedded/uC-OS3 at commit 9a3fc5f45d75 (Source/). Version V3.08.02 (os.h:38). Citations are file:line within Source/. Several mechanisms often attributed to uC/OS-III were removed before 3.08 and are absent from this tree (verified by grep): no os_pend_multi.c or OSPendMulti, no os_int.c deferred-post queue, no tick task, no tick wheel (only a vestigial OS_ERR_TICK_WHEEL_SIZE enum at os.h:537), no timer wheel. Their replacements are described below. Synthesized in R-001.


Q1 Scheduler

  • Bitmap: OSPrioTbl[OS_PRIO_TBL_SIZE] (os.h:118, 1194), MSB is priority 0. OS_PrioGetHighest (os_prio.c:78-107) has three compile-time variants (one word, two words, loop) around CPU_CntLeadZeros; insert and remove at os_prio.c:126-181.

  • Ready list: OSRdyList[OS_CFG_PRIO_MAX] of OS_RDY_LIST {HeadPtr, TailPtr, debug NbrEntries} (os.h:651-657). OS_RdyListInsert (os_core.c:1636-1645): a readied task goes to the tail only if its priority equals OSPrioCur, otherwise to the head. InsertHead, InsertTail, MoveHeadToTail at os_core.c:1705, 1790, 1871.

  • OSSched (os_core.c:430-515): returns if OSIntNestingCtr > 0 or OSSchedLockNestingCtr > 0, picks OSRdyList[OS_PrioGetHighest()].HeadPtr, no switch if the same TCB (453), else OS_TASK_SW() (482). New in 3.08: with OS_CFG_TASK_IDLE_EN == 0 the idle loop runs inside OSSched (484-510) and there is no idle task.

  • OSIntEnter (263-276) just increments OSIntNestingCtr (cap 250, 271) without masking interrupts; OSIntExit (299-386) masks (313), decrements (319), returns if still nested or scheduler locked, optionally checks the ISR-stack redzone (about 336), then OSIntCtxSw() (384) if a higher task is ready (345).

  • Scheduler lock: OSSchedLock and OSSchedUnlock (540-651) nest via the 8-bit OSSchedLockNestingCtr (os_type.h:59), overflow guard at 250 (565); unlock reschedules (632-645). Lock duration is measured with OS_TS_GET (2017-2040, OSSchedLockTimeMax os.h:1215-1217).

  • Round robin: OSSchedRoundRobinCfg (670) sets enable plus the default quantum (tick rate / 10); per-TCB TimeQuanta and TimeQuantaCtr are set at OSTaskCreate (os_task.c:387) or OSTaskTimeQuantaSet (2328). OS_SchedRoundRobin (os_core.c:2063-2120), called from OSTimeTick (os_time.c:565), decrements (2089) and, when zero with more than one task at that priority and the scheduler unlocked, MoveHeadToTail (2107). Yield at 724-770. Round robin is forbidden with dynamic tick (os.h:2175).

  • Idle task OS_IdleTask (907-931) at OS_CFG_PRIO_MAX-1 bumps OSIdleTaskCtr and OSStatTaskCtr and calls OSIdleTaskHook (929); application hook pointers os.h:1122-1132. OS_TS_GET is port-supplied (not in the tree) and feeds TCB and object TS fields, OSTickTime and Max (os_tick.c:111), OSTmrTaskTime, OSStatTaskTime.

Q2 Interrupts and critical sections

  • Every kernel critical section is CPU_CRITICAL_ENTER and EXIT plus CPU_SR_ALLOC (interrupt disable); the macros come from uC/CPU cpu.h, not in this tree (0 definitions in os.h; about 111 uses across os_*.c). There is no scheduler-lock-based OS_CRITICAL_ENTER and no deferred ISR post method in 3.08: ISR posts run OS_Post directly under interrupt masking (for example OSSemPost os_sem.c:687-790).

  • ISR misuse: every API, when OS_CFG_CALLED_FROM_ISR_CHK_EN (a mandatory define, os.h:2138), tests OSIntNestingCtr > 0 and returns one of 25 dedicated *_ISR codes in the OS_ERR enum (os.h:375-577; OS_ERR_PEND_ISR 455, OS_ERR_SCHED_LOCK_ISR 481, OS_ERR_TIME_DLY_ISR 540, OS_ERR_TMR_ISR 559). Blocking pends from an ISR are rejected but non-blocking pends allowed (os_sem.c:368-377, os_flag.c:385). Call-site counts: os_task.c 30, os_tmr.c 14, os_flag, os_q, os_sem 12 each, os_mutex 10.

  • Interrupt-disable time measurement is implemented in uC/CPU (CPU_IntDisMeasMaxGet, not in the tree); the kernel samples it into OSIntDisTimeMax from the stat task (os_stat.c:340), keeps per-TCB IntDisTimeMax (os.h:1055, reset os_task.c:2593) which the port’s OSTaskSwHook fills.

  • A dedicated ISR stack OSCfg_ISRStk (os_cfg_app.c:134-150), redzone-checked at OSIntExit and its usage computed by the stat task (os_stat.c:460-482).

Q3 Time

  • No tick task: the tick ISR calls OSTimeTick (os_time.c:556-570), which runs the hook, OS_SchedRoundRobin, OS_TickUpdate(1u) (569). OS_TickUpdate (os_tick.c:94-128) runs under CPU_CRITICAL_ENTER (102), bumps OSTickCtr and walks the list (OS_TickListUpdate 459-560), timing itself (111).

  • No wheel: OSTickList is one doubly linked delta list (OS_TICK_LIST os.h:1078-1084; TCB TickNext, TickPrev, TickRemain). OS_TickListInsert (os_tick.c:157-274): delta = (time + tick_base) - (OSTickCtr + elapsed) (169), walks subtracting predecessors’ TickRemain (235), reduces the successor’s remain; remove adds remain back to the successor (410, 434). Update decrements only the head and pops while TickRemain == 0 (486): DLY to RDY, PEND_TIMEOUT to RDY with OS_STATUS_PEND_TIMEOUT (531), plus mutex-owner disinheritance on waiter timeout (538).

  • OSTimeDly modes (os.h:294-297) resolved in OS_TickListInsertDly (os_tick.c:297-360): relative base = OSTickCtr (346); MATCH base = 0 (315); PERIODIC base = p_tcb->TickCtrPrev (323); if one or more periods were missed (base_offset >= time, 331) the base is advanced by whole periods, then TickCtrPrev += time (340), which is drift-free. A zero delta gives OS_ERR_TIME_ZERO_DLY.

  • Timers: OSTmrListPtr is also a delta list (OS_TmrLink os_tmr.c:1175-1240, same algorithm). OS_TmrTask (1360-1460) is the callback context: it holds OSTmrMutex (1375), then OS_TmrCondWait(head->Remain) (1384) atomically releases the mutex (1557) and pends on an internal condition variable OSTmrCond (OS_COND os.h:855-871; pend at 1585-1587), so the timer task sleeps until the next expiry instead of polling at OS_CFG_TMR_TASK_RATE_HZ; OS_TmrLink signals the condition when the head changes (1192, 1215). Callbacks run at 1431 and may call timer APIs; periodic timers re-link with Period (1440). The timer unit is OSTmrToTicksMult = TickRate / TmrTaskRate (1103, 186-188).

  • Dynamic tick: OS_CFG_DYN_TICK_EN; the BSP implements OS_DynTickGet and Set (os.h:2097-2099); OS_TickUpdate programs OSTickCtrStep = head TickRemain (os_tick.c:118-126), insert and remove reprogram (191, 223, 406, 424), entry OSTimeDynTick (os_time.c:590). OS_CFG_TICK_EN == 0 removes ticking entirely (os.h:2345-2352). OS_TICK is 32-bit (os_type.h:85); CPU_TS width (32 or 64) is a uC/CPU setting, gated by OS_CFG_TS_EN (os.h:2184).

Q4 Waiting

  • Pend list: OS_PEND_LIST {HeadPtr, TailPtr} (os.h:666-671) embedded in every object via the OS_PEND_OBJ common prefix (690-703, with a comment that layouts must be kept in sync by hand); the TCB has PendNext, PendPrev, PendObjPtr, PendOn, PendStatus. OS_PendListInsertPrio (os_core.c:1313-1360) is a linear walk inserting before the first lower-priority node (1336): priority order, FIFO within equal priority. OS_PendListChangePrio (1208-1222) removes and reinserts.

  • OS_Pend (1005-1030) sets PendOn and Status, OS_TaskBlock (2137-2160: tick-list insert if timeout, PEND versus PEND_TIMEOUT, ready-list remove), pend-list insert. OS_Post (1477-1560) switches on TaskState: deposits the message in the TCB, removes from the pend list and tick list (1516), readies, PendStatus = OK (1521); suspended-pending tasks become SUSPENDED (1530-1550). OSSemPost loops for OS_OPT_POST_ALL (os_sem.c:771-785); OSQPost hands the message straight to a waiter’s TCB (os_q.c:878) else OS_MsgQPut FIFO or LIFO (864).

  • OSPendMulti is gone (no OS_TASK_PEND_ON_MULTI in os.h:166-173); the task-built-in semaphore and queue (Q5) is the intended substitute.

  • Pend abort: OS_PendAbort (1057-1118) sets PendStatus to the reason, removes from both lists, readies. OSSemPendAbort (os_sem.c:562-660) aborts the head or all; OSTaskSemPendAbort (os_task.c:1719-1790) rejects self and non-task-sem waits. Penders decode PendStatus after OSSched (os_sem.c:471-515, os_mutex.c:506-560).

  • Deletion: OS_OPT_DEL_NO_PEND fails with OS_ERR_TASK_WAITING if waiters exist (os_sem.c:237-249, os_mutex.c:237-252); OS_OPT_DEL_ALWAYS aborts each waiter with OS_STATUS_PEND_DEL (os_sem.c:253-263, os_mutex.c:256-266) and for a mutex also removes it from the owner’s group and disinherits (os_mutex.c:276-283).

  • Timeout versus post race: timeouts run in the tick ISR under CPU_CRITICAL_ENTER (os_tick.c:102) and posters hold CPU_CRITICAL_ENTER around OS_Post plus OS_TickListRemove (os_core.c:1516); whichever runs first removes the TCB from the other list and fixes PendStatus (os_tick.c:531 versus os_core.c:1521). Serialized by interrupt masking; no window, at the cost of list walks with interrupts off.

Q5 Built-in task primitives

  • Task semaphore: SemCtr in the TCB; OSTaskSemPend (os_task.c:1514-1700) consumes if SemCtr > 0 (1577) else OS_Pend with a NULL object (1626, PEND_ON_TASK_SEM), so no pend list is needed. OSTaskSemPost (1824-1935): OS_Post(NULL, …) if the target is waiting on its task semaphore (1900), else SemCtr++ with an overflow check (1891, 1918).

  • Task queue: OS_MSG_Q MsgQ in the TCB; OSTaskQPend (754-900) OS_MsgQGet (823) else a NULL-object pend (860); OSTaskQPost (1067-1170) OS_MsgQPut (1128) or direct OS_Post when PendOn == TASK_Q (1141). OS_MSG nodes come from one static pool OSCfg_MsgPool (os_msg.c:63-98), errors OS_ERR_Q_MAX and MSG_POOL_EMPTY (298-303).

  • Flags: SET or CLR times ALL or ANY plus CONSUME (os.h:229-242). OSFlagPend evaluates immediately (os_flag.c:444-620, for example 459-461), else OS_FlagBlock saves FlagsPend and FlagsOpt (1135-1136); after wake the pender itself applies consume (692-700). OSFlagPost walks the whole pend list evaluating each waiter’s own mode (1030-1070) via OS_FlagTaskRdy (1259-1303).

  • Mutex: OS_MUTEX {MutexGrpNextPtr, OwnerTCBPtr, OwnerNestingCtr} (os.h:836-838); TCB BasePrio plus MutexGrpHeadPtr (a singly linked group, OS_MutexGrpAdd os_mutex.c:986-990). OSMutexPend (355-560): acquire plus group add (442); recursive re-entry increments nesting (458) but returns OS_ERR_MUTEX_OWNER (467); inheritance: if owner.Prio > caller.Prio then OS_TaskChangePrio(owner, caller prio) (498-499). OS_TaskChangePrio (os_task.c:2760-2845) is a do-while over the blocking chain: if the raised task is itself pending on a mutex, that owner is raised next (2804-2820, 2843-2844), so inheritance is transitive. OSMutexPost (738-880): owner only (805), nesting– (821), group remove, and if inherited, prio_new = min(BasePrio, OS_MutexGrpPrioFindHighest) (842-849); FindHighest (1041-1070) scans the pend-list head of every mutex still owned, giving a correct restore with several mutexes. Disinheritance also runs on waiter timeout (os_tick.c:534-545), waiter deletion (os_task.c:567-583), waiter priority change (os_task.c:2815-2830), and mutex deletion.

  • Owner deletion: OSTaskDel leads to OS_MutexGrpPostAll (os_task.c:605; os_mutex.c:1084-1131), handing each owned mutex to its head waiter. Likely bug: os_mutex.c:1115 sets p_mutex->OwnerTCBPtr = p_tcb (the task being deleted) while 1117 adds the mutex to p_tcb_new’s group; the new owner’s OSMutexPost then fails with OS_ERR_MUTEX_NOT_OWNER (805). Compare the correct hand-off at 855.

Q6 Memory and objects

  • No kernel heap: all objects are caller-provided structs (OSTaskCreate takes OS_TCB* and stack base and size; OSSemCreate takes OS_SEM*). Kernel RAM is static arrays in os_cfg_app.c:134-160 published as const OSCfg_* (170 onward). OSMemCreate (os_mem.c:72-170) threads an intrusive free list through user memory (142), requires at least 2 blocks of pointer size, pointer aligned (116-130); Get and Put are O(1) (243, 328 OS_ERR_MEM_FULL).

  • Object tags: OS_OBJ_TYPE_* are ASCII 4-character words (os.h:191-204), present when OS_OBJ_TYPE_REQ (122), checked under OS_CFG_OBJ_TYPE_CHK_EN giving OS_ERR_OBJ_TYPE (os_sem.c:401-407); OS_CFG_OBJ_CREATED_CHK_EN rejects double creation (os_mem.c:about 157).

  • Debugger exports: os_dbg.c defines 92 const OSDbg_* values: sizeof(OS_TCB) (173), OS_SEM (146), OS_TMR (191), OS_TICK_LIST (175), OSRdyList (156), all configuration switches (for example 108-109), OS_VERSION (197), an endianness probe (54), and OSDbg_DataSize as the total kernel RAM (207-300). OS_Dbg_Init (403) references them through volatile pointers so the linker keeps them.

  • Stack check: OSTaskStkChk (os_task.c:2032-2125) counts zero words from the base (2109); needs OS_OPT_TASK_STK_CHK and STK_CLR at creation (325-327, 2082); the stat task runs it per task (os_stat.c:441). Optional redzone (os_task.c:362, 2142) and StkLimitPtr for hardware limits (384).

  • Stat task: OSStatTaskCPUUsageInit (os_stat.c:191-262) calibrates the idle counter maximum (258); OS_StatTask (291-500) computes 10000 - scaled ctr/max (367), per-task usage from profiling cycles, ISR stack usage (482).

  • Safety: OS_SAFETY_CRITICAL (application-defined OS_SAFETY_CRITICAL_EXCEPTION, 65 uses) traps NULL p_err; OS_SAFETY_CRITICAL_IEC61508 adds OSSafetyCriticalStart (os_core.c:406-409), after which every create returns OS_ERR_ILLEGAL_CREATE_RUN_TIME (os_task.c:260, os_mem.c:97-98, os_flag.c:76-77) and every delete OS_ERR_ILLEGAL_DEL_RUN_TIME (OSTaskDel os_task.c:483+, OSSemDel os_sem.c:173+, OSMutexDel os_mutex.c:169+).

  • OS_CFG_ARG_CHK_EN is a mandatory define (os.h:2128); a typical block validates pointers and the opt switch (os_sem.c:383-399). Every API reports through OS_ERR *p_err (enum os.h:371-580, alphabetic groups at 1000 steps), many also returning a value.

Ideas to borrow

  • The delta list that reuses TCB links and is popped only at the head (os_tick.c:157-274, 459-560); the same code shape for timers (os_tmr.c:1175-1240).

  • A timer task driven by an internal condition variable with timeout equal to the next expiry, releasing its mutex atomically (os_tmr.c:1384, 1541-1631): no periodic polling, and callbacks may safely re-arm timers.

  • Drift-free periodic delay via TickCtrPrev with missed-period catch-up (os_tick.c:323-340).

  • Task-built-in semaphore and queue needing no pend list (os_task.c:1626, 860) as a cheap notification primitive and a replacement for pend-multi.

  • A mutex group per task plus a highest-pending scan for correct multi-mutex priority restore (os_mutex.c:1041-1070), and transitive inheritance via the OS_TaskChangePrio chain loop (os_task.c:2843-2844).

  • The safety-critical start freeze (os_core.c:406) and debugger size and configuration exports kept alive via volatile pointers (os_dbg.c:207, 403).

  • Scheduler-lock and tick-processing time measurement (os_core.c:2017-2040, os_tick.c:111); the ISR-stack redzone check on every OSIntExit; ASCII object tags (os.h:197-204).

  • Optional idle-task elimination by idling inside OSSched (os_core.c:484-510).

Weaknesses

  • Out-parameter error style: every call needs CPU_SR_ALLOC-style boilerplate and a switch(opt) validation block; opt combinations are enumerated by hand (os_q.c:823-829, os_flag.c:about 400-420), and errors like OS_ERR_MUTEX_OWNER on a successful recursive lock (os_mutex.c:467) blur success and failure.

  • A large TCB (os.h:940-1070): about 40 fields including profiling, flags, task-queue, and mutex-group state in one struct; OSDbg_TCBSize exists precisely because the size is hard to know.

  • Public struct layouts: object structs are exposed and the generic OS_PEND_OBJ prefix relies on manual field-order synchronization (os.h:683-688); deleting a task does not zero the TCB when the redzone is on (os_task.c:about 640).

  • All kernel critical sections mask interrupts, including O(n) pend-list insertion (os_core.c:1313-1360) and tick-list walks in the tick ISR (os_tick.c:459-560); the deferred-post option that bounded interrupt latency was removed.

  • Only priority order on pend lists; FIFO among equals only via round robin (which is disabled with dynamic tick, os.h:2175); OS_TaskChangePrio and OS_PendListChangePrio are linear.

  • A likely bug in OS_MutexGrpPostAll (os_mutex.c:1115 uses p_tcb instead of p_tcb_new).

  • OSIntEnter does not mask interrupts (os_core.c:263-276), so correctness depends on the port calling it with interrupts disabled; 8-bit nesting counters capped at 250 silently stop counting.

  • Requires uC/CPU (CPU_CRITICAL_, CPU_TS, CPU_CntLeadZeros) and application-supplied OS_SAFETY_CRITICAL_EXCEPTION and OS_DynTick, so the kernel is not self-contained.