# Board bring-up — the minimal foundation **Audience:** anyone bringing up a new board / architecture. **Companion contracts:** the CPU port (`arch/`) how-to is `docs/guides/arch-porting.md` (field-by-field contract reference: `docs/architecture/arch-interface.md`); the HAL module contracts (mcu / port / dio / uart / console / board) are `docs/architecture/hal-architecture.md`. This doc is the *board-level* ladder that ties them together, and the definition of "thin." ## The principle: bring up THIN A new architecture is brought up to the **minimal viable board** and no further. The foundation is the board that **blinks, prints, and — if it gets the CPU port — preempts.** Nothing else ships by default; every feature (buses, crypto, persistence, trace, devices, drivers, connectivity) is ported **on demand**, once, only when a board actually needs it. Why thin, and why breadth-before-depth: - **Small port surface.** Features you didn't port can't break on the new arch. - **An honest contract.** Keeping the default minimal forces the core contract to be explicit instead of "whatever the last board happened to need." - **De-risk early.** A new arch falsifies hidden assumptions in the foundation (see *Portability landmines*). Finding them on a thin board is a contained fix; finding them under a stack of features is brutal. So each new board is aimed at breaking *one* assumption while the foundation is still cheap to change. Do **not** carry the full feature set onto a new board just because it exists in-tree. Root makes this a build invariant, not just a norm: every optional subsystem is gated behind a `TK_CONFIG_ENABLE_*` flag that **defaults to 0** (`kernel/tk_config.h` — trace, SystemView, introspection, MPU guard, full MPU isolation, EDF, SMP affinity); a board's `SRCS` only pull in what it turns on. ## The foundation ladder Bring the rungs up in this order; verify each on hardware (or an emulator) before the next. | Rung | Layer | Deliverable | Contract | |:--:|---|---|---| | **0** | bare sketch | a no-framework blink + polled-UART "hello" in `sketches/_bringup/` (startup + linker + clock + a GPIO toggle) | none — this de-risks toolchain / flash / clock / linker *before* the framework is in the picture | | **1** | clock | `tk_mcu_init()` to a known frequency + `tk_mcu_clock_hz()` | `hal/tk_mcu.h` | | **2** | GPIO | `tk_port_init` + `tk_dio_*`; a blink through the HAL | `hal/tk_port.h` + `hal/tk_dio.h`; author the pin table by hand (see *Bring-up discipline*) | | **3** | console | `tk_uart_init`/`tk_uart_write` TX (RX optional); `tk_console_init()` + `tk_print()` work | `hal/tk_uart.h` + `hal/tk_console.h` — printf-debug for everything after this | | **4** | board facade | `tk_board_init()` sequences mcu→port→uart, once, before `tk_console_init()`/`tk_kernel_init()` | `hal/tk_board.h` — LED toggle and debug print are calls the *app* makes itself (`tk_dio_flip_channel`, `tk_console_init`+`tk_print`), not board-facade wrappers | | **5** | CPU port + kernel *(optional)* | `arch/`: context switch + IRQ state + 1 ms tick → preemptive threads | `docs/guides/arch-porting.md` | **A board that reaches Rung 4 is already a valid tk board** (HAL-only). Rung 5 turns it into an RTOS. A RAM-tiny part is allowed to stop at 4 — `arch/msp430` is the concrete evidence this is a real, deliberate tier at root, not just a promise: it declares `ARCH_CONTEXT_SWITCH_SUPPORTED 0`, has no context struct and no `kernel_arch.c`, so a HAL/MCAL superloop links only the two interrupt-mask primitives and never the scheduler. (No MSP430 board is paired to it at root yet — the port exists specifically to prove Tier 0 is separable.) That is the entire foundation. Everything below is *not* part of it. **Caveat on Rung 1 in practice:** only `nrf52832_pca10040` implements `hal/tk_mcu.h` as its own module (`tk_mcu_nrf52.c`); `host`, `qemu_riscv32_virt`, and `rp2040_pico` all fold clock bring-up directly into `tk_board_init()` and never expose `tk_mcu_clock_hz()`. The ladder is a sequencing discipline, not a mandate to always produce four separate translation units — give clock bring-up its own `hal/tk_mcu.h` module when something *besides* board bring-up needs to query the rate; otherwise folding it into Rung 4 is fine. ## Explicitly NOT the foundation (opt-in, on demand) `tk_spi` / `tk_i2c` / `tk_pwm` / `tk_adc` / timers · crypto (`tk_sha` / `tk_hmac` / `tk_aes` / `tk_ecdsa` / `tk_vboot`) · persistence · safety (watchdog / brownout / reset-reason) · `devices/` (external chips) · `drivers/` (button/led engines) · connectivity (BLE / Ethernet) — none of these are in the root tree yet; each graduates in once some board actually needs it. What **is** in the tree today and gated off by default: introspection, the trace ring, SystemView, MPU guard, full MPU isolation, EDF, SMP affinity — each behind its own `TK_CONFIG_ENABLE_*` in `kernel/tk_config.h`. Never add one to a board's `SRCS`/config by default, and never "because it's already there." ## Bring-up discipline - **Sketch first (Rung 0).** Never debug toolchain *and* framework at once. `sketches/` is already the tree's "isolate before graduate" convention (e.g. `sketches/amp/`); a new board's Rung 0 belongs there too, before it earns a `boards//` directory. - **Hand-author the pin/clock table; verify it once against the datasheet.** Root pin tables today are small hand-written `const` arrays — see `boards/nrf52832_pca10040/tk_pins_nrf52.c`, whose own header comment says so plainly: *"Hand-written; the legacy tree generated this from pins.yaml."* The tkgen code generator (`pins.yaml`/`clocks.yaml` → validated C tables) is legacy tooling not yet migrated to root — prefer it once it lands; don't block a new board on it now. - **Verify per rung**, on hardware or an emulator, before the next. - **CPU port:** follow the `docs/guides/arch-porting.md` checklist. Root has no standalone "build every arch" harness yet (no `make check-profiles` at this tree); the real verification is building the cross-board example apps against each target board — `examples/threads` (single-core) and `examples/threads_smp` (SMP, `SMP=2`) — with `make BOARD= run` / `flash` for each board in the table below. ## Portability landmines — aim each new board at one Every board in the tree today agrees on the C execution model: gcc-family toolchain, `CHAR_BIT==8`, byte-addressable, single-core by default (`arm_v6m`/`arm_v7m`/`arm_v8m`, `risc_v`, `pthread`, `flat`). New arches break that agreement **unequally** — which is exactly why you bring them up thin: - **Word size / `CHAR_BIT`.** The **C2000 C28x has `CHAR_BIT == 16`** (`arch/c2000`, ADR 0001 — `docs/decisions/0001-foreign-architecture-policy.md`). It detonates every latent "a byte is 8 bits / buffers are byte-addressable" assumption — serialization, `sizeof` math, UART rings, a trace ring. `arch/c2000/kernel_arch.c` + `switch.asm` are fully implemented at root (not yet paired with a board or wired into `make/arch/`), so the precedent stands even without a running board. **The highest-value probe:** cheap to fix on a thin board, ruinous to discover under crypto + connectivity. - **Multicore / SMP.** Real and HW-provable at root **today** on `rp2040_pico` (`arm_v6m`, dual Cortex-M0+, `make BOARD=rp2040_pico run SMP=2`) and `qemu_riscv32_virt` (two harts, `SMP=2`, no hardware needed) — the SIO hardware spinlock stands in for the LDREX/STREX exclusive monitor ARMv6-M doesn't have. AURIX TriCore (`arch/tricore`) is the heavier, heterogeneous case still ahead: register context lives in hardware CSAs, not a stack frame (`ARCH_HAS_CONTEXT_RECLAIM`), and at root the port is currently *declarations only* — `kernel_arch.h` with no `kernel_arch.c` yet. See `docs/architecture/smp-model.md` and `docs/decisions/0002-multi-instance-kernel.md` (separate-kernel AMP islands beside SMP). - **Non-GCC toolchain.** `cl2000` (C2000) is the one vendor-compiler case in the tree today; the `make/arch/.mk` fragment is where that shock is absorbed (not yet written for c2000 at root). Every other in-tree port — including `tricore-elf-gcc` for TriCore — is GCC-family. - **Endianness / alignment.** Surfaces in trap-frame stores and descriptor structs. Bring a new board up thin **first**, so these surface against the foundation — not against a tower built on assumptions that don't hold there. ## Reference boards to copy from | Board | CPU port (`arch/`) | What it proves | Bring-up | |---|---|---|---| | `host` | `pthread` | cooperative host sim — run the RTOS on the desktop, no hardware | `make BOARD=host run` | | `nrf52832_pca10040` | `arm_v7m` (Cortex-M4F) | full ladder incl. real MPU + unprivileged threads; RTC1/LFCLK-driven tick (survives WFI sleep, unlike SysTick); optional SystemView trace (`SYSVIEW=1`) | `make BOARD=nrf52832_pca10040 flash` (J-Link over SWD) | | `qemu_riscv32_virt` | `risc_v` | trap-frame kernel, emulated; CLINT-timer preemption; `SMP=2` for a two-hart SMP kernel | `make BOARD=qemu_riscv32_virt run` (Ctrl-A X to quit) | | `rp2040_pico` | `arm_v6m` (dual Cortex-M0+, no MPU/FPU) | dual-core SMP on real hardware (`SMP=2`) | `make BOARD=rp2040_pico run` (SRAM via picotool) / `rp2040_pico_flash` for a UF2 | Each board's tick source is board-specific, never arch-hardcoded, and the four boards above prove it three different ways: nRF52 derives it from the RTC1/LFCLK (SysTick would stop in sleep), rp2040 hardcodes a `clk_sys` constant its own `clocks_init()` sets up, and `qemu_riscv32_virt`'s `risc_v` port reads a board `.mk` compile define (`TK_RISCV_MTIME_HZ`) instead of a HAL call at all. `arch/` only ever consumes the rate the board hands it — it never assumes one.