The arch interface: common-denominator analysis¶
What a CPU port owns, what the kernel provides, and why the surface tiers the way it
does. See also arch-porting.md (the how-to for adding a
port) and ADR 0001 (which axes of
foreignness the contract absorbs vs quarantines).
Method. Read every arch/<cpu>/kernel_arch.h in the tree (9 ports) plus the shared
arch/arch_defaults.h, and diffed the declared surface element-by-element. The port
header is ground truth; arch_defaults.h is the idealized contract (“a port owns
context switch, the interrupt-state primitive, and the timer tick — everything else is
opt-in”). Below is what the ports actually declare, the tiering that falls out, and the
findings that shaped it.
Legend: R = real implementation · ∘ = no-op / trivial default (from
arch_defaults.h) · – = absent (not provided by that port) · (hdr) = declared in
the port header but not implemented at root (no kernel_arch.c, no board).
1. The observed surface (interface element × port)¶
The 9 root ports: arm_v7m arm_v6m arm_v8m risc_v c2000 pthread flat msp430 tricore.
msp430 and tricore ship no kernel_arch.c — see §7.
Element |
v7m |
v6m |
v8m |
riscv |
c2000 |
pthread |
flat |
msp430 |
tricore |
|---|---|---|---|---|---|---|---|---|---|
IRQ state |
R |
R |
R |
R |
R |
∘ |
∘ |
R |
R(hdr) |
|
R |
R |
R |
R |
R |
∘ |
∘ |
R |
R(hdr) |
|
R |
R |
R |
R |
R |
∘ |
∘ |
R |
R(hdr) |
|
1 |
1 |
1 |
1 |
1 |
1 |
0 |
0 |
1(hdr) |
|
1 |
1 |
1 |
1 |
1 |
0 |
0 |
0 |
1(hdr) |
|
R |
R |
R |
R |
R |
R |
R |
– |
R(hdr) |
Context |
R |
R |
R |
R |
R |
R |
R |
– |
R(hdr) |
|
R |
R |
R |
R |
R |
R |
R |
– |
R(hdr) |
|
R |
R |
R |
R |
R |
R |
R |
– |
R(hdr) |
|
R |
R |
R |
R |
R |
R |
∘ |
– |
R(hdr) |
|
R |
R |
R |
R |
R |
R |
– |
– |
R(hdr) |
|
board |
board |
board |
R |
R |
R (OS clock) |
∘ |
– |
R(hdr) |
|
R |
R |
R |
R |
R |
R |
– |
– |
R(hdr) |
SMP |
1 (fixed) |
cfg |
cfg |
cfg |
1 (fixed) |
1 |
1 |
– |
cfg(hdr) |
|
∘ |
∘/R |
∘/R |
∘/R |
∘ |
∘ |
∘ |
– |
∘/R(hdr) |
|
∘ |
∘/R |
∘/R |
∘/R |
∘ |
∘ |
∘ |
– |
∘/R(hdr) |
|
– |
R¹ |
R¹ |
R¹ |
– |
– |
– |
– |
R¹(hdr) |
Safety |
R |
∘ |
R |
R |
∘ |
∘ |
∘ |
– |
∘ |
|
R |
∘ |
R |
R |
∘ |
∘ |
∘ |
– |
∘ |
|
R |
∘ |
R |
R |
∘ |
∘ |
∘ |
– |
∘ |
|
R |
∘ |
R |
R |
∘ |
∘ |
∘ |
– |
∘ |
|
– |
– |
– |
– |
– |
– |
– |
– |
R(hdr) |
|
– |
– |
– |
– |
– |
– |
– |
– |
R(hdr) |
¹ only when built TK_SMP_CORES>1.
Port-private extras (not part of the kernel-facing contract): arch_sched_init +
arch_kernel_tick (the ARM family’s board-tick glue — see §6, still live and
un-renamed); arch_trap_init + riscv_external_irq (riscv); ARCH_HAS_STACK_LIMIT
(v8m’s PSPLIM register, replaces the v7m MPU-region stack guard);
ARCH_CONTEXT_START_CLEARS_SCHED_ENTERING (arm single-core, c2000, tricore: the launch
path clears g_sched_entering itself once the context is valid, so a tick from an
already-running SDK timer can never preempt onto a not-yet-launched stack — see
tk_kernel_start_this_core in kernel/tk_kernel.c); tk_fault_report (v7m/v8m).
Core identity rule (SMP). arch_core_id() is boot-established, never discovered
at runtime. A core’s id is fixed by which entry/vector table it comes up on and read
back through an architectural anchor — mhartid (RISC-V), the banked VTOR
(Cortex-M with no per-core id register — arm_v6m/arm_v8m map VTOR → id), or a
board’s own register. No kernel or arch code probes a chip peripheral to ask “who am
I?”; the question is answered once per arch/board and never re-raised. Cross-core
locks still use a chip atomic (RP2040’s SIO hardware spinlock — ARMv6-M has no
LDREX/STREX), and that lives entirely in arch_spin_*.
2. The tiering that falls out¶
The surface separates cleanly into four concentric tiers — this is the structural
finding the tree is built on. arch/arch_defaults.h, included at the end of every
kernel_arch.h, supplies a central #ifndef ARCH_HAS_*-guarded no-op for each tier a
port doesn’t declare, so an absent feature costs nothing and is never hand-stubbed:
Tier 0 —
arch_irq.h(truly universal). The only thing all 9 ports provide, including the HAL-onlymsp430, which has no kernel at all:arch_irq_state_t,arch_irq_save/restore,arch_irq_disable/enable. Independent of the scheduler — the HAL/MCAL needs it on its own — so it lives in its own tiny header, includable without the kernel contract.Tier 1 — scheduler core (
kernel_arch.h, every switching port).struct arch_context(+_clear/_init/_switch/_start),arch_idle, the two capability macros,ARCH_MIN_STACK_SIZE, and ONE preempt-integration hook (§4). Plus two Tier-1 properties, each defaulted inarch_defaults.hand overridden only where true:ARCH_STACK_GROWS_UP(default 0; c2000 sets 1) andARCH_OWNS_TIMEBASE(default 0 — the kernel’s tick counter is authoritative;pthreadsets 1 and readsCLOCK_MONOTONICinstead).flatis the degenerate member (ARCH_CONTEXT_SWITCH_SUPPORTED 0, no_start/_idle);msp430has no Tier 1 at all.Tier 2 — SMP (opt-in,
TK_SMP_CORES>1).ARCH_CORE_COUNT,arch_spinlock_t+arch_spin_*,arch_core_id,arch_ipi,arch_smp_start,arch_sched_switch. Real onarm_v6m(RP2040, 2×M0+),arm_v8m(M33 target),risc_v(multi-hart), and declared-but-unimplemented ontricore(AURIX, 3-core — see §7); absent onarm_v7mandc2000(neither target has multi-core silicon in this tree); no-op single-core default everywhere else.Tier 3 — safety/security (opt-in). MPU/stack-guard, per-thread isolation, privilege/unpriv, syscall-return. Real on
arm_v7m,arm_v8m, andrisc_v(ARCH_HAS_MPU+ARCH_HAS_UNPRIV, the latter two further gated at the.clevel onTK_CONFIG_UNPRIV_THREADS); no-op everywhere else, includingtricore, whose header says outright: “No MPU / no unprivileged mode in this port yet.”
3. The interface as built (tiered, defaulted)¶
The port author writes Tier 0 + Tier 1 only; Tiers 2–3 default to no-ops unless the
port declares a capability flag. A port that implements a tier #defines its
ARCH_HAS_* flag and provides the real prototypes; the matching default group in
arch_defaults.h then compiles out — real and default definitions never collide.
Tier 0 — arch/<cpu>/arch_irq.h (universal; kernel-independent, HAL may use alone)¶
typedef <uint> arch_irq_state_t;
static inline arch_irq_state_t arch_irq_save(void); /* nesting-safe: returns prior, masks */
static inline void arch_irq_restore(arch_irq_state_t); /* restore exact prior state */
static inline void arch_irq_disable(void); /* flat mask (kernel cooperative yield) */
static inline void arch_irq_enable(void); /* unconditional enable */
Tier 1 — arch/<cpu>/kernel_arch.h (mandatory for any scheduling port)¶
#define ARCH_CONTEXT_SWITCH_SUPPORTED {0|1}
#define ARCH_PREEMPTION_SUPPORTED {0|1}
#define ARCH_MIN_STACK_SIZE <cells> /* addressable cells, not bytes — ADR 0001 */
struct arch_context { uintptr_t stack_pointer; /* SP or opaque token; MUST be first word (F2) */ };
static inline void arch_context_clear(struct arch_context*);
void arch_context_init (struct arch_context*, void* stack, size_t cells,
void* (*fn)(void*), void* arg);
void arch_context_switch(struct arch_context* from, struct arch_context* to); /* cooperative */
void arch_context_start (struct arch_context* to); /* launch first */
void arch_idle(void); /* wait-for-irq */
/* preempt integration — the port wires its tick to exactly ONE (§4): */
void tk_kernel_tick_preempt(void); /* deferred-switch ports (kernel-provided) */
uintptr_t tk_kernel_preempt_select(uintptr_t); /* trap-frame ports (kernel-provided) */
void arch_timer_init(void); /* owner-provided ms base; see docs/architecture/tick-policy.md */
uint32_t arch_timer_now_ms(void);
arch_timer_init/_now_ms are still declared inside kernel_arch.h at root — the
Q2-era idea of splitting them into a standalone time-contract header (§6, F6) was not
carried out as a file split. What did ship is the layering it was chasing: the app
never calls arch_timer_now_ms directly, it reads kernel/tk_time.h’s tk_time_ms() /
tk_kernel_now_ms() (Tier A, a consumed service) — see
timekeeping.md.
Tier 2 — SMP (only if #define ARCH_HAS_SMP 1; else defaulted no-op single-core)¶
#define ARCH_CORE_COUNT <n>
typedef <atomic> arch_spinlock_t;
uint32_t arch_core_id(void);
void arch_spin_init/lock/unlock(arch_spinlock_t*);
void arch_ipi(uint32_t core);
void arch_smp_start(void);
void arch_sched_switch(void); /* SMP-safe cooperative switch (select-in-trap) */
See smp-model.md for the scheduler-side half of this contract
(the giant lock, affinity, the g_sched_entering re-entry guard).
Tier 3 — safety/security (each behind its own capability flag)¶
#define ARCH_HAS_MPU 1 -> arch_mpu_init, arch_stack_guard_{set,active,suspend,resume},
arch_stack_isolatable, arch_thread_region_set
#define ARCH_HAS_UNPRIV 1 -> arch_context_set_privilege, arch_context_is_unprivileged,
arch_syscall_set_return
#define ARCH_HAS_CONTEXT_RECLAIM 1 -> arch_context_reclaim(token) (tricore CSA)
#define ARCH_HAS_LAZY_CONTEXT 1 -> arch_context_prepare(ctx) (tricore SMP)
#define ARCH_HAS_SUPERVISOR_FENCE 1 -> arch_instance_fence_apply(deny) (arm_v8m, arm_v7m, risc_v S-mode, tricore; ADR 0003)
Everything not declared is a central default no-op in arch/arch_defaults.h — the
port file never mentions it. ARCH_HAS_SUPERVISOR_FENCE is the per-instance memory fence that
binds even the kernel (C3-B): a fence-capable port implements the mechanism-neutral deny-list seam and
an app requesting TK_CONFIG_ENABLE_INSTANCE_ISOLATION on an arch that lacks it fails loud
(0003-supervisor-fence-seam.md).
Minimal new-port checklist¶
Tier 0 (4 fns + type) + Tier 1 (arch_context + init/switch/start/clear, idle, one
preempt hook, timer, 3 macros). ~10 functions. SMP and safety are added later, in
isolation, by flipping a capability flag — never by editing the core or stubbing
unrelated features. Walked step by step in
arch-porting.md.
4. Findings — what’s proven and what’s still open¶
F1 — the “stub tax” is solved. Without
arch_defaults.h, every port would hand-copy ~15-20 no-op stubs for tiers it doesn’t have. Measured reduction on the two ports checked in at the time:arm_v7mkernel_arch.h192→96 lines,arm_v6m150→73, with Tier 0 split into a 38–45-linearch_irq.h.msp430’s single-core probe reduces to the four Tier-0 inlines with zero scheduler symbols — proof Tier 0 truly stands alone.F2 — the
stack_pointer-first convention is load-bearing and stays implicit. Everystruct arch_contextat root keepsuintptr_t stack_pointeras its first member — eventricore(a PCXI token, not an SP) andpthread(unused, kept only so the shared preempt path compiles) — specifically so the kernel/asm can treat&ctxas&saved_sp. Still enforced only by convention + comments, not a static assert.F3 — two preempt-integration models, both real, both stay — split by entry point, not by hardware mechanism. Deferred-switch ports call the void entry
tk_kernel_tick_preempt()(or the friendliertk_tick()front door) from their tick ISR; the kernel computes from/to and the port’sarch_context_switch(from,to)does the swap. Trap-frame ports calltk_kernel_preempt_select(sp)instead: the trap already saved the full frame at a raw SP, the kernel picks next and returns the SP to resume, and the trap’s own return path restores it — there is no ordinaryarch_context_switchcall in this path. The two are usually read as “PendSV vs no PendSV,” butc2000breaks that reading: it has no PendSV (the C28x switch is an immediate, synchronousctx_switchcall with no deferral hardware at all) and yetarch/c2000/kernel_arch.cwires its tick ISR throughtk_tick()→tk_kernel_tick_preempt()→arch_context_switch(), the deferred-family entry — because C28x’s switch is an ordinary callable function on twoarch_context*, not a raw-SP trap-return convention. So the real split is deferred-entry (arm_v6m/arm_v7m/arm_v8m/c2000— has a callablearch_context_switch) vs trap-frame-entry (risc_v, andtricoreper its header, unconfirmed body — the switch is baked into the trap’s SP save/restore). Both funnel into the sametk_kernel_tick()core inkernel/tk_kernel.c. Seetick-policy.md.F4 — cooperative vs preemptive switch are separate arch entry points, and the globals-vs-explicit-args question is settled. Single-core cooperative switch uses
arch_context_switch(from,to)(explicit args); SMP cooperative switch usesarch_sched_switch()(select-in-trap, because the from/to globals aren’t SMP-safe). Resolved in full insmp-model.md.F5 —
arch_sched_init/arch_kernel_tickare ARM-family board glue, still arch-shaped. They exist because the ARM tick lives on the board (nRF52 RTC1 vs generic SysTick), not the arch. A rename to a clearly board-glue name (armv7m_tick_isr/armv7m_sched_init) was proposed intick-policy.mdP5 but not adopted —arch_sched_init/arch_kernel_tickare still the live names inarch/arm_v6m|arm_v7m|arm_v8m/kernel_arch.htoday. Open, not blocking.F6 —
arch_timer_now_msstill lives in the Tier-1 switch contract. The intent (time as a consumed service, not part of the switch contract) shipped askernel/tk_time.h; the mechanical split ofarch_timer_now_ms/arch_timer_initout ofkernel_arch.hinto their own header did not happen. Not a correctness problem — every port already treats it as “the owner provides a ms counter” — just an unfinished file move. Seetimekeeping.md§5–6.F7 — naming stays consistent.
arch_*= the port implements it;tk_kernel_*/tk_*= kernel-provided hook the port calls (tk_kernel_tick_preempt,tk_kernel_preempt_select,tk_tick,tk_tick_advance). Confirmed unchanged at root.F8 —
fnis pthread-shaped (void* (*fn)(void*)) throughout the arch layer, including on ports (c2000,tricore) where the return value is unused. Consistent across all 9 ports.
5. The absorbed-axis ports (ADR 0001)¶
c2000 is the first port that isn’t GCC + 8-bit-byte + downward-stack — the silent
assumption baked into the other 8. Per ADR 0001
(“draw the line by axis”), the two cheap axes it hits are absorbed into the shared
contract rather than special-cased:
CHAR_BIT == 16, nouint8_t. The stack contract isvoid*+ asize_tcount of addressable cells (ARCH_MIN_STACK_SIZEis a cell count, not a byte count), andarch_defaults.h’s no-op spinlock is typedunsigned char, neveruint8_t.Stack grows up.
#define ARCH_STACK_GROWS_UP 1(default 0); read only by the stack-guard/high-water scan —arch_context_initbuilds its own frame in its own direction regardless.Non-GCC vendor compiler.
c2000/arch_irq.huses cl2000’sDINT/EINT/__disable/enable/restore_interrupts, not GCC inline asm — proof the gate idiom is compiler-agnostic.
The CHAR_BIT absorption didn’t stop at the arch headers: ADR 0001’s addenda record it
completing into the kernel core (kernel/tk_kernel.c, tk_sync.c) once C2000
actually had to compile the scheduler, not just the contract, and the final settled
spelling (kind-1 values are plain unsigned char, no project typedef). Full history in
the ADR.
6. Board-owned time and tick glue (arm family)¶
arch_timer_init() is declared in kernel_arch.h but is provided by the board: RTC1
on nRF52 (SysTick can’t survive its sleep mode), plain SysTick on a generic Cortex-M.
Its ISR calls arch_sched_init() once, then arch_kernel_tick() per tick — both are the
port-private glue named in F5, not part of the cross-arch contract a new port needs to
know about. risc_v, c2000, and tricore fold the equivalent into their trap
handlers directly and never declare these two names.
7. msp430 and tricore: two different kinds of “header-only”¶
Both ports ship arch_irq.h + kernel_arch.h and no kernel_arch.c — but for
opposite reasons, and conflating them is a mistake:
msp430is Tier-0-only by design.ARCH_CONTEXT_SWITCH_SUPPORTEDandARCH_PREEMPTION_SUPPORTEDare both0; there is nostruct arch_context, no switch, no tick. This is the proof that Tier 0 is genuinely separable: an HAL/MCAL superloop with no kernel at all can include justarch_irq.h. A kernel build never selects this arch.tricoredeclares the FULL Tier 1 contract (plusARCH_HAS_CONTEXT_RECLAIM, andARCH_HAS_LAZY_CONTEXT/ARCH_HAS_SMPunderTK_SMP_CORES>1) but has nokernel_arch.cbody and noboards/entry at root. The header is real —struct arch_contextholds a PCXI token (not an SP; the first-word convention still holds), and the comments describe a working CSA-based context switch — but the implementation exists only in isolated bring-up sketches (AURIX TC275, 3-core SMP + AMP-island proofs), not yet grafted into this tree. This port is a prepared, unimplemented precedent, not a working target: selecting-DTK_ARCH_TRICOREand linking will fail on every undefinedarch_*symbol. Tracked as a graft, alongside the un-globaling in ADR 0002.