Chip timing and the emulator loop
Last updated:
The NES has a few different chips — CPU, graphics (PPU), audio (APU) — and they all share a single source of time: a crystal oscillator on the board that sends out a pulse every ~47 nanoseconds. The stream of pulses it produces is called the master clock, and one pulse is one clock (or tick, or cycle).
Every chip is wired to the same pulse stream, but one cycle of work for each chip takes a different number of clocks to complete:
- PPU — one cycle every 4 clocks (~186 ns).
- CPU — one cycle every 12 clocks (~560 ns).
- APU — same as the CPU, 12 clocks. Some of its internal units run at half that rate (one cycle every 24 clocks).
The chips run at different cycle rates, but they are all in-sync to the same master clock.
Aside on vocabulary: in NES and 6502 documentation you'll see clock, tick and cycle sometimes used interchangeably to mean "one beat of some chip's clock." I prefer to use the terms "master clock" and "cpu cycle", "ppu cycle", but it's not a hard rule. Also, throughout these notes I count clocks and cycles starting at 1, not 0 — so the first clock of a CPU cycle is clock 1, the last is clock 12.
Coding the parallelism of chips
On the real console the chips run in parallel. In an emulator running on a modern CPU we can't reproduce that — using one thread per chip wouldn't make sense because the OS scheduler's has jitter, it can't be made to tick at exact intervals.
But we don't need real-time parallelism. What makes the emulation accurate is the order in which the chips tick in relation to each other.
There's two ways I noticed this can be coded:
- CPU-centric — the CPU drives, and the other chips are ticked from within its work.
- Master-clock centric — the master clock drives, and each chip ticks when its own divider says so. I barely mention this because I feel it's harder to implement
Chip Parallelism #1: CPU-centric
First, let's define what a CPU step is. It is one full CPU operation — either an instruction (2–7 cycles) or an interrupt service sequence (7 cycles). A CPU step is several CPU cycles long; a CPU cycle is 12 clocks.
Why multiple cycles per step? Because every read or write the
CPU does on the bus takes one full cycle, and an instruction is
defined as a sequence of bus accesses. LDA $1234 runs as:
- Read the opcode.
- Read the low byte of the address.
- Read the high byte of the address.
- Read the value at
$1234.
Four bus accesses, four cycles. The cycle count of an instruction is essentially "how many times it touches the bus."
A simple but incorrect first attempt:
while (true) {
cycles = cpu.step(); // run one operation; returns # cycles (2–7)
ppu.tick(cycles * 3); // 3 PPU cycles per CPU cycle
apu.tick(cycles); // 1 APU cycle per CPU cycle
}
The arithmetic is right — each chip gets the correct total amount of catch-up — but the order is wrong. The loop runs the CPU's entire step in one shot, then the PPU, then the APU. For a 2-cycle step it looks like this:
order of work:
CPU: ▓ ▓
APU: ▓ ▓
PPU: ▓ ▓ ▓ ▓ ▓ ▓
But, on the real console the three chips are running side by side, ticked off the same master clock. Each chip's cycle is a span of master clocks (12 for CPU and APU, 4 for PPU), and the spans overlap in time:
clocks: 0 4 8 12 24
CPU: ├───────────┼───────────┤ 2 CPU cycles (12 clocks per cycle)
APU: ├───────────┼───────────┤ 2 APU cycles (same as CPU)
PPU: ├───┼───┼───┼───┼───┼───┤ 6 PPU cycles (4 clocks per cycle)
This is a good proof of concept of the interleaving we want.
Reproducing this interleaving is what emulation accuracy hinges on: the chips affect each other through the bus, and when a write or read lands relative to the other chips' progress changes what they observe. Not interleaving the order of chip ticks makes emulation diverge from real hardware.
To achieve this, every time the CPU does a
bus access (which is once per cycle), it should clock() 12 times
before moving on, because a CPU cycle takes 12 clocks:
fn cpu.step() {
// ... while running the operation, for each cycle:
bus.read(addr); // bus.read internally calls tick_others()
}
fn bus.read(addr) {
clock(12);
const byte = read_byte(addr);
return byte;
}
fn clock(n) {
for (0..n) {
master_clocks += 1;
if (master_clocks % 4 == 0) ppu.tick(3);
if (master_clocks % 12 == 0) apu.tick(1);
}
}
Actually accurate cycle emulation
The former explanation was simpler for learning purposes, but for the emulation to be accurate the graph would be more like this:
clocks: 3 7 11 15 19 23
PPU: ───┼───┼───┼───┼───┼───┼─
CPU: ├────R──────┼──────W────┤
clocks: 0 5 12 19 24
R = read (early, clock 5/12)
W = write (late, clock 7/12)
We introduced two real hardware details here:
PPU/CPU phase offset. The PPU's ticks don't align with the
CPU's cycle boundaries — above they fire at clocks 3, 7, 11, …,
not 4, 8, 12, …. The offset is one of three values picked randomly
at power-on; I use -1. In clock function:
const ppu_offset = -1;
fn clock(n) {
...
if ((master_clocks + ppu_offset) % 4 == 0) ppu.cycle();
...
}
Read vs write timing. The CPU's bus access lands at a specific master clock inside the 12-clock window with an offset altered by the current CPU cycle being a read or a write.
Above, cycle 1, which is a read, gets the value in clock 5, but cycle 2, which is a write, happens at clock 7 (late).
Reflected in bus.read / bus.write:
fn bus.read(addr) {
clock(5);
const byte = read_byte(addr);
clock(7);
return byte;
}
fn bus.write(addr, byte) {
clock(7);
write_byte(addr, byte);
clock(5);
}
Aside: I'm not fully sure these are the "correct" splits — on real silicon both reads and writes happen during the second half of the cycle, and the exact master clock isn't fixed at a 1-clock granularity. The 5/7 and 7/5 numbers here are what Mesen uses for the common case, which is a reasonable starting point but not the final word.
Chip Parallelism #2: Master-clock centric
This way is worse for reasons specified further down.
The outer loop advances the master clock one clock at a time, and on each clock a switch decides which chips are due to step:
let clock = 0;
while (true) {
clock += 1;
if (clock % 4 == 0) ppu.cycle();
if (clock % 12 == 0) cpu.cycle();
if (clock % 12 == 0) apu.cycle();
}
Now each chip moves exactly when its divider fires, no chip ever runs ahead of another. That makes mid-cycle effects natural to express — a PPU event that happens on clock N+3 of a CPU cycle just happens, without the CPU loop having to know about it.
This has two challenges to implement:
1. The CPU has to be written as a state machine
In the master-clock-centric loop, cpu.cycle() is called once
per CPU cycle (every 12 clocks), but a full CPU instruction
takes 2–7 cycles. So a single instruction can't run top-to-
bottom in one call — it has to be sliced across as many calls as
the instruction has cycles, with the CPU itself remembering
where in the instruction it left off.
To see what this looks like in general: a straight-line function that does its work in one go looks like this:
fn cook_pizza() {
knead_dough();
add_toppings();
bake();
}
If we can only do one step per call, the body has to become a switch on externalised state:
let stage = 0;
fn cook_pizza_step() {
if (stage == 0) knead_dough();
else if (stage == 1) add_toppings();
else if (stage == 2) bake();
stage += 1;
}
Every NES instruction has to be written like this, with the
cycle index as the stage. The CPU-centric model avoids it —
instructions there stay as straight-line code.
This makes the code painful to write and slow at runtime: every
instruction grows a switch, and the outer loop now runs 12× per
CPU cycle instead of once.
2. Read/write timing inside a CPU cycle
Reads and writes land at different clocks within a CPU cycle, so
the boundary check has to shift by an offset that depends on
the access type of the next cycle. That means having a a per-
instruction, per-cycle table of "is read or write?" so the loop
has to know the right offset ahead of time:
while (true) {
...
if ((clock + offset) % 12 == 0) cpu.cycle();
...
}
Again, annoying.