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:
5VandIOREFdrive 5 V atR_SUPPLY;3V3drives 3.3 V;GND*drive 0 V.VINis an input the board ignores (always USB powered). The board is a supply.RESETis 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..13andA0..A5are inout GPIO. Port mapping:0..7= PD0..PD7,8..13= PB0..PB5,A0..A5= PC0..PC5.SDAis strapped toA4andSCLtoA5(same net,R_STRAP). Output (DDR bit set): push-pull at 5 V or 0 V throughR_STRONG. Input (DDR clear): high-Z, plus a 35 k pull-up to 5 V when the PORT bit is set (andPUDclear). Input threshold 2.5 V; Unknown and Floating read 0.AREFis an input; when the ADC reference isAREF(REFS = 0) its voltage is the reference, otherwise it is high-Z. - Pin
13has the on-board LED through 1 k to ground (a load; the visual shows it lit from a probe bit, see section 5). Pins0/1are also USART0 RX/TX on the real board; here the USART talks to the host channel and the pins stay GPIO. - ADC:
A0..A5read the net voltage at the pin against the selected reference (5 V forAVCC, 1.1 V for the internal reference, theAREFpin 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.