This is the engineering reference for the model: what it implements, register by register, for people writing firmware against it. For using the board in the editor, start with The Arduino Uno and the board page.

Arduino Uno board contract (Phase 3)

The second microcontroller. One kernel component (uno in crates/parts) wraps an ATmega328P SoC model (crates/atmega328p) built on the AVR core (crates/avr-core, proven in proofs/avr/REPORT.md). Firmware is an AVR ELF (or raw flash image) that talks to the peripherals below at their real ATmega328P addresses, so a program built for Mokxi runs on a real Uno R3. Running binaries built with the Arduino core and avr-libc is a goal: the peripherals here are the ones the Arduino core touches for pinMode, digitalWrite, analogWrite, analogRead, millis, delay, Serial, attachInterrupt; a vendor-built blink should run unchanged once those are right (test it if a prebuilt one is at hand; do not fetch the Arduino core sources, they are LGPL).

The catalog type is uno (Arduino Uno R3). Three agents build against this file: the SoC and part (Rust), the firmware runtime and examples (C++, clang), the board visual (TypeScript). Change it only by editing this file first. Everything in docs/esp32c3.md about how a board part behaves (slices, host channel, reset, crash line) applies here unless this file says otherwise.

1. Board pins (catalog order)

Top view, USB and barrel jack on the left. Top header, left to right: SCL, SDA, AREF, GND, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0. Bottom header, left to right: IOREF, RESET, 3V3, 5V, GNDb, GNDc, VIN, A0, A1, A2, A3, A4, A5. 31 pins. The ICSP headers are not pins.

Electrical:

  • 5V and IOREF drive 5 V at R_SUPPLY; 3V3 drives 3.3 V; GND* drive 0 V. VIN is an input the board ignores (always USB powered). The board is a supply.
  • RESET is an input with a 10 k pull-up to 5 V; below 2.5 V holds the CPU in reset; release restarts at the reset vector.
  • Digital 0..13 and A0..A5 are inout GPIO. Port mapping: 0..7 = PD0..PD7, 8..13 = PB0..PB5, A0..A5 = PC0..PC5. SDA is strapped to A4 and SCL to A5 (same net, R_STRAP). Output (DDR bit set): push-pull at 5 V or 0 V through R_STRONG. Input (DDR clear): high-Z, plus a 35 k pull-up to 5 V when the PORT bit is set (and PUD clear). Input threshold 2.5 V; Unknown and Floating read 0. AREF is an input; when the ADC reference is AREF (REFS = 0) its voltage is the reference, otherwise it is high-Z.
  • Pin 13 has the on-board LED through 1 k to ground (a load; the visual shows it lit from a probe bit, see section 5). Pins 0/1 are also USART0 RX/TX on the real board; here the USART talks to the host channel and the pins stay GPIO.
  • ADC: A0..A5 read the net voltage at the pin against the selected reference (5 V for AVCC, 1.1 V for the internal reference, the AREF pin voltage for external), 10 bits, sample-and-hold ignored.

2. Memory map (real ATmega328P)

space address size
flash (program) word address 0x0000 32 KB (0x4000 words); boot section not modeled, SPM writes go straight to flash
registers data 0x0000..0x001F R0..R31 (inside the core)
I/O data 0x0020..0x005F IN/OUT addresses 0x00..0x3F
extended I/O data 0x0060..0x00FF
SRAM data 0x0100..0x08FF 2 KB
EEPROM separate, 1 KB via EECR/EEDR/EEAR, read and write with no timing (writes complete immediately, EEPE reads 0)

loadProgram takes an ELF32 AVR executable (e_machine 83; PT_LOAD at flash addresses, .data initializers at their LMA in flash the way avr-gcc and clang lay them out; the loader in crates/avr-core/src/elf.rs already does this) or a raw binary flash image when the bytes are not an ELF. Reset: pc = 0, SP = 0x08FF, SREG = 0, I/O at datasheet reset values, SRAM and registers undefined (zero). A load at run time resets the board.

3. Peripherals (real addresses, data-space numbering, real bit positions)

Everything not listed reads its reset value and ignores writes, with a diagnostics counter.

3.1 Ports

data address register
0x23 0x24 0x25 PINB DDRB PORTB (bits 0..5 = pins 8..13; 6, 7 are the crystal, never GPIO)
0x26 0x27 0x28 PINC DDRC PORTC (bits 0..5 = A0..A5)
0x29 0x2A 0x2B PIND DDRD PORTD (bits 0..7 = pins 0..7)
0x55 MCUCR bit 4 PUD disables every pull-up

