SPEC-012 - ATmega328P Port (arch/avr) and the arduino_uno Board

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 §14. Specification work item 11 of the roadmap (07 §3). First hardware target of M1 (ADR-001, alongside the native port). Requirements: docs/requirements/ARCH-AVR.md (new group, from 001). Builds on: SPEC-011, SPEC-002 §6.2, §7.2, §13, SPEC-003 §13, SPEC-004 §11, §12, SPEC-008 §10, 03 §11, and the refreshed document 09 (EmbCC AVR script/program-memory/DWARF/atomic capabilities and their RTOS integration limits); R-003 §4 targets T1 and T2. Compiler premises in §8/§10 were refreshed on 2026-10-14; this does not qualify the current EmbCC port or change the accepted kernel protocols. Hardware: ATmega328P: AVR5 core (AVRe+), 16 MHz on the Uno, 32 KB flash (Optiboot occupies the top 512 bytes), 2 KB SRAM at 0x0100..0x08FF, 1 KB EEPROM, 26 interrupt vectors, Timer0 (8-bit), Timer1 (16-bit), Timer2 (8-bit), USART0, SPI, TWI, ADC, watchdog, six sleep modes. No MPU, no fault exceptions, no cycle counter.


1. Why this target first

A 2 KB SRAM part forces every “compiles to nothing” claim of the architecture to be true (03 §11, ADR-036, R-003 T1 and T2). What fits here fits everywhere; what does not is found before the Cortex-M port makes it easy to ignore. The port is also small enough to be written and reviewed in full in M1 and runs under QEMU’s AVR machine in CI (SIM-002).

2. Manifest

arch: avr
variants: [atmega328p]
word_bits: 8                    # 16-bit pointers; the kernel's "word" for bitmaps is 8 bits
endian: little
stack: { align: 1, growth: down, min_thread: 96, context_frame: 37 }
features:
  irq_nesting: false            # kernel-aware handlers run with I cleared
  zero_latency_irqs: []         # cli/sei is all or nothing
  cas: []                       # transitions in a critical section (SPEC-004 §12)
  interrupt_stack: false        # frames on the interrupted thread's stack; per-thread reserve
  hw_stack_limit: []
  mpu: none
  fpu_lazy: []
  cycle_counter: []             # Timer1 count serves as a coarse cycle source (§7)
  tickless: true                # idle and ADC-noise-reduction sleep only
  smp: false
  syscall_trap: none
  fault_entry: false
  priority_bits: none           # one interrupt priority level: vector order decides
tcb_extension_bytes: 0

Configuration consequences: the isolated profile, the zero-latency class, SMP, and the fault-based stack guard are refused; CONFIG_EMB_SCHED_TABLE=y, CONFIG_EMB_TICK_32BIT=y, CONFIG_EMB_NOTIFY_BITS=8, CONFIG_EMB_SYSTEM_WORKQ=n, CONFIG_EMB_IDLE_THREAD=n are the tiny defaults (ADR-036, 03 §11).

3. Startup

  • Boot is the toolchain’s .S sequence (09 §8): SPH:SPL set to RAMEND, r1 cleared, .data copied from flash, .bss zeroed, then main(). Optiboot jumps to address 0 after its timeout, so the image is linked at 0x0000 with --rom-limit 32256 (EmbCC) or the equivalent MEMORY length (avr-gcc), leaving the bootloader section alone.

  • emb_arch_early_init(): cli; nothing else (no vector relocation on this part; IVSEL stays 0).

  • emb_arch_init(): Timer1 configured per §7, sleep mode select left to emb_arch_idle, watchdog left to the board or the software watchdog service (FLT-004).

  • emb_arch_kernel_start(first, idle): saves the pre-kernel context in the idle control block and loads SP from the first thread’s frame and runs the restore sequence of §4 ending in ret; the pre-kernel stack (top of SRAM) becomes the inline idle context’s stack (CONFIG_EMB_IDLE_THREAD=n, SPEC-008 §10) or the idle thread’s stack when one is configured.

4. Context frame and switch

4.1 Frame

One layout serves the thread-context switch (P2), the interrupt epilogue switch (P1), and the initial frame, growing down from the return address the hardware or the call pushed:

higher addresses
  return address low byte      (pushed by call/interrupt: low byte at the higher address)
  return address high byte
  r0
  SREG                         (saved through r0: in r0, SREG)
  r1                           (always 0 for compiled code)
  r2 .. r31                    (r24:r25 carry the entry pointer; r22:r23 carry its argument in the initial frame)
lower addresses  <- saved SP (points to the free byte below r31)

