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 — |
crt0 / startup |
The kernel is plain C; no pre-main magic. |
C2 |
A known, stable core clock |
board clock init ( |
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 |
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 |
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.Suntilg_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/HardFaulton 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 isolatedTK_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 bothboards/nrf52832_pca10040/nrf52832_pca10040.ld(ARM MPU, sub-region-aligned) andboards/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_stackszones, 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 → |
the |
H2 Vectors |
the exceptions the kernel owns |
|
the arch owns the one trap vector ( |
H3 Switch setup |
called by |
|
|
H4 SMP |
bring peers into the scheduler |
|
|
H5 MPU |
the memory map |
|
RISC-V PMP + U-mode, zones from |
H6 Lifecycle |
the app entry |
|
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>.cwith a strongarch_timer_init. On RISC-V the tick source is the CLINT machine timer, architectural on every M-mode RV32 platform, soarch/risc_vownsarch_timer_initand 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(ownstartup_nrf52.cnrf52832_pca10040.ld),rp2040_pico(ownstartup_rp2040.c+rp2040.ld+boot2.S, SRAM image via picotool),qemu_riscv32_virt(ownstart.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-returnsmain, 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:
Feature-gating. A single-core no-MPU build needs ~half of what SMP+ISOLATION needs → common core + addenda per enabled feature.
Arch tails. PendSV/SVC vs
ecallvs CSA are the same roles, different silicon → one contract + per-arch appendices.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 |
|---|---|---|---|---|---|---|
|
pthread |
none (cooperative) |
OS |
n/a |
cooperative; |
runs |
|
arm_v7m (M4F) |
deferred PendSV |
bare-metal (a) |
weak-override |
preempt · stack-check · MPU-guard · UNPRIV · isolation-A (zones in board |
HW ✅ |
|
arm_v6m (M0+) |
deferred PendSV |
bare-metal (a), SRAM |
weak + bridge |
preempt · SMP=2 · IPI (SIO spinlock + FIFO launch) |
HW ✅ |
|
risc_v (RV32IMAC) |
trap-frame |
bare-metal (a) |
own |
preempt · SMP=2 · IPI · isolation (PMP + U-mode, zones in |
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_stackszones 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_legacyhistory.
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, strongarch_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) — onemtvectrap (direct mode) saves the full integer frame on the running thread’s own stack;mcausedispatches machine-timer / machine-software(IPI) /ecall/ external. CLINT: per-hartmtimecmp+MSIP, sharedmtime; cooperative yield =ecall. SMP:mhartid,amoswapspinlock,MSIPIPI, secondaries park untilg_smp_release. Now also isolation-capable:ARCH_HAS_MPU+ARCH_HAS_UNPRIVvia PMP + U-mode (zones invirt.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 + UNPRIVSMP + 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.