Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to procedurally determine the number of cycles a particular instruction takes on a 6502?

Tags:

emulation

6502

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?

like image 501
w.brian Avatar asked Mar 10 '23 08:03

w.brian


1 Answers

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:

  1. read opcode
  2. read operand
  3. read from $32 to get low byte of address
  4. read from $33 to get high byte of address, add Y to low byte of address
  5. read from (high byte of address):(low byte of address + Y), as there's only been time to perform the low-byte calculation
  6. oh, wait, if there was carry then the last result was wrong, better read again. If not then, great, everything's fine, don't bother with this cycle.

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 and SEI**. 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 and SEI adjust the bit during the final cycle. So even if an interrupt is pending then a CLI won't result in an interrupt until after the instruction after the CLI. Which could itself be an SEI. So a CLI/SEI pair while an interrupt is pending should result in a trip to the interrupt handler after the SEI 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.

like image 116
Tommy Avatar answered Apr 25 '23 21:04

Tommy