Research note: Eclipse ThreadX

Kind: Read-only source analysis, 2026-10-07, on a sparse shallow clone of eclipse-threadx/threadx at commit 93387b0a6038 (common/, ports/cortex_m4/gnu, ports/cortex_m33/gnu, docs/). Version string “ThreadX Cortex-M4/GNU 6.5.2.202603” (ports/cortex_m4/gnu/inc/tx_port.h:706). Abbreviations: C = common/src, I = common/inc, M4 = ports/cortex_m4/gnu, M33 = ports/cortex_m33/gnu. The clone has no common_modules/, utility/, or tests. Line numbers drift; paths and symbols are the durable reference. Synthesized in R-001.


Q1 Scheduler

  • Priority 0 is highest; TX_MAX_PRIORITIES defaults to 32 and must be a multiple of 32 (M4/inc/tx_port.h:114; I/tx_api.h:2111). State: _tx_thread_priority_maps[TX_MAX_PRIORITIES/32] (I/tx_thread.h:333), _tx_thread_priority_map_active only above 32 (:341), the per-priority head array _tx_thread_priority_list[] (:377), cached _tx_thread_highest_priority (:369), _tx_thread_execute_ptr versus _tx_thread_current_ptr (:299, 306).

  • Highest ready is the lowest set bit: generic TX_LOWEST_SET_BIT_CALCULATE isolates m & (~m+1) then shifts (I/tx_thread.h:141-200); M4 overrides with RBIT plus CLZ (M4/inc/tx_port.h:518).

  • The ready list per priority is a self-linked circular doubly linked list: the first thread becomes the head pointing to itself (C/tx_thread_system_resume.c:189-196), later ones append at the tail (:383-387), the bit is OR’d (:209-210), highest and execute updated (:213-230). Suspend unlinks and clears the bit when empty, then recomputes highest from the maps, NULL execute when none (C/tx_thread_system_suspend.c:198-263, 311-331).

  • Round robin: per-thread tx_thread_time_slice and new_time_slice (I/tx_api.h:436-437) mirrored into the global _tx_timer_time_slice on switch (M4/src/tx_thread_schedule.S:173-184, 214-221); on expiry _tx_thread_time_slice rotates the head to ready_next only if priority equals threshold (“Preemption-threshold overrides time-slicing”, C/tx_thread_time_slice.c:116-128); relinquish does the same (C/tx_thread_relinquish.c:106-113).

  • Preemption threshold: tx_thread_preempt_threshold is at most the priority (I/tx_api.h:455; C/txe_thread_create.c:252). A newly ready thread displaces the execute thread only if priority < execute_ptr->tx_thread_preempt_threshold (resume.c:238). If a thread whose threshold differs from its priority is preempted, its priority bit is recorded in _tx_thread_preempted_maps (resume.c:244-259; I/tx_thread.h:353). When the running thread suspends, suspend.c:403-477 finds the lowest set preempted priority and, if _tx_thread_highest_priority >= that thread's threshold, re-selects the preempted thread instead of the highest ready one; interrupts are briefly re-enabled there “to keep the interrupt lockout time deterministic” (:412-424). Bits are cleared when the thread leaves the ready list (:226-231) or raises its threshold (C/tx_thread_preemption_change.c:167-212). Net effect: threads in [threshold, priority) cannot preempt, so fewer context switches; threshold 0 is a non-preemptible section. With TX_DISABLE_PREEMPTION_THRESHOLD any threshold collapses to 0 (:88-98).

  • _tx_thread_preempt_disable (I/tx_thread.h:384) is an internal counter; TX_THREAD_SYSTEM_RETURN_CHECK ORs it with system_state (:117); the Cortex-M port drops system_state from the check (M4/inc/tx_port.h:499). Non-zero also forbids blocking inside services (C/tx_queue_send.c:282 returns TX_QUEUE_FULL; C/tx_event_flags_get.c:269).

  • No idle thread: PendSV __tx_ts_wait spins with interrupts masked, optional WFI under TX_ENABLE_WFI and TX_LOW_POWER hooks (M4/src/tx_thread_schedule.S:251-296).

  • _tx_thread_system_state (I/tx_thread.h:325): 0xF0F0F0F0 during init (I/tx_initialize.h:48; C/tx_initialize_kernel_enter.c:99, 137), 0 is thread level; Cortex-M ORs IPSR for ISR detection (M4/inc/tx_port.h:478). TX_DISABLE and TX_RESTORE save PRIMASK and CPSID i, or BASEPRI with TX_PORT_USE_BASEPRI (M4/inc/tx_port.h:139, 564-600).

