/* NES V-Blank / NMI / CPU timing skeleton, platform-independent C++03 code * Written by and copyright 2011 Joel Yliluoma - http://iki.fi/bisqwit/ */ #include #include #include #include // Integer types typedef unsigned char u8; typedef signed char s8; typedef unsigned short u16; typedef unsigned int u32; namespace Pak { std::vector ROM; unsigned char* banks[4] = { 0,0,0,0 }; u8 Read(unsigned addr) { return banks[ (addr >> 14) & 3] [addr & 0x3FFF]; } void Write(unsigned addr, u8 value) { // Treat data from Blargg's test ROMs if(addr >= 0x6004 && addr < 0x6200 && value) { fputc(value, stderr); } if(addr == 0x6000) { fprintf(stderr, "[6000] <- 0x%02X\n", value); if(value < 0x80) exit(value); } if(addr == 0x6001) fprintf(stderr, "[6001] <- 0x%02X\n", value); if(addr == 0x6002) fprintf(stderr, "[6002] <- 0x%02X\n", value); if(addr == 0x6003) fprintf(stderr, "[6003] <- 0x%02X\n", value); } void Init() { for(unsigned v=0; v<4; ++v) banks[v] = &ROM[0]; banks[3] = &ROM[ROM.size()-0x4000]; } } namespace CPU { bool reset=true, nmi=false, nmi_edge_detected=false; // IRQ functions ignored } namespace PPU { u8 regs[3] = { 0, 0, 0}; int scanline=241, x=0, slend=341, VBlankState=0; bool even_odd_toggle=false; void tick() // Process one PPU cycle { // Set/clear vblank where needed switch(VBlankState) { case -5: regs[2] = 0x00; break; // clear vblank/sprite flags case 2: regs[2] |= 0x80; break; // set vblank flag case 0: CPU::nmi = regs[0] & regs[2] & 0x80; break; // if vblank & nmi } if(VBlankState != 0) VBlankState += (VBlankState < 0 ? 1 : -1); // Scanline/Cycle loop // Enter vblank at the beginning of first vblank scanline if(x == 0 && scanline == 241) VBlankState = 2; // Exit vblank at the beginning of the pre-render scanline if(x == 0 && scanline == -1) VBlankState = -5; // Begin rendering: if(x == 0 && scanline == -1) even_odd_toggle = !even_odd_toggle; // The length of the pre-render scanline is decided at cycle 338 of that scanline. if(x == 338 && scanline == -1 && (regs[1] & 0x08) && even_odd_toggle) slend = 340; // Memory functions ignored // Rendering functions ignored // OAM functions ignored if(++x >= slend) { // Begin new scanline scanline = scanline<260 ? scanline+1 : -1; x = 0; slend = 341; } } } namespace CPU { u8 RAM[0x800], A=0,X=0,Y=0,S=0, P=0; u16 PC=0xC000; enum { C=1, Z=2, I=4, D=8, V=64, N=128 }; void SetFlag(u8 mask, bool state) { if(state) P |= mask; else P &= ~mask; } void tick() { // PPU will not receive clock while reset is being signalled if(reset) return; // PPU clock: 3 times the CPU rate PPU::tick(); PPU::tick(); PPU::tick(); } u8 RB(u16 addr) { tick(); u8 r=0; /**/ if(addr < 0x2000) r = RAM[addr & 0x7FF]; else if(addr < 0x4000) // PPU register { if((addr & 7) == 2) { r = PPU::regs[2]; // Cancel the oncoming setting of VBlank flag if(PPU::VBlankState != -5) PPU::VBlankState = 0; PPU::regs[2] &= 0x7F; // Clear VBlank flag } // OAM, Video, memory, latch/openbus functions ignored } else if(addr < 0x4018) r = 0; // I/O function ignored else r = Pak::Read(addr); return r; } void WB(u16 addr, u8 v) { tick(); // Memory writes are turned into reads while reset is being signalled if(reset) { /* Read ignored */ } else if(addr < 0x2000) RAM[addr & 0x7FF] = v; else if(addr < 0x4000) // PPU register { if((addr & 7) == 0) PPU::regs[0] = v; if((addr & 7) == 1) PPU::regs[1] = v; // OAM, Video, memory, latch/openbus functions ignored } else if(addr == 0x4014) // OAM DMA, required for timing for(unsigned b=0; b<256; ++b) WB(0x2004, RB((v&7)*0x0100+b)); else if(addr < 0x4018) {} // I/O function ignored else Pak::Write(addr, v); } u16 wrap(u16 oldaddr, u16 newaddr) { return (oldaddr & 0xFF00) + u8(newaddr); } u16 RW(u16 addr) { unsigned q=RB(addr); return q + 256*RB(wrap(addr,addr+1)); } void Misfire(u16 old, u16 addr) { u16 q = wrap(old, addr); if(q != addr) RB(q); } u8 PB() { return RB(PC++); } u8 Pop() { return RB(0x100 | u8(++S)); } void Push(u8 v) { WB(0x100 | u8(S--), v); } void PushW(u16 v) { Push(v>>8); Push(v); } void Op() { // Pull NMI state before reading the opcode bool nmi_now = nmi; // Read opcode unsigned op = PB(); if(reset) { op=0x101; } else if(nmi_now && !nmi_edge_detected) { op=0x100; nmi_edge_detected = true; } // IRQ functions ignored if(!nmi_now) nmi_edge_detected=false; // Note: op 0x100 means "NMI", 0x101 means "Reset", 0x102 means "IRQ". They are implemented in terms of "BRK". // User is responsible for ensuring that WB() will not store into memory while Reset is being processed. unsigned addr=0, d=0, t=0xFF, c=0, sb=0, pbits = op<0x100 ? 0x30 : 0x20; // Define the opcode decoding matrix, which decides which micro-operations constitute // any particular opcode. (Note: The PLA of 6502 works on a slightly different principle.) #define i(str,code) { int i=str[op/6], b=i-":) .55;;"[i/16]; if(b & (1<<(op%6))) { code; } } /* Decode address operand */ i(" E ", addr = 0xFFFA); // NMI vector location i(" U ", addr = 0xFFFC); // Reset vector location i("! !", addr = 0xFFFE); // Interrupt vector location i("u1zzujDwzfjykzjyu1zzufDwzfjykzjyu1zzufDwzfD ", addr = PB()); i("8 wkf k1j0 Dw8 wkf k X0 1A8 wkf k1D ", d = X); // register index i(" U18 Cc sU0 U18 CfA sgm U18 Cc ", d = Y); i("8 w c k1U0 D 8 w c k1U0 D 8 w c k1 ", tick(); addr = u8(addr+d); d=0; );// add zeropage-index i(" D u5 w cD k1Uy D u1 w cD k1Uy D u1 w cD ", addr = u8(addr) + 256 * PB()); // absolute address i("9 U1 c C U0 s 8 V1 c C U0 s 8 U1 c C k!", addr = RW(addr)); // indirect, with page wrap i(" U m 061 6UA U m sUy U m 061 ", Misfire(addr, addr+d)); // read:misread when cross-page i(" 161 AUA k m 161 CcD 161 AUA ", RB(wrap(addr, addr+d)));// store:misread always /* Load source operand */ i("iueeeufjeeefjeee2k ceeee8 3 eeeee0 ", t &= A); // Many operations take A or X as operand. Some try in i(" UAnAnA k 2F2 ", t &= X); // error to take both; the outcome is an AND operation. i(" 2F2E2! F2! ", t &= Y); // sty,dey,iny,tya,cpy i(" 1 ", t &= S); // tsx, las i("!2E ! !2 2E E ! ! 2E E ! !2k!", t &= P|pbits; c = t);// brk, php, flag test/set/clear i("e8eeeefgeeeeWeee vefee4!2 ", c = t; t = 0xFF); // save as second operand i("e1ivefDwifiuUviue1ive1 Uykvjyu1jvefDwifC ", t &= RB(addr+d)); // memory operand i("2c EU0 !8 2c IU0 E!8 3c IU0 ", t &= PB()); // immediate operand /* Operations that mogrify memory operands directly */ i(" !2 ", SetFlag(V, t&0x40); SetFlag(N, t&0x80)); // bit i(" kAlAXA AnAn61 ", sb = P&1); // rol,rla, ror,rra,arr i("AnAn6nAnAXA U ", SetFlag(C, t&0x80)); // rol,rla, asl,slo,[arr,anc] i(" nAnAmAHAn61 ", SetFlag(C, t&0x01)); // lsr,sre, ror,rra,asr i("AHAn6nAlAXA ", t = (t << 1) | (sb << 0)); i(" nAnAmAnAn61 ", t = (t >> 1) | (sb << 7)); i(" E 6HAn61 ", t = u8(t - 1)); // dec,dex,dey,dcp i(" 2 UQkAXA ", t = u8(t + 1)); // inc,inx,iny,isb /* Store modified value (memory) */ i("61An6XAkAXAm nAm61An6fDwy90 61An6XAkAXA ", WB(addr+d, t)); i(" UB ", WB(wrap(addr, addr+d), t &= ((addr+d) >> 8))); // [shx,shy,shs,sha?] /* Some operations used up one clock cycle that we did not account for yet */ i("NLBrRbQlQrBq3nRrNLArR1E! I 3 E!6LArRXQlQrx!", tick()); // nop,flag ops,inc,dec,shifts,stack,transregister,interrupts /* Stack operations and unconditional jumps */ i(" E E 2 ", tick(); t = Pop()); // pla,plp,rti i(" E ! ", PB(); PC = Pop(); PC |= (Pop() << 8)); // rti,rts i(" ! ", PB()); // rts i("! 2 k!", PushW(PC + (op?-1:1)));// jsr, interrupts (brk,reset,intr,nmi) i("! 2 E ! k!", PC = addr); // jmp, jsr, interrupts i("!2 ! k!", Push(t)); // pha, php, interrupts /* Bitmasks */ i("! E ! !2 2E E ! ! 2E E ! !2k!", t = 1); i(" E ! E ! !2 ", t <<= 1); i("! E ! 2E E ! E ! 2k!", t <<= 2); i(" E ! 2 E E ", t <<= 4); i(" ! E E !ceeee8 ", t = u8(~t)); // sbc, isb, clear flag i("f8eee0 2 ! 2k!", t = c | t); // ora, slo, set flag i(" E !cfgfe8 2E E ! 2E E ! ! ", t = c & t); // and, bit, rla, clear/test flag i(" UeWeee ", t = c ^ t); // eor, sre /* Conditional branches */ i(" ! E 2 ! ", if(t) { tick(); Misfire(PC, addr = s8(addr) + PC); PC=addr; }); i(" E 2 ! E ", if(!t) { tick(); Misfire(PC, addr = s8(addr) + PC); PC=addr; }); /* Addition and subtraction */ i(" e8eee0 ceeee8 ", c = t; t += A + (P&1); SetFlag(V, (c^t) & (A^t) & 0x80); SetFlag(C, t & 0x100)); i(" vefee4!2 ", t = c - t; SetFlag(C, ~t & 0x100)); // cmp,cpx,cpy, dcp, sbx /* Store modified value (register) */ i("iueeeuefeeefieeeiyeee0 1 2Ueeeee ceeee8 ", A = t); i(" nAnAn k E ", X = t); // ldx, dex, tax, inx, tsx,lax,las,sbx i(" E E2F !2 2 ", Y = t); // ldy, dey, tay, iny i(" k 0 ", S = t); // txs, las, shs i("! ! E 2E E ! E ! 2k!", P = t & ~0x30); // plp, rti, flag set/clear /* Generic status flag updates */ i("ivivevivifiviviuizive1E1 2kzzvjzvzjvejzyifC ", SetFlag(N, t & 0x80)); i("ivivevjzifiviviuizive1E1 2kzzvjzvzjvejzyifC ", SetFlag(Z, u8(t) == 0)); i(" U ", SetFlag(V, (((t >> 5)+1)&2))); // [arr] #undef i /* All implemented opcodes are cycle-accurate and memory-access-accurate. * Cycle-accuracy depends on memory access functions RB and WB executing tick(). * [] in the comments indicates that this particular separate rule * exists only because of the indicated unofficial opcode(s). * Note: Unofficial NOP opcodes may do memory accesses that * they are not supposed to do. There was no comprehensive test. * Note: All KIL/JAM opcodes do (some) actual work instead of halting. * Note: Reset,Interrupt,NMI are implemented as instructions 101,102,100 respectively. * They are variants of BRK. This is the same as what real 6502 does. * Note that for Reset to work properly, you will have to disable RAM writes * while the instruction is in progress. */ reset = false; } } int main(int argc, char** argv) { FILE* fp = fopen(argv[1], "rb"); assert(fgetc(fp)=='N' && fgetc(fp)=='E' && fgetc(fp)=='S' && fgetc(fp)=='\32'); u8 rom16count = fgetc(fp); fgetc(fp);fgetc(fp);fgetc(fp); // VROM, mirroring, mapper data ignored fgetc(fp);fgetc(fp);fgetc(fp);fgetc(fp);fgetc(fp);fgetc(fp);fgetc(fp);fgetc(fp); Pak::ROM.resize(rom16count * 0x4000); fread(&Pak::ROM[0], rom16count, 0x4000, fp); // VROM ignored // Mirroring ignored fclose(fp); Pak::Init(); for(;;) { CPU::Op(); } }