Skip to content
Phosphoricwiki
Contents

The master clock

All of Phosphoric rests on one promise: one call to emu_cycle(), one cycle of the whole machine. Here is the contract, the order of the chips within the cycle, and the defect that taught how to keep it.

On this page

Before: three clocks

Before version 2.0, the machine’s time lived in three places. The processor advanced its own clock; a callback defined in main.c moved the peripherals; and the main loop computed the scan position itself to know when to draw a line. As a result, only the main loop knew how to clock the machine. The debugger, the tests or the replay could not ask for “one cycle forward”, only for “run one instruction”.

The contract

bool emu_cycle(emulator_t* emu);   /* one cycle of the WHOLE machine */
int  emu_step (emulator_t* emu);   /* one instruction, through emu_cycle */

emu_cycle() (src/emu_clock.c) returns true when the cycle it has just run ended an instruction. Any caller can now clock the machine: the main loop, the single-stepping debugger, the tests, the replay of a movie.

The order within the cycle

One machine cycle, in orderEach call to emu_cycle() gives the whole machine one cycle of one microsecond, in a fixed order: first the 6502's single bus access, then the peripherals clocked by φ2 move one cycle forward, and finally the ULA reads the screen cell of that same cycle. A processor write at cycle c is therefore seen by cell c; until 2.0.1, the reverse order shifted every split one cell to the right.One cycle = 1 µs = one call to emu_cycle()start of the cyclenext cycle① 6502the cycle's singlebus access: read,write or dummy access② φ2 peripheralsVIA, FDC, ACIA, tape,cards: moved forwardby exactly one cycle③ ULAreads one 6-pixel cell(the cycle's column),then advances the scanA 6502 write at cycle c is seen by cell c, not c + 1.The order comes from oscilloscope measurements on a real machine, published in a ULA guide.Never an empty call: each call costs the processor exactly one cycle.
The order is fixed: processor, then φ2 peripherals, then ULA.
OrderWhoWhat
1the 6502the cycle’s single bus access: read, write, or NMOS dummy access
2the φ2 peripheralsVIA, floppy controllers, ACIA, DTL 2000, MIDI, tape: moved forward by one cycle
3the ULAreads one 6-pixel cell (this cycle’s column), then advances the scan

Why the processor comes before the ULA

The ULA and the 6502 share memory in two halves of the same microsecond, without ever competing for it: there is no stolen cycle to model. The order of these two halves was measured with an oscilloscope on a real machine and published in an unofficial ULA guide: on the rising edge of the clock, the 6502 makes its access; then the ULA’s read begins.

Observable consequence: a processor write at cycle c is seen by cell c. The test test_cpu_write_visible_in_the_same_cell fails if the order is reversed.

No batches

With the micro-sequenced core, each cycle is a bus access. The clock callback therefore always receives cycles = 1, and peripherals move one cycle at a time, never in batches. The test test_peripherals_get_one_cycle_at_a_time would fail if a batch came back.

The order of the peripherals among themselves is written down too, in a separate table (io_bus_tick_order[], decision 0003): the VIA first, which drives the timers and the interrupt, then the tape, then the cards. This order has observable effects; it is not changed without proof.

Never an empty call

The contract has a converse: each call costs the processor exactly one cycle. A micro-operation that returned without a bus access would move the ULA forward by a cycle that neither the processor nor the VIA had lived.

It happened, in 2.0.0-alpha.8. A branch not taken made its decision one cycle too late, in a micro-operation with no bus access. The processor’s counter stayed right, the 65x02 oracle was green at 100%. But the master clock was called once too often: about 410 cycles of scan lead per frame on the BASIC ROM, one frame of drift per second between the picture and the rest of the machine.

What the emulator carries

The scan counters used to be local variables of the main loop. Carrying them in emulator_t is what lets anyone clock the machine:

FieldRole
raster_cyclethe current cycle in the frame (0 to 19,967)
raster_renderedthe visible lines already drawn (0 to 224)
raster_ng_linethe line already processed by the ULA-NG (0 to 311)
raster_next_linethe cycle of the next line change

emu_raster_pos() gives the beam position: PAL line (0 to 311) and cycle within the line (0 to 63).

Resuming mid-frame

Because the scan is in the emulator’s state, a save state (.ost) can be taken in the middle of a frame. On loading, Phosphoric re-aligns the scan, redraws from RAM the lines already scanned, and replays the cells already read on the current line to recover the attribute state. The test make test-savestate-determinism checks that resuming gives the same raster stops, the same VIA and the same RAM as an uninterrupted run.

Finer than the cycle

Only one place in the project goes below the cycle: the expansion bus of the LOCI cartridge, where a race is run between the card and the edge of the PHI2 clock. The motherboard chips do not need it, since they always win that race. See The LOCI.

The loop of a frame

Around the clock, the main loop (emulator_run()) runs each frame as named steps, in a fixed and observable order: the frame’s instructions (with debugger, trace and profiler), then the LOCI, sound with no display, external commands, fast loading, automatic typing, display and events, screenshots, recording, and finally the pacing at 50 frames per second. A screenshot taken at a given cycle therefore sees the screen after the automatic typing of the same frame, never before.

Sources in the repository : docs/architecture/master-clock.md, docs/adr/0003-ordre-des-ticks.md, docs/articles/v2-cycle-stepped.md, README.md.