# 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](kernel-tiers.md) (the Tier-B port contract these hooks call), [composability.md](composability.md) (why board glue that names a kernel symbol is a tracked leak), [hal-architecture.md](hal-architecture.md) (the HAL internals + the runtime pin-claim API this doc references at the boundary), ADR [0002](../decisions/0002-multi-instance-kernel.md) (§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 `malloc`s. 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](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_` 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_.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](composability.md) §8.) **Pin surface at the boundary.** The board owns *pin identity* — the physical pin table (`boards//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](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//`**: the board-specific, *kernel-facing* glue that ships **with** the kernel — the Layer-2 files: `tk_tick_.c` (H1/H3), `tk_smp_.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//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.*