Catakig/Source/LibAppleII/A2Computer/CPU.m

630 lines
17 KiB
Objective-C

/* class A2Computer (category CPU)
Routines and tables for emulating the 65c02 microprocessor, and the
low-level I/O behavior of the Apple II.
*/
#import "LibAppleII-Priv.h"
#import "CPU-Macros.h"
//mport "CPU-Journal.h"
@implementation A2Computer (CPU)
//---------------------------------------------------------------------------
#if !JOURNALING
#define JOURNAL_OP
#define JOURNAL_EA
#endif
enum
{
DFLAG(0, D)
DFLAG(1, C)
DFLAG(2, V)
DFLAG(3, ZF) // when ZF = 1, forces Z = 1
DFLAG(4, LSB)
kfCD = kfC | kfD,
kfDV = kfD | kfV,
kfCDV = kfC | kfDV,
kfN = 0x80 << ksLSB,
kmZ8 = 0xFF << ksLSB,
kmPHP = LENGTH(A2T.tPHP) - 1,
// kCyclesPerStep = 17030, // CPU cycles in one time step (262 * 65)
};
static uint8_t gOldTimer, // residual cycles from last step
gSpkrState; // speaker state: 0 or 0xFF
static uint32_t gSpkrOut[kA2SamplesPerStep + kTapRatio - 1];
//---------------------------------------------------------------------------
static void FillMemoryMapRow(int8_t map[0x81], uint32_t f, BOOL wmap)
{/*
Utility function used by +_InitCPU (below) to initialize one row of a
memory mapping table -- either 'A2T.rmaps' or 'A2T.wmaps'.
*/
#define ORAM(BANK) \
offsetof(A2Memory, RAM[page>>5][BANK][page<<8 & 0x1F00])
#define OROM(BANK) \
offsetof(A2Memory, ROM[BANK][(page-0xC0)*256L])
#define PUT_PAGES(PGLO, PGHI, OFFSET) \
for (int page = PGLO; page <= PGHI; page += 2) \
map[page/2] = ((OFFSET) - ozp - 256L*page) / kChunkSize
enum { kChunkSize = 1L << 11, // = 0x800
OWOM = offsetof(A2Memory, WOM),
};
int ramrw = f>>(wmap? ksRAMWRT : ksRAMRD) & 1,
altzp = f>>ksALTZP & 1,
cxrom = f>>ksCXROM & 1,
hotSlot = f>>ksHotSlot & 7;
long ozp = offsetof(A2Memory, RAM[0][altzp][0]);
map[0] = 0;
map[0x80] = -(k64KB / kChunkSize);
PUT_PAGES(0x02, 0xBF, ORAM(ramrw));
if (f & kf80STOREm)
{
int page2 = f>>ksPAGE2m & 1;
PUT_PAGES(0x04, 0x07, ORAM(page2));
if (f & kfHIRESm)
PUT_PAGES(0x20, 0x3F, ORAM(page2));
}
if (wmap) // then setting write-map
{
PUT_PAGES(0xC0, 0xFF, OWOM);
}
else if (hotSlot == 0) // then setting read-map on IIc
{
PUT_PAGES(0xC0, 0xFF, OROM(cxrom));
}
else // setting read-map on IIe or earlier
{
int c3rom = f>>ksC3ROM & 1;
BOOL c8_internal;
PUT_PAGES(0xD0, 0xFF, OROM(0));
PUT_PAGES(0xC0, 0xC7, OROM(!cxrom));
PUT_PAGES(0xC3, 0xC3, OROM(!cxrom & c3rom));
if (cxrom) // CXROM on, C3ROM irrelevant
c8_internal = YES;
else if (c3rom) // CXROM off, C3ROM on
c8_internal = NO;
else // CXROM and C3ROM both off
c8_internal = YES; //!! (hotSlot == 3);
if (c8_internal)
PUT_PAGES(0xC8, 0xCF, OROM(0));
else
PUT_PAGES(0xC8, 0xCF, OROM(1) + 0x800*(hotSlot-1));
}
if (f & (wmap? kfLCWRThi : kfLCRD)) // then $D0-FF sees LC RAM
{
int shiftDx = (f & kfLCBANK2)? 0x1000 : 0;
PUT_PAGES(0xD0, 0xDF, ORAM(altzp) - shiftDx);
PUT_PAGES(0xE0, 0xFF, ORAM(altzp));
}
#undef ORAM
#undef OROM
#undef PUT_PAGES
}
//---------------------------------------------------------------------------
static int16_t FixAP(uint16_t oap)
{
unsigned p = kfLSB;
if (oap & 0x80) p |= 0x80;
if (oap & 0x40) p |= kfV;
if (oap & 0x08) p |= kfD;
if (oap & 0x02) p |= kfZF;
if (oap & 0x01) p |= kfC;
return (p << 8) | (oap >> 8);
}
//---------------------------------------------------------------------------
static BOOL InitADC_SBC(void)
{
// Fill in the ADC and SBC lookup tables in global structure 'A2T'.
static uint8_t tbl
[2/*D*/][2/*C*/][256/*B*/][2/*ADC,SBC*/][2/*A,P*/][256/*A*/];
static uint16_t tadc[0xD00], tsbc[0xD00];
BOOL adc_good = YES, sbc_good = YES;
FILE* fin;
fin = fopen([[[[NSBundle mainBundle] bundlePath]
stringByAppendingPathComponent:@"../ADSBC.dat"]
fileSystemRepresentation], "rb");
if (fin == NULL)
return NO;
fread(tbl, 1, sizeof(tbl), fin);
fclose(fin);
memset(tadc, 0xFF, sizeof(tadc));
memset(tsbc, 0xFF, sizeof(tsbc));
for (int i = 256; --i >= 0;)
tadc[i] = tsbc[i] = 128 + 2 *
( (i & 0x80)*15/4 + (i & 0x70)*2 + (i & 0xF) );
for (int d = 2; --d >= 0;)
for (int c = 2; --c >= 0;)
for (int b = 256; --b >= 0;)
for (int a = 256; --a >= 0;)
{
unsigned adc, sbc, i;
adc = tbl[d][c][b][0][0][a] << 8
| tbl[d][c][b][0][1][a] & 0xCB;
sbc = tbl[d][c][b][1][0][a] << 8
| tbl[d][c][b][1][1][a] & 0xCB;
i = tadc[a] + tadc[b] + 2*c + d;
if (tadc[i] == 0xFFFF)
tadc[i] = adc;
else if (tadc[i] != adc)
adc_good = NO;
i = tsbc[a] + tsbc[b^0xFF] + 2*c + d;
if (tsbc[i] == 0xFFFF)
tsbc[i] = sbc;
else if (tsbc[i] != sbc)
sbc_good = NO;
}
NSLog(@"adc_good? %c sbc_good? %c", "ny"[adc_good], "ny"[sbc_good]);
if (adc_good and sbc_good)
{
memcpy(A2T.tADC, tadc, 2*256);
memcpy(A2T.tSBC, tsbc, 2*256);
for (int i = 256; i < LENGTH(A2T.tADC); ++i)
{
A2T.tADC[i] = FixAP(tadc[i]);
A2T.tSBC[i] = FixAP(tsbc[i]);
}
memcpy(A2T.tADCo, A2T.tADC, sizeof(A2T.tADC));
memcpy(A2T.tSBCo, A2T.tSBC, sizeof(A2T.tSBC));
if (NO) for (int i = 256; i < LENGTH(A2T.tADCo); i += 2)
{
A2T.tADCo[i+1] =
A2T.tADCo[i ] & 0xFF00 |
A2T.tADCo[i+1] & 0x00FF;
A2T.tSBCo[i+1] =
A2T.tSBCo[i ] & 0xFF00 |
A2T.tSBCo[i+1] & 0x00FF;
}
}
return YES;
}
//---------------------------------------------------------------------------
+ (void)_InitCPU
{/*
Initializes various lookup tables used in method '-RunForOneStep',
defined below. Called only once, from '+initialize'.
*/
#if JOURNALING
atexit(LogJournal);
#endif
#if 0
for (int p = LENGTH(A2T.tPHP); --p >= 0;)
{
uint8_t php = 0x30; // the true P reg: NV1BDIZC
if (p & kfC) php |= 0x01;
if (Z_SET) php |= 0x02;
if (p & kfD) php |= 0x08;
if (p & kfV) php |= 0x40;
if (p & kfN) php |= 0x80;
A2T.tPHP[p] = php;
}
for (int i = LENGTH(A2T.tPLP); --i >= 0;)
{
unsigned plp = kfLSB; // assume Z = 0 (datum non-zero)
if (i & 0x01) plp |= kfC;
if (i & 0x02) plp |= kfZF;
if (i & 0x08) plp |= kfD;
if (i & 0x40) plp |= kfV;
if (i & 0x80) plp |= kfN;
A2T.tPLP[i] = plp;
}
for (long i = LENGTH(A2T.tROR), j; --i >= 0;)
{
j = i>>3 | (i&kfC)<<7;
j = (j<<9 | j) << 3;
A2T.tROR[i] = j&0xFF0 | i&kfDV | j>>2&kfC;
j >>= 7;
A2T.tROL[i] = j&0xFF0 | i&kfDV | j>>2&kfC;
}
InitADC_SBC();
for (long i = kmRMap+1; --i >= 0;)
FillMemoryMapRow(A2T.rmaps[i], i<<ksRMap, NO);
for (long i = kmWMap+1; --i >= 0;)
FillMemoryMapRow(A2T.wmaps[i], i<<ksWMap, YES);
A2DumpArray("tADC", A2T.tADC, sizeof(A2T.tADC), -2);
A2DumpArray("tSBC", A2T.tSBC, sizeof(A2T.tSBC), -2);
A2DumpArray("tADCo", A2T.tADCo, sizeof(A2T.tADCo), -2);
A2DumpArray("tSBCo", A2T.tSBCo, sizeof(A2T.tSBCo), -2);
A2DumpArray("tROL", A2T.tROL, sizeof(A2T.tROL), 2);
A2DumpArray("tROR", A2T.tROR, sizeof(A2T.tROR), 2);
A2DumpArray("tPLP", A2T.tPLP, sizeof(A2T.tPLP), 2);
A2DumpArray("tPHP", A2T.tPHP, sizeof(A2T.tPHP), 1);
A2DumpArray("rmaps", A2T.rmaps, sizeof(A2T.rmaps), -1);
A2DumpArray("wmaps", A2T.wmaps, sizeof(A2T.wmaps), -1);
#endif
}
//---------------------------------------------------------------------------
- (void)RunForOneStep:(uint8_t [])audioOut
{/*
Executes 65c02 instructions for one time step: about 17,030 CPU cyles,
or 1/60th of an emulated second. Also computes the 8-bit audio waveform
for the time step and writes it to the array 'audioOut'.
No attempt is made here to keep emulation time in sync with real time.
It's up to the library user to call this method every 60th of a second,
and keep the audio stream playing.
*/
if (mHalts) // != 0, then emulation is forbidden just now
{
[self _DefaultAudio:audioOut:kA2SamplesPerStep];
return;
}
unsigned p = mP;
unsigned pc = mPC;
int32_t t = gOldTimer;
uint8_t *zp;
int8_t *rmap, *wmap;
REMAP_(mFlags);
mCycles += 17030; // CPU cycles per step
for (int scanLine = 0; scanLine < 262;)
{
uint32_t d;
unsigned ea;
uint8_t curOp;
mVideoFlags[scanLine++] = mFlags;
t -= 65;
--pc;
//----------------------------------------------------------------
NewOpcode:
d = READ_PC; // = next opcode to interpret
JOURNAL_OP
switch (t >> 7 & d)
{
/*--------------------------------------------------------
BRK, JSR, JMP, RTI, and RTS.
*/
#define PHPC2 ea = pc+2; PUSH(ea>>8); PUSH(ea)
#define PLPC pc = PULL; pc |= PULL<<8
case 0x00: if (t >= 0)
continue; // next scan line
/* BRK */ PHPC2; PHP;
mI = 4; ea = 0xFFFE; ++t;
goto IndirJMP;
case 0x7C: EAAX; goto IndirJMP;
case 0x6C: EAA; // and fall into IndirJMP
IndirJMP: pc = READ(ea) - 1; ++ea; t += 6;
pc += READ(ea) << 8;
goto NewOpcode;
case 0x20: PHPC2; t += 3; // and fall into JMP-abs
case 0x4C: EAA; pc = ea - 1; t += 3; goto NewOpcode;
case 0x60: PLPC; t += 6; goto NewOpcode;
case 0x40: PLP; PLPC; --pc; t += 6; goto NewOpcode;
#undef PHPC2
#undef PLPC
/*--------------------------------------------------------
No-op instructions.
*/
case 0x54: case 0xD4: case 0xF4: ++t;
case 0x44: ++t;
case 0x02: case 0x22: case 0x42:
case 0x62: case 0x82: case 0xC2:
case 0xE2: ++pc;
case 0xEA: t += 2; goto NewOpcode;
case 0x5C: t += 4;
case 0xDC: case 0xFC: t += 4; pc += 2; goto NewOpcode;
default:
CASE16(0x03,16): CASE16(0x07,16):
CASE16(0x0B,16): CASE16(0x0F,16): ++t; goto NewOpcode;
/*--------------------------------------------------------
Relative branches.
*/
#define BRANCHES(OP, COND) \
case OP: if (COND) goto DoBRA; \
++pc; t += 2; goto NewOpcode; \
case OP ^ 0x20: if (not (COND)) goto DoBRA; \
++pc; t += 2; goto NewOpcode;
BRANCHES(0xB0, p & kfC) // BCS & BCC
BRANCHES(0x70, p & kfV) // BVS & BVC
BRANCHES(0x30, p & kfN) // BMI & BPL
BRANCHES(0xF0, Z_SET ) // BEQ & BNE
DoBRA: case 0x80:
pc += (signed char) READ_PC;
t += 3;
goto NewOpcode;
#undef BRANCHES
/*--------------------------------------------------------
Implied- and Accumulator-mode instructions:
*/
#define IMPI(OP, DT, STMT) \
case OP: t += DT; STMT; goto NewOpcode;
#define LD_(REG,VAL) REG = d = (VAL); SET_NZ
IMPI(0xB8, 2, p &= ~kfV)
IMPI(0x18, 2, p &= ~kfC) IMPI(0x38, 2, p |= kfC)
IMPI(0xD8, 2, p &= ~kfD) IMPI(0xF8, 2, p |= kfD)
IMPI(0x58, 2, mI = 0) IMPI(0x78, 2, mI = 4)
IMPI(0x1A, 2, LD_(mA, mA+1)) IMPI(0x3A, 2, LD_(mA, mA-1))
IMPI(0xE8, 2, LD_(mX, mX+1)) IMPI(0xCA, 2, LD_(mX, mX-1))
IMPI(0xC8, 2, LD_(mY, mY+1)) IMPI(0x88, 2, LD_(mY, mY-1))
IMPI(0x8A, 2, LD_(mA, mX)) IMPI(0xAA, 2, LD_(mX, mA))
IMPI(0x98, 2, LD_(mA, mY)) IMPI(0xA8, 2, LD_(mY, mA))
IMPI(0xBA, 2, LD_(mX, mS)) IMPI(0x9A, 2, mS = mX)
IMPI(0x08, 3, PHP) IMPI(0x28, 4, PLP)
IMPI(0x48, 3, PUSH(mA)) IMPI(0x68, 4, LD_(mA, PULL))
IMPI(0xDA, 3, PUSH(mX)) IMPI(0xFA, 4, LD_(mX, PULL))
IMPI(0x5A, 3, PUSH(mY)) IMPI(0x7A, 4, LD_(mY, PULL))
IMPI(0x0A, 2, ASL(mA)) IMPI(0x4A, 2, LSR(mA))
IMPI(0x2A, 2, ROL(mA)) IMPI(0x6A, 2, ROR(mA))
#undef IMPI
#undef LD_
/*--------------------------------------------------------
Read and modify instructions of the Immediate and
Zero-Page addressing modes.
*/
#define RIMM(OP, STMT) case 0x##OP: \
d = READ_PC; t += 2; STMT; goto NewOpcode;
#define RZP(OP, STMT) case 0x##OP: \
EAZ; t += 3; d = zp[ea]; STMT; goto NewOpcode;
#define RZPX(OP, STMT) case 0x##OP: \
EAZX; t += 4; d = zp[ea]; STMT; goto NewOpcode;
#define MZP(OP, STMT) case 0x##OP: \
EAZ; t+=5; d=zp[ea]; STMT; zp[ea]=d; goto NewOpcode;
#define MZPX(OP, STMT) case 0x##OP: \
EAZX; t+=6; d=zp[ea]; STMT; zp[ea]=d; goto NewOpcode;
RIMM(69, ADC) RZP(65, ADC) RZPX(75, ADC)
RIMM(29, AND) RZP(25, AND) RZPX(35, AND)
MZP(06, ASL(d)) MZPX(16, ASL(d))
RIMM(89, BITZ) RZP(24, BIT) RZPX(34, BIT)
RIMM(C9, CMP) RZP(C5, CMP) RZPX(D5, CMP)
RIMM(E0, CPX) RZP(E4, CPX)
RIMM(C0, CPY) RZP(C4, CPY)
MZP(C6, DEC) MZPX(D6, DEC)
RIMM(49, EOR) RZP(45, EOR) RZPX(55, EOR)
MZP(E6, INC) MZPX(F6, INC)
RIMM(A9, LDA) RZP(A5, LDA) RZPX(B5, LDA)
RIMM(A2, LDX) RZP(A6, LDX)
RIMM(A0, LDY) RZP(A4, LDY) RZPX(B4, LDY)
MZP(46, LSR(d)) MZPX(56, LSR(d))
RIMM(09, ORA) RZP(05, ORA) RZPX(15, ORA)
MZP(26, ROL(d)) MZPX(36, ROL(d))
MZP(66, ROR(d)) MZPX(76, ROR(d))
RIMM(E9, SBC) RZP(E5, SBC) RZPX(F5, SBC)
MZP(14, TRB)
MZP(04, TSB)
case 0xB6: // LDX zp,Y
EAZY; t += 4; d = zp[ea]; LDX; goto NewOpcode;
#undef RIMM
#undef RZP
#undef RZPX
#undef MZP
#undef MZPX
/*--------------------------------------------------------
STA, STX, STY, and STZ
*/
case 0x85: EAZ ; t += 3; zp[ea] = mA; goto NewOpcode;
case 0x86: EAZ ; t += 3; zp[ea] = mX; goto NewOpcode;
case 0x84: EAZ ; t += 3; zp[ea] = mY; goto NewOpcode;
case 0x64: EAZ ; t += 3; zp[ea] = 0; goto NewOpcode;
case 0x95: EAZX; t += 4; zp[ea] = mA; goto NewOpcode;
case 0x96: EAZY; t += 4; zp[ea] = mX; goto NewOpcode;
case 0x94: EAZX; t += 4; zp[ea] = mY; goto NewOpcode;
case 0x74: EAZX; t += 4; zp[ea] = 0; goto NewOpcode;
case 0x8D: EAA ; t += 4; d = mA; goto Write;
case 0x8E: EAA ; t += 4; d = mX; goto Write;
case 0x8C: EAA ; t += 4; d = mY; goto Write;
case 0x9C: EAA ; t += 4; d = 0; goto Write;
case 0x9D: EAAX; t += 5; d = mA; goto Write;
case 0x99: EAAY; t += 5; d = mA; goto Write;
case 0x92: EAI ; t += 5; d = mA; goto Write;
case 0x81: EAIX; t += 6; d = mA; goto Write;
case 0x91: EAIY; t += 6; d = mA; goto Write;
case 0x9E: EAAX; t += 5; d = 0; goto Write;
/*--------------------------------------------------------
"Prologs" for read and modify instructions that work
on general addresses. The effective address is
computed, and the clock is bumped. Execution then
proceeds to the Read and Epilog phases below.
*/
case 0x6D: case 0x2D: case 0x0E: case 0x2C: case 0xCD:
case 0xEC: case 0xCC: case 0xCE: case 0x4D: case 0xEE:
case 0xAD: case 0xAE: case 0xAC: case 0x4E: case 0x0D:
case 0x2E: case 0x6E: case 0xED: case 0x1C: case 0x0C:
EAA; t += 4; break;
case 0x7D: case 0x3D: case 0x1E: case 0x3C: case 0xDD:
case 0xDE: case 0x5D: case 0xFE: case 0xBD: case 0xBC:
case 0x5E: case 0x1D: case 0x3E: case 0x7E: case 0xFD:
EAAX; t += 4; break;
case 0x79: case 0x39: case 0xD9: case 0x59: case 0xB9:
case 0xBE: case 0x19: case 0xF9:
EAAY; t += 4; break;
case 0x72: case 0x32: case 0xD2: case 0x52: case 0xB2:
case 0x12: case 0xF2:
EAI; t += 5; break;
case 0x61: case 0x21: case 0xC1: case 0x41: case 0xA1:
case 0x01: case 0xE1:
EAIX; t += 6; break;
case 0x71: case 0x31: case 0xD1: case 0x51: case 0xB1:
case 0x11: case 0xF1:
EAIY; t += 5; break;
} // end of switch (t>>16 & d)
//----------------------------------------------------------------
Read:
curOp = d;
JOURNAL_EA
#define READ_PHASE 1
#include "CPU-RW.h"
#undef READ_PHASE
d = READ(ea); // default read behavior, when 'ea' not in I/O area
// fall into Epilog...
//----------------------------------------------------------------
Epilog:
switch (curOp)
{
#define OP_ACC(OP, STMT) case OP+0x12: \
case OP+0x01: case OP+0x05: case OP+0x09: case OP+0x0D: \
case OP+0x11: case OP+0x15: case OP+0x19: case OP+0x1D: \
STMT; goto NewOpcode;
OP_ACC(0x00, ORA) OP_ACC(0x20, AND) OP_ACC(0x40, EOR)
OP_ACC(0x60, ADC) OP_ACC(0xA0, LDA) OP_ACC(0xC0, CMP)
OP_ACC(0xE0, SBC)
case 0x2C: case 0x3C: BIT; goto NewOpcode;
case 0xEC: CPX; goto NewOpcode;
case 0xCC: CPY; goto NewOpcode;
case 0xAE: case 0xBE: LDX; goto NewOpcode;
case 0xAC: case 0xBC: LDY; goto NewOpcode;
case 0x0E: case 0x1E: ASL(d); break;
case 0x4E: case 0x5E: LSR(d); break;
case 0x2E: case 0x3E: ROL(d); break;
case 0x6E: case 0x7E: ROR(d); break;
case 0xCE: case 0xDE: DEC; break;
case 0xEE: case 0xFE: INC; break;
case 0x0C: TSB; break;
case 0x1C: TRB; break;
case 0x4C: case 0x5C: case 0x6C: case 0x7C: case 0x8C:
case 0x9C: case 0xDC: case 0xFC: case 0x8E: case 0x9E:
CASE16(0x00, 0x10): CASE8 (0x02, 0x20): CASE16(0x03, 0x10):
CASE16(0x04, 0x10): CASE16(0x06, 0x10): CASE16(0x07, 0x10):
CASE16(0x08, 0x10): CASE16(0x0A, 0x10): CASE16(0x0B, 0x10):
CASE16(0x0F, 0x10): default:
OP_ACC(0x80, ;) // goto NewOpcode
#undef OP_ACC
}
// Modify instructions reach here. We need to burn 2 more
// cycles before falling into Write.
t += 2;
//----------------------------------------------------------------
Write:
#include "CPU-RW.h"
JOURNAL_EA
WRITE(ea) = d; // default write, when 'ea' not in I/O area
goto NewOpcode;
} // end of for (scanLine ...)
mPC = pc;
mP = p;
gOldTimer = t;
p = gSpkrState;
for (int i = 0; i < kA2SamplesPerStep; ++i)
{
t = gSpkrOut[i];
p ^= t>>8;
audioOut[i] = p ^ t>>24;
p ^= t;
}
gSpkrState = p;
for (int i = kTapRatio-1; --i >= 0;)
gSpkrOut[i] = (gSpkrOut + kA2SamplesPerStep)[i];
t = A2T.audio.flat;
for (int i = kA2SamplesPerStep; --i >= 0;)
(gSpkrOut + kTapRatio - 1)[i] = t;
}
//---------------------------------------------------------------------------
@end