35 bytes per saved frame (2-byte return address, 32 registers, 1 SREG); the manifest budgets 37 bytes including the return address of the call into the switch routine. There is no separate exit address in the initial frame (A6). EMB_ISR saves this 35-byte frame in its prologue, so an interrupt epilogue that decides to switch only stores SP into the outgoing TCB and loads the incoming one: the ISR frame is the context frame.

4.2 Initial frame

emb_arch_context_init(t, stack_base, size, entry, arg, privileged) (privilege ignored) writes the 35-byte frame with the return address of embk_thread_launch, r0 = 0, SREG = 0x80, r1 = 0, r24:r25 = entry, r22:r23 = arg, and the remaining saved registers zero; it stores SP in t->arch.sp. The launch hook calls the entry and then emb_thread_exit(0) (A4, A6). Launch uses the normal restore (§4.3): the actual tail is out SREG, r0; pop r0; ret. The intervening pop means a claim that the one-instruction enable delay guarantees no IRQ before ret is unsupported. Validate any pending-interrupt entry during this tail against a coherent incoming frame, current-thread identity and kernel context on physical hardware; emulator behavior alone does not close that obligation (A27).

4.3 Switch routine

emb_arch_switch_to(next) is a naked function (09 §5): push r0; in r0, SREG; cli; push r0; push r1; clr r1; push r2..r31; store SP into current->arch.sp; load SP from next->arch.sp; set current; pop r31..r2; pop r1; pop r0; out SREG, r0; pop r0; ret. Interrupts are masked from the cli until the out SREG of the incoming thread restores its own I bit (an incoming thread saved by an EMB_ISR has I clear in its saved SREG? No: the ISR prologue saves SREG as the interrupted code had it, with I set, because the hardware clears I only in the live register; the saved copy is read from SREG after entry, so the port explicitly sets bit 7 in the saved byte). The whole routine is the masked section; its length is fixed (a cycle count still requiring disassembly accounting and hardware measurement; pushes and pops are multi-cycle instructions, so counting roughly 70 instructions is not a 70-cycle measurement) and is the port’s contribution to the T7 budget on this target.

4.4 Interrupt prologue and epilogue

EMB_ISR(vector) expands to a naked __vector_n whose body is: the frame push of §4.3 with I forced set in the saved SREG (interrupts are off in the live register for the handler’s duration, SPEC-002 §7.1: no nesting), irq_nesting_depth++, a call to the C body embk_isr_<vector>(), then embk_isr_exit(), which decrements the depth and, when reschedule_pending && sched_lock_depth == 0, performs the SP exchange of §4.3 (the frame is already on the stack); then the restore sequence ending in ret, restoring the resumed context’s own I bit rather than unconditionally setting it (A7). EMB_ISR_RAW(vector) is __attribute__((signal)) on both compilers: the compiler saves only what the body clobbers, the handler may not call the kernel (SPEC-002 §3), and it runs with interrupts off.

4.5 Timer interrupt

The Timer1 compare vector is an EMB_ISR whose body calls embk_time_timer_isr() (SPEC-003 §4); expiry wakes threads through the wait protocol in the same masked section; the epilogue switches if needed. The whole tick path (prologue, tick update, one expiry, epilogue with switch) is measured and published (SPEC-002 §10).

5. Interrupts and critical sections

  • emb_arch_irq_lock(): in r, SREG; cli; key = saved SREG. emb_arch_irq_unlock(key): out SREG, key. Both always_inline with a "memory" clobber. emb_arch_irq_locked(): SREG.I == 0.

  • emb_arch_in_isr(): the kernel’s irq_nesting_depth (there is no hardware indicator).

  • Vector numbers and names come from the SoC description (soc/microchip/atmega/atmega328p.yaml); EMB_ISR(TIMER1_COMPA) resolves to __vector_11 through the generated hw_config.h. Unused vectors keep the toolchain’s weak default, which the port overrides with a handler that records a spurious_irq fault.

  • emb_arch_irq_enable/disable(irq) set or clear the peripheral’s mask bit through a generated table of (register, bit) pairs; set_level is a no-op (one level; vector order is the hardware priority and the documentation says so); clear_pending writes the flag bit; pend_soft is unsupported (EMB_ENOTSUP).

  • The per-thread interrupt reserve CONFIG_EMB_AVR_ISR_STACK_RESERVE defaults to 64 bytes: the 35-byte frame plus call/context reserve and the largest kernel-aware handler C frame from supported stack reports and disassembly/call-graph accounting (the port’s docs/ports/avr.md lists it per release).

6. Scheduler and wait protocol on this port

