Skip to content
Phosphoricwiki
Contents

The 6502 core

Phosphoric’s 6502 no longer thinks in instructions but in cycles: each instruction is a plan of micro-operations, one per cycle, each with its bus access. Here is how, and how we know.

On this page

What has to be reproduced

The Oric’s NMOS 6502 makes one bus access on every cycle, without exception. When an instruction has nothing useful to read or write during a cycle, the processor makes an access anyway: a dummy access. It re-reads the next opcode, reads an address before indexing it, writes a value back before modifying it (for read-modify-write instructions), reads the stack without using it.

These accesses are invisible to RAM. But on an input/output register they matter: reading an ACIA’s data register consumes the received byte, reading some VIA registers clears an interrupt flag.

A plan of micro-operations

Phosphoric’s core (src/cpu/) describes each instruction as a sequence of micro-operations, one per cycle, each making exactly its bus access: including the dummy accesses of zero-page indexing, page crossing, the write-back of RMW instructions, and dead stack reads.

  • 256 opcodes out of 256: the 151 official ones and the 105 NMOS “illegal” ones.
  • 13 addressing modes, the NMOS decimal mode, level-sensitive IRQ.
  • Accepted divergence: the 12 JAM opcodes stop the processor instead of locking the bus.
The dummy access that eats a byteBASIC's POKE writes with STA (zp),Y. In six cycles, the NMOS 6502 reads the opcode, the zero-page address, then the pointer, then makes a dummy read of the target address before writing to it. If the address is an ACIA's data register, that read consumes the received byte: an echo program written in BASIC loses bytes, on the real machine as in Phosphoric since 2.0.POKE 1021,C to an ACIA's data registerSTA ($zp),Y: 6 cycles, 6 bus accessescycle 1R PCreads the opcode$91cycle 2R PC+1reads the zero-page addresscycle 3R zpreads the pointer(low byte)cycle 4R zp+1reads the pointer(high byte)cycle 5R $03FDdummy readof the targetcycle 6W $03FDthe intendedwritethe received byte is consumed hereA dummy cycle is a real bus cycle: the chip does not know it is a dummy.Before 2.0, Phosphoric did not issue these accesses: it was more lenient than the hardware.Serial driver: write it in assembly, with absolute STA (no dummy cycle).
STA (zp),Y in six cycles: the fifth is a dummy read of the target address.

Interrupts on the penultimate cycle

The 6502 samples its interrupt lines during the second-to-last cycle of an instruction. Phosphoric does the same, which gives several behaviours of the real processor for free:

  • an interrupt cannot be taken before the current instruction;
  • a line that becomes active during the last cycle of an instruction is seen too late: it is only honoured after the next instruction;
  • SEI does not protect the following instruction from an IRQ already pending, and CLI or PLP symmetrically delay its arrival by one instruction;
  • an NMI that arrives during a BRK hijacks it to its own vector.

The oracle: how we know

make test-cycle replays the SingleStepTests/65x02 vectors: 10,000 cases per opcode, each with the initial state, the final state and the expected bus trace, cycle by cycle. It scores four properties separately, from weakest to strongest:

PropertyWhat it provesLevel
final state (registers and RAM)the computation is rightindependent of timing
total cycles per instructionthe counters are exactN1
bus subsequenceno stray access, no inversionN2
exact bus sequencethe right access on every cycleN3

The vectors, large (about 1 GB) and third-party, are not in the repository: tools/fetch_vectors.sh downloads them. Without them, the target skips cleanly, and so stays in make tests and in the CI.

From 44.26% to 100%

old core (--cpu-legacy)micro-sequenced core (default)
exact bus sequence44.26%100.00%
cycles without an address12.8% of boot cycles0
NMOS dummy accessesabsentall issued
interrupt takingat the instruction boundaryon the penultimate cycle
delayed I flag (CLI, SEI, PLP)noyes
NMI hijacking a BRKnoyes
cost per frame (20 ms budget)1.9%2.5%

The 44.26% figure was measured on 10 September 2026 on an exhaustive run: 244 opcodes (excluding the 12 JAM) × 10,000 cases = 2,440,000 cases, in 6.4 seconds. It is the published starting point of version 2.0. From the next step (V2-S2), the micro-sequenced core reached 100.00% on all four properties. A floor in the test forbids going back down.

The functional test

The 6502’s reference functional test, 6502_functional_test, passes in full: it reaches its success trap at $3469 after about 96 million emulated cycles. It already confirmed that the old core’s shortfall was in timing, not logic.

Two cores, one computation

Both engines share the same computation functions: flags, decimal, illegal opcodes. Only the scheduling of cycles differs. The old one remains available through --cpu-legacy; it cannot stop between two cycles, and so also forces the ULA to render line by line.

The switch to the new core was checked for identity: Oric-1 and Atmos start-ups byte-identical, thirteen real programs identical on screen, and the functional test passed in the same number of cycles (96,241,367) on both cores.

What it changes for programs

Faithfulness has a price for over-confident programs. BASIC’s POKE writes with STA (zp),Y, which makes a dummy read before writing. A POKE 1021,C to an ACIA’s data register therefore eats the byte that had just arrived. An echo program written in BASIC loses bytes, on Phosphoric as on a real machine. The bus trace shows it in two lines:

R $03FD $52    <- the POKE's dummy read: the byte "R" is eaten
W $03FD $4F    <- the intended write

A serial driver is therefore written in assembly, with absolute STA or STY, which have no dummy cycle. Before 2.0, Phosphoric let these programs through: it was more lenient than the hardware.

The traces

  • --trace FILE: one line per instruction, disassembled, with the registers.
  • --cycle-trace FILE: one line per cycle, in the format CYCLE T ADDR DATA PC A X Y SP P FLAGS IRQ, where T is R (read), W (write) or i (internal cycle). It is used to compare Phosphoric, line by line, with another emulator or with instrumented hardware. i lines only appear with --cpu-legacy: the default core makes a real access on every cycle.
  • --profile FILE: hit counts and cycles per address, an opcode histogram, the 20 hot spots.

Sources in the repository : docs/ACCURACY.md, docs/specs/V2_CYCLE_ACCURACY.md, docs/articles/v2-cycle-stepped.md, README.md.