6502 emulator in C/C++: how to separate addressing mode code from actual instruction code
Asked Answered
M

1

7

In the spare time I'm starting writing a very simple C++ emulator for the 6502 CPU. I used to write down a lot of assembly code for this CPU so all the opcodes, addressing modes and other stuff are not a big deal.

The 6502 has 56 different instructions plus 13 addressing modes giving a total of 151 different opcodes. To me speed is not an issue so instead of writing a huge switch-case statement and repeat the same code again and again (different opcodes can refer to the same instruction using a different addressing mode) I'd like to separate actual instruction code from the addressing mode code: I found this solution very neat as it would require to write only 13 addressing mode functions and 56 instruction functions without repeat myself.

here the addressing mode functions:

// Addressing modes
uint16_t Addr_ACC(); // ACCUMULATOR
uint16_t Addr_IMM(); // IMMEDIATE
uint16_t Addr_ABS(); // ABSOLUTE
uint16_t Addr_ZER(); // ZERO PAGE
uint16_t Addr_ZEX(); // INDEXED-X ZERO PAGE
uint16_t Addr_ZEY(); // INDEXED-Y ZERO PAGE
uint16_t Addr_ABX(); // INDEXED-X ABSOLUTE
uint16_t Addr_ABY(); // INDEXED-Y ABSOLUTE
uint16_t Addr_IMP(); // IMPLIED
uint16_t Addr_REL(); // RELATIVE
uint16_t Addr_INX(); // INDEXED-X INDIRECT
uint16_t Addr_INY(); // INDEXED-Y INDIRECT
uint16_t Addr_ABI(); // ABSOLUTE INDIRECT

they all returns the actual memory address (16 bit) used by the instruction to read/write the operand/result

the instruction function prototype is:

void Op_ADC(uint16_t addr);
void Op_AND(uint16_t addr);
void Op_ASL(uint16_t addr);
    ...

it takes the 16 bit address, perform its own operations, update the status flags and/or registers, and commit the results (if any) on the same memory address.

Given that code framework I found difficult to use the ACCUMULATOR addressing mode which is the only one to return the actual value of the A internal register instead of a memory address. I could return the value of A using the uin16_t return type and add a boolean flag for such addressing mode but I find it an extremely ugly solution.

The instruction functions should be completely addressing-mode agnostic.

Maximilien answered 23/11, 2013 at 15:38 Comment(14)
Internally using 16-bit addressing mode, you can just map the complete context (A,X,Y,SP,ST,PC, what else?) to address 0x10000; your functions should be modified to accept uint32_t addr, out of which only the first 0x10008 or so bytes are actually used.Decennial
If the instructions are addressing mode agnostic, then why do they need a flag to know the source of the data? You may find it helpful to look up or re-create a diagram of the processor data paths; typically there are multiplexors feeding into the input ports of the ALU, so you could focus on functional blocks which obtain that data, and those which use it. Given that all of this state, both programming-model-visible and hidden inside is factually global, using global variables (or a "machine" struct which you pass around a pointer to) is OK if that makes your implementation easier.Quintan
@Chris: At the moment they are not completely agnostic because in one case the uin16_t return value represents a memory address and in the accumulator-mode represents the actual content of register A. The instruction function takes an uint16_t argument and given that is impossible to determine if it is a memory address or a 8-bit value, it should need also a boolean flag or something like that.Maximilien
I would think your operand fetching functions should be de-referencing the memory address to obtain data, and provide that to the ALU function. I think it would really help you to get an idea from the block diagram of how the processor actually works. Basically there are a bunch of parts which are agnostic about the rest, and then (theoretically at least) a control rom which decodes the bits of the instruction word into a wider collection of signals which control the various data path switches. Or perhaps the 6502 is microprogrammed (I'll admit I haven't checked). Either should work though.Quintan
@Aki: that seems a very good solution, bravo! I can figure out a slightly different solution in which only the A register is mapped into the extra location 0x10000, I don't actually need the others. It'll require again an uint32_t as a return value.Maximilien
@Chris: it's not microprogrammed. It has a simple PLA in which the actual opcode value enable/disable all the internal parts of the CPU; the instruction set is nearly orthogonalMaximilien
Yes, using just A in the address 0x10000 was my first idea too, but then I figured out, that tax, tay, etc. could use the same mechanism internally. But it's up to you...Decennial
Why not handle each stage: { operand fetch, ALU, writeback } in it's own little switch between possible functions, based on the relevant bits of the instruction word and ignorant of everything else. It seems like it should be quite compact.Quintan
1) I think you'll find that speed matters as soon as you get it to work and try to run it on something interesting and suddenly the giant switch statement will matter 2) 151 routines of 5 lines? I think I'd just bit the bullet and do it; I suspect you'll be done in a 2 hours. (This exchange is probably just a distraction). You'll surely have common subroutines that implement the "addressing modes" and that's where you get leverage.Bukovina
Have the addressing mode functions return a pointer to the data.Beggary
The "ACCUMULATOR" mode is in fact some kind of "IMPLIED" mode. Both modes do not use an address. You should implement such instructions like separate instructions. Example: Implement two instructions for "ROL": "ROL A" and "ROL memory" (which may be ABSOLUTE or INDEXED-X ABSOLUTE).Kurd
Martin: you're absolutely right and your solution fits very well with my code... My main goal was to not repeat any instruction code but I think the solution is reasonable. There are just a couple of instruction which use accumulator mode (and the ones using implied mode don't use any other addressing modes)Maximilien
@Martin: rewrite your last comment as an answer and I'll accept it.Maximilien
hey.. I finally done it: check it out at: code.google.com/p/mos6502Maximilien
D
4

In Sharp6502 (my 6502 emulation engine written in C#) I treat internal registers and external memory both as first-class objects - the MemoryManager class instantiates an object for external memory, and another for internal registers, mapped to a different numeric range. Consequently, memory access and register access are identical at the functional level, since they are both referenced through MemoryManager according to what is basically an index.

Address-mode differentiation is simply a matter of filtering the bit-pattern of the instruction under emulation and performing a very simple calculation to determine the index to be passed to the MemoryManager - this might be implied, or require one or two further bytes, but the underlying mechanism is identical for every instruction.

Dhaulagiri answered 24/11, 2013 at 9:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.