Board bring-up — the minimal foundation

Audience: anyone bringing up a new board / architecture. Companion contracts: the CPU port (arch/<cpu>) 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/<board>_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/<cpu>: 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/<board>/ 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=<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/<cpu>.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/<cpu> only ever consumes the rate the board hands it — it never assumes one.