Board ↔ kernel boundary contract

Status: Design record. The coverage matrix (§8) is the source of truth for what is HW-proven vs emulator-proven vs compile-only, rebuilt here against the four root boards (host, nrf52832_pca10040, qemu_riscv32_virt, rp2040_pico). The trap-frame model and bare-metal Layer-1 are evidence, not inference (RISC-V/qemu). Scope: the seam a board must satisfy to host the kernel — prerequisites, hooks, per-arch tails. Companions: kernel-tiers.md (the Tier-B port contract these hooks call), composability.md (why board glue that names a kernel symbol is a tracked leak), hal-architecture.md (the HAL internals + the runtime pin-claim API this doc references at the boundary), ADR 0002 (§7, the SoC-glue relocation).

0. The claim this document backs

The kernel runs on a set of CPU architectures. To bring it up on a board: satisfy the common prerequisites (§3.1) plus the addenda for the features you enable (§3.2), then wire the fixed hook set (§4). Who provides each piece is §5; the step-by-step is §6.

The boundary is real and drawable — but it is a small structured matrix (common core

  • per-feature addenda + per-arch tail), not one flat list. §7 says why.

1. The three layers, and where the line sits

  Layer 1  BOARD BRING-UP   "C runs · clock known · the slots the kernel claims are free"
           (§5: a manual · b SDK-IDE · c shipped BSP)                     │
  ═════════════════════════════════════════════════════════ THE LINE ────┤  (prerequisites §3)
  Layer 2  KERNEL PORT GLUE  the seam — a fixed, enumerable hook set (§4)  │  (hooks §4)
  ────────────────────────────────────────────────────────────────────────┘
  Layer 3  APPLICATION       tk_kernel_init → tk_thread_create → tk_kernel_start

The line is a state, not an instant: Layer 1 is “done” when a stable C environment exists, the core clock is at a known frequency, and the exception/interrupt vectors + timer the kernel will claim are available. Before the line = prepare the hardware. Crossing upward = the prerequisites the board hands the kernel. The wiring at the line = the hooks.

2. Two directions across the line

  • Prerequisites (Layer 1 → kernel): things that must be true/done/provided before tk_kernel_start(). The kernel assumes them; it does not create them.

  • Hooks (kernel ↔ Layer 2): the wiring — a fixed set of symbols/calls the board glue supplies or overrides so board events reach the kernel and vice-versa.

3. Prerequisites — what must hold before tk_kernel_start()

3.1 Common core (every arch, every board)

#

Prerequisite

Who typically does it

Notes

C1

C runtime up — .data copied, .bss zeroed, valid MSP/main stack, _start ran

crt0 / startup

The kernel is plain C; no pre-main magic.

C2

A known, stable core clock

board clock init (tk_mcu)

The tick reload derives from it (nRF52 64 MHz; RP2040 125 MHz; qemu-virt CLINT 10 MHz).

C3

RAM for caller-owned thread stacks (+ the MSP/handler stack)

app allocates; board provides SRAM

Stacks pass to tk_thread_create; the kernel never mallocs. Sizes are cells, not bytes (ADR 0001).

C4

A ≥1 kHz-capable timer + a free interrupt slot for it

SysTick/RTC (ARM, board-owned); CLINT (RISC-V, arch-owned — ★)

Must not be the peripheral the board already uses for its own time base. On trap-frame archs the timer is architectural, so the arch satisfies this and the board gives only its frequency.

C5

The vectors the kernel claims are reachable

vector table (weak) / runtime register

ARM: PendSV, SVC, the timer IRQ. Either weak (overridable) or dynamically installable.

C6

Interrupts enabled when tk_kernel_start runs

boot

So the launch trap and the first tick fire.

3.2 Feature addenda — only when the feature is enabled (TK_CONFIG_*)

SMP (TK_SMP_CORES > 1, needs ARCH_HAS_SMP):

  • S1 Secondary core(s) in a launchable state (RP2040: bootrom-parked reading the SIO FIFO; RISC-V: harts park in start.S until g_smp_release).

  • S2 An inter-core doorbell + a hardware atomic / spinlock — ARMv6-M has no LDREX/STREX, so a SIO hardware spinlock is the only cross-core atomic; RISC-V uses amoswap (A ext) + MSIP.

  • S3 A per-core timer + a per-core IPI interrupt slot.

  • S4 Exactly one core advances the global ms base; every core preempts itself.

