mirror of
https://github.com/pevans/erc-c.git
synced 2024-10-03 22:54:42 +00:00
502 lines
15 KiB
C
502 lines
15 KiB
C
/*
|
|
* mos6502.c
|
|
*
|
|
* These functions are kind of the "top-level", if you will, for the MOS
|
|
* 6502 processor. You can create the processor struct, operate on the
|
|
* stack, etc.
|
|
*/
|
|
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
|
|
#include "log.h"
|
|
#include "mos6502/mos6502.h"
|
|
#include "mos6502/dis.h"
|
|
|
|
// All of our address modes, instructions, etc. are defined here.
|
|
#include "mos6502/enums.h"
|
|
|
|
/*
|
|
* This is a table which defines what instruction each opcode is mapped
|
|
* to. All possible (256) values are defined here. You will note many
|
|
* cases where we use NOP where opcodes are not _technically_ defined;
|
|
* this may or may not be the best behavior. It's quite possible we should
|
|
* instead crash the program when we stumble upon such malformed opcodes
|
|
*/
|
|
static int instructions[] = {
|
|
// 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
|
|
BRK, ORA, NP2, NOP, TSB, ORA, ASL, NOP, PHP, ORA, ASL, NOP, TSB, ORA, ASL, NOP, // 0x
|
|
BPL, ORA, ORA, NOP, TRB, ORA, ASL, NOP, CLC, ORA, INC, NOP, TRB, ORA, ASL, NOP, // 1x
|
|
JSR, AND, NP2, NOP, BIT, AND, ROL, NOP, PLP, AND, ROL, NOP, BIT, AND, ROL, NOP, // 2x
|
|
BMI, AND, AND, NOP, BIT, AND, ROL, NOP, SEC, AND, DEC, NOP, BIT, AND, ROL, NOP, // 3x
|
|
RTI, EOR, NP2, NOP, NP2, EOR, LSR, NOP, PHA, EOR, LSR, NOP, JMP, EOR, LSR, NOP, // 4x
|
|
BVC, EOR, EOR, NOP, NP2, EOR, LSR, NOP, CLI, EOR, PHY, NOP, NP3, EOR, LSR, NOP, // 5x
|
|
RTS, ADC, NP2, NOP, STZ, ADC, ROR, NOP, PLA, ADC, ROR, NOP, JMP, ADC, ROR, NOP, // 6x
|
|
BVS, ADC, ADC, NOP, STZ, ADC, ROR, NOP, SEI, ADC, PLY, NOP, JMP, ADC, ROR, NOP, // 7x
|
|
BRA, STA, NP2, NOP, STY, STA, STX, NOP, DEY, BIM, TXA, NOP, STY, STA, STX, NOP, // 8x
|
|
BCC, STA, STA, NOP, STY, STA, STX, NOP, TYA, STA, TXS, NOP, STZ, STA, STZ, NOP, // 9x
|
|
LDY, LDA, LDX, NOP, LDY, LDA, LDX, NOP, TAY, LDA, TAX, NOP, LDY, LDA, LDX, NOP, // Ax
|
|
BCS, LDA, LDA, NOP, LDY, LDA, LDX, NOP, CLV, LDA, TSX, NOP, LDY, LDA, LDX, NOP, // Bx
|
|
CPY, CMP, NP2, NOP, CPY, CMP, DEC, NOP, INY, CMP, DEX, NOP, CPY, CMP, DEC, NOP, // Cx
|
|
BNE, CMP, CMP, NOP, NP2, CMP, DEC, NOP, CLD, CMP, PHX, NOP, NP3, CMP, DEC, NOP, // Dx
|
|
CPX, SBC, NP2, NOP, CPX, SBC, INC, NOP, INX, SBC, NOP, NOP, CPX, SBC, INC, NOP, // Ex
|
|
BEQ, SBC, SBC, NOP, NP2, SBC, INC, NOP, SED, SBC, PLX, NOP, NP3, SBC, INC, NOP, // Fx
|
|
};
|
|
|
|
/*
|
|
* A small convenience for defining instruction handlers below.
|
|
*/
|
|
#define INST_HANDLER(x) \
|
|
mos6502_handle_##x
|
|
|
|
/*
|
|
* Here's another table, this time mapping instruction codes to
|
|
* instruction handler functions. They are listed in the order defined
|
|
* in the instruction enum (in mos6502.enums.h).
|
|
*/
|
|
static mos6502_instruction_handler instruction_handlers[] = {
|
|
INST_HANDLER(adc),
|
|
INST_HANDLER(and),
|
|
INST_HANDLER(asl),
|
|
INST_HANDLER(bad),
|
|
INST_HANDLER(bcc),
|
|
INST_HANDLER(bcs),
|
|
INST_HANDLER(beq),
|
|
INST_HANDLER(bit),
|
|
INST_HANDLER(bim),
|
|
INST_HANDLER(bmi),
|
|
INST_HANDLER(bne),
|
|
INST_HANDLER(bpl),
|
|
INST_HANDLER(bra),
|
|
INST_HANDLER(brk),
|
|
INST_HANDLER(bvc),
|
|
INST_HANDLER(bvs),
|
|
INST_HANDLER(clc),
|
|
INST_HANDLER(cld),
|
|
INST_HANDLER(cli),
|
|
INST_HANDLER(clv),
|
|
INST_HANDLER(cmp),
|
|
INST_HANDLER(cpx),
|
|
INST_HANDLER(cpy),
|
|
INST_HANDLER(dec),
|
|
INST_HANDLER(dex),
|
|
INST_HANDLER(dey),
|
|
INST_HANDLER(eor),
|
|
INST_HANDLER(inc),
|
|
INST_HANDLER(inx),
|
|
INST_HANDLER(iny),
|
|
INST_HANDLER(jmp),
|
|
INST_HANDLER(jsr),
|
|
INST_HANDLER(lda),
|
|
INST_HANDLER(ldx),
|
|
INST_HANDLER(ldy),
|
|
INST_HANDLER(lsr),
|
|
INST_HANDLER(nop),
|
|
INST_HANDLER(np2),
|
|
INST_HANDLER(np3),
|
|
INST_HANDLER(ora),
|
|
INST_HANDLER(pha),
|
|
INST_HANDLER(php),
|
|
INST_HANDLER(phx),
|
|
INST_HANDLER(ply),
|
|
INST_HANDLER(pla),
|
|
INST_HANDLER(plp),
|
|
INST_HANDLER(plx),
|
|
INST_HANDLER(ply),
|
|
INST_HANDLER(rol),
|
|
INST_HANDLER(ror),
|
|
INST_HANDLER(rti),
|
|
INST_HANDLER(rts),
|
|
INST_HANDLER(sbc),
|
|
INST_HANDLER(sec),
|
|
INST_HANDLER(sed),
|
|
INST_HANDLER(sei),
|
|
INST_HANDLER(sta),
|
|
INST_HANDLER(stx),
|
|
INST_HANDLER(sty),
|
|
INST_HANDLER(stz),
|
|
INST_HANDLER(tax),
|
|
INST_HANDLER(tay),
|
|
INST_HANDLER(trb),
|
|
INST_HANDLER(tsb),
|
|
INST_HANDLER(tsx),
|
|
INST_HANDLER(txa),
|
|
INST_HANDLER(txs),
|
|
INST_HANDLER(tya),
|
|
};
|
|
|
|
/*
|
|
* Here we have a table that maps opcodes to the number of cycles each
|
|
* should cost. In cases where no opcode is defined, we set the number
|
|
* of cycles to zero.
|
|
*/
|
|
static int cycles[] = {
|
|
// 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
|
|
7, 6, 2, 1, 5, 3, 5, 1, 3, 2, 2, 1, 6, 4, 6, 1, // 0x
|
|
2, 5, 5, 1, 5, 4, 6, 1, 2, 4, 2, 1, 6, 4, 6, 1, // 1x
|
|
6, 6, 2, 1, 3, 3, 5, 1, 4, 2, 2, 1, 4, 4, 6, 1, // 2x
|
|
2, 5, 5, 1, 4, 4, 6, 1, 2, 4, 2, 1, 4, 4, 6, 1, // 3x
|
|
6, 6, 2, 1, 3, 3, 5, 1, 3, 2, 2, 1, 3, 4, 6, 1, // 4x
|
|
2, 5, 5, 1, 4, 4, 6, 1, 2, 4, 3, 1, 8, 4, 6, 1, // 5x
|
|
6, 6, 2, 1, 3, 3, 5, 1, 4, 2, 2, 1, 5, 4, 6, 1, // 6x
|
|
2, 5, 5, 1, 4, 4, 6, 1, 2, 4, 4, 1, 6, 4, 6, 1, // 7x
|
|
3, 6, 2, 1, 3, 3, 3, 1, 2, 2, 2, 1, 4, 4, 4, 1, // 8x
|
|
2, 6, 5, 1, 4, 4, 4, 1, 2, 5, 2, 1, 4, 5, 5, 1, // 9x
|
|
2, 6, 2, 1, 3, 3, 3, 1, 2, 2, 2, 1, 4, 4, 4, 1, // Ax
|
|
2, 5, 5, 1, 4, 4, 4, 1, 2, 4, 2, 1, 4, 4, 4, 1, // Bx
|
|
2, 6, 2, 1, 3, 3, 5, 1, 2, 2, 2, 1, 4, 4, 3, 1, // Cx
|
|
2, 5, 5, 1, 4, 4, 6, 1, 2, 4, 3, 1, 4, 4, 7, 1, // Dx
|
|
2, 6, 2, 1, 3, 3, 5, 1, 2, 2, 2, 1, 4, 4, 6, 1, // Ex
|
|
2, 5, 5, 1, 4, 4, 6, 1, 2, 4, 4, 1, 4, 4, 7, 1, // Fx
|
|
};
|
|
|
|
/*
|
|
* Build a new mos6502 struct object, and also build the memory contents
|
|
* used therein. All registers should be zeroed out.
|
|
*/
|
|
mos6502 *
|
|
mos6502_create(vm_segment *rmem, vm_segment *wmem)
|
|
{
|
|
mos6502 *cpu;
|
|
|
|
cpu = malloc(sizeof(mos6502));
|
|
if (cpu == NULL) {
|
|
log_crit("Not enough memory to allocate mos6502");
|
|
exit(1);
|
|
}
|
|
|
|
mos6502_set_memory(cpu, rmem, wmem);
|
|
|
|
cpu->eff_addr = 0;
|
|
cpu->addr_mode = 0;
|
|
cpu->PC = 0;
|
|
cpu->A = 0;
|
|
cpu->X = 0;
|
|
cpu->Y = 0;
|
|
cpu->P = MOS_STATUS_DEFAULT;
|
|
cpu->S = 0xff;
|
|
|
|
return cpu;
|
|
}
|
|
|
|
/*
|
|
* Free the memory consumed by the mos6502 struct.
|
|
*/
|
|
void
|
|
mos6502_free(mos6502 *cpu)
|
|
{
|
|
// Note we do not free rmem or wmem; we consider this to be the
|
|
// responsibility of the caller that passed us those values.
|
|
free(cpu);
|
|
}
|
|
|
|
/*
|
|
* Push an 8-bit value to the stack, and then decrement the S register
|
|
* so that it points at the place where a new value would be deposited
|
|
* by a subsequent push. If S is decremented below 0, it rolls around to
|
|
* FF.
|
|
*/
|
|
void
|
|
mos6502_push_stack(mos6502 *cpu, vm_8bit addr)
|
|
{
|
|
mos6502_set(cpu, 0x100 + cpu->S, addr);
|
|
cpu->S--;
|
|
}
|
|
|
|
/*
|
|
* Increment the stack register and return the value that is pointed at
|
|
* by the new offset. Because the S register is 8-bit, the following
|
|
* (correct) behavior is exhibited: when S overflows beyond 0xFF, it
|
|
* rolls over to 0x00.
|
|
*/
|
|
vm_8bit
|
|
mos6502_pop_stack(mos6502 *cpu)
|
|
{
|
|
cpu->S++;
|
|
return mos6502_get(cpu, 0x0100 + cpu->S);
|
|
}
|
|
|
|
/*
|
|
* Here we set the status register to a given status value, regardless
|
|
* of its past contents.
|
|
*/
|
|
void
|
|
mos6502_set_status(mos6502 *cpu, vm_8bit status)
|
|
{
|
|
cpu->P = status;
|
|
}
|
|
|
|
/*
|
|
* Return the instruction that is mapped to a given opcode.
|
|
*/
|
|
int
|
|
mos6502_instruction(vm_8bit opcode)
|
|
{
|
|
return instructions[opcode];
|
|
}
|
|
|
|
/*
|
|
* Return the number of cycles an opcode may consume. The cpu is a
|
|
* required parameter, because the number of opcodes is conditional upon
|
|
* the effective address of the instruction we're executing.
|
|
*/
|
|
int
|
|
mos6502_cycles(mos6502 *cpu, vm_8bit opcode)
|
|
{
|
|
// In some contexts, we may need to return an additional cycle.
|
|
int modif = 0;
|
|
|
|
int addr_mode;
|
|
int lo_addr;
|
|
|
|
addr_mode = mos6502_addr_mode(opcode);
|
|
|
|
// Mainly we care about the lo byte of the last effective address
|
|
lo_addr = cpu->eff_addr & 0xFF;
|
|
|
|
// Ok, here's the deal: if you are using an address mode that uses
|
|
// any of the index registers, you need to return an additional
|
|
// cycle if the lo byte of the address plus that index would cross a
|
|
// memory page boundary
|
|
switch (addr_mode) {
|
|
case ABX:
|
|
if (lo_addr + cpu->X > 255) {
|
|
modif = 1;
|
|
}
|
|
break;
|
|
|
|
case ABY:
|
|
case INY:
|
|
if (lo_addr + cpu->Y > 255) {
|
|
modif = 1;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return cycles[opcode] + modif;
|
|
}
|
|
|
|
/*
|
|
* Here we intend to return the proper resolver function for any given
|
|
* instruction.
|
|
*/
|
|
mos6502_instruction_handler
|
|
mos6502_get_instruction_handler(vm_8bit opcode)
|
|
{
|
|
return instruction_handlers[mos6502_instruction(opcode)];
|
|
}
|
|
|
|
/*
|
|
* This code does the execution step that the 6502 processor would take,
|
|
* from soup to nuts.
|
|
*/
|
|
void
|
|
mos6502_execute(mos6502 *cpu)
|
|
{
|
|
vm_8bit opcode, operand = 0;
|
|
int /*cycles,*/ bytes;
|
|
mos6502_address_resolver resolver;
|
|
mos6502_instruction_handler handler;
|
|
|
|
cpu->opcode = opcode = mos6502_get(cpu, cpu->PC);
|
|
cpu->addr_mode = mos6502_addr_mode(opcode);
|
|
|
|
// The disassembler knows how many bytes each operand requires
|
|
// (maybe this code doesn't belong in the disassembler); let's use
|
|
// that to figure out the total number of bytes to skip. We add 1
|
|
// because we need to account for the opcode as well.
|
|
bytes = 1 + mos6502_dis_expected_bytes(cpu->addr_mode);
|
|
|
|
// First, we need to know how to resolve our effective address and
|
|
// how to execute anything.
|
|
resolver = mos6502_get_address_resolver(cpu->addr_mode);
|
|
handler = mos6502_get_instruction_handler(opcode);
|
|
|
|
// The operand is the effective operand, the value that the
|
|
// instruction handler cares about (if it cares about any such
|
|
// value). For example, the operand could be the literal value that
|
|
// you pass into an instruction via immediate mode. As a
|
|
// side-effect, resolver will set the eff_addr field in cpu to the
|
|
// effective address where the operand can be found in memory, or
|
|
// zero if that does not apply (such as in immediate mode).
|
|
//
|
|
// Note also that resolver may be NULL, as there may not be any
|
|
// operand for this instruction! If so, we let the default for
|
|
// operand stand, which is zero.
|
|
cpu->operand = 0;
|
|
if (resolver) {
|
|
cpu->operand = operand = resolver(cpu);
|
|
}
|
|
|
|
// Here's where the magic happens. Whatever the instruction does, it
|
|
// happens in the handler function.
|
|
handler(cpu, operand);
|
|
|
|
// This will be the number of cycles we should spend on the
|
|
// instruction. Of course, we can execute instructions pretty
|
|
// quickly in a modern architecture, but a lot of code was written
|
|
// with the idea that certain instructions -- in certain address
|
|
// modes -- were more expensive than others, and you want those
|
|
// programs to feel faster or slower in relation to that.
|
|
//cycles = mos6502_cycles(cpu, opcode);
|
|
|
|
// If we need to jump, then the handler has to take care of updating
|
|
// PC. If not, then we need to do it.
|
|
if (!mos6502_would_jump(mos6502_instruction(opcode))) {
|
|
cpu->PC += bytes;
|
|
}
|
|
|
|
// FIXME: uh this probably isn't right, but I wanted to do
|
|
// something.
|
|
//usleep(cycles);
|
|
|
|
cpu->P |= MOS_UNUSED | MOS_BREAK;
|
|
|
|
// Ok -- we're done! This wasn't so hard, was it?
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Given pointers for an opcode, operand, and effective address, set the
|
|
* dereferenced values of those pointers to what the CPU knows to have
|
|
* been the last of each.
|
|
*/
|
|
void
|
|
mos6502_last_executed(mos6502 *cpu, vm_8bit *opcode,
|
|
vm_8bit *operand, vm_16bit *addr)
|
|
{
|
|
if (opcode) {
|
|
*opcode = cpu->opcode;
|
|
}
|
|
|
|
if (operand) {
|
|
*operand = cpu->operand;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Return true if the given instruction would require that we jump
|
|
* to somewhere else in the program.
|
|
*/
|
|
inline bool
|
|
mos6502_would_jump(int inst_code)
|
|
{
|
|
return
|
|
inst_code == BCC ||
|
|
inst_code == BCS ||
|
|
inst_code == BEQ ||
|
|
inst_code == BMI ||
|
|
inst_code == BNE ||
|
|
inst_code == BPL ||
|
|
inst_code == BRK ||
|
|
inst_code == BVC ||
|
|
inst_code == BVS ||
|
|
inst_code == JMP ||
|
|
inst_code == JSR ||
|
|
inst_code == NP2 || // this is KIND of a hack, but it works!
|
|
inst_code == NP3 || // these jump ahead by 2 or 3 bytes (respectively)
|
|
inst_code == RTS ||
|
|
inst_code == RTI;
|
|
}
|
|
|
|
/*
|
|
* Return true if the given instruction _might_ write to memory, if used
|
|
* with an appropriate address mode (e.g. anything other than ACC).
|
|
*/
|
|
inline bool
|
|
mos6502_would_write_mem(int inst_code)
|
|
{
|
|
return
|
|
inst_code == ASL ||
|
|
inst_code == DEC ||
|
|
inst_code == INC ||
|
|
inst_code == LSR ||
|
|
inst_code == ROL ||
|
|
inst_code == ROR;
|
|
}
|
|
|
|
/*
|
|
* This is a _kind_ of factory method, except we're obviously not
|
|
* instantiating an object. Given an address mode, we return the
|
|
* resolver function which will give you the right value (for a given
|
|
* cpu) that an instruction will use.
|
|
*/
|
|
mos6502_address_resolver
|
|
mos6502_get_address_resolver(int addr_mode)
|
|
{
|
|
switch (addr_mode) {
|
|
case ACC: return mos6502_resolve_acc;
|
|
case ABS: return mos6502_resolve_abs;
|
|
case ABX: return mos6502_resolve_abx;
|
|
case ABY: return mos6502_resolve_aby;
|
|
case IMM: return mos6502_resolve_imm;
|
|
case IND: return mos6502_resolve_ind;
|
|
case IDX: return mos6502_resolve_idx;
|
|
case IDY: return mos6502_resolve_idy;
|
|
case REL: return mos6502_resolve_rel;
|
|
case ZPG: return mos6502_resolve_zpg;
|
|
case ZPX: return mos6502_resolve_zpx;
|
|
case ZPY: return mos6502_resolve_zpy;
|
|
case IMP: // FALLTHRU
|
|
default: break;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* Get the byte at a given address from whatever the read memory segment
|
|
* is.
|
|
*/
|
|
inline vm_8bit
|
|
mos6502_get(mos6502 *cpu, size_t addr)
|
|
{
|
|
return vm_segment_get(cpu->rmem, addr);
|
|
}
|
|
|
|
/*
|
|
* Return the 16-bit value from a given address using the read memory
|
|
* segment.
|
|
*/
|
|
inline vm_16bit
|
|
mos6502_get16(mos6502 *cpu, size_t addr)
|
|
{
|
|
return vm_segment_get16(cpu->rmem, addr);
|
|
}
|
|
|
|
/*
|
|
* Set the byte at a given address to the given value, using whatever
|
|
* the write segment is.
|
|
*/
|
|
inline void
|
|
mos6502_set(mos6502 *cpu, size_t addr, vm_8bit value)
|
|
{
|
|
vm_segment_set(cpu->wmem, addr, value);
|
|
}
|
|
|
|
/*
|
|
* Set the two bytes at a given address to the given value, using the
|
|
* write segment.
|
|
*/
|
|
inline void
|
|
mos6502_set16(mos6502 *cpu, size_t addr, vm_16bit value)
|
|
{
|
|
vm_segment_set16(cpu->wmem, addr, value);
|
|
}
|
|
|
|
/*
|
|
* Given a read and write memory segment, update the CPU to use those
|
|
* implicitly when handling the mos6502_set/get functions.
|
|
*/
|
|
void
|
|
mos6502_set_memory(mos6502 *cpu, vm_segment *rmem, vm_segment *wmem)
|
|
{
|
|
cpu->rmem = rmem;
|
|
cpu->wmem = wmem;
|
|
}
|