Q2 Context switch and interrupts

  • Cortex-M ISRs are ordinary handlers; _tx_thread_context_save and _restore are stubs that only call execution-profile hooks (“Context is already saved - just return”, M4/src/tx_thread_context_save.S:66-75; “only needed for legacy applications”, tx_thread_context_restore.S:46).

  • Rescheduling is setting ICSR.PENDSVSET: inline _tx_thread_system_return_inline (M4/inc/tx_port.h:578-600, 681); the assembly variant checks IPSR to skip the interrupt toggle when called from an ISR (M4/src/tx_thread_system_return.S:70-80). PendSV is lowest priority (M4/example_build/tx_initialize_low_level.S:127), so ISR-driven preemption is deferred until nesting unwinds.

  • PendSV body (schedule.S:121-245): save r4-r11, s16-s31 if EXC_RETURN bit 4 is clear, LR; PSP stored at TCB offset 8 (I/tx_api.h:432, assembly-fixed layout :425-442); run_count++ at offset 4 (:213-217); if execute is NULL go wait; __tx_ts_ready clears a redundant PendSV (:301-304).

  • Decision: _tx_thread_system_preempt_check (C/tx_thread_system_preempt_check.c:65-118): combined flags 0 and current differs from execute leads to system_return; services inline the same (resume.c:356-368). The tick ISR pends PendSV itself after a time slice if execute differs from current and preempt_disable is 0 (M4/src/tx_timer_interrupt.S:221-233).

  • Misuse rejection via TX_THREAD_GET_SYSTEM_STATE(): TX_WAIT_ERROR for blocking from an ISR, TX_CALLER_ERROR for ISR callers; the timer thread may not block (C/txe_mutex_get.c:111, 122-126, 146); tx_thread_sleep refuses when preempt is disabled (C/tx_thread_sleep.c:134).

  • tx_interrupt_control swaps PRIMASK or BASEPRI and returns the old posture (M4/src/tx_thread_interrupt_control.S:66-73); the trace build substitutes _tx_trace_interrupt_control (I/tx_api.h:1338-1341).

  • Low power and execution profile exist only as hooks (tx_low_power_enter and _exit, _tx_execution_*: schedule.S:33-35, 129-146, 223-228; C/tx_initialize_kernel_enter.c:144; TCB fields I/tx_api.h:578-581); implementations live in upstream utility/, absent here.

  • Initial frame: a 68-byte fake exception frame, EXC_RETURN 0xFFFFFFFD, xPSR T bit, poisoned LR (M4/src/tx_thread_stack_build.S:96-122). M33 uses 0xFFFFFFBC for the NS return (M33/src/tx_thread_stack_build.S:99-101).