MPU stack-guard (TK_CONFIG_ENABLE_MPU_GUARD → TK_FEAT_MPU_GUARD, needs ARCH_HAS_MPU):

  • M1 An MPU is present (a runtime-checked capability; absent ⇒ the guard is compiled out via the TK_FEAT_* gate — see kernel-tiers.md §6).

Unprivileged confinement (TK_CONFIG_UNPRIV_THREADS → TK_FEAT_UNPRIV, needs ARCH_HAS_UNPRIV):

  • B1 Flash + RAM extents the arch can grant to a confined thread (its own stack + shared globals; kernel RAM + peripherals denied).

  • B2 The fault vector (MemManage/HardFault on ARM; the trap on RISC-V) reachable for the confinement fault path.

Full isolation (TK_CONFIG_ENABLE_MPU_ISOLATION → TK_FEAT_ISOLATION, needs UNPRIV + ARCH_HAS_MPU):

  • A1 Linker zones: .privileged_bss (kernel RAM = the singleton + sync pools) → _sprivileged/_eprivileged; .thread_stacks (every isolated TK_THREAD_STACK) → _sstacks/_estacks. The arch computes the region mask from these ADDRESSES, so the zones need only region alignment, not fixed positions. Verified at root in both boards/nrf52832_pca10040/nrf52832_pca10040.ld (ARM MPU, sub-region-aligned) and boards/qemu_riscv32_virt/virt.ld (RISC-V PMP, 4 KiB/8 KiB page-aligned).

  • A2 The kernel self-initializes its privileged pools at tk_kernel_init, so the NOLOAD zones need no startup zeroing.

  • A3 Because the root owns its linker scripts fully (bare-metal, §5a), the zones are written directly — no SDK-generated-linker fight. The compute-from-symbols design (A1) is what would let the same feature drop into an SDK-generated linker later.

The single mechanism spanning ARM MPU and RISC-V PMP — same .privileged_bss/ .thread_stacks zones, same compute-from-symbols arch code — is the evidence that isolation is an arch capability behind one contract, not an ARM-specific hack.

3.3 Per-arch tail

Same roles, different silicon — see §9. ARM = PSP/MSP split + EXC_RETURN + NVIC priority-bit count; RISC-V = mtvec + ecall + mtimecmp + PMP/U-mode; TriCore = CSA free-list + BTV/BIV; C2000 = 16-bit char + stack-grows-up (ADR 0001).

4. Hooks — the Layer-2 seam (fixed and enumerable)

Hook

What the board wires

Deferred (ARM)

Trap-frame (RISC-V)

H1 Tick

the 1 ms ISR reaches the kernel

tick ISR → arch_kernel_tick → tk_kernel_tick_preempt (ms++ + pend the switch). SMP: advance the base on core 0, pend PendSV per core

the mtvec trap saves the full frame → boot hart advances the ms base → tk_kernel_preempt_select(sp) returns the next SP, installed in place. SMP: every hart rearms its own mtimecmp + preempt-selects; only hart 0 advances the base

H2 Vectors

the exceptions the kernel owns

PendSV_Handler + SVC_Handler = the arch’s (weak-override, or a naked-branch bridge when the SDK uses other names)

the arch owns the one trap vector (mtvec, direct mode); no separate PendSV/SVC. ecall is the cooperative-yield trap; mcause dispatches timer / soft-IPI / ecall / external

H3 Switch setup

called by tk_kernel_start

arch_timer_init() (weak) — a tick-owning board overrides it to arm its timer + set PendSV lowest; else the weak default just sets priority

arch_timer_init() is arch-owned and strong — it installs mtvec AND arms the CLINT mtimecmp; the board supplies only the mtime frequency (-DTK_RISCV_MTIME_HZ) (★)

H4 SMP

bring peers into the scheduler

arch_smp_start() releases secondaries into tk_kernel_start_this_core; arch_core_id/arch_spin_*/arch_ipi are board SIO (tk_smp_rp2040.c)

mhartid + amoswap spinlock + MSIP IPI; secondaries park in start.S

H5 MPU

the memory map

arch_mpu_init consumes the extents (B) or the linker zones (A); the fault vector reports the signal

RISC-V PMP + U-mode, zones from virt.ld

H6 Lifecycle

the app entry

tk_kernel_init() → tk_thread_create(fn, arg, stack, cells, prio) → tk_kernel_start() (never returns)

identical

