mirror of
https://github.com/edmccard/twoapple-reboot.git
synced 2024-12-21 20:29:18 +00:00
666 lines
13 KiB
D
666 lines
13 KiB
D
/*
|
|
* Test functionality that either depends on a specific cpu
|
|
* implementation, or that may be useful to any test which requires a
|
|
* cpu (as opposed to testing the cpu itself).
|
|
*/
|
|
module test.cpu;
|
|
|
|
|
|
import std.conv, std.exception, std.random, std.string, std.traits;
|
|
|
|
public import d6502.nmosundoc : NmosUndoc;
|
|
public import d6502.cmos : Cmos;
|
|
|
|
|
|
// True if T is the type of a cpu.
|
|
template isCpu(T)
|
|
{
|
|
enum isCpu = __traits(hasMember, T, "_isCpuBase");
|
|
}
|
|
|
|
// True if the cpu type T represents a 6502.
|
|
template isNMOS(T)
|
|
{
|
|
enum isNMOS = __traits(hasMember, T, "_isNMOS");
|
|
}
|
|
|
|
// True if the cpu type T represents a 65C02.
|
|
template isCMOS(T)
|
|
{
|
|
enum isCMOS = __traits(hasMember, T, "_isCMOS");
|
|
}
|
|
|
|
// True if the cpu type T accesses memory on every cycle.
|
|
template isStrict(T)
|
|
{
|
|
enum isStrict = __traits(hasMember, T, "_isStrict");
|
|
}
|
|
|
|
// True if the cpu type T aggregates ticks.
|
|
template isCumulative(T)
|
|
{
|
|
enum isCumulative = __traits(hasMember, T, "_isCumulative");
|
|
}
|
|
|
|
|
|
/*
|
|
* The type of a cpu, based on its architecture (6502 or 65C02) and
|
|
* its timing characteristics (strict or not bus access, cumulative or
|
|
* not cycle reporting).
|
|
*/
|
|
template CPU(string arch, bool strict, bool cumulative)
|
|
{
|
|
static if (arch == "65c02" || arch == "65C02")
|
|
alias Cmos!(strict, cumulative) CPU;
|
|
else static if (arch == "6502")
|
|
alias NmosUndoc!(strict, cumulative) CPU;
|
|
else static assert(0);
|
|
}
|
|
|
|
|
|
auto makeCpu(T)(CpuInfo info)
|
|
if (isCpu!T)
|
|
{
|
|
auto cpu = new T();
|
|
cpu.programCounter = info.PC;
|
|
cpu.stackPointer = info.SP;
|
|
cpu.flag.fromByte(info.S);
|
|
cpu.accumulator = info.A;
|
|
cpu.xIndex = info.X;
|
|
cpu.yIndex = info.Y;
|
|
return cpu;
|
|
}
|
|
|
|
// Connects test memory to a cpu.
|
|
void connectMem(T, S)(T cpu, ref S mem)
|
|
if (isCpu!T)
|
|
{
|
|
static if (isCumulative!T)
|
|
void tick(int cycles) {}
|
|
else
|
|
void tick() {}
|
|
|
|
cpu.memoryRead = &mem.read;
|
|
cpu.memoryWrite = &mem.write;
|
|
cpu.tick = &tick;
|
|
}
|
|
|
|
|
|
/*
|
|
* Sets up a cpu to record the number of cycles executed.
|
|
*
|
|
* For example:
|
|
*
|
|
* auto cycles = recordCycles(cpu);
|
|
* // run a test
|
|
* if (cycles != expected) ...
|
|
*/
|
|
auto recordCycles(T)(T cpu)
|
|
if (isCpu!T)
|
|
{
|
|
auto cycles = new int;
|
|
auto wrappedTick = cpu.tick;
|
|
|
|
static if (isCumulative!T)
|
|
{
|
|
void tick(int cyc)
|
|
{
|
|
(*cycles) += cyc;
|
|
wrappedTick(cyc);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
void tick()
|
|
{
|
|
(*cycles)++;
|
|
wrappedTick();
|
|
}
|
|
}
|
|
cpu.tick = &tick;
|
|
|
|
return constRef(cycles);
|
|
}
|
|
|
|
struct Ref(T)
|
|
if (isPointer!T)
|
|
{
|
|
private const(T) data;
|
|
this(T ptr) { data = ptr; }
|
|
auto deref() { return *data; }
|
|
alias deref this;
|
|
|
|
string toString () const { return format("%s", *data); }
|
|
}
|
|
|
|
auto constRef(T)(T ptr)
|
|
if (isPointer!T)
|
|
{
|
|
return Ref!(const(T))(ptr);
|
|
}
|
|
|
|
|
|
/*
|
|
* Sets up a cpu to record bus accesses during execution.
|
|
*
|
|
* For example:
|
|
*
|
|
* auto accesses = recordBus(cpu);
|
|
* // run a test
|
|
* if (accesses != expected) ...
|
|
*/
|
|
const(Bus[]) recordBus(T)(T cpu, int actions = 8)
|
|
if (isCpu!T)
|
|
{
|
|
auto record = new Bus[actions];
|
|
int c;
|
|
|
|
enforce(cpu.memoryRead !is null && cpu.memoryWrite !is null);
|
|
auto wrappedRead = cpu.memoryRead;
|
|
auto wrappedWrite = cpu.memoryWrite;
|
|
|
|
ubyte read(ushort addr)
|
|
{
|
|
if (c == actions)
|
|
throw new TestException(
|
|
format("cannot record more than %d actions", actions));
|
|
record[c++] = Bus(Action.READ, addr);
|
|
return wrappedRead(addr);
|
|
}
|
|
|
|
void write(ushort addr, ubyte val)
|
|
{
|
|
if (c == actions)
|
|
throw new TestException(
|
|
format("cannot record more than %d actions", actions));
|
|
record[c++] = Bus(Action.WRITE, addr);
|
|
wrappedWrite(addr, val);
|
|
}
|
|
|
|
cpu.memoryRead = &read;
|
|
cpu.memoryWrite = &write;
|
|
|
|
return record;
|
|
}
|
|
|
|
|
|
// A record of a bus access with its type and address.
|
|
struct Bus
|
|
{
|
|
Action action;
|
|
ushort addr;
|
|
|
|
this(Action action, int addr)
|
|
{
|
|
this.action = action; this.addr = cast(ushort)addr;
|
|
}
|
|
|
|
string toString() const
|
|
{
|
|
return format("Bus(%s, %0.4X)", to!string(action), addr);
|
|
}
|
|
}
|
|
|
|
|
|
// Types of bus accesses.
|
|
enum Action : ushort { NONE, READ, WRITE }
|
|
|
|
|
|
// Runs the cpu until a BRK instruction.
|
|
void runUntilBRK(T)(T cpu)
|
|
if (isCpu!T)
|
|
{
|
|
assert(cpu.memoryRead !is null);
|
|
auto wrappedRead = cpu.memoryRead;
|
|
|
|
ubyte read(ushort addr)
|
|
{
|
|
if (addr == 0xFFFE) throw new StopException("BRK");
|
|
return wrappedRead(addr);
|
|
}
|
|
|
|
cpu.memoryRead = &read;
|
|
|
|
try { cpu.run(true); } catch (StopException e) {}
|
|
}
|
|
|
|
class StopException : Exception { this(string msg) { super(msg); } }
|
|
|
|
|
|
// Runs the cpu for one opcode.
|
|
void runOneOpcode(T)(T cpu)
|
|
if (isCpu!T)
|
|
{
|
|
cpu.run(false);
|
|
}
|
|
|
|
|
|
struct CpuInfo
|
|
{
|
|
ushort PC;
|
|
ubyte SP = 0xFF;
|
|
ubyte A, X, Y;
|
|
ubyte S = 0x30;
|
|
|
|
string toString() const
|
|
{
|
|
return format("PC %0.4X SP %0.2X S %0.2X A %0.2X X %0.2X Y %0.2X",
|
|
PC, SP, S, A, X, Y);
|
|
}
|
|
|
|
static CpuInfo fromCpu(T)(T cpu)
|
|
{
|
|
CpuInfo info;
|
|
info.PC = cpu.programCounter;
|
|
info.SP = cpu.stackPointer;
|
|
info.A = cpu.accumulator;
|
|
info.X = cpu.xIndex;
|
|
info.Y = cpu.yIndex;
|
|
info.S = cpu.flag.toByte();
|
|
return info;
|
|
}
|
|
}
|
|
|
|
|
|
// Sets the program counter.
|
|
void setPC(T)(T cpu, int addr)
|
|
if (isCpu!T)
|
|
{
|
|
cpu.programCounter = cast(ushort)addr;
|
|
}
|
|
|
|
void setPC(T : CpuInfo)(ref T cpu, int addr)
|
|
{
|
|
cpu.PC = cast(ushort)addr;
|
|
}
|
|
|
|
void incPC(T : CpuInfo)(ref T cpu, int amt = 1)
|
|
{
|
|
cpu.PC = pageCrossAdd(cpu.PC, amt);
|
|
}
|
|
|
|
// Returns the program counter.
|
|
ushort getPC(T)(T cpu)
|
|
if (isCpu!T)
|
|
{
|
|
return cpu.programCounter;
|
|
}
|
|
|
|
ushort getPC(T : CpuInfo)(ref T cpu)
|
|
{
|
|
return cpu.PC;
|
|
}
|
|
|
|
|
|
/*
|
|
* Sets the stack pointer.
|
|
*
|
|
* Can be called with either an offset (e.g., 0xFE) or an absolute
|
|
* stack address (e.g., 0x01FE).
|
|
*/
|
|
void setSP(T)(T cpu, int val)
|
|
if (isCpu!T)
|
|
{
|
|
assert(val < 0x0200);
|
|
cpu.stackPointer = cast(ubyte)val;
|
|
}
|
|
|
|
void setSP(T : CpuInfo)(ref T cpu, int val)
|
|
{
|
|
assert(val < 0x0200);
|
|
cpu.SP = cast(ubyte)val;
|
|
}
|
|
|
|
void incSP(T : CpuInfo)(ref T cpu, int amt = 1)
|
|
{
|
|
cpu.SP = cast(ubyte)pageWrapAdd(cpu.SP, amt);
|
|
}
|
|
|
|
void decSP(T : CpuInfo)(ref T cpu, int amt = -1)
|
|
{
|
|
cpu.SP = cast(ubyte)pageWrapAdd(cpu.SP, amt);
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns the stack address (in the range 0x0100-0x01FF) represented
|
|
* by the stack pointer.
|
|
*/
|
|
ushort getSP(T)(T cpu)
|
|
if (isCpu!T)
|
|
{
|
|
return 0x100 | cpu.stackPointer;
|
|
}
|
|
|
|
ushort getSP(T : CpuInfo)(ref T cpu)
|
|
{
|
|
return 0x0100 | cpu.SP;
|
|
}
|
|
|
|
|
|
// The names of the registers.
|
|
enum Reg
|
|
{
|
|
A, X, Y
|
|
}
|
|
|
|
// Sets a register.
|
|
void setReg(T)(ref T cpu, Reg reg, int val)
|
|
if (isCpu!T || is(T == CpuInfo))
|
|
{
|
|
final switch (reg)
|
|
{
|
|
case Reg.A: setA(cpu, val); break;
|
|
case Reg.X: setX(cpu, val); break;
|
|
case Reg.Y: setY(cpu, val); break;
|
|
}
|
|
}
|
|
|
|
// Returns a register
|
|
ubyte getReg(T)(ref T cpu, Reg reg)
|
|
if (isCpu!T || is(T == CpuInfo))
|
|
{
|
|
final switch (reg)
|
|
{
|
|
case Reg.A: return getA(cpu);
|
|
case Reg.X: return getX(cpu);
|
|
case Reg.Y: return getY(cpu);
|
|
}
|
|
}
|
|
|
|
// Sets the A register.
|
|
void setA(T)(T cpu, int val)
|
|
if (isCpu!T)
|
|
{
|
|
cpu.accumulator = cast(ubyte)val;
|
|
}
|
|
|
|
void setA(T : CpuInfo)(ref T cpu, int val)
|
|
{
|
|
cpu.A = cast(ubyte)val;
|
|
}
|
|
|
|
// Returns the A register.
|
|
ubyte getA(T)(T cpu, int val)
|
|
if (isCpu!T)
|
|
{
|
|
return cpu.accumulator;
|
|
}
|
|
|
|
ubyte getA(T : CpuInfo)(ref T cpu)
|
|
{
|
|
return cpu.A;
|
|
}
|
|
|
|
|
|
// Sets the X register.
|
|
void setX(T)(T cpu, int val)
|
|
if (isCpu!T)
|
|
{
|
|
cpu.xIndex = cast(ubyte)val;
|
|
}
|
|
|
|
void setX(T : CpuInfo)(ref T cpu, int val)
|
|
{
|
|
cpu.X = cast(ubyte)val;
|
|
}
|
|
|
|
// Returns the X register.
|
|
ubyte getX(T)(T cpu)
|
|
if (isCpu!T)
|
|
{
|
|
return cpu.xIndex;
|
|
}
|
|
|
|
ubyte getX(T : CpuInfo)(ref T cpu)
|
|
{
|
|
return cpu.X;
|
|
}
|
|
|
|
|
|
// Sets the Y register.
|
|
void setY(T)(T cpu, int val)
|
|
if (isCpu!T)
|
|
{
|
|
cpu.yIndex = cast(ubyte)val;
|
|
}
|
|
|
|
void setY(T : CpuInfo)(ref T cpu, int val)
|
|
{
|
|
cpu.Y = cast(ubyte)val;
|
|
}
|
|
|
|
// Returns the Y register.
|
|
ubyte getY(T)(T cpu)
|
|
if (isCpu!T)
|
|
{
|
|
return cpu.yIndex;
|
|
}
|
|
|
|
ubyte getY(T : CpuInfo)(ref T cpu)
|
|
{
|
|
return cpu.Y;
|
|
}
|
|
|
|
|
|
// The names of the status flags.
|
|
enum Flag : ubyte
|
|
{
|
|
C = 0x01,
|
|
Z = 0x02,
|
|
I = 0x04,
|
|
D = 0x08,
|
|
V = 0x40,
|
|
N = 0x80
|
|
}
|
|
|
|
string flagToString(Flag f)
|
|
{
|
|
switch (f)
|
|
{
|
|
case Flag.C: return "C";
|
|
case Flag.Z: return "Z";
|
|
case Flag.I: return "I";
|
|
case Flag.D: return "D";
|
|
case Flag.V: return "V";
|
|
case Flag.N: return "N";
|
|
default: return "?";
|
|
}
|
|
}
|
|
|
|
// Sets one or more status flags.
|
|
void setFlag(T)(T cpu, Flag[] flags...)
|
|
if (isCpu!T)
|
|
{
|
|
auto reg = cpu.flag.toByte();
|
|
foreach (flag; flags) reg |= flag;
|
|
cpu.flag.fromByte(reg);
|
|
}
|
|
|
|
void setFlag(T : CpuInfo)(ref T cpu, Flag[] flags...)
|
|
{
|
|
foreach (flag; flags) cpu.S |= flag;
|
|
}
|
|
|
|
// Clears one or more status flags.
|
|
void clearFlag(T)(T cpu, Flag[] flags...)
|
|
if (isCpu!T)
|
|
{
|
|
auto reg = cpu.flag.toByte();
|
|
foreach (flag; flags) reg &= ~flag;
|
|
cpu.flag.fromByte(reg);
|
|
}
|
|
|
|
void clearFlag(T : CpuInfo)(ref T cpu, Flag[] flags...)
|
|
{
|
|
foreach (flag; flags) cpu.S &= ~flag;
|
|
}
|
|
|
|
// Returns a status flag.
|
|
bool getFlag(T)(T cpu, Flag f)
|
|
if (isCpu!T)
|
|
{
|
|
return (cpu.flag.toByte() & f) != 0;
|
|
}
|
|
|
|
bool getFlag(T : CpuInfo)(ref T cpu, Flag f)
|
|
{
|
|
return (cpu.S & f) != 0;
|
|
}
|
|
|
|
|
|
// Sets the status register from a byte.
|
|
void setStatus(T)(T cpu, int val)
|
|
if (isCpu!T)
|
|
{
|
|
cpu.flag.fromByte(cast(ubyte)val);
|
|
}
|
|
|
|
void setStatus(T : CpuInfo)(ref T cpu, int val)
|
|
{
|
|
cpu.S = cast(ubyte)val | 0x30;
|
|
}
|
|
|
|
// Returns the status register as a byte.
|
|
ubyte getStatus(T)(T cpu)
|
|
if (isCpu!T)
|
|
{
|
|
return cpu.flag.toByte();
|
|
}
|
|
|
|
ubyte getStatus(T : CpuInfo)(ref T cpu)
|
|
{
|
|
return cpu.S | 0x30;
|
|
}
|
|
|
|
// Sets or clears a single status flag.
|
|
void updateFlag(T)(ref T cpu, Flag f, bool val)
|
|
if (isCpu!T || is(T == CpuInfo))
|
|
{
|
|
if (val)
|
|
setFlag(cpu, f);
|
|
else
|
|
clearFlag(cpu, f);
|
|
}
|
|
|
|
void setNZ(T : CpuInfo)(ref T cpu, ubyte val)
|
|
{
|
|
updateFlag(cpu, Flag.Z, (val == 0));
|
|
updateFlag(cpu, Flag.N, (val >= 0x80));
|
|
}
|
|
|
|
// Sets or clears the flag required for a given opcode to branch.
|
|
void expectBranch(T)(ref T cpu, ubyte opcode)
|
|
if (isCpu!T || is(T == CpuInfo))
|
|
{
|
|
switch (opcode)
|
|
{
|
|
case /*BPL*/ 0x10: clearFlag(cpu, Flag.N); break;
|
|
case /*BMI*/ 0x30: setFlag(cpu, Flag.N); break;
|
|
case /*BVC*/ 0x50: clearFlag(cpu, Flag.V); break;
|
|
case /*BVS*/ 0x70: setFlag(cpu, Flag.V); break;
|
|
case /*BRA*/ 0x80: break;
|
|
case /*BCC*/ 0x90: clearFlag(cpu, Flag.C); break;
|
|
case /*BCS*/ 0xB0: setFlag(cpu, Flag.C); break;
|
|
case /*BNE*/ 0xD0: clearFlag(cpu, Flag.Z); break;
|
|
case /*BEQ*/ 0xF0: setFlag(cpu, Flag.Z); break;
|
|
default:
|
|
enforce(0, format("not a branching opcpde %0.2X", opcode));
|
|
}
|
|
}
|
|
|
|
// Returns whether an opcode would branch if executed.
|
|
bool wouldBranch(T)(ref T cpu, ubyte opcode)
|
|
if (isCpu!T || is(T == CpuInfo))
|
|
{
|
|
switch (opcode)
|
|
{
|
|
case /*BPL*/ 0x10: return !getFlag(cpu, Flag.N);
|
|
case /*BMI*/ 0x30: return getFlag(cpu, Flag.N);
|
|
case /*BVC*/ 0x50: return !getFlag(cpu, Flag.V);
|
|
case /*BVS*/ 0x70: return getFlag(cpu, Flag.V);
|
|
case /*BRA*/ 0x80: return true;
|
|
case /*BCC*/ 0x90: return !getFlag(cpu, Flag.C);
|
|
case /*BCS*/ 0xB0: return getFlag(cpu, Flag.C);
|
|
case /*BNE*/ 0xD0: return !getFlag(cpu, Flag.Z);
|
|
case /*BEQ*/ 0xF0: return getFlag(cpu, Flag.Z);
|
|
default:
|
|
assert(0, format("not a branching opcpde %0.2X", opcode));
|
|
}
|
|
}
|
|
|
|
// Sets or clears the flag required for a given opcode to not branch.
|
|
void expectNoBranch(T)(ref T cpu, ubyte opcode)
|
|
if (isCpu!T || is(T == CpuInfo))
|
|
{
|
|
switch (opcode)
|
|
{
|
|
case /*BPL*/ 0x10: setFlag(cpu, Flag.N); break;
|
|
case /*BMI*/ 0x30: clearFlag(cpu, Flag.N); break;
|
|
case /*BVC*/ 0x50: setFlag(cpu, Flag.V); break;
|
|
case /*BVS*/ 0x70: clearFlag(cpu, Flag.V); break;
|
|
case /*BCC*/ 0x90: setFlag(cpu, Flag.C); break;
|
|
case /*BCS*/ 0xB0: clearFlag(cpu, Flag.C); break;
|
|
case /*BNE*/ 0xD0: setFlag(cpu, Flag.Z); break;
|
|
case /*BEQ*/ 0xF0: clearFlag(cpu, Flag.Z); break;
|
|
default:
|
|
if (isCMOS!T)
|
|
enforce(opcode != 0x80, "BRA can never not branch");
|
|
enforce(0, format("not a branching opcpde %0.2X", opcode));
|
|
}
|
|
}
|
|
|
|
|
|
// Constructs an address from its low and high bytes.
|
|
ushort address(ubyte l, ubyte h)
|
|
{
|
|
return cast(ushort)((h << 8) | l);
|
|
}
|
|
|
|
|
|
ubyte addrLo(ushort addr)
|
|
{
|
|
return cast(ubyte)(addr & 0xFF);
|
|
}
|
|
|
|
ubyte addrHi(ushort addr)
|
|
{
|
|
return cast(ubyte)(addr >> 8);
|
|
}
|
|
|
|
|
|
/*
|
|
* Adds an offset to an address, resulting in an address in the same
|
|
* page.
|
|
*/
|
|
ushort pageWrapAdd(ushort base, int offset)
|
|
{
|
|
return (base & 0xFF00) + cast(ubyte)((base & 0xFF) + offset);
|
|
}
|
|
|
|
|
|
/*
|
|
* Adds an offset to an address, possibly resulting in an address in a
|
|
* different page.
|
|
*/
|
|
ushort pageCrossAdd(ushort base, int offset)
|
|
{
|
|
return cast(ushort)(base + offset);
|
|
}
|
|
|
|
|
|
// A random value to use for "uninitialized" memory.
|
|
ubyte XX()
|
|
{
|
|
return cast(ubyte)uniform(0, 256);
|
|
}
|
|
|
|
// A number different from some other number.
|
|
ubyte notXX(ubyte val)
|
|
{
|
|
return cast(ubyte)~val;
|
|
}
|
|
|
|
|
|
class TestException : Exception { this(string msg) { super(msg); } }
|