Q3 Time

  • Timer wheel: _tx_timer_list[TX_TIMER_ENTRIES=32] of circular doubly linked TX_TIMER_INTERNAL lists (I/tx_timer.h:49, 95; struct I/tx_api.h:339-364 with remaining and reinit ticks, callback plus ULONG parameter, list_head back-pointer). _tx_timer_current_ptr advances one slot per tick with wrap (I/tx_timer.h:108; M4/src/tx_timer_interrupt.S:151-161).

  • Insertion at current + min(remaining, 32) - 1 (C/tx_timer_system_activate.c:100-119). Timers beyond 32 ticks are parked in the farthest slot; on “expiry” remaining -= 32 and they are re-queued without callback (C/tx_timer_thread_entry.c:184-215; “expiration adjust” performance counter).

  • The ISR does only: clock++, slice–, set _tx_timer_expired if the current slot is non-empty (tx_timer_interrupt.S:88-140); then _tx_timer_expiration_process resumes the system timer thread (C/tx_timer_expiration_process.c:120). With TX_TIMER_PROCESS_IN_ISR callbacks run inside the ISR guarded by _tx_timer_processing_active (:125-347, 146).

  • Timer thread “System Timer Thread”, priority TX_TIMER_THREAD_PRIORITY default 0, static stack (C/tx_timer_initialize.c:251-255; M4/inc/tx_port.h:134). Loop: detach the slot list, run callbacks with interrupts enabled (thread_entry.c:299), reload periodic timers from re_initialize_ticks (:264) and re-insert relative to the current slot (:408), self-suspend when nothing expired (:454). Because reinsertion is relative to the slot pointer at processing time, a late timer thread shifts subsequent periods (drift), not an absolute deadline.

  • Sleep: TX_SLEEP plus remaining_ticks (C/tx_thread_sleep.c:155, 176); system_suspend activates the TCB-embedded tx_thread_timer (I/tx_api.h:473; suspend.c:118-134) whose callback _tx_thread_timeout was bound at create (I/tx_thread.h:124-125); the timeout resumes a sleeper or calls the object cleanup (C/tx_thread_timeout.c:88, 149).

  • The tick is a 32-bit ULONG (I/tx_timer.h:77); tx_time_get reads it (C/tx_time_get.c:80); TX_WAIT_FOREVER is 0xFFFFFFFF and never enters the wheel (system_activate.c:88); 100 ticks per second by default (I/tx_api.h:272).

Q4 Blocking and waiting

  • The waiter list per object is a circular doubly linked list of TCBs (tx_thread_suspended_next and _previous, I/tx_api.h:481-483; for example tx_queue_suspension_list :952-954). A service stores tx_thread_suspend_cleanup, tx_thread_suspend_control_block, extra info, bumps tx_thread_suspension_sequence (I/tx_api.h:479-487, 585; C/tx_queue_send.c:302-314), sets the state code (I/tx_api.h:139-152), sets tx_thread_suspending, preempt_disable++, re-enables interrupts, then calls _tx_thread_system_suspend, which proceeds only if tx_thread_suspending is still TRUE (suspend.c:156). A resume that lands in that window clears the flag and the suspension evaporates (resume.c:401-431).

  • Timeout and wakeup race: cleanup functions re-validate cleanup pointer identity, sequence number, object id, and state before touching lists (C/tx_queue_cleanup.c:93-199; C/tx_mutex_cleanup.c:93, 176). Every waker nulls tx_thread_suspend_cleanup before resuming (tx_queue_send.c:221). Wait-abort reuses cleanup with TX_WAIT_ABORTED (C/tx_thread_wait_abort.c:113, 163).

  • Ordering is FIFO: new waiters append at the tail (tx_queue_send.c body; C/tx_mutex_get.c:240 region). Exceptions: tx_queue_front_send suspends at the head (C/tx_queue_front_send.c:317-342); explicit tx_*_prioritize moves the highest-priority waiter to the front (C/tx_mutex_prioritize.c:105-233, restarts if the list changed :167-177); a mutex put with TX_INHERIT calls prioritize automatically (C/tx_mutex_put.c:332-335, 587).

  • Delete with waiters resumes all with TX_DELETED (C/tx_queue_delete.c:153-154).

  • Event flags: AND or OR and clear options (C/tx_event_flags_get.c:121-245); tx_event_flags_group_delayed_clear defers clears while a set is walking the list (:245; I/tx_api.h:790); set nulls the head “to thwart manipulation” by ISRs, an ISR set marks reset_search to restart the walk (C/tx_event_flags_set.c:314, 583), satisfied threads are collected then resumed (:486). Event chaining is the tx_event_flags_set_notify callback (C/tx_event_flags_set_notify.c:92) invoked after every set (set.c:597); queue and semaphore have analogous *_send_notify and *_put_notify (I/tx_api.h:987, 1042).

  • No multi-object wait: the TCB holds exactly one cleanup and control-block pair (I/tx_api.h:479-480); notify callbacks forwarding into one event-flags group are the documented substitute.

