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.
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. A contended (Unknown) net reads 0. A floating input (nothing driving its net, no pull resistor, noINPUT_PULLUP) reads noise, as it does on the bench: it holds the level it was last driven to for 2 ms (the charge on the pin), then reads a fresh random bit every millisecond, so a button wired to 5 V with no pull-down flickers once it is let go.analogReadof a floating pin wanders smoothly between about 15 and 65 percent of the rail, starting from wherever the pin was last held. The noise is a hash of the board, the pin and the simulated time (crates/parts/src/floating.rs, shared by every board), so a run repeats exactly. A header hole with no wire at all still reads what its own pad holds: an output reads back its level andINPUT_PULLUPreads high.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, a write takes 3.3 ms (26,368 cycles of the 8 MHz RC oscillator, as the data sheet gives it; 1.8 ms in erase-only or write-only mode), EEPE reads 1 until it ends and EERIE raises EE_READY as a level |
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). In fast PWM the compare match acts one timer clock after TCNT0 equals OCR0x, so a non-inverting output is high for OCR0x + 1 of the 256 counts and OCR0x = 0 is the datasheet's one-clock spike, not a constant low. 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 only while TXEN0 is set, which it is not from reset until Serial.begin: a write to UDR0 with the transmitter off is dropped and UDRE0 stays set, so a sketch that prints without Serial.begin puts nothing on the serial monitor, as on a real board. The one exception is a line of Mokxi's own ([Mokxi] ..., mokxi_notice), which reaches the monitor whatever TXEN0 says, because it is the simulator talking rather than the sketch. With TXEN0 set, 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 if no other byte followed it into the shift register (the datasheet's "no new data currently present in the transmit buffer"), so flush() waits for the last byte. 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.7a SPI (named, not modeled)
| data address | register | bits |
|---|---|---|
0x4C |
SPCR |
SPIE 7, SPE 6, DORD 5, MSTR 4, CPOL 3, CPHA 2, SPR1 1, SPR0 0 |
0x4D |
SPSR |
SPIF 7, WCOL 6, SPI2X 0 |
0x4E |
SPDR |
the data byte |
The model has no SPI block: these addresses read 0 and ignore writes, so SPIF never sets and the transfer-complete interrupt never fires. uno.h names them so a sketch written against them, such as an SPI slave with ISR(SPI_STC_vect), compiles as it does on a real Uno. SPI.h is a bit-banged master on pins 10 to 13. Vector: SPI_STC 17.
3.8 Sleep, watchdog, misc
| data address | register |
|---|---|
0x53 |
SMCR (SM2:0 bits 3..1, SE bit 0) |
0x54 |
MCUSR (reset flags; PORF set after power-on, EXTRF after a RESET pin reset; WDRF is cleared again before the sketch runs, as Optiboot does) |
0x60 |
WDTCSR (the watchdog: below) |
0x61 |
CLKPR (stored; the clock stays at 16 MHz) |
0x5F 0x5E 0x5D |
SREG SPH SPL (inside the core, never reach the bus) |
SMCR and MCUSR were at 0x33 and 0x34 here and in firmware/uno/include/uno.h until 2026-11 (their I/O addresses taken as data addresses); the model still answers there as well, so images built against that header keep working, while an ELF built with avr-libc reaches them at the data sheet's addresses above.
SLEEP with SE set parks the core. In idle mode (SM = 0) any enabled interrupt that is pending wakes it. The other modes stop the I/O clock, so only what the data sheet's sleep-mode table lists wakes them: in power-down and standby the external interrupts, pin change, the watchdog and TWI; power-save and extended standby add timer 2; ADC noise reduction adds timer 2, the ADC and EEPROM/SPM ready. Timers 0 and 1 and the USART cannot wake a power-down sleep, so a SLEEP_MODE_PWR_DOWN sketch sleeps through millis()'s tick until its button or watchdog. INT0 and INT1 wake those modes, ADC noise reduction included, only on a LOW level (ISCn1:0 = 00): their edge detector runs on the I/O clock, so while it is stopped an edge is neither seen nor latched in EIFR, and attachInterrupt(0, wake, FALLING) sleeps through the button as on a real Uno. Pin change interrupts are asynchronous and wake on any change. The timers keep counting while asleep here, where a real part's stop, so millis() comes back one tick on (the pending overflow) rather than exactly where it was. The runtime's delay() sleeps in idle mode, which is how an idle sketch becomes cheap.
The watchdog is the data sheet's (crates/avr-core/src/wdt.rs): it counts its own 128 kHz oscillator, so WDTO_15MS to WDTO_8S are 16 ms to 8.2 s whatever the CPU clock; WDE and WDIE pick reset, interrupt (the WDT vector, which clears WDIF when taken) or interrupt-then-reset mode; clearing WDE or changing the prescaler takes the four-cycle WDCE sequence; WDR restarts the count. A timeout in reset mode restarts the program from its reset vector with SRAM kept, as the chip does. On the chip WDRF would be set and WDE would stay on, at 16 ms, until the sketch cleared MCUSR; but the Uno, Nano and Pro Mini ship Optiboot, and Optiboot 4.4 (optiboot_atmega328.hex) runs ch = MCUSR; MCUSR = 0; and then appStart, whose watchdogConfig(WATCHDOG_OFF) writes WDCE|WDE and then WDTCSR = 0 before it jumps to the sketch. The model does the same: after a watchdog reset the sketch starts with WDRF clear and the watchdog stopped, so wdt_enable(WDTO_15MS); while (1); is a working software reset however slow setup() is. PORF and EXTRF are kept (Optiboot 4.4 would clear those too; newer Optiboot keeps them in r2), so a sketch can print its reset cause.
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 (64-byte receive and transmit rings, filled by USART_RX and emptied by USART_UDRE, as the Arduino core's HardwareSerial does), 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.
- Start-up is what the fuses set, 66 ms. The Uno ships with low fuse
0xFF(CKSEL = 1111,SUT = 11: a crystal, slowly rising power), which the datasheet's table 9-4 gives as 16K CK of oscillator start-up and 14 CK + 65 ms of delay from reset. Optiboot jumps straight to the sketch after a power-on reset, sosetup()starts 66.0 ms after power is applied (Run), with every pin high-Z until then, and a sensor's own power-up time has passed by the time the sketch talks to it. After a later reset (RESETlet go, or the watchdog) the sketch restarts at once: the time-out after those, and Optiboot's wait for a programmer after an external reset, are not modeled. - 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
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.