# HAL architecture — the peripheral abstraction layer **Status:** Design record, trimmed to the **minimal root HAL**. The layer model, the Option-C resolver seam, the boot order, and the conventions below are **in the tree** (`hal/` + all four boards). This record reflects the *distilled* HAL the reshape kept — clock, pins, digital I/O (incl. edge-IRQ), TX UART, and the portable text console — and documents the **runtime pin-claim API** the original design predated. The wider peripheral set (SPI/I2C/ADC/PWM/GPT), the external-device layer, and the DET/error module are **`_legacy`-only** and are called out where cut. **Scope:** HAL internals. **Companions:** [composability.md](composability.md) (the layering law: HAL never depends on the kernel), [board-contract.md](board-contract.md) (the board↔kernel boundary + the pin-identity side of the pin surface), [kernel-tiers.md](kernel-tiers.md) (the arch critical-section primitive the HAL borrows is the kernel's, not the HAL's). --- ## 1. Goals and non-goals **Goals** — one portable peripheral API the kernel, examples, and applications call, with hardware isolated behind it (no `#ifdef PLAT_*` in portable code); **ISO 26262 intent as a teacher** (static config, bounded loops, defined error handling, clear module boundaries, MISRA-amenable code); portability across ARM Cortex-M, RISC-V, MSP430, and a register-less host build; grow on demand — start tiny, add modules only when a real need appears. **Non-goals** — not a full AUTOSAR stack (borrow MCAL's *structure* — module decomposition, static config, a `Std_ReturnType`-style status, naming discipline — skip ARXML/MemMap/BSW-scheduler weight); not a code generator yet (config is hand-written; the struct formats are designed so a generator *could* emit them later). ## 2. The layered architecture The HAL is **not a single linear stack**. `arch/` and the MCAL both touch silicon but serve different masters; the only dependency between them is thin (the MCAL borrows arch's critical-section primitive). ``` kernel / examples / applications │ ┌──────────────┴───────────────────────────────┐ │ tk_board (boot orchestration) │ │ tk_console (portable text console = tk_print) │ names: user LED, debug console └──────────────┬───────────────────────────────┘ │ ── MCAL ── on-chip peripheral mechanism tk_mcu · tk_port · tk_dio · tk_dio_irq · tk_uart │ borrows arch's critical section (IRQ/lock) only ── arch/ ── kernel CPU port: context switch, kernel tick, THE critical-section primitive │ silicon ``` **Layer responsibilities** - **`arch/`** — the kernel's downward port. Context switch, the kernel tick, the critical-section primitive. *Not* a peripheral HAL and never MCAL-ified. - **MCAL** — on-chip peripheral *mechanism*: `tk_mcu`, `tk_port`, `tk_dio`, `tk_dio_irq`, `tk_uart`. Pure hardware drivers. - **`tk_board`** — the board bring-up entry (§4). **`tk_console`** — the portable text console (`tk_print`) over `tk_uart`, one implementation for every board. - **kernel / app** — call the MCAL directly for mechanism, `tk_board_init()` for one-call bring-up, `tk_console_init()`/`tk_print` for text. > **Cut from the minimal root (see `_legacy/docs/hal-architecture.md`):** the > *external-device* layer (`devices/` — mcp2515/pn532/pcf8574/hd44780) and its > AUTOSAR-ECU-abstraction framing. External chips are a grow-on-demand layer above the > MCAL, absent at root. ## 3. The three durable rules These resolve the apparent overlaps between `arch/` and the MCAL: 1. **The kernel tick and context switch stay in `arch/`** and are never exposed as MCAL. The scheduler heartbeat must never route through the application-facing HAL. (Who arms the tick — board vs arch — is [board-contract.md](board-contract.md) §4 ★.) 2. **One critical-section primitive lives in `arch/`.** The MCAL borrows the nesting-safe `arch_irq_save()`/`arch_irq_restore()` pair; it never invents its own interrupt masking. Its *contract* is mutual exclusion — on SMP that needs a spinlock, not just IRQ masking (§11). The flat `arch_irq_disable/enable` pair stays the kernel's own cooperative-yield mask. 3. **`tk_board` owns boot ordering.** MCAL modules do not self-sequence; the board calls them in dependency order (§10). Corollary: **`tk_board` and any device code DELEGATE to the MCAL; they never reimplement a peripheral.** A board LED toggle calls `tk_dio_flip_channel()`; there is never a board-level register poke. A peripheral *operation* belongs in the MCAL. ## 4. tk_board and tk_console — bring-up and text `tk_board_init()` (each board implements it) initialises exactly the resources the board owns before the kernel runs: the clock (`tk_mcu`), the pin table (`tk_port`/`tk_dio`), and the UART instance(s) (`tk_uart`). Call it once at the top of `main()`. It is **`void`** — the minimal root drops the legacy `tk_status_t` return; a dead clock source can't hang boot because each sub-init is best-effort with an internal bounded wait. **Which UART is the console is an application decision, not the board's.** `tk_console_init(ch)` binds `tk_print`/`tk_print_u32` to a UART channel; the console does no hardware init (the board already brought the UART up). One implementation — `hal/tk_console.c` — serves every board that provides `tk_uart_write`. Cross-core output is serialized by `tk_console_lock`/ `tk_console_unlock` (weak no-ops single-core; an SMP board overrides them with a hardware spinlock — a §2 stud, not glue). > This is the ADR [0002](../decisions/0002-multi-instance-kernel.md) §8 rename in force: > `tk_board_log` → `tk_print` — the console is an *application* choice, so it is not "board > logging." The legacy debug-sink indirection (`tk_board_set_debug_sink`) is gone; binding > the console to a channel is the whole mechanism now. ## 5. Settled decisions | # | Decision | Resolution | |---|----------|------------| | 1 | Config: generated vs hand-written | Hand-written tables; struct formats designed to be **generator-emittable** later. | | 2 | Minimal scope | `tk_mcu` + `tk_port`/`tk_dio` (+ `tk_dio_irq`) + `tk_uart` (TX) + `tk_console`. Grow on demand. | | 3 | Dio↔Port coupling | **Option C** — one source-of-truth pin table (owned by Port), reached by Dio through a single narrow **resolver seam** (§9). Not two duplicated tables; not raw struct-poking smeared through Dio. | | 4 | Types | `` widths; **no `Std_Types` aliases**. One status type, `tk_status_t`. | | 5 | Naming | Project-wide prefix **`tk_`**, snake_case, module-first (`tk_dio_write_channel`, `TK_DIO_HIGH`). One vocabulary, no exceptions. | ## 6. Module contracts (minimal root) Portable headers in `hal/`; per-board implementations + config in `boards//`. Signatures verified against the root headers. - **`tk_mcu`** — `tk_mcu_init()` (clock tree; runs first), `tk_mcu_clock_hz()` (the frequency the arch consumes for the kernel tick). MCAL owns the clock; the arch queries it. *(Minimal: no status/DET layer.)* - **`tk_port`** — `tk_port_init(const tk_port_config_t* cfg)`. One-time pin setup (direction/mode/pull) from the board pin table. The config struct is **opaque** to portable code; each board defines its layout in `tk_port_cfg.h`. - **`tk_dio`** — `tk_dio_read_channel` / `tk_dio_write_channel` / `tk_dio_flip_channel`. The fast runtime path; pins must already be configured. `void` write/flip hot path (an invalid channel is a programmer error, not a per-call branch); `flip` returns the level after the flip. - **`tk_dio_irq`** — a digital-input **edge as an interrupt** (the reactive complement to Dio's read/write): `tk_dio_irq_init()`, `tk_dio_irq_enable_channel/​_pin(edge, cb, ctx)`, `tk_dio_irq_disable_channel/​_pin`. The callback runs in interrupt context; it MAY `tk_sem_post()` to wake a thread (safe while unprivileged threads are off — the default build). A scarce detect unit per enabled pin (`TK_ERR_BUSY` when none is free). This is the canonical §2 callback stud (edge → registered handler), not a kernel dependency. - **`tk_uart`** — `tk_uart_init()`, `tk_uart_write(ch, data, len)` (blocking, bounded per-byte wait). **Instance-addressed** — a second UART is a new `TK_UART_CH_*` row in `tk_uart_cfg.h`, not an API change. *(Minimal root is TX-only; the RX ring returns when a demo needs receive.)* ### 6.1 The runtime pin-claim API (added since the original design) The original HAL configured every pin from the board's static table. The root adds a **runtime claim** path so an application can grab a *free* header pin without editing board code — the board publishes pin *identity* (labels in `tk_board_pins.h`, the table via `tk_*_cfg.h`); the app declares the *role* at point-of-use: - **`tk_port_config_pin(uint8_t pin, tk_dir_t dir, tk_pull_t pull)`** — configure one pin by its SoC pin number at runtime, without touching the board table (`tk_dir_t` = IN/OUT, `tk_pull_t` = NONE/DOWN/UP). - **`tk_dio_write_pin` / `tk_dio_read_pin` / `tk_dio_flip_pin(uint8_t pin, …)`** — the fast set/clear/in path for a pin claimed above, addressed by raw SoC pin number rather than a board channel. - **`tk_uart_setup(ch, tx_pin, rx_pin, baud)`** — reconfigure a UART channel's pins + baud at runtime (either line `0xFF` to leave it unconnected), overriding the `tk_uart_cfg.h` defaults with no board edits. This is the "board = pin facts, app = role at point-of-use" boundary; its board-side half (who owns identity, what "free" means) is [board-contract.md](board-contract.md) §4. ## 7. The frozen public contract The **only** things callers may depend on, and the only things kept stable: the symbolic channel/pin IDs (`TK_DIO_CH_*`, `TK_UART_CH_*`) defined per board in the `*_cfg.h` headers; the function signatures in the `hal/` headers; and `tk_status_t`. Everything else — config struct layout, whether Dio and Port share a table, how registers are accessed, the resolver internals — is **private and refactorable.** Freezing the API (not the wiring) is what protects existing functionality from internal change. ## 8. Status model — class vs detail `tk_status_t` (`hal/tk_status.h`) is a **small enum of failure CLASSES** for control flow: `TK_OK`(0) · `TK_ERR_PARAM` · `TK_ERR_BUSY` · `TK_ERR_STATE` · `TK_ERR_TIMEOUT` · `TK_ERR_UNSUPPORTED` · `TK_ERR`. It is intentionally coarse — the precise *why* (module + api + code) is the DET path's job, reported at the failure site. > The class constants are **numbered to match `kernel/tk_det.h`** so HAL and kernel describe > one conceptual model, yet live in **separate headers on purpose** (HAL code includes > `tk_status.h`, kernel code includes `tk_det.h`) — the enum names would otherwise collide > in one TU. The fuller `tk_error`/DET reporting module (hook + per-module counters) is > **`_legacy`-only** in the minimal root. ## 9. The Option-C resolver seam Dio's hot path resolves a channel to the hardware it needs through **one** board-private inline accessor — the single place that knows the pin-table layout and performs any integer→pointer cast (also the single home for the MISRA 11.4/11.6 int↔pointer deviation). The seam abstracts the register model completely; the same Dio API sits above radically different resolvers: | target | what `dio_resolve()` returns | |--------|------------------------------| | nRF52 | pointers to OUTSET/OUTCLR/IN registers + mask (int→ptr cast) | | host | a pointer to a simulated level cell (**no cast at all**) | If the config representation changes, or a generator emits separate Dio tables, **only `dio_resolve` changes** — callers and the public API do not. The runtime pin-claim path (§6.1) resolves the raw SoC pin through the same register model, so it shares this seam. ## 10. Boot ordering `tk_board_init()` is the single owner of the bring-up sequence, because only the board knows its recipe: ``` main(): tk_board_init(): tk_mcu_init(); // 1. clock tree / PLL tk_port_init(&cfg); // 2. pin direction / mux / pull tk_uart_init(); // 3. board UART instance(s) tk_console_init(ch); // 4. app binds the console to a UART channel (its choice) tk_kernel_init(); tk_thread_create(...); tk_kernel_start(); // arch_timer_init() (the kernel tick) fires inside // tk_kernel_start — it stays in arch/, off the MCAL path ``` ## 11. Multi-core / SMP readiness **Nothing in `hal/` needs to change for SMP** — the contract is largely SMP-ready by construction. The resolver seam is a pure, stateless function of the channel; config tables are `const` (shared read-only); APIs are instance-addressed (distinct instances touch distinct state). What needs care lives in `arch/`, not the HAL contract: 1. **The critical-section primitive's contract is "mutual exclusion"**, its implementation target-dependent. Masking IRQs suffices on a uniprocessor; on SMP it does **not** — masking core A does nothing to core B, so an SMP port extends `arch_irq_save/restore` with a spinlock. (Proven at root: `tk_console_lock/unlock` weak→hardware-spinlock on the rp2040 SMP board.) 2. **Shared RAM state needs true atomics on SMP, not `volatile`.** 3. **Per-instance mutable driver state** is race-free across *different* channels but not when one instance is shared by two cores — "do not share one instance across cores without external sync." ## 12. Conventions - **Prefix:** `tk_` on every public symbol; types `tk_<...>_t`, constants `TK_…`. - **Types:** `` widths only. (Stack sizes are **cells**, not bytes — ADR 0001.) - **Bounded loops:** every hardware wait has a timeout — no unbounded spins (a 26262 reflex). - **No dynamic memory** in the HAL: static `const` config + static instance state. - **Register casts** (int→pointer) confined to the resolver / register-access macros, where the MISRA deviation is documented. - **`tk_*_cfg.h` filenames are a load-bearing seam — do NOT board-prefix them.** Apps include the *generic* name (`tk_dio.h` pulls in `tk_dio_cfg.h`; `tk_uart.h` pulls in `tk_uart_cfg.h`); the build selects the board with `-Iboards/` (each board's `TK_BOARD_INC` also adds `-Ihal`). Every board ships that exact filename. This is the Option-C binding (decision #3) expressed as a filename — renaming to `nrf52_dio_cfg.h` would force the app to name the board in its `#include` and break one-source/many-board examples. ## 13. Portability evidence One set of `hal/` contracts, four targets, zero changes to `hal/`: | target | register model | result | |--------|----------------|--------| | nRF52832 (`nrf52832_pca10040`) | 32-bit MMIO | HAL app (blinky) runs on hardware | | RISC-V (`qemu_riscv32_virt`) | UART-only (no GPIO/clock tree) | runs under qemu; `led_toggle` a legitimate no-op | | RP2040 (`rp2040_pico`) | 32-bit MMIO (SIO) | runs on hardware | | host (`boards/host`) | none (simulated) | links and runs on the desktop | The only things that differ across ports are the things designed to differ: the per-board config struct and the resolver internals. The host port — no registers at all — is the strongest proof the API is not secretly ARM-shaped. ## 14. Scope and roadmap (minimal root) **Kept at root** — a *synchronous, polled* peripheral mechanism plus one interrupt source: `tk_mcu` (clock), `tk_port`/`tk_dio` (read/write/flip + runtime pin-claim), `tk_dio_irq` (edge→callback), `tk_uart` (TX, instance-addressed), `tk_console` (`tk_print`); the arch context switch + kernel tick + `arch_irq_save/restore`. **Deferred to `_legacy` / grow-on-demand** (present in the legacy HAL, cut from the minimal root): `tk_uart` RX ring; `tk_spi` / `tk_i2c` (bus mechanism); `tk_adc` / `tk_pwm` / `tk_gpt` (application timer, distinct from the kernel tick); the `devices/` external-chip layer (mcp2515/pn532/pcf8574/hd44780) and `tk_can`; the `tk_error`/DET reporting module; the config generator. Each returns when a real consumer at root needs it — the `hal/` contracts above do not change to accommodate them (decision #1/#3). > **Correction — MSP430 is NOT dropped.** The legacy roadmap listed MSP430 as "off the > roadmap" (the 512 B-RAM class judged below the floor for a stackful kernel). That is > superseded: **`arch/msp430/` is a live CPU port** (`kernel_arch.h` + `arch_irq.h`), and > ADR [0001](../decisions/0001-foreign-architecture-policy.md) explicitly places the MSP430X > word-size/banking family in the "absorb" set. What remains true is a **board-sizing > caution**, not an architecture drop: a 512 B-RAM part is tight for per-thread stacks, so > no MSP430 *HAL board* ships at root yet. The arch is supported; a board awaits a target.