Q5 Synchronization and priority inheritance

  • Mutex fields: recursive tx_mutex_ownership_count, owner, inherit flag, tx_mutex_highest_priority_waiting, owned_next and prev threaded into the owner’s tx_thread_owned_mutex_list (I/tx_api.h:857-888, 517-518); the TCB keeps tx_thread_user_priority, tx_thread_user_preempt_threshold, tx_thread_inherit_priority (:511-513).

  • Get: free leads to own and link (C/tx_mutex_get.c:159); re-get increments the count (:175). Contended with TX_INHERIT: record the highest waiter (:352), lower the owner’s inherit_priority (:360), raise the owner via _tx_mutex_priority_change (:369), which rewrites priority and threshold for a non-ready owner or suspend-plus-resumes a ready one to relocate it (C/tx_mutex_priority_change.c:97-105, 186, 215) and fixes the preempted map and execute pointer (:274).

  • Not transitive: only the direct owner is raised; if that owner is itself blocked on another mutex, nothing walks the chain (no loop in tx_mutex_get.c).

  • Put (C/tx_mutex_put.c): owner check skipped when preempt_disable is set (:139); when the count reaches 0 and inherit, scan all owned mutexes for the maximum highest_priority_waiting to get the new inherit_priority (:253-295), restore priority (:382 no waiters; :552 and 619 after prioritize :332-335 and 587 hands ownership to the highest waiter).

  • Owner death: _tx_thread_mutex_release (I/tx_thread.h:390) installed by C/tx_mutex_create.c:93 leads to _tx_mutex_thread_release, which forces count to 1 and puts every owned mutex (C/tx_mutex_cleanup.c:251-280); called on completion (C/tx_thread_shell_entry.c:118) and terminate (C/tx_thread_terminate.c:281).

  • Semaphore ceiling put returns TX_CEILING_EXCEEDED when the count is at or above the ceiling and nobody waits (C/tx_semaphore_ceiling_put.c:121).

Q6 IPC and memory

  • Queues: 1 to 16 ULONG messages (I/tx_api.h:109-113, 279), unrolled word copy TX_QUEUE_MESSAGE_COPY (I/tx_queue.h:83-94), ring start, end, read, write (I/tx_api.h:942-948); if a receiver waits, the message is copied straight into its buffer (C/tx_queue_send.c:222-227); front_send steps the read pointer backwards; a send-notify callback (:170).

  • Block pool: a singly linked free list, header word rewritten to point at the pool when allocated (C/tx_block_allocate.c:160), suspends with cleanup when empty (:238).

  • Byte pool: first-fit (C/tx_byte_pool_search.c:162) over an implicit list of [next][FREE 0xFFFFEEEE or pool pointer] headers (I/tx_byte_pool.h:51; minimum split 20 bytes :55); a rotating search pointer set to the first free block seen (:145), adjacent free blocks coalesced lazily during search (:182), split when the remainder is at least the minimum (:272), early reject by theoretical availability (:99). The search runs with interrupts periodically enabled; tx_byte_pool_owner detects preemption and restarts (:253; allocate retry loop C/tx_byte_allocate.c:150-171). Release moves the search pointer back (C/tx_byte_release.c:200). Fragmentation is possible; no size classes.

  • Static model: every control block and stack is caller memory; public structs expose the full layout (I/tx_api.h:423-599); the kernel allocates nothing, only passes _tx_initialize_unused_memory to tx_application_define (C/tx_initialize_kernel_enter.c:133). The only exception: M33 secure stacks use malloc (M33/src/tx_thread_secure_stack.c:253).