Writing 1 to a PINx bit toggles the PORTx bit (datasheet 14.2.2). SBI/CBI on these addresses are read-modify-write at the register, as on the chip.

3.2 Timer/Counter0 (8-bit, the millis() timer)

data address register bits
0x44 TCCR0A COM0A1:0 bits 7..6, COM0B1:0 bits 5..4, WGM01:0 bits 1..0
0x45 TCCR0B FOC0A 7, FOC0B 6, WGM02 3, CS02:0 2..0
0x46 TCNT0
0x47 OCR0A
0x48 OCR0B
0x6E TIMSK0 OCIE0B 2, OCIE0A 1, TOIE0 0
0x35 TIFR0 OCF0B 2, OCF0A 1, TOV0 0; write 1 clears

Modes (WGM02:0): 0 normal, 1 phase-correct PWM (top 0xFF), 2 CTC (top OCR0A), 3 fast PWM (top 0xFF), 5 phase-correct (top OCR0A), 7 fast PWM (top OCR0A). Prescaler CS02:0: 0 stopped, 1 = 1, 2 = 8, 3 = 64, 4 = 256, 5 = 1024, 6/7 external (not modeled, count 0). Compare outputs: OC0A on pin 6 (PD6), OC0B on pin 5 (PD5); COM modes per the datasheet tables (0 disconnected, 1 toggle in non-PWM / reserved-or-toggle in PWM per table, 2 non-inverting, 3 inverting). The output override applies only while the DDR bit is set. Interrupt vectors: TIMER0_COMPA 14, TIMER0_COMPB 15, TIMER0_OVF 16.

3.3 Timer/Counter1 (16-bit)

data address register bits
0x80 TCCR1A COM1A1:0 7..6, COM1B1:0 5..4, WGM11:10 1..0
0x81 TCCR1B ICNC1 7, ICES1 6, WGM13:12 4..3, CS12:0 2..0
0x82 TCCR1C FOC1A 7, FOC1B 6
0x84 0x85 TCNT1L TCNT1H 16-bit access through the shared TEMP byte (read low first latches high; write high first)
0x86 0x87 ICR1L ICR1H
0x88 0x89 OCR1AL OCR1AH
0x8A 0x8B OCR1BL OCR1BH
0x6F TIMSK1 ICIE1 5, OCIE1B 2, OCIE1A 1, TOIE1 0
0x36 TIFR1 ICF1 5, OCF1B 2, OCF1A 1, TOV1 0

All 16 WGM modes per the datasheet table (normal, PWM 8/9/10-bit, CTC on OCR1A or ICR1, fast PWM with ICR1 or OCR1A top, phase and frequency correct). OC1A on pin 9 (PB1), OC1B on pin 10 (PB2). Input capture from pin 8 (ICP1, PB0) on the selected edge, noise canceler ignored. Vectors: TIMER1_CAPT 10, COMPA 11, COMPB 12, OVF 13.

3.4 Timer/Counter2 (8-bit)

data address register
0xB0 0xB1 0xB2 0xB3 0xB4 TCCR2A TCCR2B TCNT2 OCR2A OCR2B (same bit layout as timer 0)
0x70 TIMSK2
0x37 TIFR2
0xB6 ASSR (reads 0; asynchronous mode not modeled)

Prescaler CS22:0: 0 stopped, 1, 8, 32, 64, 128, 256, 1024. OC2A on pin 11 (PB3), OC2B on pin 3 (PD3). Vectors: TIMER2_COMPA 7, COMPB 8, OVF 9.

3.5 USART0

data address register bits
0xC0 UCSR0A RXC0 7, TXC0 6, UDRE0 5, FE0 4, DOR0 3, UPE0 2, U2X0 1, MPCM0 0
0xC1 UCSR0B RXCIE0 7, TXCIE0 6, UDRIE0 5, RXEN0 4, TXEN0 3, UCSZ02 2
0xC2 UCSR0C frame format, stored and read back; 8N1 assumed for pacing
0xC4 0xC5 UBRR0L UBRR0H baud = 16 MHz / (16 (UBRR + 1)), or / 8 with U2X0
0xC6 UDR0 write: transmit (when UDRE0); read: received byte

Transmit: a write to UDR0 with UDRE0 set goes to a one-byte transmit buffer; UDRE0 clears, then the shift register takes it and UDRE0 sets again (one byte of double buffering, like the chip); the byte reaches the host channel one frame time (10 bits at the baud) after it entered the shift register, and TXC0 sets then. Receive: host bytes queue in a FIFO the size of the real two-level receive buffer plus a host-side backlog of 256 bytes so typed text is not lost; RXC0 set while data waits; reading UDR0 pops. Vectors: USART_RX 18, USART_UDRE 19, USART_TX 20. Writing TXC0 = 1 clears it.