The tiny defaults make the kernel’s hot paths bit operations: the ready set and every wait queue are one byte (ADR-036, SPEC-004 §11), wait_first is a lookup-table or clz emulation over 8 bits (one instruction sequence of about 6 cycles), unique priorities 1..7 plus idle. Wait-state transitions run inside the critical section (no compare-and-swap, SPEC-004 §12); the three sections of SPEC-004 §5.1 are preserved, so interrupts are enabled between them.

7. Time

Mode

Configuration

Resolution

Range

periodic tick (default)

Timer1 CTC, prescaler 64, OCR1A = 249 at 16 MHz

1 ms tick, exact

32-bit tick counter (CONFIG_EMB_TICK_32BIT)

tickless

Timer1 normal mode free-running, prescaler 256, OCR1A as the deadline compare

16 µs

MAX_INTERVAL 1.048 s (half the 16-bit range, SPEC-003 §4); longer sleeps hop

  • emb_arch_timer_now_raw() reads TCNT1 (16-bit, atomic read through the temporary register with interrupts masked); the kernel’s 32-bit tick is maintained by embk_time_on_wake folding elapsed raw ticks (SPEC-003 §4).

  • Arming latency is measured once at timer_init (program a compare two counts ahead, count the cycles to the interrupt) and reported through emb_arch_timer_set_latency_ticks() (KRN-TIM-036); on this core it is a constant the documentation also states.

  • emb_arch_cycles() returns a 32-bit count built from TCNT1 and a software overflow counter at the prescaled rate (not true CPU cycles; the documentation states the resolution and the harness metadata records it). Hard timing numbers for this port are taken with an external logic analyzer on a GPIO, which the harness supports (R-003 §6).

  • Deep sleep stops Timer1; tickless is limited to IDLE and ADC noise reduction modes (SPEC-003 §13). The power core’s emb_arch_sleep(SLEEP) and deeper are EMB_ENOTSUP on this part unless an external wake source and a tick-loss policy are configured (FUTURE).

  • Clock reads use the critical section (KRN-TIM-016 table).

8. Memory

  • The tiny T1 reference feature set targets 64 bytes of kernel static RAM and 32 bytes per TCB; its recorded AVR map subtotals are in the port page. TCB size comes from the layout probe, not a hand-added field list. Port/board/runtime RAM, every configured thread and interrupt reserve, inline idle state, alignment and application buffers must also fit the 2 KB SRAM. The kernel-subtotal gate does not establish how much application RAM remains or that any chosen stack sizes are sufficient.

  • Program-memory support is available upstream (09 §2, item 3): EmbCC implements __flash and avr-libc-compatible progmem; generic const still costs SRAM. Current RTOS EMB_FLASH_CONST/read macros select flash access under GCC but fall back to generic const/direct reads under EmbCC. Wire that portability boundary explicitly before claiming flash savings. Large optional tables remain compiled out on tiny. The recorded RTOS footprint gate is avr-gcc only; focused upstream-fixture EmbSim runs do not qualify RTOS memory use.

  • AVR linker scripts are now supported by EmbCC (09 §7, item 2). HW-006 retains a GNU ld script plus a compatibility option set from one region model; the latter is no longer required by a compiler restriction. M1 avr-gcc uses a hand-derived board script; its registration entries use EMB_TABLE_CONST and flash accessors (A8). Generated linker inputs and current EmbCC RTOS registration/link qualification remain planned.

  • Stack checking: guard words at each thread stack’s limit checked at every switch-out (CONFIG_EMB_STACK_CHECK, default on in checked builds); the inline idle context’s stack is the pre-kernel stack and gets the same guard. There is no hardware limit and no MPU.

  • Retained region: .noinit SRAM survives a watchdog or external reset (not a power cycle); the small crash record (EMB_CRASH_RECORD_SMALL: magic, reason, thread index, SP, return address, uptime, checksum) lives there (FLT-001 on this target).

9. Faults and robustness

There is no fault entry: an illegal memory access is silent, a stack overflow is caught only by the guard check at the next switch, and a runaway thread only by the watchdog. The port therefore enables, by default in checked builds: the stack guard check at every switch, the ISR-stack reserve check at every kernel-aware exit, and the software watchdog service (FLT-004) feeding WDT with a 1 s window; a checked-build kernel fault writes the small crash record and resets through the watchdog (CONFIG_EMB_FAULT_ACTION=REBOOT default on this target because there is no debugger halt path under EmbCC).