Q7 Isolation

  • Modules: sources absent; README.md:15, 28 lists common_modules and ports_modules and :105-106 describes dynamically loaded modules in one paragraph. No MPU code in the base kernel.

  • M33 TrustZone: the NS kernel calls secure CMSE entry functions (M33/src/tx_thread_secure_stack.c:103, 200, 347, 452, 545) that manage a fixed table of TX_MAX_SECURE_CONTEXTS=32 (:49); secure stacks of 256 to 1024 bytes are malloc’d with seal 0xFEF5EDA5 (:36, 40, 45, 253); index+1 kept in the TCB extension tx_thread_secure_stack_context (M33/inc/tx_port.h:240, 251), read by assembly at offset 0x90 (M33/src/tx_thread_schedule.S:162); PendSV calls save and restore which load and zero PSP_S and PSPLIM_S (:166, 235; secure_stack.c:126, 495). NS requests go through SVC 1, 2, 3 (M33/src/tx_thread_secure_stack_allocate.S:73; SVC_Handler schedule.S:334-358). The timer thread gets a 256-byte secure stack (tx_port.h:310-311); freed at delete (:302).

  • PSPLIM: the scheduler loads PSPLIM from tx_thread_stack_start before restoring (schedule.S:242); the UsageFault handler checks STKOF and calls _tx_thread_stack_error_handler (M33/example_build/tx_initialize_low_level.S:199-221; TX_PORT_THREAD_STACK_ERROR_HANDLING tx_port.h:134). M4 has no hardware limit.

  • Software stack checking: fill 0xEFEFEFEF or random (I/tx_api.h:131-133; C/tx_thread_create.c:106, 123), guard words at both ends checked on every suspend, resume, and slice (I/tx_thread.h:210-236), binary-search high-water (C/tx_thread_stack_analyze.c:116-132), application handler (C/tx_thread_stack_error_handler.c:80); minimum stack 200 (M4/inc/tx_port.h:122).

Q8 Observability

  • TraceX: one caller buffer with header, object registry, and a circular event ring (I/tx_trace.h:30-40; header :74; registry entry with 32-character name :93, 106-115; event: thread pointer, priority with threshold<<16, id, timestamp, 4 info words :242-257). TX_TRACE_IN_LINE_INSERT is inlined into every service (:295-329); wrap-around invokes the buffer-full callback (:493-499; C/tx_trace_buffer_full_notify.c:75); class filter bits (C/tx_trace_event_filter.c:72); registry slots reused by object pointer (C/tx_trace_object_register.c:123-125); enable registers all live objects (C/tx_trace_enable.c:235). Scheduling code patches the “next thread” field after the decision if the timestamp is unchanged (resume.c:325-338). The M4 timestamp is DWT CYCCNT at 0xE0001004 (M4/inc/tx_port.h:161). ISR enter and exit events are application-inserted.

  • Performance info: global and per-TCB counters (I/tx_thread.h:439-515 including an 8-entry execute log; I/tx_api.h:520-555), read via C/tx_thread_performance_info_get.c:79+; tx_thread_info_get returns user priority and threshold, run count (incremented in PendSV schedule.S:213-217), created and suspended-list neighbours (C/tx_thread_info_get.c:122-150).

  • Error layer: 68 txe_* shells; tx_api.h maps names to _txe_ by default, _tx_ under TX_DISABLE_ERROR_CHECKING, _txr_ under TX_ENABLE_MULTI_ERROR_CHECKING (I/tx_api.h:1057-1065, 1191). Shells check NULL, id magic (C/txe_mutex_get.c:92), caller context (:111, 146), control-block size via a sizeof macro (I/tx_api.h:1266, 1377; C/txe_thread_create.c:116), stack overlap against all created threads (:166-182), minimum stack (:232). Internals never call txe.

Q9 Configuration, portability, quality

  • tx_user.h via TX_INCLUDE_USER_DEFINE_FILE; I/tx_user_sample.h:89-302 enumerates options, :55-76 gives “max speed” and “min size” recipes (TX_NOT_INTERRUPTABLE selects _ni_ variants, resume.c:533-615; TX_INLINE_THREAD_RESUME_SUSPEND, TX_TIMER_PROCESS_IN_ISR, TX_REACTIVATE_INLINE, TX_NO_TIMER, TX_DISABLE_NOTIFY_CALLBACKS). _tx_build_options records the build (I/tx_thread.h:393-426). TX_SAFETY_CRITICAL forbids disabling error checks, ISR timers, inline reactivation, stack checking (I/tx_api.h:2103-2140) and raises an exception if the scheduler ever returns (:2221-2222; C/tx_initialize_kernel_enter.c:150-153).

  • Port contract: tx_port.h supplies types, TX_MAX_PRIORITIES, TX_MINIMUM_STACK, timer-thread defaults, TX_DISABLE, TX_RESTORE, TX_SAVE_AREA, TX_THREAD_GET_SYSTEM_STATE, optional TX_LOWEST_SET_BIT_CALCULATE, object *_EXTENSION and create, delete, completed, terminated hooks (M4/inc/tx_port.h:216-400), TX_PORT_SPECIFIC_* init hooks (I/tx_initialize.h); assembly: schedule, stack_build, system_return, context_save and restore, interrupt_control, disable, restore, timer_interrupt, plus the example low_level (M4/src file list). TCB words 0 to 9 are assembly-fixed (I/tx_api.h:425-442).

  • MISRA: TX_MISRA_ENABLE routes casts, memset, pointer math, and message copy through 70 assembly shims (M4/src/tx_misra.S:131-237; C fallback C/tx_misra.c:50-121); the revision history records “modified code for MISRA compliance” across files (docs/revision_history.txt:39-270). File headers carry component, release, author only (C/tx_mutex_get.c:35-44).

  • Certification: no IEC 61508 or ISO 26262 text in the repository; only CONTRIBUTING.md:7 “safety-certified OS” and “safety critical exception logic” (revision_history.txt:350-460). Supporting artifacts: the TX_SAFETY_CRITICAL gate, the MISRA shims, the txe layer.

  • Tests: none in the clone; README.md:31 points to utility/ “Test cases and utilities”; CMakeLists.txt:27-32 enables ctest for port example builds; build-all.log and ctest-all.log are artifacts only.