3.6 ADC

data address register bits
0x78 0x79 ADCL ADCH result, right-adjusted unless ADLAR
0x7A ADCSRA ADEN 7, ADSC 6, ADATE 5, ADIF 4, ADIE 3, ADPS2:0 2..0
0x7B ADCSRB stored
0x7C ADMUX REFS1:0 7..6, ADLAR 5, MUX3:0 3..0 (0..5 = A0..A5, 8 = temperature (reads 0x150), 14 = 1.1 V bandgap, 15 = 0 V)
0x7E DIDR0 stored

A conversion takes 13 ADC clocks (25 for the first after ADEN) at 16 MHz / prescaler; ADSC reads 1 until done, then ADIF sets and the result is round(1023 * Vin / Vref) clamped, from the pin's net voltage at the time the conversion started. Vector: ADC 21.

3.7 External and pin-change interrupts

data address register
0x69 EICRA (ISC11:10 bits 3..2, ISC01:00 bits 1..0: 0 low level, 1 any change, 2 falling, 3 rising)
0x3D EIMSK (INT1 1, INT0 0)
0x3C EIFR (write 1 clears)
0x68 PCICR (PCIE2 2 = PORTD, PCIE1 1 = PORTC, PCIE0 0 = PORTB)
0x6B 0x6C 0x6D PCMSK0 PCMSK1 PCMSK2
0x3B PCIFR

INT0 is pin 2 (PD2), INT1 is pin 3 (PD3). Vectors: INT0 1, INT1 2, PCINT0 3, PCINT1 4, PCINT2 5.

3.8 Sleep, watchdog, misc

data address register
0x33 SMCR (SM2:0 bits 3..1, SE bit 0)
0x34 MCUSR (reset flags; PORF set after power-on, EXTRF after a RESET pin reset)
0x60 WDTCSR (stored; the watchdog never fires in Phase 3)
0x61 CLKPR (stored; the clock stays at 16 MHz)
0x5F 0x5E 0x5D SREG SPH SPL (inside the core, never reach the bus)

SLEEP with SE set in idle mode (SM = 0) parks the core until any enabled interrupt is pending; other sleep modes behave as idle. That is how an idle sketch becomes cheap: the runtime's delay() sleeps.

3.9 Interrupts

The core's set_interrupt(vector, pending) is a level per vector: the SoC asserts a vector while its flag bit is set and its enable bit is set (and, for the USART, while the condition holds), and the core clears the flag on entry for the timer, external and ADC vectors exactly as the hardware does (the flag registers are the SoC's, so the core tells the SoC which vector it took via a bus hook and the SoC clears the flag). USART RXC0/UDRE0/TXC0 flags are not cleared by entry, only by the corresponding action, as on the chip.

4. Host channel

host = "utf-8 bytes on USART0". Same semantics as the ESP32-C3 channel.

5. Time model and the part

16 MHz, cycle counts from the core (run(cycle_budget) returns cycles; the core's cycle counter is the clock). Slices of 10 us = 160 cycles. Timers are advanced in bulk per slice from the cycle count with exact event times for compare matches and overflows (each timer's next event is computable from its count, top, prescaler and mode; compute it, run the core up to that cycle, apply the event, continue), so a PWM edge or a timer interrupt lands on the exact cycle. Pin changes carry their in-slice cycle offset into drive_after. Reads of PINx see the pin levels as of the slice start plus any change the sketch itself made in the slice.

Probe: bit 0 = running (1 while executing, 0 in reset or halted), bit 1 = sleeping, bit 2 = the on-board LED (pin 13 PORTB5 high and DDRB5 set), bit 3 = TX activity in the last 20 ms, bit 4 = RX activity in the last 20 ms. So probe is a small bitmask, 0 with no program. Poke: 2 = press RESET, 3 = release.

Crash handling as in docs/esp32c3.md section 5: an AVR cannot trap, so "crashed" means the core executed an illegal opcode (BREAK or an opcode the part does not have) or the PC left the flash. The part halts and prints firmware crashed at pc=0x0123 (word address), reason once on the host channel.

6. Firmware runtime (firmware/uno/)