Front-door wiring (all archs): the app includes tk_kernel.h; the build compiles tk_kernel.c + tk_sync.c + tk_trace_ring.c (TK_KERNEL_SRCS) + the arch port (TK_ARCH_SRCS) + the board (TK_BOARD_SRCS). tk_arch.h auto-selects the port from the compiler’s target (__ARM_ARCH_*, __riscv && __riscv_xlen==32), or -DTK_ARCH_<NAME> forces it — the qemu board passes no -DTK_ARCH_* and resolves risc_v automatically.

★ Ownership shift — who owns the timer is arch-dependent. On ARM the 1 ms timer is a board peripheral (SysTick / nRF52 RTC1), so H1/H3/C4 are board work — each ARM board ships a tk_tick_<board>.c with a strong arch_timer_init. On RISC-V the tick source is the CLINT machine timer, architectural on every M-mode RV32 platform, so arch/risc_v owns arch_timer_init and the trap entirely; the board contributes only the mtime frequency and the reset/linker glue. Net: C4 is satisfied by the arch, not the board, on trap-frame targets. (This ARM board→kernel tick call is also the one tracked composability leak — composability.md §8.)

Pin surface at the boundary. The board owns pin identity — the physical pin table (boards/<board>/tk_*_cfg.h, g_port_config) and the human labels (tk_board_pins.h). An application claims a free header pin at runtime via tk_port_config_pin() + tk_dio_*_pin() without editing board code. The boundary is: board = pin facts, app = role at point-of-use. The resolver mechanics + the runtime-claim API surface are hal-architecture.md §9/§6 — this doc owns only that the board must publish identity and leave free pins claimable.

5. Who provides Layer 1 — a / b / c

  • (a) Manual (datasheet): hand-written startup, clock, and linker — the bare-metal ports. This is what every root board is: nrf52832_pca10040 (own startup_nrf52.c

    • nrf52832_pca10040.ld), rp2040_pico (own startup_rp2040.c + rp2040.ld + boot2.S, SRAM image via picotool), qemu_riscv32_virt (own start.S + virt.ld + mtvec; no tick file — the arch owns the CLINT ★). Highest effort, total control.

  • (b) SDK / IDE-generated (FSP / Pico-SDK / CubeMX): Layer 1 done for you; integration is “the SDK boots, the kernel takes over.” Not present at root — this was proven in the reshape’s sketch phase (now under _legacy/sketches/) and informs the contract (§7’s SDK-wiring axis), but no root board integrates an SDK today.

  • (c) Shipped BSP = boards/<board>/: the board-specific, kernel-facing glue that ships with the kernel — the Layer-2 files: tk_tick_<board>.c (H1/H3), tk_smp_<board>.c (H4), the MPU map (H5), a vector bridge (H2) when names differ.

6. Port a board — the 1-2-3

  • Step 1 — a running bare-metal board (no kernel). Layer 1 only; the stock blinky/console builds and runs. Rule: stable C environment + a known clock + LED/console working.

  • Step 2 — add the kernel support hooks into Step 1: the tick ISR → arch_kernel_tick, the vectors (H2), arch_timer_init/arch_smp_start/arch_mpu_init, the memory map. Rule: the §4 hooks, gated by the features you enabled (§3.2).

  • Step 3 — the kernel runs. Layer 3: tk_kernel_init → tk_thread_create → tk_kernel_start. Rule: caller-owned stacks, a never-returns main, old main-loop work re-homed into threads.

One-page new-board checklist (tick each; N/A if the feature is off): [ ] C1–C6 hold · [ ] tick ISR → arch_kernel_tick · [ ] PendSV/SVC reachable (ARM) · [ ] arch_timer_init armed · [ ] (SMP) arch_smp_start + atomics + per-core tick + IPI · [ ] (UNPRIV) fault vector + extents · [ ] (ISOLATION) .privileged_bss/.thread_stacks zones · [ ] init/create/start in main.

7. Why it is a matrix, not a flat list

Nothing blocks the boundary — three real forces make it structured:

  1. Feature-gating. A single-core no-MPU build needs ~half of what SMP+ISOLATION needs → common core + addenda per enabled feature.

  2. Arch tails. PendSV/SVC vs ecall vs CSA are the same roles, different silicon → one contract + per-arch appendices.

  3. SDK-specific wiring (the leakiest axis, per-SDK not per-arch): vector ownership is weak-override vs RAM-vector-table vs strong-rename collision; linker-section support is trivial (own the linker, §5a) vs a fight (auto-generated SDK linker). At root every board owns its linker + vectors, so this axis is dormant — it reappears the moment an SDK-integrated board (§5b) is added, which is why the contract keeps the distinction.

8. Coverage matrix — the four root boards

Board

Arch

Switch model

Layer-1

