SPEC-011 - Architecture Port Contract¶
Implementation and release scope: Project status records the implemented subset; roadmap defines release gates. Acceptance of this design does not establish implementation, verification, or support. Reconciled 2026-10-14.
Status: Accepted 2026-10-07 by the project owner (“do all”), with the defaults of §17. Specification work item 10 of the roadmap (07 §3), in enough detail to implement the native and AVR ports (M1) and to constrain the Cortex-M (M3) and RISC-V (M5) ports.
Requirements: docs/requirements/PORT.md (v0.1 §21 identifiers restated where they exist; new from the first free number).
Builds on: 02 §2 (what the port owns and must never do), §5 (dependency rules), §8 (native port); SPEC-002 (critical sections, preemption points, nesting, stacks, faults, per-architecture mapping), SPEC-003 §4 and §13 (timer contract and sources), SPEC-004 §12 (compare-and-set availability), SPEC-008 §4, §5, §10 (context init, switch-out completion, stacks), SPEC-010 §4, §5 (protection and trap hooks); 09 (EmbCC: inline assembly vocabulary, naked functions, startup forms, linking, atomics, debug-info limits); ADR-001, ADR-011.
Research: R-001 §2 and §9: the smallest port contracts (NuttX’s returned-frame switch, ChibiOS’s port_* set, FreeRTOS’s portmacro.h) versus the largest (Zephyr’s about 112 arch_* entry points, RTEMS’s no_cpu template). This contract aims at the small end: one switch path per architecture, a fixed list of entry points, and everything the kernel needs stated as a feature flag in a manifest the generator reads.
1. Scope and terms¶
Term |
Meaning |
|---|---|
Port |
|
Variant |
A sub-family selected at build time within a port (Armv6-M, Armv7-M, Armv8-M; RV32, RV64) |
Manifest |
|
Contract function |
An |
Kernel hook |
An |
The port owns: CPU context, first-thread launch, context switch, interrupt and exception entry and exit, critical sections and masking, atomics and barriers where the compiler needs help, privilege transitions, protection-hardware primitives, the architecture timer hooks, the idle instruction, fault context decode, SMP boot and inter-processor interrupts (02 §2). It never decides scheduling policy, touches a ready structure, or knows which thread is highest priority (v0.1 §21).
2. Manifest¶
arch: cortex_m
variants: [armv6m, armv7m, armv8m]
word_bits: 32
endian: little
stack: { align: 8, growth: down, min_thread: 192, context_frame: 64, context_frame_fpu: 136 }
features:
irq_nesting: true # hardware or software nesting of kernel-aware interrupts
zero_latency_irqs: [armv7m, armv8m] # kernel-independent class available (BASEPRI)
cas: [armv7m, armv8m] # atomic compare-and-swap in the ISA (SPEC-004 §12)
interrupt_stack: true # dedicated stack for interrupts
hw_stack_limit: [armv8m] # PSPLIM / MSPLIM
mpu: [armv7m, armv8m] # protection hardware model: armv7m | armv8m | pmp | none
fpu_lazy: [armv7m, armv8m]
cycle_counter: [armv7m, armv8m] # DWT_CYCCNT; M0+ uses an SoC timer
tickless: true
smp: false # single core in 1.0 ports; RP2350 AMP via per-core images
syscall_trap: svc # svc | ecall | none
fault_entry: true
priority_bits: soc # NVIC implemented bits come from the SoC description
tcb_extension_bytes: 0 # per-thread bytes the port needs in the TCB (native: host thread handle)
The kernel’s configuration derives EMB_ARCH_HAS_* macros from the manifest; a kernel feature whose requirement the port lacks is refused at configuration time with the manifest line named (BLD-007 pattern): for example the kernel-independent interrupt class on Armv6-M, or the isolated profile on a port without mpu.
3. Types and the thread control block¶
typedef uintptr_t emb_arch_sp_t; /* saved stack pointer of a switched-out thread */
/* emb_irq_key_t is port-defined: uint8_t on AVR, unsigned int elsewhere (A9). */
typedef struct emb_arch_tcb { /* embedded in every TCB; size from the manifest */
emb_arch_sp_t sp; /* the only field the kernel reads: for the debug descriptor */
/* present only when the extension is nonzero; alignment is port-defined */
EMB_ALIGNED(EMB_ARCH_TCB_EXTENSION_ALIGN) unsigned char ext[EMB_ARCH_TCB_EXTENSION]; /* port-private (native: host thread handle, gate state) */
} emb_arch_tcb_t;
typedef uint32_t emb_cycle_t; /* cycle counter width is 32 bits on every 1.0 port */
The thread’s register context lives on its own stack; the TCB holds only the stack pointer (plus the port extension). This is the smallest possible TCB contribution, it keeps the kernel’s layout independent of the register set, and it is what every C kernel in R-001 does except the MPU retrofits that moved contexts into the TCB. Isolated ports keep the partition’s protection table pointer in the partition, not per thread.
4. Startup and kernel start¶
Function |
Called by |
Does |
|---|---|---|
|
SoC startup, before |
vector table base, FPU enable and lazy stacking, stack limit registers for the pre-kernel stack, cache enable where the SoC asks, trap vector ( |
|
|
interrupt stack installation, priority grouping, PendSV and SVC priorities (SPEC-002 §13), timer source selection, cycle counter start |
|
|
switches from the pre-kernel stack to the first thread’s context and never returns; afterwards the pre-kernel stack is the interrupt stack (ports with |
Startup forms per target follow 09 §8: C Reset_Handler on Cortex-M, a .S entry on RISC-V and AVR; the SoC layer owns clocks and memory setup and calls emb_arch_early_init before touching anything interrupt-related.
5. Context¶
void emb_arch_context_init(embk_thread_t *t, void *stack_base, size_t stack_size,
emb_thread_entry_t entry, void *arg, bool privileged);
void emb_arch_switch_to(embk_thread_t *next); /* P2: save current, restore next; returns when current runs again */
void emb_arch_reschedule_pend(void); /* P1 request: PendSV, flag, or soft interrupt (SPEC-002 §6.2) */
EMB_NORETURN void emb_arch_switch_final(embk_thread_t *next); /* final switch without saving dying context */
bool emb_arch_switch_out_done(const embk_thread_t *t); /* SMP: has the old stack been released (SPEC-008 §5 step 5)? UP: port reports actual completion, including deferred PendSV */
context_initbuilds the initial frame so that the first dispatch entersembk_thread_launch(entry, arg), which initializes the fresh context, callsentry(arg), and exits with code 0 (KRN-THR-005);privilegedselects the privilege bit in the frame on isolated ports and is ignored elsewhere. The frame size isstack.context_framefrom the manifest (plus the FPU frame when the thread touches the FPU on lazy-stacking ports); SPEC-008 §10 adds it to the stack budget.switch_tosaves the callee-saved registers of the running thread on its stack, stores the stack pointer in itsemb_arch_tcb_t, loads the next thread’s pointer, restores, and resumes it. On Cortex-M it is implemented by pending PendSV and letting it run at the next instruction boundary with interrupts enabled, so that P1 and P2 share one switch path (SPEC-002 §6.3); on RISC-V and AVR it is a direct call into thenakedswitch routine; native: the gate (SPEC-013).The port decides the FPU strategy per variant: lazy stacking on Cortex-M (
FPCCR.LSPEN), and on RISC-V with hardware float (not in 1.0, soft float only, 09 §2) a dirty-bit check in the switch. The kernel never saves FPU state itself.The switch is a full barrier (KRN-MM-002): the port guarantees it with the instructions its memory model needs (
dsb; isbon ARM,fenceon RISC-V, nothing extra on AVR).
6. Interrupts and critical sections¶
emb_irq_key_t emb_arch_irq_lock(void); /* SPEC-002 §4.1 table; always_inline with "memory" clobber */
void emb_arch_irq_unlock(emb_irq_key_t key);
bool emb_arch_irq_locked(void);
bool emb_arch_in_isr(void); /* IPSR, mcause-derived flag, or the kernel's depth counter (AVR) */
emb_status_t emb_arch_irq_enable(emb_irq_t irq); /* controller operations for architecture-standard controllers */
emb_status_t emb_arch_irq_disable(emb_irq_t irq);
emb_status_t emb_arch_irq_set_level(emb_irq_t irq, uint8_t generic_level); /* generic numbering of SPEC-002 §8 */
emb_status_t emb_arch_irq_clear_pending(emb_irq_t irq);
emb_status_t emb_arch_irq_pend_soft(emb_irq_t irq);
bool emb_arch_irq_is_enabled(emb_irq_t irq);
The port implements the kernel-aware entry and exit sequence of SPEC-002 §6: increment
irq_nesting_depth, dispatch, decrement, and at the outermost exit perform the switch whenreschedule_pendingand the scheduler is not locked. Cortex-M: handlers are plain C functions and PendSV at the lowest priority is the switch; RISC-V: trap entry in.Ssaves caller-saved registers on the interrupt stack and checks the flag at depth 1; AVR:EMB_ISR(vector)expands to anakedvector that saves the full register file on the interrupted thread’s stack and calls the epilogue (SPEC-002 §6.2).EMB_ISR(name)andEMB_ISR_RAW(name)are provided by every port with the semantics of SPEC-002 §8; on Cortex-MEMB_ISRis a plain function definition andEMB_ISR_RAWadds nothing but documentation because the hardware does the saving.Controller ownership: the port drives architecture-standard controllers (NVIC; RISC-V CLINT and CLIC); a platform-level controller instance (PLIC) is an SoC device driven through the same
emb_arch_irq_*entry points, which the port routes by interrupt number range from the generated tables (02 §2: the SoC owns the instance, the port owns entry and exit).Interrupt stacks per SPEC-002 §7.2: the port installs the dedicated stack where the manifest says
interrupt_stack: trueand documents the per-thread reserve where it saysfalse(CONFIG_EMB_AVR_ISR_STACK_RESERVE).Zero-latency interrupts (
features.zero_latency_irqs): the port reserves the levels above the kernel mask and never touches kernel state from them (SPEC-002 §3).
7. Timer and cycle counter¶
The architecture timer contract of SPEC-003 §4 is part of this contract:
emb_status_t emb_arch_timer_init(void); /* source and mode from configuration */
void emb_arch_timer_start_periodic(uint32_t hz); /* periodic tick mode */
emb_arch_timer_raw_t emb_arch_timer_now_raw(void); /* tickless: free-running counter */
void emb_arch_timer_set_deadline_raw(emb_arch_timer_raw_t when);
void emb_arch_timer_cancel(void);
uint32_t emb_arch_timer_hz(void);
emb_tick_t emb_arch_timer_max_ticks(void); /* MAX_INTERVAL */
uint32_t emb_arch_timer_set_latency_ticks(void); /* arming latency, measured at init or declared (KRN-TIM-036) */
emb_cycle_t emb_arch_cycles(void); /* DWT_CYCCNT, mcycle, Timer1, host clock */
uint32_t emb_arch_cycles_hz(void);
The port’s timer interrupt calls embk_time_timer_isr() (SPEC-003 §4); the sources per architecture are SPEC-003 §13. The target contract calibrates or declares arming latency and accounts for resolution/uncertainty (KRN-TIM-036). M1 ports can return zero through this hook; that is not evidence of measured zero latency, and embedded tickless accuracy remains unvalidated.
8. Atomics, barriers, compare-and-swap¶
The target atomic API is C11 atomics through the portability layer (SPEC-005 §9, 09 §6); the port provides nothing hand-written for them. The port’s manifest states features.cas; the future concurrent wait protocol selects CAS or a supported lock implementation; current UP transitions are serialized by critical sections (SPEC-004 §12). emb_arch_mb(), emb_arch_rmb(), emb_arch_wmb() are the port’s barriers, used by the kernel only where the C11 fences are insufficient for device memory (never on the 1.0 targets; present for the port that will need them).
9. Idle and power¶
void emb_arch_idle(void); /* enable interrupts and wait for one, atomically */
void emb_arch_sleep(emb_power_state_t s); /* planned; absent from the current contract */ /* enter the SoC-mapped state; the power core calls it (04 §4) */
emb_arch_idle must not lose a wake-up between enabling interrupts and waiting: wfi on ARM and RISC-V wakes on a pending interrupt even while masked, so the port masks, checks nothing, executes wfi, then unmasks; AVR uses sei; sleep, which the hardware guarantees to be atomic. The idle thread (or the inline idle of ADR-036) calls it when the power core is disabled (PWR-004).
10. Stacks¶
Item |
Port provides |
|---|---|
|
from the manifest; |
|
at switch-out when |
|
Armv8-M |
interrupt stack fill and check |
SPEC-002 §7.2 |
|
supported compiler reports plus disassembly/call-graph accounting where naked functions cannot be reported (09 §7); the port’s |
11. Faults¶
void emb_arch_fault_entry(void); /* the architecture's fault vectors land here (SPEC-002 §9) */
typedef struct emb_fault_info { ... } emb_fault_info_t; /* class, code, address, pc, sp, lr/ra, status registers, regs */
The port decodes the architecture state (Cortex-M CFSR, MMFAR, BFAR, EXC_RETURN; RISC-V mcause, mtval, mepc; AVR has no fault entry) into emb_fault_info_t, determines whether the faulting context was an unprivileged thread (SPEC-010 §6) or kernel or interrupt context, and calls embk_fault_dispatch(&info). The register snapshot’s layout is exported in the debug descriptor (§14). A recoverable-fault hook may ask the port to return to the faulting context with a modified pc (SPEC-002 §9); otherwise the port never returns from a fault.
12. Privilege and protection (isolated ports)¶
void emb_arch_mpu_init(void); /* background map, kernel regions, enable */
void emb_arch_mpu_apply(const emb_arch_region_set_t *set); /* cross-partition switch (SPEC-010 §4.4) */
void emb_arch_mpu_guard_set(void *stack_limit); /* CONFIG_EMB_STACK_GUARD_MPU */
bool emb_arch_user_access_ok(const emb_arch_region_set_t *set, const void *p, size_t len, uint8_t access); /* marshallers (SPEC-010 §5.4) */
void emb_arch_syscall_enter(void); /* SVC / ecall vector: stack switch and return into embk_syscall_dispatch (SPEC-010 §5.2) */
void emb_arch_syscall_return(uintptr_t status) __attribute__((noreturn));
emb_arch_region_set_t is the generator’s output per partition in the port’s encoding (RBAR/RASR pairs on Armv7-M, RBAR/RLAR on Armv8-M, pmpaddr/pmpcfg on RISC-V); the kernel treats it as opaque. The generator consults the manifest’s mpu model for the constraints it enforces (SPEC-010 §4.3). These are planned hooks, absent from the current include/emb/arch.h; MPU/PMP isolation is not implemented. Ports without protection hardware do not provide these functions and the configuration refuses the isolated profile.
13. SMP (FUTURE, names fixed)¶
emb_arch_cpu_id(), emb_arch_cpu_count(), emb_arch_cpu_start(cpu, entry, sp), emb_arch_ipi_send(cpu), emb_arch_ipi_ack(), and per-CPU state access emb_arch_percpu() are reserved; spinlocks are C11 atomics over emb_arch_irq_lock (SPEC-002 §4.3). RP2350 AMP and SMP both remain later gates; neither is required for the first 1.0 release (ADR-039).
14. Debug and trace hooks¶
The port contributes to the debug descriptor (ADR-011): the context frame layout (register offsets from the saved stack pointer), the
emb_arch_tcb_tlayout, the fault record register layout, the interrupt stack bounds, and the port name and variant.emb_arch_breakpoint()(bkpt,ebreak,breakon AVR via the debugWIRE convention, a host trap on native) is whatEMB_CHECKand kernel faults use to stop under a debugger whenCONFIG_EMB_FAULT_HALT_FOR_DEBUGis set.emb_arch_trace_timestamp()isemb_arch_cycles()unless the port has a better trace clock.The current fault API uses
emb_arch_fault_haltandemb_arch_reset; retained crash records and the descriptor are planned.EmbCC now emits AVR function/global/line DWARF and has an AVR remote register layout (09 §7 and §10). The descriptor remains a target for RTOS context/thread awareness independent of compiler debug info. Current EmbCC RTOS debugging is unqualified; no AVR
.debug_frameis emitted and EmbDBG unwinding stops at frame 0. GCC debugging remains a useful reference.
15. Toolchain rules for port code¶
From 09 §5 and §7, binding on every port so that one source serves EmbCC, GCC, and Clang:
Context switch, first-thread launch, and the AVR
EMB_ISRprologue and epilogue arenakedfunctions or.Sfiles; no labels inside inline asm; branch targets.+Nonly.Inline asm templates never name or clobber callee-saved registers (r4-r11, s0-s11, r2-r17 and Y).
Critical-section primitives are
static inline __attribute__((always_inline))with a"memory"clobber (SPEC-002 §4.1).No LTO, no
-fPIC, no-fstack-protector, no-fshort-enums; attribute fields are fixed-width integers.Vector tables are
constarrays withused; on AVR the 26jmpentries are the toolchain’s and the port supplies__vector_nbodies.Linker inputs target the generator (HW-006): GNU ld scripts, now also accepted by EmbCC on AVR, plus the retained AVR compatibility option set; bracketed sections remain the registration mechanism. Generated inputs are not yet implemented.
Use C11 atomic protocols and
.Sfiles at the existing portability boundary. Verify each core’s required instruction vocabulary rather than retaining obsolete compiler gaps: Cortex-Mclrex,ldrd/strdand subword exclusives are now documented upstream; RISC-V inline-atomic vocabulary remains restricted (09 §5).
16. Port deliverables and conformance¶
A port is complete when it ships:
arch/<arch>/arch.yaml,include/emb_arch.h, the sources, anddocs/ports/<arch>.mdwith: context frame layout, stack reserves and minimums, interrupt stack rules, latency figures from the harness (SPEC-002 §10), timer sources and their limits (KRN-TIM-034), fault classes decoded, protection model, toolchain gaps worked around;the
tests/arch/group green: context init and switch, irq lock and unlock semantics and LIFO check, nesting depth, outermost-exit switch, timer accuracy and MAX_INTERVAL hop, cycle counter monotonicity, idle wake without loss, stack check and limit register, fault entry decode (fault injection), syscall trap round trip and user-access checks on isolated ports;the full kernel conformance suite green on the port (SIM-001 for native; the same suite on hardware for the others; emulated results are never timing evidence, SIM-003).
17. Per-architecture summary¶
native |
AVR (ATmega328P) |
Cortex-M Armv6-M |
Cortex-M Armv7-M / Armv8-M |
RISC-V RV32 |
|
|---|---|---|---|---|---|
Context location |
host thread; |
thread stack: 32 GPRs, |
thread stack: r4-r11 + hardware frame |
same + lazy FPU |
thread stack: callee-saved + |
Switch mechanism |
gate hand-over (SPEC-013) |
|
PendSV |
PendSV |
|
Interrupt stack |
host |
none: per-thread reserve |
|
|
|
Critical section key |
gate lock state |
|
|
|
|
CAS |
C11 |
critical section |
critical section |
|
|
Timer |
virtual clock |
Timer1 compare |
|
SoC timer or |
|
Cycle counter |
host monotonic |
Timer1 |
SoC timer |
|
|
Fault entry |
signals |
none |
HardFault |
HardFault, MemManage, BusFault, UsageFault |
trap with |
Protection |
software checks in the gate |
none |
none (Armv6-M MPU unsupported in 1.0) |
MPU v7 / v8 |
PMP (M5) |
Syscall trap |
simulated |
none |
n/a |
|
|
First M |
M1 |
M1 |
M3 |
M3 |
M5 |
18. Decisions taken at acceptance (2026-10-07)¶
The register context lives on the thread’s stack; the TCB holds the saved stack pointer and a port-declared extension only.
The port’s properties are a manifest the generator and the configuration read; a kernel feature the port cannot support is refused at configuration time.
One switch path per architecture: PendSV on Cortex-M for both P1 and P2, direct
nakedroutine on AVR and RISC-V, the gate on native.Architecture-standard interrupt controllers are driven by the port; platform controllers are SoC devices behind the same entry points.
The target atomic API is C11 atomics through the portability layer; the manifest’s
casflag selects the wait protocol’s transition implementation.The port measures its timer arming latency at init unless the hardware description declares it.
emb_arch_idleis the atomic enable-and-wait; the power core’s deeper states go throughemb_arch_sleep.Protection region sets are generator output in the port’s encoding and opaque to the kernel.
Armv6-M MPU support is not part of 1.0 (its atomics helpers mask with
PRIMASK, which does nothing unprivileged, 09 §2).A port is complete only with its manifest, documentation page, the
tests/arch/group, and the full conformance suite green.