Ideas to borrow

  1. Preemption threshold with the preempted-map bookkeeping (resume.c:238-259; suspend.c:403-477): a cheap bounded-preemption band that doubles as a non-preemptible section.

  2. Suspension sequence number plus cleanup-pointer identity checks to make timeout, wakeup, and abort races idempotent (I/tx_api.h:585; tx_queue_cleanup.c:93-199).

  3. A timer wheel with lazy “more than 32 ticks” re-queue plus processing in a dedicated thread with interrupts enabled around callbacks (thread_entry.c:184-299).

  4. The txe, tx, txr name mapping: a zero-cost removable validation layer, with sizeof(control block) passed by macro to catch ABI mismatches (I/tx_api.h:1057-1065, 1266).

  5. Event chaining via per-object notify callbacks as a lightweight multi-source wait (set.c:597).

  6. An inline trace macro with post-hoc “next thread” patching and per-class filters (I/tx_trace.h:295-329; resume.c:325-338).

  7. The byte-pool “owner token” to allow interrupt windows inside a long search (tx_byte_pool_search.c:253).

  8. The TX_SAFETY_CRITICAL compile-time gate rejecting unsafe option combinations (I/tx_api.h:2103-2140).

Weaknesses

  1. Public control-block layouts are the ABI; assembly hard-codes TCB offsets (I/tx_api.h:425-442; schedule.S:174, 213; M33 schedule.S:162 offset 0x90), and tx_thread_create must pass sizeof(TX_THREAD) to detect drift (txe_thread_create.c:116).

  2. FIFO-only waiter order by default; priority order requires explicit *_prioritize calls or TX_INHERIT mutexes (tx_mutex_prioritize.c; tx_queue_send.c tail append).

  3. Priority inheritance is single-level, not transitive (tx_mutex_get.c:360-369 touches only the direct owner).

  4. No multi-object wait; notify callbacks run in the setter’s context and are one per object (I/tx_api.h:479-480, 987).

  5. The timer wheel is a 32-slot circular array: long timers are re-queued every 32 ticks and periodic reload is relative to processing time, so a delayed timer thread drifts (thread_entry.c:184-215, 408).

  6. The timer thread runs at priority 0 and all callbacks share one stack and priority; TX_TIMER_PROCESS_IN_ISR moves callbacks into the ISR (expiration_process.c:125-347).

  7. Isolation is thin in the base kernel: no MPU, Modules absent here, M33 secure stacks rely on malloc and a fixed 32-context table (secure_stack.c:49, 253).

  8. The byte pool is first-fit with lazy coalescing and no free list; worst-case search is O(fragments) under interrupt windows (tx_byte_pool_search.c:99-272).

  9. Idle is a busy loop unless TX_ENABLE_WFI or TX_LOW_POWER is defined (schedule.S:251-296); low-power and execution-profile code are external hooks only.

  10. Certification and MISRA claims are not substantiated inside the repository (no test suite, no certificate artifacts; CONTRIBUTING.md:7 only).