A freestanding C and C++ runtime for the ATmega328P built with clang 18 (--target=avr -mmcu=atmega328p -Os) and lld, no avr-libc, no Arduino core, no GPL: vector table and _start (copy .data from flash with LPM, clear .bss, set the stack, enable interrupts, main), uno.h (the registers above as macros with the real names), the same arduino.h surface as the ESP32-C3 runtime plus analogRead(A0..A5), analogWrite(pin, 0..255) on the six PWM pins using the timers the way the Arduino core does (timer 0 fast PWM for 5 and 6 at 976 Hz, timers 1 and 2 phase-correct for 9, 10, 11, 3 at 490 Hz), millis()/micros() from timer 0 overflow at 1024 us with the Arduino fractional correction, delay() sleeping in idle mode between timer ticks, delayMicroseconds busy, Serial on USART0 (interrupt-driven receive ring buffer, polled or interrupt transmit), attachInterrupt(digitalPinToInterrupt(2|3), fn, mode), tone/noTone optional. Examples under firmware/uno/examples/<name>/sketch.ino, built to web/public/firmware/uno/<name>.elf with web/public/firmware/uno/index.json in the same shape as the ESP32 index plus a "program": "elf32 avr atmega328p" field on each entry: blink (pin 13), button (pin 2 INPUT_PULLUP to pin 13), serial, fade (analogWrite on 9), analog (prints analogRead(A0) five times a second), counter (74HC595 on 4/5/6), interrupt (attachInterrupt on 2). The UI picks the firmware list by the part's catalog program string: "elf32 riscv32imc" reads /firmware/index.json, "elf32 avr atmega328p" reads /firmware/uno/index.json.

7. What this model is sure of, and what it is not

Three groups, the same sorting docs/esp32c6.md section 7 uses. It exists because a board model that quietly guesses an address is worse than one that says it did not know.

Taken from the data sheet, and run through by real firmware. Every address, bit position and reset value in section 3 is from the ATmega48A/PA/88A/PA/168A/PA/328/P Data Sheet (DS40002061), in data-space numbering, and none of it is a guess: this part is twenty years old and its register map is public and stable. Each block is also exercised by a test that loads a real ELF and drives it the way firmware does: crates/atmega328p/tests/peripherals.rs writes the registers through the core's I/O window on a running image, and tests/runtime.rs runs the six sketches the product actually ships. Ports, all three timer/counters, USART0, the ADC with its prescaler and its three references, the external and pin-change interrupts, EEPROM, sleep, and SPM writing flash are all in that set. The 16 MHz crystal is the Uno's own.

Modeled, but not held to the data sheet's numbers. These are behaviors rather than addresses, and each is an approximation this document is choosing:

  • The ADC has no error in it. A conversion is round(1023 × Vin / Vref) sampled at the instant it started. There is no sample-and-hold, no input impedance, no integral or differential nonlinearity, no noise and no 1 LSB offset, all of which the data sheet gives figures for. A divider read here gives the same count every time.
  • Time is exact. The crystal is 16 MHz with no tolerance, no drift and no jitter, so millis() never gains or loses against the wall clock the way a real ceramic resonator does.
  • The pads are three states and a threshold. Push-pull at 5 V, high-Z, or a 35 kΩ pull-up (the data sheet gives 20 kΩ to 50 kΩ), read back against half the supply. There is no input hysteresis, no output resistance, no rise or fall time, no source or sink current limit and no per-pin or total current budget.
  • The board is an ideal supply. 5 V, 3.3 V and the grounds are held at rail impedance for ever; nothing browns out, and the regulator and USB current limit are not there. The on-board LED is an LED behind 1 kΩ on pin 13 and nothing else.
  • Startup is a microsecond. A real Uno spends about 65 ms in the bootloader waiting for a programmer before a sketch starts. This one waits long enough for the rest of the circuit's pull-ups to settle and then runs.
  • A slice is 10 µs. The core is stepped one instruction at a time inside it, so a pad change carries its exact cycle, but the SoC and the circuit only meet at slice boundaries or at whatever the chip is waiting for.

Deliberately absent. Left out rather than invented, and their registers read their reset value and are counted as strays: SPI, TWI/I2C, the analog comparator, the watchdog, the bootloader section and its fuses, the clock prescaler, timer 2's asynchronous mode, and the USB-to-serial bridge (Serial reaches the monitor through the host channel, and pins 0 and 1 stay plain GPIO). Shields are not parts; the Arduino core and its library ecosystem are not here, and the runtime in section 6 is ours.

Questions

Which Uno is it?

The R3, with a complete ATmega328P behind it.

Why is the board beside the breadboard?

Its headers are two inches apart, so it will not push into one. A real Uno is wired with jumpers, and so is this one.