#include #include #include #include #define MAX(a, b) ((a > b) ? a : b) #define STATUS_UPDATE_ZERO(r) \ (regs.status.zero = r == 0) #define STATUS_UPDATE_NEGATIVE(r) \ (regs.status.negative = ((r & (1 << 7)) > 0)) struct cpu_flags { uint8_t carry : 1; uint8_t zero : 1; uint8_t interrupt_disable : 1; uint8_t decimal_mode : 1; uint8_t brk : 1; uint8_t unused : 1; uint8_t overflow : 1; uint8_t negative : 1; }; struct registers { uint8_t a, x, y, sp; struct cpu_flags status; uint16_t pc; }; struct registers regs; enum addressing_mode { AM_ACC, AM_IMM, AM_ZP, AM_ZP_X, AM_ZP_Y, AM_REL, AM_ABS, AM_ABS_X, AM_ABS_Y, AM_IND, AM_IND_X, AM_IND_Y, }; /* 64K address space, 16bit words */ uint8_t memory[0x16000]; /* example program taken from https://bugzmanov.github.io/nes_ebook/chapter_3_1.html */ uint8_t program[] = { 0xa9, 0xc0, 0xaa, 0xe8, 0x00 }; uint8_t peek(uint16_t addr) { return memory[addr]; } uint16_t peek16(uint16_t addr) { /* bytes are stored in little-endian (low then high) */ return (uint16_t)memory[addr] | ((uint16_t)memory[addr + 1] << 8); } void adc(enum addressing_mode mode) { uint8_t arg, val; uint16_t sum; // 16-bit sum makes it easier to determine carry flag if (mode != AM_ABS && mode != AM_ABS_X && mode != AM_ABS_Y) arg = peek(regs.pc++); else arg = peek16(regs.pc++); switch (mode) { case AM_IMM: /* $69 */ val = arg; break; case AM_ZP: /* $65 */ val = peek(arg % 256); break; case AM_ABS: /* $6D */ val = peek16(arg); break; case AM_ZP_X: /* $75 */ val = peek((arg + regs.x) % 256); break; case AM_ABS_X: /* $7D */ val = peek16(arg + regs.x); break; case AM_ABS_Y: /* $79 */ val = peek16(arg + regs.y); break; case AM_IND_X: /* $61 */ val = peek(peek((arg + regs.x) % 256) + peek((arg + regs.x + 1) % 256) * 256); break; case AM_IND_Y: /* $71 */ val = peek(peek(arg) + peek((arg + 1) % 256) * 256 + regs.y); break; default: fprintf(stderr, "INVALID ADC ADDRESSING MODE\n"); abort(); return; } sum = regs.a + val + regs.status.carry; regs.a = sum & 0xFF; regs.status.carry = sum > 0xFF; /* overflow flag formula: https://stackoverflow.com/a/29224684 */ regs.status.overflow = ~(regs.a ^ arg) & (regs.a ^ sum) & 0x80; STATUS_UPDATE_ZERO(regs.a); STATUS_UPDATE_NEGATIVE(regs.a); } void brk(void) { /* $00 */ /* TODO: push regs.pc and regs.status to stack and load IRQ vector */ regs.status.brk = 1; return; } void tax(void) { /* $AA */ regs.x = regs.a; STATUS_UPDATE_ZERO(regs.x); STATUS_UPDATE_NEGATIVE(regs.x); } void inx(void) { /* $E8 */ regs.x++; STATUS_UPDATE_ZERO(regs.x); STATUS_UPDATE_NEGATIVE(regs.x); } void lda(enum addressing_mode mode) { uint8_t arg, val; if (mode != AM_ABS && mode != AM_ABS_X && mode != AM_ABS_Y) arg = peek(regs.pc++); else arg = peek16(regs.pc++); switch (mode) { case AM_IMM: /* $A9 */ val = arg; break; case AM_ZP: /* $A5 */ val = peek(arg % 256); break; case AM_ABS: /* $AD */ val = peek16(arg); break; case AM_ZP_X: /* $B5 */ val = peek((arg + regs.x) % 256); break; case AM_ABS_X: /* $BD */ val = peek16(arg + regs.x); break; case AM_ABS_Y: /* $B9 */ val = peek16(arg + regs.y); break; case AM_IND_X: /* $A1 */ val = peek(peek((arg + regs.x) % 256) + peek((arg + regs.x + 1) % 256) * 256); break; case AM_IND_Y: /* $B1 */ val = peek(peek(arg) + peek((arg + 1) % 256) * 256 + regs.y); break; default: fprintf(stderr, "INVALID LDA ADDRESSING MODE\n"); abort(); return; } printf("arg1 $%02X\n", arg); regs.a = val; STATUS_UPDATE_ZERO(regs.a); STATUS_UPDATE_NEGATIVE(regs.a); } void interpret(void) { uint8_t opcode; for (;;) { opcode = peek(regs.pc++); printf("opcode: $%02X\n", opcode); switch (opcode) { case 0x00: brk(); return; case 0x61: adc(AM_IND_X); break; case 0x65: adc(AM_ZP); break; case 0x69: adc(AM_IMM); break; case 0x6D: adc(AM_ABS); break; case 0x71: adc(AM_IND_Y); break; case 0x75: adc(AM_ZP_X); break; case 0x79: adc(AM_ABS_Y); break; case 0x7D: adc(AM_ABS_X); break; case 0xa1: lda(AM_IND_X); break; case 0xa5: lda(AM_ZP); break; case 0xa9: lda(AM_IMM); break; case 0xaa: tax(); break; case 0xad: lda(AM_ABS); break; case 0xb1: lda(AM_IND_Y); break; case 0xb5: lda(AM_ZP_X); break; case 0xb9: lda(AM_ABS_Y); break; case 0xbd: lda(AM_ABS_X); break; case 0xe8: inx(); break; default: printf("opcode $%02X not implemented\n", opcode); break; } printf("status: %i%i%i%i%i%i%i%i\n", regs.status.carry, regs.status.zero, regs.status.interrupt_disable, regs.status.decimal_mode, regs.status.brk, regs.status.unused, regs.status.overflow, regs.status.negative); printf("A: $%02X, X: $%02X, Y: $%02X, SP: $%02X, PC: $%02X\n", regs.a, regs.x, regs.y, regs.sp, regs.pc); } } /* https://www.nesdev.org/wiki/CPU_power_up_state */ void cpu_init(void) { regs.a = regs.x = regs.y = 0; regs.pc = 0xFFFC; regs.sp = 0xFD; memset(®s.status, 0, sizeof(regs.status)); regs.status.unused = 1; } int main(void) { cpu_init(); if (sizeof(program) > (0x10000 - 0x8000)) { fprintf(stderr, "program is too big for memory\n"); return 1; } memcpy(memory + 0x8000, program, sizeof(program)); regs.pc = 0x8000; printf("Initial State:\n"); printf("status: %i%i%i%i%i%i%i%i\n", regs.status.carry, regs.status.zero, regs.status.interrupt_disable, regs.status.decimal_mode, regs.status.brk, regs.status.unused, regs.status.overflow, regs.status.negative); printf("A: $%02X, X: $%02X, Y: $%02X, SP: $%02X, PC: $%02X\n", regs.a, regs.x, regs.y, regs.sp, regs.pc); putchar('\n'); interpret(); return 0; }