10. Debug and tooling

  • EmbCC now emits AVR DWARF and EmbDBG has the remote register layout (09 §7, items 4/5). The local review reads function/global/line records and checks register offsets; it does not execute a remote RTOS session. AVR has no .debug_frame, EmbDBG’s backtrace stops at frame 0, and -g can enlarge code. The same RTOS conformance suite and planned debug descriptor must qualify any adopted compiler path; that EmbCC gate has not passed here.

  • Flashing: avrdude -c arduino -p m328p -P <port> -b 115200 behind emb flash (BLD-005); no EmbFlash yet (09 §9).

  • Emulation: QEMU arduino-uno machine (EmbCC’s AVR harness, 09 §9) runs the kernel conformance suite in CI; timing results from it are never published (SIM-003).

  • Trace: the tiny profile’s trace is off by default; the plain-text log backend over USART0 at 115200 is the diagnostic channel (ADR-009 fallback, 09 §7).

11. Board arduino_uno

board: arduino_uno
soc: microchip/atmega/atmega328p
oscillators: { main: { hz: 16000000, source: external_crystal } }
console: { device: usart0, baud: 115200, pins: { tx: PD1, rx: PD0 } }
leds: [ { name: user, gpio: PB5 } ]
flash_partitions: [ { name: app, base: 0x0000, size: 32256 }, { name: bootloader, base: 0x7E00, size: 512, reserved: true } ]
defaults: { CONFIG_EMB_PROFILE: tiny, CONFIG_EMB_TICK_NS: 1000000 }

Board init configures USART0 for the console and PB5 as output; nothing else (pins default to inputs). The generator emits hw_config.h with vector numbers, register addresses, and the SRAM and flash regions; the same SoC file serves any other ATmega328P board with a different crystal or console.

12. Conformance

tests/arch/avr: frame layout and launch with a pending interrupt, EMB_ISR prologue and epilogue switch, critical section key semantics, Timer1 tick exactness over one minute against the host clock (HIL), tickless hop across MAX_INTERVAL, arming latency measurement, guard-word detection, .noinit crash record survival across a watchdog reset. The full kernel conformance suite runs on QEMU and on the Uno; tests needing faults, isolation, FPU, nesting, or zero-latency interrupts are skipped by manifest, and the skip list is part of the port documentation.

13. Footprint and latency targets for this port

Metric

Target

Source

kernel text, tiny configuration, EmbCC -Os and avr-gcc -Os

<= 4 KB

R-003 T1

kernel static RAM

<= 64 bytes

R-003 T2

TCB

<= 32 bytes

R-003 T2, §8

context switch (P2, emb_arch_switch_to)

establish a physical-board baseline including multi-cycle frame instructions; historical 80-cycle target is withdrawn pending analysis

§4.3, ADR-039

interrupt to thread (Timer1 compare to the woken thread’s first instruction)

<= 300 cycles (19 µs)

SPEC-002 §10

longest masked section in kernel paths

<= 150 cycles

R-003 T7

Targets become baselines after the first HIL measurement (OBS-012).

14. Decisions taken at acceptance (2026-10-07)

  1. One 35-byte saved frame (37-byte call/context reserve) for the switch, the interrupt epilogue, and the initial frame; the EMB_ISR frame is the context frame, so an interrupt-exit switch is an SP exchange.

  2. Launch, switch and ISR restore end in out SREG; pop r0; ret, preserving the incoming I bit. Validate pending IRQ behavior during that complete tail on physical hardware; do not infer an atomic out; ret pair (A7, A27).

  3. Periodic 1 ms tick on Timer1 CTC with prescaler 64 is the default; tickless uses prescaler 256 with 16 µs resolution and a 1.048 s MAX_INTERVAL; deep sleep modes are unsupported until a tick-loss policy exists.

  4. Tiny defaults are mandatory on this port: table scheduler, 32-bit time, 8-bit notifications, no system work queue, no idle thread, 7 application priorities.

  5. EMB_FLASH_CONST and its accessors are the portability boundary; large kernel tables are compiled out on tiny. Current upstream program-memory capability does not wire the EmbCC RTOS macros automatically (§8).

  6. The generator targets both the compatibility embld option set and a GNU ld script from one region model; it remains unimplemented. The earlier missing-AVR-script compiler premise is obsolete (§8).

  7. The small crash record lives in .noinit and the default fault action is a watchdog reset; the software watchdog service is on in checked builds.

  8. At acceptance, symbolic debugging used avr-gcc pending EmbCC DWARF. That compiler gap is now closed; current EmbCC RTOS conformance/debug integration and the descriptor remain unqualified (§10).

  9. Hard timing on this port is measured with a logic analyzer on a GPIO; the Timer1-based cycle counter is for coarse statistics.