Most emulators store the number of cycles a particular instruction takes in a lookup table, and then add any conditional cycles if needed (when crossing page boundaries, for example).
I'm wondering if there is a way to procedurally determine the number of cycles an instruction will take based solely on addressing mode and memory reads/writes.
To give an example, I've noticed that all instructions that use immediate or relative addressing take 2 cycles.
All zero-page instructions take 3 cycles, plus an additional 2 cycles if altering memory in-place.
All indexed zero-page instructions take 4 cycles, plus an additional 2 cycles if altering memory in-place.
...And so on.
So, is there some fully documented, procedural way of determining the number of cycles for an instruction like the above? Are there exceptions that would break determinism in such a formula?
Yes — and this is how almost all accurate emulators are written*; see documents such as 64doc.txt. It's not much more complicated than simple memory access counting though — the 6502 will perform a memory access every single cycle, it can usually get a meaningful result within the remainder of the cycle after an access (i.e. I'm handwaving a little to avoid a discussion of what's pipelined and what isn't; see the documentation).
So e.g. for
ADC #54
the processor must (i) read the opcode; (ii) read the operand. That's two cycles.For
ADC ($32), Y
that's:So it's either 5 or 6 cycles.
You can always emulate the memory access more as a step-by-step timed thing, and perform the actual operation as an orthogonal step. It's also easy to use the same logic for read, write or read-modify-write: reads and writes have the same timing but do a different memory access at the end, tead-modify-writes all write the read value back for a cycle while working out the real result, then write the real result.
*) because performing all the memory accesses simultaneously, not including any that are redundant, then warping time forward a little is absolutely nothing like the real hardware. And it'll trip you up as soon as a memory access is to anything with an independent concept of time — a timer or anything that might generate interrupts, or just RAM itself if the machine scans RAM for its video output; never mind that it requires you to add special cases around instructions like
CLI
andSEI
**. Emulators needn't be structured like they were in the 1990s any more.**) IRQ status is sampled on the penultimate cycle of every operation.
CLI
andSEI
adjust the bit during the final cycle. So even if an interrupt is pending then aCLI
won't result in an interrupt until after the instruction after theCLI
. Which could itself be anSEI
. So aCLI
/SEI
pair while an interrupt is pending should result in a trip to the interrupt handler after theSEI
has executed, with the interrupt flag set. This happens naturally if you're emulating the cycle-by-cycle behaviour of a 6502, tends to be a huge hack if you're working operation-by-operation and time warping. Or, much more likely, such emulators just plain get the behaviour wrong.