# ADR 0001 — Foreign-architecture support policy ("draw the line by axis") **Status:** Accepted (2026-07-28). **Context:** Q1–Q3 extracted the arch interface from the nine tree ports — all of which are GCC + 8-bit-byte + downward-stack, i.e. the ARM/RISC-V "convergence." The user flagged that this silently baked those three assumptions into the "common denominator," and named a family of platforms that break them and are wanted eventually: **TI C2000 (C28x)**, **TI MSP430X**, **NXP MC56F8xxx (56800E)**, **NXP S12/S12X**, **Analog Devices SHARC**, **Motorola DSP56000**. ## Decision **Draw the transparency line by *axis of foreignness*, not by *architecture*.** Absorb the cheap, universally-honest axes into the common core; quarantine the viral, rare axes behind an explicit opt-in the core is built *without*. (Chosen over "quarantine every foreign arch wholesale" and "build full transparency for everything up front.") ### The axes and where each lands | Axis | Targets that hit it | Cost to the **scheduler core** | Verdict | |---|---|---|---| | **CHAR_BIT ≠ 8** (word-addressed) | C2000 (16), 56800E (16), DSP56000 (24), SHARC (32) | Low — only the stack-size *unit* + no `uint8_t` in the contract; the scheduler is structs/indices/an opaque SP token | **Absorb** | | **Stack grows up** | C2000 | Low — `arch_context_init` is arch-owned; only the overflow-guard / high-water scan cares about "which end" | **Absorb** (arch-declared property) | | **Non-GCC vendor compiler** | *all* of them | Required regardless — compiler-abstraction + arch-owned `.asm` switch is the porting layer | **Neither** (it's the plan) | | **Harvard / multiple memory spaces** | DSP56000 (X/Y/P), SHARC (PM/DM), 56800E (milder) | High + viral — `fn`/`arg`/stack can live in different spaces; a uniform `void*` breaks | **Quarantine** | | **Banked / wide pointers** | MSP430X (20-bit), S12X (paged) | Medium — context saves bank regs (arch-owned) but cross-bank calls tax the toolchain | Mostly **quarantine** / toolchain config | | **Big-endian** | S12X, others | Low for the scheduler; matters for serialization (HAL/crypto, not the core) | Absorb (core is endian-neutral) | ### What "absorb the cheap axes" means concretely (applied now) 1. **CHAR_BIT-neutral stack contract.** The kernel↔arch contract must not use `uint8_t` (optional in C, *absent* on C2000) or say "bytes = 8 bits." Stacks are a `void*` region + a `size_t` count of **addressable cells** (C `sizeof` units: 8-bit on ARM, 16-bit on C2000, 24 on a 56000, 32 on SHARC). This is *correct C by the standard* — ARM merely let us ignore it. `arch_context_init(ctx, void* stack, size_t cells, fn, arg)`. `ARCH_MIN_STACK_SIZE` is a cell count. The no-op spinlock default type stops being `uint8_t` (→ `unsigned char`, the addressable cell, valid everywhere). 2. **Stack direction as an arch property.** `#define ARCH_STACK_GROWS_UP {0|1}` (default 0 via `arch_defaults.h`). Read *only* by the stack-guard / high-water scan; `arch_context_init` builds the frame in its own direction. C2000 sets it to 1. ### What "quarantine the viral axes" means (deferred, by design) The core carries **no** multiple-address-space or banking model. When a real Harvard / multi-space target (DSP56000, SHARC) is actually on the bench, it is added as an **explicit opt-in "exotic memory model" extension** — the core must be buildable, and ship, *without* it. No speculative machinery for hardware we don't have. ## Consequences - **The core gets more honest, not more cluttered.** The two absorbed axes cost almost nothing on ordinary targets (a byte *is* a cell on ARM; direction default is down) and remove two false assumptions — this improves the "clean, refined" goal, doesn't dilute it. - **C2000 becomes the first integrated foreign precedent.** It exercises *only* the absorb-axes (CHAR_BIT 16, stack-up, cl2000) and is a **flat, single-address-space** machine — no Harvard, no banking. So it validates the "absorb" half cleanly and defers the "quarantine" question honestly to a future DSP56000/SHARC. The precedent generalizes to the whole word-size/stack-direction family (C2000 → 56800E → MSP430X). - **A concrete leak was already caught by including C2000:** `uint8_t` in the shared defaults — proof the outlier belongs in the analysis, not beside it. ## Follow-through (this change set) - `arch/arch_defaults.h`: spinlock default `uint8_t`→`unsigned char`; add `ARCH_STACK_GROWS_UP` default (0). - `arch/arm_v7m`, `arch/arm_v6m`: `arch_context_init` signature → `void*` + `size_t` cells (behaviourally identical on ARM). - New `arch/c2000/{arch_irq.h,kernel_arch.h}`: cl2000 `DINT`/`EINT` Tier 0; `ARCH_STACK_GROWS_UP 1`; cell-based stack; no SMP/MPU/unpriv → all defaulted. - **Validated under cl2000 25.11.1** (`/c/ti/.../ti-cgt-c2000_25.11.1.LTS`) — the vendor compiler R1.1 promised but only GCC had exercised. --- ## Addendum (2026-08-10) — completing CHAR_BIT absorption into the CORE + the type vocabulary **Status:** Accepted (extends the original; does not revise it). *See Addendum 2 below — the concrete `tk_u8_t` typedef this addendum introduced was itself later superseded; the four-kinds framework it established still stands.* **Context.** The original change set neutralized the kernel↔arch *contract* (the `arch_defaults.h` spinlock, `arch_context_init`'s cells, the C2000 arch headers) and "validated under cl2000" — but that validation only ever compiled the **arch headers**. The first time the reshaped kernel **core** (`tk_kernel.*`, `tk_sync.*`, `tk_mutex.h`, the TCB) actually met cl2000 (starting the C2000 *reshape port*, R1.3), it did **not** compile: ~40 `uint8_t` uses remained in the core internals + public API — kind-1 *values* (priority, flags, core masks, enum tags) mis-spelled with a type that is **absent on C2000**. So the "absorb CHAR_BIT" decision had been applied to the contract but not carried into the core. A full-core byte-width sweep confirmed `uint8_t` is the **only** leak: the core has **no** byte-pointer arithmetic, **no** serialization / bit-packing, **no** `memcpy`. ADR 0001's "the scheduler is structs/indices/an opaque SP token" analysis holds; only the type *spelling* leaked. **The type vocabulary (four kinds).** Byte-width portability stays honest by never conflating four distinct quantities — each has its own type; mixing them is the bug: 1. **Small integer value** (priority, flags, masks, enum tags) → a value in 0..255, never a byte of memory. *(Addendum 2: spelled `unsigned char` directly at root — see below.)* `uint8_t` is optional in C and absent on C2000 — it must never appear in the core. 2. **Addressable memory cell** (stacks, RAM buffers) → **`tk_stack_cell_t` = `unsigned char`** + a `size_t` count of cells. `sizeof`/`memcpy`/indexing/`alignof` are all cell-correct (C guarantees `sizeof(char)==1`) on every target. 3. **Machine word / register / stack pointer** → **opaque**: `struct arch_context` (the arch picks the width) + `uintptr_t` for the in-transit SP token. The core never interprets it; registers live only in `arch/`. 4. **Wire octet** (data leaving the CPU) → an explicit octet type + explicit (de)serialization, owned by the serializing **feature/HAL**, never the core, never by struct-aliasing a wire buffer (a "byte" buffer holds 16-bit cells on C2000). No such surface exists in the core today; this rule attaches to trace/introspect export when they are ported. The core lives entirely in kinds 1–3, all portable by construction. **Decision (as made 2026-08-10; superseded in part — see Addendum 2).** Complete the CHAR_BIT absorption into the core: add **`tk_u8_t = uint_least8_t`** (in `kernel/tk_types.h`) and use it for the kind-1 fields; keep kind-2 as `tk_stack_cell_t`. Chosen over **(a)** a C2000-only `typedef … uint8_t` shim (makes `uint8_t` a 16-bit lie; leaves the core non-neutral so the next word-addressed arch inherits the shim) and **(b)** a single unifying `tk_byte_t` for both kinds 1 & 2 (conceptual economy, but types integer *values* as memory bytes and erases the kind-1/2 seam that governs aliasing and the kind-4 wire boundary). Behaviorally identical on the 8 GCC/host ports (`uint_least8_t == uint8_t` there); a one-time core change that makes **R4.9** ("a new arch adds no core edits") genuinely true for the whole word-addressed family (C2000 → 56800E(16) → MSP430X → DSP56000(24) → SHARC(32)). **Verified (evidence, not assertion).** The reshaped kernel **core** compiles under **cl2000 25.11.1** for the first time (`tk_kernel.c` + `tk_sync.c` + `tk_arch_impl.c`→`arch/c2000`, `--c99`), and still compiles clean under **RISC-V GCC** with `-Wall -Wextra` (no regression). Two build/portability notes captured here so they are not re-discovered: - **The C2000 build requires `--c99`** — the core uses C99 inline `for (int i …)` declarations; cl2000 defaults to C89. A build flag (R9), not a code change; the C2000 board/CCS project must set it. - **`int` is 16-bit on C2000** (32 on ARM). The core compiled clean (no 32-bit-int declarations), but *runtime* int-range correctness is a standing check the full-core cl2000 build now guards; watch it as new core code lands. --- ## Addendum 2 (2026-08-19) — `tk_u8_t` dropped; kind-1 is spelled `unsigned char` directly **Status:** Accepted (supersedes Addendum's concrete kind-1 typedef only; the four-kinds framework is unchanged). Recorded when this ADR was migrated into the root `docs/`. **What changed.** Addendum (2026-08-10) resolved kind-1 (small integer values) to a named typedef `tk_u8_t = uint_least8_t`. The root tree took the simpler road it had left open: kind-1 is spelled **`unsigned char` directly**, with **no project typedef at all**. `kernel/tk_types.h` now defines only `tk_stack_cell_t` (kind 2) and documents kind 1 as: *"Spell it `unsigned char` directly — it is the smallest addressable unsigned type, holds 0..255 on every target, and (unlike `uint8_t`, which is optional in C and absent on C2000) is guaranteed to exist. No project typedef needed."* **Why.** `unsigned char` already satisfies every constraint `tk_u8_t` was introduced to meet (exists everywhere, holds 0..255, never `uint8_t`), so the typedef bought a name without buying a guarantee — a layer to learn for no portability gain. Dropping it keeps the vocabulary to exactly one named type (`tk_stack_cell_t`, whose cell-counting rule is the load-bearing one) and leaves kind 1 as ordinary C. The **four-kinds analysis stands verbatim** — only kind 1's *spelling* changed (typedef → direct `unsigned char`). **Residual leak (open, tracked as roadmap, not fixed here).** `kernel/tk_kernel.h`'s public introspection snapshot `struct tk_thread_info` still declares `uint8_t base_priority` / `uint8_t state` — the exact kind-1-as-`uint8_t` anti-pattern this ADR forbids. It is a public-header leak (not core internals) and predates this migration; fix it to `unsigned char` when the introspection surface is next touched.