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
.Ssequence (09 §8):SPH:SPLset toRAMEND,r1cleared,.datacopied from flash,.bsszeroed, thenmain(). Optiboot jumps to address 0 after its timeout, so the image is linked at0x0000with--rom-limit 32256(EmbCC) or the equivalentMEMORYlength (avr-gcc), leaving the bootloader section alone.emb_arch_early_init():cli; nothing else (no vector relocation on this part;IVSELstays 0).emb_arch_init(): Timer1 configured per §7, sleep mode select left toemb_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 loadsSPfrom the first thread’s frame and runs the restore sequence of §4 ending inret; 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 = savedSREG.emb_arch_irq_unlock(key):out SREG, key. Bothalways_inlinewith a"memory"clobber.emb_arch_irq_locked():SREG.I == 0.emb_arch_in_isr(): the kernel’sirq_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_11through the generatedhw_config.h. Unused vectors keep the toolchain’s weak default, which the port overrides with a handler that records aspurious_irqfault.emb_arch_irq_enable/disable(irq)set or clear the peripheral’s mask bit through a generated table of(register, bit)pairs;set_levelis a no-op (one level; vector order is the hardware priority and the documentation says so);clear_pendingwrites the flag bit;pend_softis unsupported (EMB_ENOTSUP).The per-thread interrupt reserve
CONFIG_EMB_AVR_ISR_STACK_RESERVEdefaults 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’sdocs/ports/avr.mdlists 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, |
1 ms tick, exact |
32-bit tick counter ( |
tickless |
Timer1 normal mode free-running, prescaler 256, |
16 µs |
|
emb_arch_timer_now_raw()readsTCNT1(16-bit, atomic read through the temporary register with interrupts masked); the kernel’s 32-bit tick is maintained byembk_time_on_wakefolding 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 throughemb_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 fromTCNT1and 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
IDLEandADC noise reductionmodes (SPEC-003 §13). The power core’semb_arch_sleep(SLEEP)and deeper areEMB_ENOTSUPon 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
__flashand avr-libc-compatibleprogmem; genericconststill costs SRAM. Current RTOSEMB_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_CONSTand 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:
.noinitSRAM 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-gcan 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 115200behindemb flash(BLD-005); no EmbFlash yet (09 §9).Emulation: QEMU
arduino-unomachine (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 |
<= 4 KB |
R-003 T1 |
kernel static RAM |
<= 64 bytes |
R-003 T2 |
TCB |
<= 32 bytes |
R-003 T2, §8 |
context switch (P2, |
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)¶
One 35-byte saved frame (37-byte call/context reserve) for the switch, the interrupt epilogue, and the initial frame; the
EMB_ISRframe is the context frame, so an interrupt-exit switch is anSPexchange.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 atomicout; retpair (A7, A27).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.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.
EMB_FLASH_CONSTand 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).The generator targets both the compatibility
embldoption set and a GNU ld script from one region model; it remains unimplemented. The earlier missing-AVR-script compiler premise is obsolete (§8).The small crash record lives in
.noinitand the default fault action is a watchdog reset; the software watchdog service is on in checked builds.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).
Hard timing on this port is measured with a logic analyzer on a GPIO; the Timer1-based cycle counter is for coarse statistics.