Vectors

Features exercised

Proof

host

pthread

none (cooperative)

OS

n/a

cooperative; ARCH_OWNS_TIMEBASE (OS clock, no tick)

runs

nrf52832_pca10040

arm_v7m (M4F)

deferred PendSV

bare-metal (a)

weak-override

preempt · stack-check · MPU-guard · UNPRIV · isolation-A (zones in board .ld)

HW ✅

rp2040_pico

arm_v6m (M0+)

deferred PendSV

bare-metal (a), SRAM

weak + bridge

preempt · SMP=2 · IPI (SIO spinlock + FIFO launch)

HW ✅

qemu_riscv32_virt

risc_v (RV32IMAC)

trap-frame

bare-metal (a)

own mtvec

preempt · SMP=2 · IPI · isolation (PMP + U-mode, zones in virt.ld)

qemu ✅

—

arm_v8m · c2000 · tricore · msp430 · flat

(various)

—

—

capability-gate / compile only at root

compile

Capability facts driving the matrix (from each arch/<cpu>/kernel_arch.h): arm_v6m = ARCH_HAS_SMP; arm_v7m = ARCH_HAS_MPU+ARCH_HAS_UNPRIV; arm_v8m = MPU+UNPRIV+SMP+ARCH_HAS_STACK_LIMIT; risc_v = MPU+UNPRIV+SMP; tricore = SMP+ARCH_HAS_LAZY_CONTEXT+ARCH_HAS_CONTEXT_RECLAIM; c2000 = ARCH_STACK_GROWS_UP; pthread = ARCH_OWNS_TIMEBASE; flat = none (host_test, no switch).

Provenance note. The full-isolation (A) mechanism on arm_v7m was HW-proven on an M4F in the reshape sketch phase; at root the zones live in the nRF52 board linker (verified) and the RISC-V PMP/U-mode path is qemu-verified. The two linkers carrying the same .privileged_bss/.thread_stacks zones is the standing evidence for the MPU↔PMP generalization (§3.2). The legacy EK-RA4M1 / “RP2040-Zero” boards are not at root; their FSP one-line-linker and Pico-SDK integration notes are _legacy history.

What the matrix does not yet cover: an SDK-integrated board (§5b) — the only axis whose wiring is uncharacterized at root (vector strong-rename collision, SDK-generated linker vs the isolation zones). Every root board is bare-metal (§5a), so this is the open gap the contract is deliberately drawn across.

9. Per-arch appendices

  • arm_v7m (M4F, nrf52832_pca10040) — PSP/MSP; EXC_RETURN 0xFFFFFFFD; NVIC priority bits; deferred PendSV; SVC launch + syscall trap; real MPU + UNPRIV; full isolation (A): region-mask-from-symbols + per-thread own-stack grant. Tick = RTC1 (tk_tick_nrf52.c, strong arch_timer_init). HW-proven.

  • arm_v6m (M0+, rp2040_pico) — as v7m minus MPU/FPU; SMP via SIO spinlock + FIFO core-launch (tk_smp_rp2040.c, which reaches into Tier-B — the tracked leak); tick = SysTick. HW-proven (SMP=2).

  • pthread (host) — cooperative baton; ARCH_OWNS_TIMEBASE (OS clock); no tick, no vectors. Runs.

  • risc_v (RV32IMAC, M-mode, qemu_riscv32_virt) — one mtvec trap (direct mode) saves the full integer frame on the running thread’s own stack; mcause dispatches machine-timer / machine-software(IPI) / ecall / external. CLINT: per-hart mtimecmp + MSIP, shared mtime; cooperative yield = ecall. SMP: mhartid, amoswap spinlock, MSIP IPI, secondaries park until g_smp_release. Now also isolation-capable: ARCH_HAS_MPU+ARCH_HAS_UNPRIV via PMP + U-mode (zones in virt.ld) — a change from the arch’s earlier feature-lean state. mtime frequency via -DTK_RISCV_MTIME_HZ (qemu virt = 10 MHz). qemu-proven (single-hart + SMP=2, incl. isolation).

  • arm_v8m (M33/M55/M85 — v8-M Mainline; M23/Baseline resolves to arm_v6m) — MPU + UNPRIV

    • SMP + hardware stack-limit (ARCH_HAS_STACK_LIMIT). Compile / gate only at root (no board).

  • tricore / c2000 / msp430 — CSA free-list + lazy context (TriCore); ADR-0001 axes (16-bit char / stack-up for C2000); Timer_A (MSP430). Root-proven elsewhere / compile.