Last active
October 9, 2022 14:21
-
-
Save mrorigo/7e2dd56367b5a74be68612cc029ccb29 to your computer and use it in GitHub Desktop.
Cycle exact 6502 (2A03) emulator in <1k lines of C
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright (C) 2022 orIgo <[email protected]> | |
* | |
* Cycle exact 6502 (2A03) emulator. | |
* | |
* - To initialize the cpu, provide an initialized `cpu_bus_t *` with memory read/write primitives to `cpu_init()` . | |
* - For each tick/cycle, call `cpu_tick()` with the returned `cpu_t *`, checking the return value to detect HLT. | |
* - To cause an NMI request, call `cpu_nmi()`. | |
*/ | |
#include "cpu.h" | |
#include <stdio.h> | |
#include <assert.h> | |
#include <stdlib.h> | |
static void cpu_write_byte(cpu_t *cpu, const uint16_t addr, const uint8_t val) | |
{ | |
cpu->bus->write(addr, val); | |
} | |
static uint8_t cpu_read_byte(cpu_t *cpu, const uint16_t addr) | |
{ | |
return cpu->bus->read(addr); | |
} | |
static uint16_t cpu_read_word(cpu_t *cpu, const uint16_t addr) | |
{ | |
const uint8_t l = cpu->bus->read(addr); | |
const uint8_t h = cpu->bus->read(addr + 1); | |
return (h << 8) | l; | |
} | |
static void cpu_push_stack(cpu_t *cpu, const uint8_t x) | |
{ | |
cpu_write_byte(cpu, cpu->regs.sp + STACK_BASE, x); | |
cpu->regs.sp -= 1; | |
} | |
static uint8_t cpu_pop_stack(cpu_t *cpu) | |
{ | |
cpu->regs.sp += 1; | |
return cpu_read_byte(cpu, cpu->regs.sp + STACK_BASE); | |
} | |
#define SET_NZ(reg) cpu->flags.n = (reg & 0x80) == 0x80; cpu->flags.z = reg == 0 | |
#define OP_IMPL(x) static void op_impl_##x(cpu_t *cpu) | |
OP_IMPL(hlt) | |
{ | |
cpu->halted = true; | |
fprintf(stderr, "CPU: HLT\n"); | |
} | |
/** | |
* (1) The BRK instruction forces the generation of an interrupt request. | |
* (2) The program counter and processor status are pushed on the stack | |
* (3) then the IRQ interrupt vector at $FFFE/F is loaded into the PC | |
* (4) and the break flag in the status set to one. | |
*/ | |
OP_IMPL(brk) | |
{ | |
cpu->soft_interrupt = 1; | |
cpu->pc++; | |
cpu_push_stack(cpu, cpu->pc >> 8); | |
cpu_push_stack(cpu, cpu->pc & 0xff); | |
cpu_push_stack(cpu, cpu->flags.flags); | |
cpu->flags.b = 1; | |
cpu->pc = cpu_read_word(cpu, IRQ_VEC); | |
} | |
OP_IMPL(nop) { (void)cpu; } | |
#define DO_BRANCH(cond) \ | |
if((cond)) {\ | |
const uint16_t from = cpu->pc + cpu->opcode->size; \ | |
const uint16_t to = cpu->addr + cpu->opcode->size; \ | |
if((from & 0xff00) != (to & 0xff00)) { \ | |
cpu->extra_cycle = 2; \ | |
} else { \ | |
cpu->extra_cycle = 1; \ | |
} \ | |
cpu->pc = (cpu->addr);\ | |
} | |
OP_IMPL(beq) { DO_BRANCH(cpu->flags.z); } | |
OP_IMPL(bne) { DO_BRANCH(!cpu->flags.z); } | |
OP_IMPL(bmi) { DO_BRANCH(cpu->flags.n); } | |
OP_IMPL(bpl) { DO_BRANCH(!cpu->flags.n); } | |
OP_IMPL(bcs) { DO_BRANCH(cpu->flags.c); } | |
OP_IMPL(bcc) { DO_BRANCH(!cpu->flags.c); } | |
OP_IMPL(bvs) { DO_BRANCH(cpu->flags.v); } | |
OP_IMPL(bvc) { DO_BRANCH(!cpu->flags.v); } | |
OP_IMPL(rti) | |
{ | |
cpu->soft_interrupt = 0; | |
cpu->flags.flags = cpu_pop_stack(cpu) | 0x20; | |
cpu->pc = ((cpu_pop_stack(cpu) | (cpu_pop_stack(cpu)<<8)) - 1); | |
} | |
OP_IMPL(rts) | |
{ | |
cpu->pc = (cpu_pop_stack(cpu) | (cpu_pop_stack(cpu)<<8)); | |
} | |
OP_IMPL(jsr) | |
{ | |
cpu_push_stack(cpu, (cpu->pc + 2) >> 8); | |
cpu_push_stack(cpu, (cpu->pc + 2) & 0xff); | |
cpu->pc = (cpu->addr - 3); | |
} | |
OP_IMPL(jmp) | |
{ | |
cpu->pc = (cpu->addr - cpu->opcode->size); | |
} | |
OP_IMPL(sbc) | |
{ | |
const int16_t t = cpu->regs.a - cpu->arg - !cpu->flags.c; | |
const bool ovflw = ((cpu->regs.a >> 7) != (cpu->arg >> 7)) // Overflow can only occur if MSBs are different | |
&& ((cpu->regs.a >> 7) != ((t & 0xFF) >> 7)); | |
cpu->regs.a = t & 0xff; | |
cpu->flags.c = (t >= 0); | |
cpu->flags.v = ovflw; | |
SET_NZ(cpu->regs.a); | |
} | |
OP_IMPL(adc) | |
{ | |
const int16_t t = cpu->regs.a + cpu->arg + cpu->flags.c; | |
const bool ovflw = ((cpu->regs.a >> 7) == (cpu->arg >> 7)) // Overflow can only occur if MSBs are equal | |
&& ((cpu->regs.a >> 7) != ((t & 0xFF) >> 7)); | |
cpu->regs.a = t & 0xff; | |
cpu->flags.c = (t >> 8) & 0x01; | |
cpu->flags.v = ovflw; | |
SET_NZ(cpu->regs.a); | |
} | |
// The mask pattern in A is ANDed with the value in memory to set or clear the zero flag, but the result is not kept. Bits 7 and 6 of the value from memory are copied into the N and V flags. | |
OP_IMPL(bit) | |
{ | |
cpu->flags.n = (cpu->arg & (1<<7)) >> 7; | |
cpu->flags.v = (cpu->arg & (1<<6)) >> 6; | |
cpu->flags.z = (cpu->arg & cpu->regs.a) == 0; | |
} | |
OP_IMPL(and) | |
{ | |
cpu->regs.regs[cpu->opcode->reg] &= cpu->arg; | |
SET_NZ(cpu->regs.regs[cpu->opcode->reg]); | |
} | |
OP_IMPL(eor) | |
{ | |
cpu->regs.regs[cpu->opcode->reg] ^= cpu->arg; | |
SET_NZ(cpu->regs.regs[cpu->opcode->reg]); | |
} | |
OP_IMPL(ora) | |
{ | |
cpu->regs.regs[cpu->opcode->reg] |= cpu->arg; | |
SET_NZ(cpu->regs.regs[cpu->opcode->reg]); | |
} | |
OP_IMPL(inc) | |
{ | |
const uint8_t v = (cpu->arg + 1) & 0xff; | |
if(cpu->opcode->amode == AM_Accumulator) { | |
assert(cpu->opcode->reg == R_A); | |
cpu->regs.regs[cpu->opcode->reg] = v; | |
} else { | |
cpu_write_byte(cpu, cpu->addr, v); | |
} | |
SET_NZ(v); | |
} | |
OP_IMPL(dec) | |
{ | |
const uint8_t v = (cpu->arg - 1) & 0xff; | |
if(cpu->opcode->amode == AM_Accumulator) { | |
assert(cpu->opcode->reg == R_A); | |
cpu->regs.regs[cpu->opcode->reg] = v; | |
} else { | |
cpu_write_byte(cpu, cpu->addr, v); | |
} | |
SET_NZ(v); | |
} | |
OP_IMPL(inx) | |
{ | |
assert(cpu->opcode->reg == R_X); | |
cpu->regs.regs[cpu->opcode->reg]++; | |
SET_NZ(cpu->regs.regs[cpu->opcode->reg]); | |
} | |
OP_IMPL(dex) | |
{ | |
assert(cpu->opcode->reg == R_X); | |
cpu->regs.regs[cpu->opcode->reg]--; | |
SET_NZ(cpu->regs.regs[cpu->opcode->reg]); | |
} | |
OP_IMPL(iny) | |
{ | |
assert(cpu->opcode->reg == R_Y); | |
cpu->regs.regs[cpu->opcode->reg]++; | |
SET_NZ(cpu->regs.regs[cpu->opcode->reg]); | |
} | |
OP_IMPL(dey) | |
{ | |
assert(cpu->opcode->reg == R_Y); | |
cpu->regs.regs[cpu->opcode->reg]--; | |
SET_NZ(cpu->regs.regs[cpu->opcode->reg]); | |
} | |
OP_IMPL(sec) { cpu->flags.c = 1; } | |
OP_IMPL(clc) { cpu->flags.c = 0; } | |
OP_IMPL(sed) { cpu->flags.d = 1; } | |
OP_IMPL(cld) { cpu->flags.d = 0; } | |
OP_IMPL(sei) { cpu->flags.i = 1; } | |
OP_IMPL(cli) { cpu->flags.i = 0; } | |
OP_IMPL(clv) { cpu->flags.v = 0; } | |
OP_IMPL(lax) | |
{ | |
cpu->regs.x = cpu->regs.a = cpu->arg; | |
SET_NZ(cpu->regs.x); | |
} | |
// implements ld(a|x|y) | |
OP_IMPL(ld) | |
{ | |
cpu->regs.regs[cpu->opcode->reg] = cpu->arg; | |
SET_NZ(cpu->regs.regs[cpu->opcode->reg]); | |
} | |
#define op_impl_lda op_impl_ld | |
#define op_impl_ldx op_impl_ld | |
#define op_impl_ldy op_impl_ld | |
// implements st(a|x|y) | |
OP_IMPL(st) | |
{ | |
cpu_write_byte(cpu, cpu->addr, cpu->regs.regs[cpu->opcode->reg]); | |
} | |
#define op_impl_sta op_impl_st | |
#define op_impl_stx op_impl_st | |
#define op_impl_sty op_impl_st | |
static void _impl_trr(cpu_t *cpu, const op_reg_t from) | |
{ | |
cpu->regs.regs[cpu->opcode->reg] = cpu->regs.regs[from]; | |
SET_NZ(cpu->regs.regs[cpu->opcode->reg]); | |
} | |
OP_IMPL(tax) { _impl_trr(cpu, R_A); } | |
OP_IMPL(txa) { _impl_trr(cpu, R_X); } | |
OP_IMPL(tay) { _impl_trr(cpu, R_A); } | |
OP_IMPL(tya) { _impl_trr(cpu, R_Y); } | |
OP_IMPL(txs) { cpu->regs.sp = cpu->regs.x; } | |
OP_IMPL(tsx) { _impl_trr(cpu, R_SP); } | |
OP_IMPL(php) | |
{ | |
cpu_push_stack(cpu, cpu->flags.flags | 0x30); | |
} | |
OP_IMPL(pha) | |
{ | |
cpu_push_stack(cpu, cpu->regs.a); | |
} | |
OP_IMPL(pla) | |
{ | |
cpu->regs.a = cpu_pop_stack(cpu); | |
SET_NZ(cpu->regs.a); | |
} | |
OP_IMPL(plp) | |
{ | |
cpu->flags.flags = (cpu_pop_stack(cpu) & ~ 0x10) | // B flag may exist on stack but not P so it is cleared | |
0x20; // bit 5 always set | |
} | |
#define do_lsr(x) \ | |
cpu->flags.c = ((x&0x01) == 0x01); \ | |
x >>= 1; \ | |
SET_NZ(x); | |
OP_IMPL(lsr) { | |
if(cpu->opcode->amode == AM_Accumulator) { | |
do_lsr(cpu->regs.a); | |
} else { | |
do_lsr(cpu->arg); | |
cpu_write_byte(cpu, cpu->addr, cpu->arg); | |
} | |
} | |
// LSRs the contents of a memory location and then EORs the result with the accumulator | |
OP_IMPL(sre) | |
{ | |
do_lsr(cpu->arg); | |
cpu_write_byte(cpu, cpu->addr, cpu->arg); | |
cpu->regs.a ^= cpu->arg; | |
SET_NZ(cpu->regs.a); | |
} | |
#undef do_lsr | |
#define do_asl(x) \ | |
cpu->flags.c = ((x&0x80) == 0x80); \ | |
x <<= 1; \ | |
SET_NZ(x); | |
OP_IMPL(asl) | |
{ | |
if(cpu->opcode->amode == AM_Accumulator) { | |
do_asl(cpu->regs.a); | |
} else { | |
do_asl(cpu->arg); | |
cpu_write_byte(cpu, cpu->addr, cpu->arg); | |
} | |
} | |
OP_IMPL(aso) | |
{ | |
do_asl(cpu->arg); | |
cpu_write_byte(cpu, cpu->addr, cpu->arg); | |
cpu->regs.a |= cpu->arg; | |
SET_NZ(cpu->regs.a); | |
} | |
#undef do_asl | |
#define do_rol(x) \ | |
const uint8_t old_carry = cpu->flags.c; \ | |
cpu->flags.c = ((x&0x80)>>7); \ | |
x = ((x << 1) & 0xff) | old_carry; \ | |
SET_NZ(x); | |
OP_IMPL(rol) | |
{ | |
if(cpu->opcode->amode == AM_Accumulator) { | |
do_rol(cpu->regs.a); | |
} else { | |
do_rol(cpu->arg); | |
cpu_write_byte(cpu, cpu->addr, cpu->arg); | |
} | |
} | |
// ROLs the contents of a memory location and then ANDs the result with the accumulator | |
OP_IMPL(rla) | |
{ | |
do_rol(cpu->arg); | |
cpu_write_byte(cpu, cpu->addr, cpu->arg); | |
cpu->regs.a &= cpu->arg; | |
SET_NZ(cpu->regs.a); | |
} | |
#undef do_rol | |
#define do_ror(x) \ | |
const uint8_t old_carry = cpu->flags.c; \ | |
cpu->flags.c = ((x&1) == 1); \ | |
x = ((x >> 1) & 0xff) | (old_carry<<7); \ | |
SET_NZ(x); | |
OP_IMPL(ror) | |
{ | |
if(cpu->opcode->amode == AM_Accumulator) { | |
do_ror(cpu->regs.a); | |
} else { | |
do_ror(cpu->arg); | |
cpu_write_byte(cpu, cpu->addr, cpu->arg); | |
} | |
} | |
OP_IMPL(rra) | |
{ | |
do_ror(cpu->arg); | |
cpu_write_byte(cpu, cpu->addr, cpu->arg); | |
op_impl_adc(cpu); | |
} | |
#undef do_ror | |
static void _impl_cmp(cpu_t *cpu, const register_t reg) | |
{ | |
const int result = cpu->regs.regs[reg] - cpu->arg; | |
SET_NZ(result); | |
cpu->flags.c = (cpu->arg <= cpu->regs.regs[reg]) ? 1 : 0; | |
} | |
OP_IMPL(cmp) | |
{ | |
assert(cpu->opcode->reg == R_A); | |
_impl_cmp(cpu, cpu->opcode->reg); | |
} | |
OP_IMPL(cpx) | |
{ | |
assert(cpu->opcode->reg == R_X); | |
_impl_cmp(cpu, cpu->opcode->reg); | |
} | |
OP_IMPL(cpy) | |
{ | |
assert(cpu->opcode->reg == R_Y); | |
_impl_cmp(cpu, cpu->opcode->reg); | |
} | |
OP_IMPL(skw) { (void)cpu; } // SKW=SkipWord=NOP | |
// ANDs the contents of the A and X registers (without changing the contents of either register) | |
// and stores the result in memory. Does not affect any flags. | |
OP_IMPL(sax) | |
{ | |
const uint8_t v = cpu->regs.a & cpu->regs.x; // AND | |
cpu_write_byte(cpu, cpu->addr, v); // STX | |
} | |
// DECs the contents of a memory location and then CMPs the result with the A register | |
OP_IMPL(dcp) | |
{ | |
cpu->arg--; | |
cpu_write_byte(cpu, cpu->addr, cpu->arg); | |
_impl_cmp(cpu, R_A); | |
} | |
// DECs the contents of a memory location and then SBCs the result from the A register | |
OP_IMPL(isb) { | |
cpu->arg++; | |
cpu_write_byte(cpu, cpu->addr, cpu->arg); | |
op_impl_sbc(cpu); | |
} | |
#undef OP_IMPL | |
#undef SET_NZ | |
#define op(name, am, reg, sz, cy, ex) {#name, AM_##am,reg,sz,cy,ex,op_impl_##name} | |
#define op_nop op(nop, Implied, R_N, 1, 2, false) | |
#define op_hlt op(hlt, Implied, R_N, 1, 0, false) | |
#define op_nimp {0} | |
static const opcode_t opcodes[256] = { | |
/* $00 */ | |
op(brk, Implied, R_N, 1, 7, false), | |
op(ora, IndirectX, R_A, 2, 6, false), | |
op(hlt, Implied, R_N, 1, 0, false), | |
op(aso, IndirectX, R_A, 2, 8, false), | |
op(nop, Relative, R_N, 2, 3, false), // INVALID OP $0C (NOP $XX) | |
op(ora, ZeroPage, R_A, 2, 3, false), | |
op(asl, ZeroPage, R_N, 2, 5, false), | |
op(aso, ZeroPage, R_A, 2, 5, false), | |
op(php, Implied, R_N, 1, 3, false), | |
op(ora, Immediate, R_A, 2, 2, false), | |
op(asl, Accumulator, R_A, 1, 2, false), | |
op_nimp, // ANC, not used by 2A03 | |
op(skw, Absolute, R_N, 3, 4, true), | |
op(ora, Absolute, R_A, 3, 4, false), | |
op(asl, Absolute, R_N, 3, 6, false), | |
op(aso, Absolute, R_A, 3, 6, false), | |
/* $10 */ | |
op(bpl, Relative, R_N, 2, 2, false), | |
op(ora, IndirectY, R_A, 2, 5, true), | |
op_hlt, | |
op(aso, IndirectY, R_A, 2, 8, false), | |
op(nop, ZeroPageX, R_N, 2, 4, false), // NOP $XX,x | |
op(ora, ZeroPageX, R_A, 2, 4, false), | |
op(asl, ZeroPageX, R_N, 2, 6, false), | |
op(aso, ZeroPageX, R_A, 2, 6, false), | |
op(clc, Implied, R_N, 1, 2, false), | |
op(ora, AbsoluteY, R_A, 3, 4, true), | |
op_nop, | |
op(aso, AbsoluteY, R_A, 3, 7, false), | |
op(skw, AbsoluteX, R_N, 3, 4, true), | |
op(ora, AbsoluteX, R_A, 3, 4, true), | |
op(asl, AbsoluteX, R_N, 3, 7, true), | |
op(aso, AbsoluteX, R_A, 3, 7, false), | |
/* $20 */ | |
op(jsr, Absolute, R_N, 3, 6, false), | |
op(and, IndirectX, R_A, 2, 6, false), | |
op_hlt, | |
op(rla, IndirectX, R_A, 2, 8, false), | |
op(bit, ZeroPage, R_A, 2, 3, false), | |
op(and, ZeroPage, R_A, 2, 3, false), | |
op(rol, ZeroPage, R_A, 2, 5, false), | |
op(rla, ZeroPage, R_A, 2, 5, false), | |
op(plp, Implied, R_N, 1, 4, false), | |
op(and, Immediate, R_A, 2, 2, false), | |
op(rol, Accumulator, R_A, 1, 2, false), | |
op_nimp, // ANC | |
op(bit, Absolute, R_N, 3, 4, false), | |
op(and, Absolute, R_A, 3, 4, false), | |
op(rol, Absolute, R_A, 3, 6, false), | |
op(rla, Absolute, R_A, 3, 6, false), | |
/* $30 */ | |
op(bmi, Relative, R_N, 2, 2, false), | |
op(and, IndirectY, R_A, 2, 5, true), | |
op_hlt, | |
op(rla, IndirectY, R_A, 2, 8, false), | |
op(nop, ZeroPageX, R_N, 2, 4, false), // NOP $XX,X | |
op(and, ZeroPageX, R_A, 2, 4, false), | |
op(rol, ZeroPageX, R_A, 2, 6, false), | |
op(rla, ZeroPageX, R_A, 2, 6, false), | |
op(sec, Implied, R_N, 1, 2, false), | |
op(and, AbsoluteY, R_A, 3, 4, true), | |
op_nop, | |
op(rla, AbsoluteY, R_A, 3, 7, false), | |
op(skw, AbsoluteX, R_N, 3, 4, true), | |
op(and, AbsoluteX, R_A, 3, 4, true), | |
op(rol, AbsoluteX, R_A, 3, 7, false), | |
op(rla, AbsoluteX, R_A, 3, 7, false), | |
/* $40 */ | |
op(rti, Implied, R_N, 1, 6, false), | |
op(eor, IndirectX, R_A, 2, 6, false), | |
op_hlt, | |
op(sre, IndirectX, R_A, 2, 8, false), | |
op(nop, Relative, R_N, 2, 3, false), | |
op(eor, ZeroPage, R_A, 2, 3, false), | |
op(lsr, ZeroPage, R_N, 2, 5, false), | |
op(sre, ZeroPage, R_A, 2, 5, false), | |
op(pha, Implied, R_A, 1, 3, false), | |
op(eor, Immediate, R_A, 2, 2, false), | |
op(lsr, Accumulator, R_A, 1, 2, false), | |
op_nimp, // ALR | |
op(jmp, Absolute, R_N, 3, 3, false), | |
op(eor, Absolute, R_A, 3, 4, false), | |
op(lsr, Absolute, R_N, 3, 6, false), | |
op(sre, Absolute, R_A, 3, 6, false), | |
/* $50 */ | |
op(bvc, Relative, R_N, 2, 2, false), | |
op(eor, IndirectY, R_A, 2, 5, true), | |
op_hlt, | |
op(sre, IndirectY, R_A, 2, 8, false), | |
op(nop, ZeroPageX, R_N, 2, 4, false), // NOP $XX,X | |
op(eor, ZeroPageX, R_A, 2, 4, false), | |
op(lsr, ZeroPageX, R_A, 2, 6, false), | |
op(sre, ZeroPageX, R_A, 2, 6, false), | |
op(cli, Implied, R_N, 1, 2, false), | |
op(eor, AbsoluteY, R_A, 3, 4, true), | |
op_nop, | |
op(sre, AbsoluteY, R_A, 3, 7, false), | |
op(skw, AbsoluteX, R_N, 3, 4, true), | |
op(eor, AbsoluteX, R_A, 3, 4, true), | |
op(lsr, AbsoluteX, R_A, 3, 7, false), | |
op(sre, AbsoluteX, R_A, 3, 7, false), | |
/* $60 */ | |
op(rts, Implied, R_N, 1, 6, false), | |
op(adc, IndirectX, R_A, 2, 6,false), | |
op_hlt, | |
op(rra, IndirectX, R_A, 2, 8, false), | |
op(nop, Relative, R_N, 2, 3, false), | |
op(adc, ZeroPage, R_A, 2, 3, false), | |
op(ror, ZeroPage, R_N, 2, 5, false), | |
op(rra, ZeroPage, R_A, 2, 5, false), | |
op(pla, Implied, R_A, 1, 4, false), | |
op(adc, Immediate, R_A, 2, 2, false), | |
op(ror, Accumulator, R_A, 1, 2, false), | |
op_nimp, // ARR | |
op(jmp, Indirect, R_N, 3, 5, false), | |
op(adc, Absolute, R_A, 3, 4, false), | |
op(ror, Absolute, R_N, 3, 6, false), | |
op(rra, Absolute, R_A, 3, 6, false), | |
/* $70 */ | |
op(bvs, Relative, R_N, 2, 2, false), | |
op(adc, IndirectY, R_A, 2, 5, true), | |
op_hlt, | |
op(rra, IndirectY, R_A, 2, 8, false), | |
op(nop, ZeroPageX, R_N, 2, 4, false), // NOP $XX,X | |
op(adc, ZeroPageX, R_N, 2, 4, false), | |
op(ror, ZeroPageX, R_N, 2, 6, false), | |
op(rra, ZeroPageX, R_A, 2, 6, false), | |
op(sei, Implied, R_N, 1, 2, false), | |
op(adc, AbsoluteY, R_A, 3, 4, true), | |
op_nop, | |
op(rra, AbsoluteY, R_A, 3, 7, false), | |
op(skw, AbsoluteX, R_N, 3, 4, true), | |
op(adc, AbsoluteX, R_A, 3, 4, true), | |
op(ror, AbsoluteX, R_N, 3, 7, false), | |
op(rra, AbsoluteX, R_A, 3, 7, false), | |
/* $80 */ | |
op(nop, Immediate, R_N, 2, 2, false), // NOP #$XX | |
op(sta, IndirectX, R_A, 2, 6, false), | |
op_nimp, // "may be HLT" | |
op(sax, IndirectX, R_X, 2, 6, false), | |
op(sty, ZeroPage, R_Y, 2, 3, false), | |
op(sta, ZeroPage, R_A, 2, 3, false), | |
op(stx, ZeroPage, R_X, 2, 3, false), | |
op(sax, ZeroPage, R_X, 2, 3, false), | |
op(dey, Implied, R_Y, 1, 2, false), | |
op_nimp, | |
op(txa, Implied, R_A, 1, 2, false), | |
op_nimp, | |
op(sty, Absolute, R_Y, 3, 4, false), | |
op(sta, Absolute, R_A, 3, 4, false), | |
op(stx, Absolute, R_X, 3, 4, false), | |
op(sax, Absolute, R_X, 3, 4, false), | |
/* $90 */ | |
op(bcc, Relative, R_N, 2, 2, false), | |
op(sta, IndirectY, R_A, 2, 6, false), | |
op_hlt, | |
op_nimp, | |
op(sty, ZeroPageX, R_Y, 2, 4, false), | |
op(sta, ZeroPageX, R_A, 2, 4, false), | |
op(stx, ZeroPageY, R_X, 2, 4, false), | |
op(sax, ZeroPageY, R_X, 2, 4, false), | |
op(tya, Implied, R_A, 1, 2, false), | |
op(sta, AbsoluteY, R_A, 3, 5, false), | |
op(txs, Implied, R_SP, 1, 2, false), | |
op_nimp, | |
op_nimp, | |
op(sta, AbsoluteX, R_A, 3, 5, false), | |
op_nimp, | |
op_nimp, | |
/* $A0 */ | |
op(ldy, Immediate, R_Y, 2, 2, false), | |
op(lda, IndirectX, R_A, 2, 6, false), | |
op(ldx, Immediate, R_X, 2, 2, false), | |
op(lax, IndirectX, R_X, 2, 6, false), | |
op(ldy, ZeroPage, R_Y, 2, 3, false), | |
op(lda, ZeroPage, R_A, 2, 3, false), | |
op(ldx, ZeroPage, R_X, 2, 3, false), | |
op(lax, ZeroPage, R_X, 2, 3, false), | |
op(tay, Implied, R_Y, 1, 2, false), | |
op(lda, Immediate, R_A, 2, 2, false), | |
op(tax, Implied, R_X, 1, 2, false), | |
op_nimp, | |
op(ldy, Absolute, R_Y, 3, 4, false), | |
op(lda, Absolute, R_A, 3, 4, false), | |
op(ldx, Absolute, R_X, 3, 4, false), | |
op(lax, Absolute, R_X, 3, 4, false), | |
/* $B0 */ | |
op(bcs, Relative, R_N, 2, 2, false), | |
op(lda, IndirectY, R_A, 2, 5, true), | |
op_hlt, | |
op(lax, IndirectY, R_X, 2, 5, true), | |
op(ldy, ZeroPageX, R_Y, 2, 4, false), | |
op(lda, ZeroPageX, R_A, 2, 4, false), | |
op(ldx, ZeroPageY, R_X, 2, 4, false), | |
op(lax, ZeroPageY, R_X, 2, 4, false), | |
op(clv, Implied, R_N, 1, 2, false), | |
op(lda, AbsoluteY, R_A, 3, 4, true), | |
op(tsx, Implied, R_X, 1, 2, false), | |
op_nimp, | |
op(ldy, AbsoluteX, R_Y, 3, 4, true), | |
op(lda, AbsoluteX, R_A, 3, 4, true), | |
op(ldx, AbsoluteY, R_X, 3, 4, true), | |
op(lax, AbsoluteY, R_X, 3, 4, true), | |
/* $C0 */ | |
op(cpy, Immediate, R_Y, 2, 2, false), | |
op(cmp, IndirectX, R_A, 2, 6, false), | |
op_hlt, // "may be HLT" | |
op(dcp, IndirectX, R_A, 2, 8, false), | |
op(cpy, ZeroPage, R_Y, 2, 3, false), | |
op(cmp, ZeroPage, R_A, 2, 3, false), | |
op(dec, ZeroPage, R_A, 2, 5, false), | |
op(dcp, ZeroPage, R_A, 2, 5, false), | |
op(iny, Implied, R_Y, 1, 2, false), | |
op(cmp, Immediate, R_A, 2, 2, false), | |
op(dex, Implied, R_X, 1, 2, false), | |
op_nimp, | |
op(cpy, Absolute, R_Y, 3, 4, false), | |
op(cmp, Absolute, R_A, 3, 4, false), | |
op(dec, Absolute, R_A, 3, 6, false), | |
op(dcp, Absolute, R_A, 3, 6, false), | |
/* $D0 */ | |
op(bne, Relative, R_N, 2, 2, false), | |
op(cmp, IndirectY, R_A, 2, 5, true), | |
op_hlt, | |
op(dcp, IndirectY, R_A, 2, 8, false), | |
op(nop, ZeroPageX, R_N, 2, 4, false), // NOP $XX,X | |
op(cmp, ZeroPageX, R_A, 2, 4, false), | |
op(dec, ZeroPageX, R_A, 2, 6, false), | |
op(dcp, ZeroPageX, R_A, 2, 6, false), | |
op(cld, Implied, R_N, 1, 2, false), | |
op(cmp, AbsoluteY, R_A, 3, 4, true), | |
op_nop, | |
op(dcp, AbsoluteY, R_A, 3, 7, false), | |
op(skw, AbsoluteX, R_N, 3, 4, true), | |
op(cmp, AbsoluteX, R_A, 3, 4, true), | |
op(dec, AbsoluteX, R_A, 3, 7, false), | |
op(dcp, AbsoluteX, R_A, 3, 7, false), | |
/* $E0 */ | |
op(cpx, Immediate, R_X, 2, 2, false), | |
op(sbc, IndirectX, R_A, 2, 6, false), | |
op_nimp, | |
op(isb, IndirectX, R_A, 2, 8, false), | |
op(cpx, ZeroPage, R_X, 2, 3, false), | |
op(sbc, ZeroPage, R_A, 2, 3, false), | |
op(inc, ZeroPage, R_A, 2, 5, false), | |
op(isb, ZeroPage, R_A, 2, 5, false), | |
op(inx, Implied, R_X, 1, 2, false), | |
op(sbc, Immediate, R_A, 2, 2, false), | |
op_nop, | |
op(sbc, Immediate, R_A, 2, 2, false), | |
op(cpx, Absolute, R_X, 3, 4, false), | |
op(sbc, Absolute, R_A, 3, 4, false), | |
op(inc, Absolute, R_A, 3, 6, false), | |
op(isb, Absolute, R_A, 3, 6, false), | |
/* $F0 */ | |
op(beq, Relative, R_N, 2, 2, false), | |
op(sbc, IndirectY, R_A, 2, 5, true), | |
op_hlt, | |
op(isb, IndirectY, R_A, 2, 8, false), | |
op(nop, ZeroPageX, R_N, 2, 4, false), // NOP $XX,X | |
op(sbc, ZeroPageX, R_A, 2, 4, false), | |
op(inc, ZeroPageX, R_A, 2, 6, false), | |
op(isb, ZeroPageX, R_A, 2, 6, false), | |
op(sed, Implied, R_N, 1, 2, false), | |
op(sbc, AbsoluteY, R_Y, 3, 4, true), | |
op_nop, | |
op(isb, AbsoluteY, R_A, 3, 7, false), | |
op(skw, AbsoluteX, R_N, 3, 4, true), | |
op(sbc, AbsoluteX, R_A, 3, 4, true), | |
op(inc, AbsoluteX, R_A, 3, 7, false), | |
op(isb, AbsoluteX, R_A, 3, 7, false) | |
}; | |
static void check_extra(cpu_t *cpu, const uint16_t low, const uint16_t offs) | |
{ | |
const uint16_t val = (low & 0xff) + offs; | |
if(cpu->opcode->extra_cycle && (val > 0xff)) { | |
cpu->extra_cycle = 1; | |
} | |
} | |
static void _cpu_decode_am_implied(cpu_t *cpu) { (void)cpu; } | |
static void _cpu_decode_am_immediate(cpu_t *cpu) | |
{ | |
assert(!cpu->opcode->extra_cycle); | |
assert(cpu->opcode->size == 2); | |
cpu->arg = cpu_read_byte(cpu, cpu->pc + 1); | |
} | |
static void _cpu_decode_am_zeropage(cpu_t *cpu) | |
{ | |
assert(!cpu->opcode->extra_cycle); | |
assert(cpu->opcode->size == 2); | |
cpu->addr = cpu_read_byte(cpu, cpu->pc + 1); | |
cpu->arg = cpu_read_byte(cpu, cpu->addr ); | |
} | |
static void _cpu_decode_am_zeropagex(cpu_t *cpu) | |
{ | |
assert(!cpu->opcode->extra_cycle); | |
assert(cpu->opcode->size == 2); | |
cpu->addr = ((uint8_t)(cpu_read_byte(cpu, cpu->pc + 1) + cpu->regs.x)); | |
cpu->arg = cpu_read_byte(cpu, cpu->addr & 0xff ); | |
} | |
static void _cpu_decode_am_zeropagey(cpu_t *cpu) | |
{ | |
assert(!cpu->opcode->extra_cycle); | |
assert(cpu->opcode->size == 2); | |
cpu->addr = cpu_read_byte(cpu, cpu->pc + 1); | |
cpu->addr = (cpu->addr + (uint16_t)cpu->regs.y) & 0xff; | |
cpu->arg = cpu_read_byte(cpu, cpu->addr); | |
} | |
static void _cpu_decode_am_absolute(cpu_t *cpu) | |
{ | |
assert(cpu->opcode->size == 3); | |
cpu->addr = cpu_read_word(cpu, cpu->pc + 1); | |
cpu->arg = cpu_read_byte(cpu, cpu->addr ); | |
} | |
static void _cpu_decode_am_absolutex(cpu_t *cpu) | |
{ | |
assert(cpu->opcode->size == 3); | |
const uint16_t tmp = cpu_read_word(cpu, cpu->pc + 1); | |
check_extra(cpu, tmp, cpu->regs.x); | |
cpu->addr = (tmp + (uint16_t)cpu->regs.x); | |
cpu->arg = cpu_read_byte(cpu, cpu->addr ); | |
} | |
static void _cpu_decode_am_absolutey(cpu_t *cpu) | |
{ | |
assert(cpu->opcode->size == 3); | |
const uint16_t tmp = cpu_read_word(cpu, cpu->pc + 1); | |
check_extra(cpu, tmp, cpu->regs.y); | |
cpu->addr = (tmp + cpu->regs.y); | |
cpu->arg = cpu_read_byte(cpu, cpu->addr ); | |
} | |
static void _cpu_decode_am_indirect(cpu_t *cpu) | |
{ | |
assert(cpu->opcode->size == 3); | |
assert(!cpu->opcode->extra_cycle); | |
cpu->addr = cpu_read_word(cpu, cpu->pc + 1); | |
const uint8_t addr_lo = cpu_read_byte(cpu, cpu->addr); | |
if((cpu->addr & 0x00ff) == 0xff) { | |
cpu->addr &= 0xff00; | |
} else { | |
cpu->addr++; | |
} | |
const uint8_t addr_hi = cpu_read_byte(cpu, cpu->addr); | |
cpu->addr = (addr_hi << 8) | (addr_lo); | |
cpu->arg = 0; | |
} | |
static void _cpu_decode_am_indirectx(cpu_t *cpu) | |
{ | |
assert(cpu->opcode->size == 2); | |
assert(!cpu->opcode->extra_cycle); | |
const uint16_t tmp = cpu_read_byte(cpu, cpu->pc + 1); | |
cpu->addr = tmp + (int16_t)cpu->regs.x; | |
cpu->addr = (cpu_read_byte(cpu, cpu->addr & 0xff)) | | |
(cpu_read_byte(cpu, (cpu->addr + 1) & 0xff ) << 8); | |
cpu->arg = cpu_read_byte(cpu, cpu->addr); | |
} | |
static void _cpu_decode_am_indirecty(cpu_t *cpu) | |
{ | |
assert(cpu->opcode->size == 2); | |
const uint16_t tmp = cpu_read_byte(cpu, cpu->pc + 1); | |
const uint8_t addr_lo = cpu_read_byte(cpu, tmp); | |
const uint8_t addr_hi = cpu_read_byte(cpu, (tmp + 1) & 0xff); | |
cpu->addr = (addr_hi << 8 ) | addr_lo; | |
check_extra(cpu, cpu->addr, cpu->regs.y); | |
cpu->addr = ((((addr_hi << 8 ) | addr_lo) + (int16_t)cpu->regs.y) & 0xffff); | |
cpu->arg = cpu_read_byte(cpu, cpu->addr); | |
} | |
static void _cpu_decode_am_relative(cpu_t *cpu) | |
{ | |
assert(cpu->opcode->size == 2); | |
assert(!cpu->opcode->extra_cycle); | |
cpu->addr = cpu->pc + (int8_t)cpu_read_byte(cpu, cpu->pc + 1); | |
} | |
static void _cpu_decode_am_accumulator(cpu_t *cpu) { (void)cpu; } | |
typedef void (*cpu_amode_callback)(cpu_t *cpu); | |
static const cpu_amode_callback cpu_address_mode_callbacks[] = { | |
_cpu_decode_am_implied, _cpu_decode_am_immediate, _cpu_decode_am_zeropage, | |
_cpu_decode_am_zeropagex, _cpu_decode_am_zeropagey, _cpu_decode_am_absolute, | |
_cpu_decode_am_absolutex, _cpu_decode_am_absolutey, _cpu_decode_am_indirect, | |
_cpu_decode_am_indirectx, _cpu_decode_am_indirecty, _cpu_decode_am_relative, _cpu_decode_am_accumulator | |
}; | |
bool cpu_tick(cpu_t *cpu) | |
{ | |
cpu->ticks++; | |
if(cpu->ticks > cpu->clock) { | |
const uint8_t op = cpu_read_byte(cpu, cpu->pc); | |
const opcode_t *opcode = cpu->opcode = &opcodes[op]; | |
cpu->extra_cycle = 0; | |
cpu_address_mode_callbacks[opcode->amode](cpu); | |
cpu_dump_state(cpu, op); | |
assert(opcode->fn != NULL); | |
opcode->fn(cpu); | |
cpu->pc += opcode->size; | |
cpu->clock += opcode->cycles + cpu->extra_cycle; | |
} | |
return !cpu->halted; | |
} | |
void cpu_nmi(cpu_t *cpu) | |
{ | |
cpu->soft_interrupt = 1; | |
cpu->pc-=1; | |
cpu_push_stack(cpu, cpu->pc >> 8); | |
cpu_push_stack(cpu, cpu->pc & 0xff); | |
cpu->flags.b = 0; // push P on stack (with B flag *clear*), | |
cpu_push_stack(cpu, cpu->flags.flags); | |
cpu->pc = cpu_read_word(cpu, NMI_VEC); | |
fprintf(stderr, "CPU: NMI: pc=0x%04x\n", cpu->pc); | |
} | |
cpu_t *cpu_init(cpu_bus_t * restrict bus) | |
{ | |
cpu_t *cpu = malloc(sizeof(cpu_t)); | |
if(cpu) { | |
cpu->bus = bus; | |
cpu->regs.a = 0; | |
cpu->regs.x = 0; | |
cpu->regs.y = 0; | |
cpu->regs.sp = 0xfd; | |
cpu->pc = 0xc000; | |
cpu->clock = cpu->ticks = 7; | |
cpu_reset(cpu); | |
} | |
return cpu; | |
} | |
void cpu_dump_state(const cpu_t *cpu, const uint8_t op) | |
{ | |
fprintf(stderr, "%04x %02x %02x %02x %s ---- A:%02x X:%02x Y:%02x P:%02x SP:%02x CYC:%d\n", | |
cpu->pc, op, | |
(cpu->addr >> 8) & 0xff, | |
(cpu->addr & 0xff), | |
cpu->opcode->name, | |
cpu->regs.a, cpu->regs.x, cpu->regs.y, cpu->flags.flags, cpu->regs.sp, | |
cpu->clock | |
); | |
} | |
void cpu_reset(cpu_t *cpu) | |
{ | |
cpu->pc = cpu_read_word(cpu, RESET_VEC); | |
cpu->halted = false; | |
cpu->soft_interrupt = false; | |
cpu->flags.flags = 0x24; | |
fprintf(stderr, "CPU: RESET. PC=%04x\n", cpu->pc); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef __CPU_H__ | |
#define __CPU_H__ | |
#include <stdbool.h> | |
#include <sys/types.h> | |
#define STACK_BASE (0x100) | |
#define NMI_VEC 0xfffa | |
#define RESET_VEC 0xfffc | |
#define IRQ_VEC 0xfffe | |
typedef enum { | |
AM_Implied=0, AM_Immediate, AM_ZeroPage, AM_ZeroPageX, AM_ZeroPageY, AM_Absolute, AM_AbsoluteX, | |
AM_AbsoluteY, AM_Indirect, AM_IndirectX, AM_IndirectY, AM_Relative, AM_Accumulator | |
} addr_mode_t; | |
typedef enum { R_A, R_X, R_Y, R_SP, R_N } op_reg_t; | |
typedef union { | |
struct { uint8_t a,x,y,sp; }; | |
uint8_t regs[4]; | |
} regs_t; | |
typedef union { | |
struct { | |
uint8_t c:1; // carry (0) | |
uint8_t z:1; // zero | |
uint8_t i:1; // interrupt disable | |
uint8_t d:1; // decimal mode | |
uint8_t b:1; // break | |
uint8_t unused:1; // unused | |
uint8_t v:1; // overflow | |
uint8_t n:1; // negative (7) | |
}; | |
uint8_t flags:8; | |
} cpu_flags_t; | |
typedef uint8_t (*bus_read_byte)(const uint16_t addr); | |
typedef void (*bus_write_byte)(const uint16_t addr, const uint8_t val); | |
typedef struct _cpu_bus_t { | |
bus_read_byte read; | |
bus_write_byte write; | |
} cpu_bus_t; | |
typedef struct _cpu_t { | |
uint16_t pc; | |
regs_t regs; | |
cpu_flags_t flags; | |
uint32_t clock; | |
uint32_t ticks; | |
uint8_t extra_cycle; | |
bool halted; | |
bool soft_interrupt; | |
uint8_t arg; | |
cpu_bus_t *bus; | |
const struct _opcode_t *opcode; | |
uint16_t addr; | |
} cpu_t; | |
typedef void (*op_impl)(struct _cpu_t *); | |
typedef struct _opcode_t { | |
const char *name; | |
const addr_mode_t amode; | |
const op_reg_t reg; | |
const uint8_t size; | |
const uint8_t cycles; | |
const bool extra_cycle; | |
const op_impl fn; | |
} opcode_t; | |
void cpu_reset(cpu_t *); | |
cpu_t *cpu_init(cpu_bus_t *); | |
bool cpu_tick(cpu_t *); | |
void cpu_dump_state(const cpu_t *, const uint8_t); | |
void cpu_nmi(cpu_t *); | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment