twoapple-reboot/test/base.d

3337 lines
83 KiB
D

module test.base;
import std.algorithm, std.array, std.conv, std.exception, std.stdio,
std.string;
import test.cpu, test.opcodes;
/*
* Emulates zero page, stack, and 3 additional pages of "main memory"
* starting at a user-defined address. Accesses outside the defined
* address space raise an exception.
*
* The contents are initialized to 0xFF. Individual locations can be
* read and written using array index syntax.
*/
struct TestMemory
{
private:
ubyte[0x200] data1 = 0xFF;
ubyte[0x300] data2 = 0xFF;
ushort data2_base;
size_t data2_max;
public:
/*
* Constructs a TestMemory with data filled in from an array of
* Blocks.
*
* The blocks do not need to be contiguous, or ordered by their
* base address, but note that the base of the 3-page "main
* memory" will be the start of the page that contains the first
* block with a base address greater than 0x01FF (there must be at
* least one such block).
*/
this(const(Block[]) blocks ...)
{
foreach (block; blocks)
{
auto base = block.base;
auto data = block.data;
if (base < 0x200)
{
enforce(base + data.length <= 0x200,
format("Address out of bounds %0.4x", base));
data1[base..base + data.length] = data[];
}
else
{
if (!data2_base)
{
if (base > 0xFD00)
data2_base = 0xFD00;
else
data2_base = base & 0xFF00;
data2_max = data2_base + 0x300;
}
enforce(base + data.length <= data2_max,
format("Address out of bounds %0.4x", base));
auto last = base + data.length;
data2[base-data2_base..last-data2_base] = data[];
}
}
enforce(data2_base, "Missing memory > 0x0200");
}
ubyte read(ushort addr) const
{
if (addr < 0x0200)
return data1[addr];
else if (addr >= data2_base && addr < data2_max)
return data2[addr - data2_base];
else
throw new TestException(format("read %0.4x", addr));
}
void write(ushort addr, ubyte val)
{
if (addr < 0x0200)
data1[addr] = val;
else if (addr >= data2_base && addr < data2_max)
data2[addr - data2_base] = val;
else
throw new TestException(format("write %0.4x", addr));
}
ubyte opIndex(size_t i1) const
{
auto addr = cast(ushort)i1;
enforce(addr < 0x0200 || (addr >= data2_base && addr < data2_max),
"Read out of bounds");
return read(addr);
}
ubyte opIndexAssign(ubyte val, size_t i1)
{
auto addr = cast(ushort)i1;
enforce(addr < 0x0200 || (addr >= data2_base && addr < data2_max),
"Write out of bounds");
write(addr, val);
return val;
}
}
// A block of memory with a given base address.
struct Block
{
const ushort base;
const(ubyte[]) data;
string toString() const
{
return format("Block(%0.4X, [%s])", base, formatMemory(data));
}
}
/*
* Formats data as a string of 2-digit hex bytes, separated by spaces.
*
* If data is longer than max, the string will end with an indication
* of the number of extra bytes.
*/
string formatMemory(const(ubyte[]) data, size_t max = 3)
{
if (max > data.length) max = data.length;
auto hexbytes = map!(`format("%0.2X", a)`)(data[0..max]);
auto ret = join(array(hexbytes), " ");
if (data.length > max)
ret ~= format(" (%d more bytes)", data.length - max);
return ret;
}
struct OpInfo
{
ushort addr;
ubyte data;
int len;
bool write;
}
alias void delegate(ubyte, CpuInfo, Block[], OpInfo, string, TestSetup*)
testsetup;
class TestSetup
{
testsetup setup;
TestSetup next;
auto static opCall(testsetup d)
{
auto obj = new TestSetup();
obj.setup = d;
return obj;
}
void run(ubyte opcode, CpuInfo cpu = CpuInfo(), Block[] data = [],
OpInfo info = OpInfo(), string msg = "")
{
setup(opcode, cpu, data, info, msg, &next);
}
}
TestSetup connect(TestSetup first, TestSetup[] rest...)
{
if (!(rest.empty))
{
auto x = first;
while (x.next !is null) x = x.next;
x.next = connect(rest[0], rest[1..$]);
}
return first;
}
template testCallNext()
{
void callNext(string newMsg = "", Block[] newData = [])
{
if (*next !is null)
next.run(opcode, cpu, data ~ newData, info, msg ~ newMsg);
}
}
// Does nothing.
auto setup_none()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
if (*next !is null) next.run(opcode, cpu, data, info, msg);
}
return TestSetup(&setup);
}
// Aborts a test.
auto setup_op_abort()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
}
return TestSetup(&setup);
}
// Prints the current message.
auto setup_debug()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
import std.stdio;
writeln(format("%s\n data %s\n addr %0.4X op %0.2X %s",
msg, data, info.addr, opcode, cpu));
if (*next !is null) next.run(opcode, cpu, data, info, msg);
}
return TestSetup(&setup);
}
// Splits with the given flag set, then cleared.
auto setup_flag(Flag f)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
clearFlag(cpu, f);
callNext(format("%s0 ", flagToString(f)));
setFlag(cpu, f);
callNext(format("%s1 ", flagToString(f)));
}
return TestSetup(&setup);
}
// Splits with all flags set, then all flags cleared.
auto setup_mask_flags()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
clearFlag(cpu, Flag.C, Flag.Z, Flag.I, Flag.D, Flag.V, Flag.N);
callNext("F0 ");
setFlag(cpu, Flag.C, Flag.Z, Flag.I, Flag.D, Flag.V, Flag.N);
callNext("F1 ");
}
return TestSetup(&setup);
}
/*
* Used after setup_mask_flags, guarantees coverage of all
* combinations of N and Z.
*/
auto setup_nz()
{
return setup_flag(Flag.N);
}
/*
* Can be used after setup_mask_flags to cover all combinations of
* flags.
*/
auto setup_nvdzc()
{
return connect(setup_flag(Flag.N), setup_flag(Flag.Z),
setup_flag(Flag.V), setup_flag(Flag.D),
setup_flag(Flag.C));
}
// Splits with info.data 0x00, 0x40, and 0x80.
auto setup_data()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.data = 0x00;
callNext("zero ");
info.data = 0x40;
callNext("positive ");
info.data = 0x80;
callNext("negative ");
}
return connect(TestSetup(&setup));
}
/*
* For register transfer opcodes.
*
* Splits on 0x00, 0x40, and 0x80 in the source register.
*/
auto setup_op_transfer(Reg source, Reg dest)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
ubyte other = (info.data > 0x00 && info.data < 0x80) ? 0x00 : 0x01;
setX(cpu, other); setY(cpu, other); setA(cpu, other);
setReg(cpu, source, info.data);
callNext("xfer ");
}
return connect(setup_nz(), setup_data(), TestSetup(&setup));
}
// For TXS and TSX.
auto setup_op_stack_xfer()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setSP(cpu, 0x00);
setX(cpu, 0xFF);
callNext("TXS ");
}
return connect(setup_nz(), TestSetup(&setup));
}
/*
* For implied address mode.
*
* Sets PC to 0x1000 and puts a (1-byte) opcode in memory.
*/
auto setup_addr_implied()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setPC(cpu, 0x1000);
info.len = 1;
callNext("", [Block(0x1000, [opcode])]);
}
return TestSetup(&setup);
}
/*
* For immediate address mode.
*
* Sets PC to 0x1000 and info.addr to 0x1001.
*/
auto setup_addr_imm()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setPC(cpu, 0x1000);
info.len = 2;
info.addr = 0x1001;
callNext("imm ", [Block(0x1000, [opcode])]);
}
return TestSetup(&setup);
}
/*
* For branch opcodes.
*/
auto setup_addr_branch(bool cmos)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
if (!cmos || opcode != 0x80)
{
expectNoBranch(cpu, opcode);
setPC(cpu, 0x1000);
info.addr = 0x1002;
callNext("no-branch ", [Block(0x1000, [opcode, 0x10])]);
}
expectBranch(cpu, opcode);
setPC(cpu, 0x1000);
info.addr = 0x1012;
callNext("branch-fwd ", [Block(0x1000, [opcode, 0x10])]);
setPC(cpu, 0x1081);
info.addr = 0x1102;
callNext("branch-fwd-px ", [Block(0x1081, [opcode, 0x7F])]);
setPC(cpu, 0x1000);
info.addr = 0x1000;
callNext("branch-bkwd ", [Block(0x1000, [opcode, 0xFE])]);
setPC(cpu, 0x1100);
info.addr = 0x10F7;
callNext("branch-bkwd-px ", [Block(0x1000, []),
Block(0x1100, [opcode, 0xF5])]);
}
return TestSetup(&setup);
}
/*
* For zeropage adddress mode.
*
* Sets PC to 0x1000 and puts [opcode, 0x70] in memory.
*/
auto setup_addr_zpg()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setPC(cpu, 0x1000);
info.len = 2;
info.addr = 0x0070;
callNext("zpg ", [Block(0x1000, [opcode, 0x70])]);
}
return TestSetup(&setup);
}
/*
* For absolute address mode.
*
* Sets PC to 0x1000 and puts [opcode, 0xC5, 0x10] in memory.
*/
auto setup_addr_abs()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setPC(cpu, 0x1000);
info.len = 3;
info.addr = 0x10C5;
callNext("abs ", [Block(0x1000, [opcode, 0xC5, 0x10])]);
}
return TestSetup(&setup);
}
/*
* For zeropage,x/y address modes.
*
* Sets PC to 0x1000 and puts [opcode, 0x70] in memory.
*/
auto setup_addr_zpxy(Reg reg)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
string name = (reg == Reg.X ? "zpx " : "zpy ");
info.len = 2;
setPC(cpu, 0x1000);
setX(cpu, 0x10); setY(cpu, 0x10); setA(cpu, 0x10);
setReg(cpu, reg, 0x00);
info.addr = pageWrapAdd(0x0070, 0x00);
callNext(name ~ "no-idx ", [Block(0x1000, [opcode, 0x70])]);
setX(cpu, 0x10); setY(cpu, 0x10); setA(cpu, 0x10);
setReg(cpu, reg, 0x20);
info.addr = pageWrapAdd(0x0070, 0x20);
callNext(name ~ "no-wrap ", [Block(0x1000, [opcode, 0x70])]);
setX(cpu, 0x10); setY(cpu, 0x10); setA(cpu, 0x10);
setReg(cpu, reg, 0xA0);
info.addr = pageWrapAdd(0x0070, 0xA0);
callNext(name ~ "wrap ", [Block(0x1000, [opcode, 0x70])]);
}
return TestSetup(&setup);
}
// For absolute,x/y address modes.
auto setup_addr_abxy(Reg reg)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
string name = (reg == Reg.X ? "abx " : "aby ");
info.len = 3;
setPC(cpu, 0x1000);
setX(cpu, 0x10); setY(cpu, 0x10); setA(cpu, 0x10);
setReg(cpu, reg, 0x00);
info.addr = pageCrossAdd(0x10C5, 0x00);
callNext(name ~ "no-idx ", [Block(0x1000, [opcode, 0xC5, 0x10])]);
setX(cpu, 0x10); setY(cpu, 0x10); setA(cpu, 0x10);
setReg(cpu, reg, 0x20);
info.addr = pageCrossAdd(0x10C5, 0x20);
callNext(name ~ "no-px ", [Block(0x1000, [opcode, 0xC5, 0x10])]);
setX(cpu, 0x10); setY(cpu, 0x10); setA(cpu, 0x10);
setReg(cpu, reg, 0x50);
info.addr = pageCrossAdd(0x10C5, 0x50);
callNext(name ~ "px ", [Block(0x1000, [opcode, 0xC5, 0x10])]);
}
return TestSetup(&setup);
}
// For zeropage indirect,x address mode.
auto setup_addr_izx()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.addr = 0x10C5;
info.len = 2;
setPC(cpu, 0x1000);
setA(cpu, 0x01); setY(cpu, 0x01);
setX(cpu, 0x20);
callNext("izx no-wrap ", [Block(0x1000, [opcode, 0x70]),
Block(0x0090, [0xC5, 0x10])]);
setX(cpu, 0xA0);
callNext("izx wrap ", [Block(0x1000, [opcode, 0x70]),
Block(0x0010, [0xC5, 0x10])]);
setX(cpu, 0x8F);
callNext("izx px ", [Block(0x1000, [opcode, 0x70]),
Block(0x00FF, [0xC5]),
Block(0x0000, [0x10])]);
}
return TestSetup(&setup);
}
// For zeropage indirect,y address mode.
auto setup_addr_izy()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.len = 2;
setPC(cpu, 0x1000);
setA(cpu, 0x01); setX(cpu, 0x01);
info.addr = pageCrossAdd(0x10C5, 0x20);
setY(cpu, 0x20);
callNext("izy no-wrap ", [Block(0x1000, [opcode, 0x70]),
Block(0x0070, [0xC5, 0x10])]);
callNext("izy wrap ", [Block(0x1000, [opcode, 0xFF]),
Block(0x00FF, [0xC5]),
Block(0x0000, [0x10])]);
info.addr = pageCrossAdd(0x10C5, 0x50);
setY(cpu, 0x50);
callNext("izy px ", [Block(0x1000, [opcode, 0x70]),
Block(0x0070, [0xC5, 0x10])]);
}
return TestSetup(&setup);
}
// For zeropage indirect address mode.
auto setup_addr_zpi()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.len = 2;
info.addr = 0x10C5;
setPC(cpu, 0x1000);
callNext("zpi no-wrap ", [Block(0x1000, [opcode, 0x70]),
Block(0x0070, [0xC5, 0x10])]);
callNext("zpi wrap ", [Block(0x1000, [opcode, 0xFF]),
Block(0x00FF, [0xC5]),
Block(0x0000, [0x10])]);
}
return TestSetup(&setup);
}
/*
* Splits with the SP at 0xFF, and then at 0x00 (to cover the case of
* pull operations when the stack is "empty").
*/
auto setup_pull_wrap()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setSP(cpu, 0xFF);
callNext("wrap ");
setSP(cpu, 0x00);
callNext("no-wrap ");
}
return TestSetup(&setup);
}
/*
* For pull opcodes.
*
* Splits with 0x00, 0x40, and 0x80 at the top of the stack (to
* exercise different potential combinations of N and Z).
*/
auto setup_op_pull(Reg reg)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
auto sp = pageWrapAdd(getSP(cpu), 1);
ubyte other = (info.data > 0x00 && info.data < 0x80) ? 0x00 : 0x01;
setX(cpu, other); setY(cpu, other); setA(cpu, other);
callNext("pull ", [Block(sp, [info.data])]);
}
return connect(setup_pull_wrap(), setup_nz(), setup_data(),
TestSetup(&setup));
}
/*
* Splits with each possible meaningful status byte value on the
* stack, and with values with break/reserved bits set.
*/
auto setup_status()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 16; j++)
{
info.data = cast(ubyte)(i * 0x40 + j);
callNext(format("S %0.2X ", info.data | 0x30));
}
}
string[] msgs = ["B ", "R ", "BR "];
foreach (val; [0x10, 0x20, 0x30])
{
info.data = cast(ubyte)val;
callNext("S " ~ msgs[(info.data >> 4) - 1]);
}
}
return TestSetup(&setup);
}
// For PLP.
auto setup_op_PLP()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
auto sp = pageWrapAdd(getSP(cpu), 1);
setStatus(cpu, ~info.data);
callNext("PLP ", [Block(sp, [info.data])]);
}
return connect(setup_pull_wrap(), setup_status(), TestSetup(&setup));
}
/*
* Splits with the SP at 0x00, and then at 0xFF (to cover the case of
* push operations when the stack is "full").
*/
auto setup_push_wrap()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setSP(cpu, 0x00);
callNext("wrap ");
setSP(cpu, 0xFF);
callNext("no-wrap ");
}
return TestSetup(&setup);
}
/*
* For push opcodes.
*
* Splits with 0x00, 0x40, and 0x80 in the appropriate register.
*/
auto setup_op_push(Reg reg)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setX(cpu, 0x01); setY(cpu, 0x01); setA(cpu, 0x01);
setReg(cpu, reg, info.data);
callNext("push ", [Block(getSP(cpu), [~info.data])]);
}
return connect(setup_push_wrap(), setup_data(), TestSetup(&setup));
}
/*
* For PHP.
*
* Puts a garbage value (that does not correspond to the current
* status) just below the top of the stack.
*/
auto setup_op_PHP()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.data = getStatus(cpu);
callNext("PHP ", [Block(getSP(cpu), [~info.data])]);
}
return connect(setup_nvdzc(), setup_push_wrap(), TestSetup(&setup));
}
/*
* For load opcodes.
*
* Splits with 0x00, 0x40, and 0x80 in the source memory location.
*/
auto setup_op_load(Reg reg)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
callNext("load ", [Block(info.addr, [info.data])]);
}
return connect(setup_nz(), setup_data(), TestSetup(&setup));
}
// Splits with 0x00, 0x40, 0x80 in the source register.
auto setup_op_store(Reg reg)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setReg(cpu, reg, info.data);
callNext("store ", [Block(info.addr, [~info.data])]);
}
return connect(setup_data(), TestSetup(&setup));
}
// For SAX.
auto setup_op_SAX()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
if (opcode == 0x83)
{
setA(cpu, 0x00);
callNext("zero SAX ");
setA(cpu, 0x2B);
callNext("positive SAX ");
setA(cpu, 0x8B);
callNext("negative SAX ");
}
else
{
setA(cpu, 0x00); setX(cpu, 0x00);
callNext("zero SAX ");
setA(cpu, 0x10); setX(cpu, 0x01);
callNext("zero SAX ");
setA(cpu, 0x0B); setX(cpu, 0x0D);
callNext("positive SAX ");
setA(cpu, 0x8B); setX(cpu, 0x8D);
callNext("negative SAX ");
}
}
return TestSetup(&setup);
}
// For STZ.
auto setup_op_STZ()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.data = 0;
callNext("STZ ", [Block(info.addr, [~info.data])]);
}
return TestSetup(&setup);
}
// setup_bit_val, puts in memory;
auto setup_bit_data()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.data = 0x80;
callNext("N1 ", [Block(info.addr, [info.data])]);
info.data = 0x40;
callNext("V1 ", [Block(info.addr, [info.data])]);
info.data = 0xC0;
callNext("N1V1 ", [Block(info.addr, [info.data])]);
info.data = 0x20;
callNext("N0V0 ", [Block(info.addr, [info.data])]);
}
return TestSetup(&setup);
}
// For BIT.
auto setup_op_BIT()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setA(cpu, info.data);
callNext("BIT non-zero ");
setA(cpu, ~info.data);
callNext("BIT zero");
}
return connect(setup_nz(), setup_flag(Flag.V), setup_bit_data(),
TestSetup(&setup));
}
// For TRB.
auto setup_op_TRB()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setA(cpu, 0); setX(cpu, 0xFF); setY(cpu, 0xFF);
info.data = 0x01;
callNext("TRB Z1 1 ", [Block(info.addr, [info.data])]);
setA(cpu, 0x40); setX(cpu, 0x00); setY(cpu, 0x00);
info.data = 0x40;
callNext("TRB Z0 0 ", [Block(info.addr, [info.data])]);
setA(cpu, 0x40); setX(cpu, 0x00); setY(cpu, 0x00);
info.data = 0x41;
callNext("TRB Z0 1 ", [Block(info.addr, [info.data])]);
}
return TestSetup(&setup);
}
// For TSB.
auto setup_op_TSB()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setA(cpu, 0x01); setX(cpu, 0x00); setY(cpu, 0x00);
info.data = 0x00;
callNext("TSB Z1 1 ", [Block(info.addr, [info.data])]);
setA(cpu, 0x02); setX(cpu, 0x00); setY(cpu, 0x00);
info.data = 0x02;
callNext("TSB Z0 2 ", [Block(info.addr, [info.data])]);
setA(cpu, 0x30); setX(cpu, 0x00); setY(cpu, 0x00);
info.data = 0x80;
callNext("TSB Z1 B0 ", [Block(info.addr, [info.data])]);
setA(cpu, 0x00); setX(cpu, 0xFF); setY(cpu, 0xFF);
info.data = 0x02;
callNext("TSB Z1 2 ", [Block(info.addr, [info.data])]);
}
return TestSetup(&setup);
}
// For BRK.
auto setup_op_BRK()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.addr = 0xFE55;
setPC(cpu, 0xFD00);
ushort ret = 0xFD02;
auto sp = getSP(cpu);
auto sp1 = pageWrapAdd(sp, -1);
auto sp2 = pageWrapAdd(sp, -2);
info.data = getStatus(cpu);
callNext("BRK ", [Block(0xFD00, [opcode]),
Block(sp, [~addrHi(ret)]),
Block(sp1, [~addrLo(ret)]),
Block(sp2, [~info.data]),
Block(0xFFFE, [0x55, 0xFE])]);
}
return connect(setup_nvdzc(), setup_push_wrap(), TestSetup(&setup));
}
auto setup_RTx(bool isRTI)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
string name = (isRTI ? "RTI " : "RTS ");
info.addr = 0x1211;
setPC(cpu, 0x1000);
auto sp = getSP(cpu);
auto sp1 = pageWrapAdd(sp, (isRTI ? 1 : 0));
auto sp2 = pageWrapAdd(sp, (isRTI ? 2 : 1));
auto sp3 = pageWrapAdd(sp, (isRTI ? 3 : 2));
callNext(name, [Block(0x1000, [opcode])] ~
(isRTI ? [Block(sp1, [info.data])] : []) ~
[Block(sp2, [addrLo(info.addr)]),
Block(sp3, [addrHi(info.addr)])]);
}
return connect(setup_pull_wrap(), TestSetup(&setup));
}
// For RTI.
auto setup_op_RTI()
{
return connect(setup_status(), setup_RTx(true));
}
// For RTS.
auto setup_op_RTS()
{
return setup_RTx(false);
}
// For JMP inx.
auto setup_op_JMP_inx()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.addr = 0x1234;
info.len = 3;
setPC(cpu, 0x1000);
setX(cpu, 0x20);
callNext("JMP inx A ", [Block(0x1000, [opcode, 0xC5, 0x10]),
Block(0x10E5, [0x34, 0x12])]);
setX(cpu, 0x40);
callNext("JMP inx B ", [Block(0x1000, [opcode, 0xC5, 0x10]),
Block(0x1105, [0x34, 0x12])]);
setX(cpu, 0x30);
callNext("JMP inx C ", [Block(0x1000, [opcode, 0xCF, 0x10]),
Block(0x10FF, [0x34, 0x12])]);
}
return TestSetup(&setup);
}
// For JMP ind.
auto setup_op_JMP_ind(bool cmos)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.addr = 0x1234;
info.len = 3;
setPC(cpu, 0x1000);
setX(cpu, 0x40); setY(cpu, 0x40); setA(cpu, 0x40);
callNext("JMP ind px ", [Block(0x1000, [opcode, 0xFF, 0x11])] ~
(cmos ? [Block(0x11FF, [0x34, 0x12])]
: [Block(0x11FF, [0x34]),
Block(0x1100, [0x12])]));
callNext("JMP ind no px ", [Block(0x1000, [opcode, 0xC5, 0x10]),
Block(0x10C5, [0x34, 0x12])]);
}
return TestSetup(&setup);
}
// For JSR.
auto setup_op_JSR()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.addr = 0x10C5;
setPC(cpu, 0x1000);
ushort ret = 0x1002;
auto sp = getSP(cpu);
auto sp1 = pageWrapAdd(sp, 1);
callNext("JSR ", [Block(0x1000, [opcode, 0xC5, 0x10]),
Block(sp, [~addrHi(ret)]),
Block(sp, [~addrLo(ret)])]);
}
return connect(setup_push_wrap(), TestSetup(&setup));
}
// For CMOS opcode 5C.
auto setup_op_5C()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.len = 3;
setPC(cpu, 0xFD00);
callNext("NOP8 ", [Block(0xFD00, [0x5C, 0x72])]);
}
return TestSetup(&setup);
}
auto setup_dec_data()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.data = 0x01;
callNext("zero ");
info.data = 0x00;
callNext("negative ");
info.data = 0x80;
callNext("positive ");
}
return TestSetup(&setup);
}
// For DEX, DEY, DEA.
auto setup_op_DEC_reg(Reg reg)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setX(cpu, 0x40); setY(cpu, 0x40); setA(cpu, 0x40);
setReg(cpu, reg, info.data);
callNext("DEC r ", [Block(0x1000, [opcode])]);
}
return connect(setup_nz(), setup_dec_data(), TestSetup(&setup));
}
// For DEC.
auto setup_op_DEC()
{
return setup_rmw(false, "DEC ", setup_dec_data());
}
auto setup_inc_data()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.data = 0xFF;
callNext("zero ");
info.data = 0x7F;
callNext("negative ");
info.data = 0x00;
callNext("positive ");
}
return TestSetup(&setup);
}
// For INX, INY, INA
auto setup_op_INC_reg(Reg reg)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setX(cpu, 0x40); setY(cpu, 0x40); setA(cpu, 0x40);
setReg(cpu, reg, info.data);
callNext("INC r ", [Block(0x1000, [opcode])]);
}
return connect(setup_nz(), setup_inc_data(), TestSetup(&setup));
}
// For INC.
auto setup_op_INC()
{
return setup_rmw(false, "INC ", setup_inc_data());
}
auto setup_rmw(bool isAcc, string name, TestSetup data_setup)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
if (isAcc)
{
setA(cpu, info.data); setX(cpu, 0x10); setY(cpu, 0x10);
callNext(name ~ "a ");
}
else
{
callNext(name, [Block(info.addr, [info.data])]);
}
}
return connect(setup_nz(), data_setup, TestSetup(&setup));
}
// For ROL.
auto setup_op_ROL(bool isAcc)
{
return setup_rmw(isAcc, "ROL ", setup_data());
}
auto setup_asl_data()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.data = 0x00;
callNext("zero ");
info.data = 0x80;
callNext("zero carry ");
info.data = 0x40;
callNext("negative ");
info.data = 0x01;
callNext("positive ");
}
return TestSetup(&setup);
}
// For ASL.
auto setup_op_ASL(bool isAcc)
{
return setup_rmw(isAcc, "ASL ", setup_asl_data());
}
auto setup_right_data()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
info.data = 0x01;
callNext("0x01 ");
info.data = 0x80;
callNext("0x80 ");
info.data = 0x00;
callNext("0x00 ");
}
return TestSetup(&setup);
}
// For ROR.
auto setup_op_ROR(bool isAcc)
{
return setup_rmw(isAcc, "ROR ", setup_right_data());
}
// For LSR.
auto setup_op_LSR(bool isAcc)
{
return setup_rmw(isAcc, "LSR ", setup_right_data());
}
// For ASO.
auto setup_op_ASO()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setA(cpu, 0x00);
callNext("acc 0x00 ");
setA(cpu, 0xFF);
callNext("acc 0xFF ");
setA(cpu, 0x20);
callNext("acc 0x20 ");
}
return connect(TestSetup(&setup),
setup_rmw(false, "ASO ", setup_asl_data()));
}
// For RLA.
auto setup_op_RLA()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setA(cpu, 0x00);
callNext("acc 0x00 ");
setA(cpu, 0xFF);
callNext("acc 0xFF ");
}
return connect(TestSetup(&setup),
setup_rmw(false, "RLA ", setup_data()));
}
// For LSE.
auto setup_op_LSE()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setA(cpu, 0x00);
callNext("acc 0x00 ");
setA(cpu, 0xFF);
callNext("acc 0xFF ");
}
return connect(TestSetup(&setup),
setup_rmw(false, "LSE ", setup_right_data()));
}
/*
* For ADC.
*
* This opcode is extensively tested elsewhere.
*/
auto setup_op_ADC(bool cmos)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setA(cpu, 0x99);
info.data = 0x01;
callNext("ADC 1", [Block(info.addr, [info.data])]);
setA(cpu, 0x5B);
info.data = 0x46;
callNext("ADC 2", [Block(info.addr, [info.data])]);
setA(cpu, 0x50);
info.data = 0x60;
callNext("ADC 3", [Block(info.addr, [info.data])]);
setA(cpu, 0x90);
info.data = 0x90;
callNext("ADC 4", [Block(info.addr, [info.data])]);
setA(cpu, 0x90);
info.data = 0x0F;
callNext("ADC 5", [Block(info.addr, [info.data])]);
}
return connect(setup_nz(), setup_flag(Flag.C), setup_flag(Flag.D),
TestSetup(&setup));
}
// For RRA.
auto setup_op_RRA()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setA(cpu, 0x00);
callNext("acc 0x00 ");
setA(cpu, 0x10);
callNext("acc 0x10 ");
setA(cpu, 0xBF);
callNext("acc 0xBF ");
}
return connect(TestSetup(&setup), setup_flag(Flag.C), setup_flag(Flag.D),
setup_rmw(false, "RRA ", setup_right_data()));
}
/*
* For SBC.
*
* This opcode is extensively tested elsewhere.
*/
auto setup_op_SBC(bool cmos)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setA(cpu, 0x00);
info.data = 0x01;
callNext("SBC 1", [Block(info.addr, [info.data])]);
setA(cpu, 0x46);
info.data = 0x12;
callNext("SBC 2", [Block(info.addr, [info.data])]);
setA(cpu, 0x50);
info.data = 0x60;
callNext("SBC 3", [Block(info.addr, [info.data])]);
setA(cpu, 0x32);
info.data = 0x02;
callNext("SBC 4", [Block(info.addr, [info.data])]);
setA(cpu, 0x90);
info.data = 0x0F;
callNext("SBC 5", [Block(info.addr, [info.data])]);
}
return connect(setup_nz(), setup_flag(Flag.C), setup_flag(Flag.D),
TestSetup(&setup));
}
// For INS.
auto setup_op_INS()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setA(cpu, 0x00);
callNext("acc 0x00 ");
setA(cpu, 0x10);
callNext("acc 0x10 ");
setA(cpu, 0xBF);
callNext("acc 0xBF ");
}
return connect(TestSetup(&setup), setup_flag(Flag.C), setup_flag(Flag.D),
setup_rmw(false, "INS ", setup_inc_data()));
}
// For CMP, CPY, CPX.
auto setup_op_cmp(Reg reg)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setReg(cpu, reg, 0x10);
info.data = 0x20;
callNext("compare <", [Block(info.addr, [info.data])]);
setReg(cpu, reg, 0x10);
info.data = 0x10;
callNext("compare =", [Block(info.addr, [info.data])]);
setReg(cpu, reg, 0x20);
info.data = 0x10;
callNext("compare >", [Block(info.addr, [info.data])]);
}
return connect(setup_nz(), TestSetup(&setup));
}
// For DCM.
auto setup_op_DCM()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setA(cpu, 0x00);
callNext("acc 0x00 ");
setA(cpu, 0x10);
callNext("acc 0x10 ");
setA(cpu, 0xBF);
callNext("acc 0xBF ");
}
return connect(TestSetup(&setup), setup_flag(Flag.C),
setup_rmw(false, "DCM ", setup_dec_data()));
}
// For ORA.
auto setup_op_ORA()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setA(cpu, 0x00);
info.data = 0x00;
callNext("ORA zero", [Block(info.addr, [info.data])]);
setA(cpu, 0x10);
info.data = 0x01;
callNext("ORA positive", [Block(info.addr, [info.data])]);
setA(cpu, 0x80);
info.data = 0x00;
callNext("ORA negative", [Block(info.addr, [info.data])]);
}
return connect(setup_nz(),TestSetup(&setup));
}
// For AND.
auto setup_op_AND()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setA(cpu, 0x10);
info.data = 0x01;
callNext("AND zero", [Block(info.addr, [info.data])]);
setA(cpu, 0x0B);
info.data = 0x0D;
callNext("AND positive", [Block(info.addr, [info.data])]);
setA(cpu, 0x80);
info.data = 0x80;
callNext("AND negative", [Block(info.addr, [info.data])]);
}
return connect(setup_nz(),TestSetup(&setup));
}
// For EOR.
auto setup_op_EOR()
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
setA(cpu, 0xFF);
info.data = 0xFF;
callNext("EOR zero", [Block(info.addr, [info.data])]);
setA(cpu, 0x81);
info.data = 0x82;
callNext("EOR positive", [Block(info.addr, [info.data])]);
setA(cpu, 0x40);
info.data = 0x80;
callNext("EOR negative", [Block(info.addr, [info.data])]);
}
return connect(setup_nz(),TestSetup(&setup));
}
/*
* A range over the differences in two TestMemory structs. Each
* element is an address with up to eight bytes of subsequent data
* from the two structs.
*/
struct MemDiff
{
static struct DiffBlock
{
ushort base;
const(ubyte[]) a;
const(ubyte[]) b;
}
const(TestMemory*) a, b;
int i;
bool _empty, _emptyChecked;
this(const ref TestMemory a, const ref TestMemory b)
{
assert(a.data2_base == b.data2_base && a.data2_max == b.data2_max);
this.a = &a; this.b = &b;
}
auto front()
{
assert(_emptyChecked && !_empty);
if (i < 0x200)
{
auto extent = min(8, 0x200 - i);
return DiffBlock(cast(ushort)i,
a.data1[i..i+extent].dup,
b.data1[i..i+extent].dup);
}
else
{
auto extent = min(8, a.data2_max - i);
auto di = i - a.data2_base;
return DiffBlock(cast(ushort)i,
a.data2[di..di+extent].dup,
b.data2[di..di+extent].dup);
}
assert(0);
}
void popFront()
{
assert(_emptyChecked && !_empty);
if (i < 0x200)
i += min(8, 0x200 - i);
else
i += min(8, a.data2_max - i);
}
bool empty()
{
_emptyChecked = true;
while (i < 0x200)
{
if (a.data1[i] != b.data1[i]) { _empty = false; return false; }
i++;
}
if (i < a.data2_base) i = a.data2_base;
while (i < a.data2_max)
{
auto di = i - a.data2_base;
if (a.data2[di] != b.data2[di]) { _empty = false; return false; }
i++;
}
_empty = true; return true;
}
}
struct Expected
{
CpuInfo cpu;
TestMemory mem;
}
alias void delegate(ref Expected, const OpInfo)
testexpect;
// Expects nothing to have changed.
auto expect_none()
{
void expect(ref Expected expected, const OpInfo info)
{
}
return &expect;
}
// For opcodes which modify a register and set N/Z.
void expect_basic(Reg reg, ubyte val, ref Expected expected, const OpInfo info)
{
with(expected)
{
setReg(cpu, reg, val);
setNZ(cpu, val);
incPC(cpu, info.len);
}
}
// For flag opcodes.
auto expect_flag(Flag f, bool val)
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
updateFlag(cpu, f, val);
incPC(cpu, info.len);
}
}
return &expect;
}
// For NOP.
auto expect_NOP()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected) { incPC(cpu, info.len); }
}
return &expect;
}
// For register transfer opcodes.
auto expect_transfer(Reg source, Reg dest)
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = getReg(expected.cpu, source);
expect_basic(dest, val, expected, info);
}
return &expect;
}
// For push opcodes.
auto expect_push(Reg reg)
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
mem[getSP(cpu)] = getReg(cpu, reg);
decSP(cpu);
incPC(cpu, info.len);
}
}
return &expect;
}
// For PHP.
auto expect_PHP()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
mem[getSP(cpu)] = getStatus(cpu);
decSP(cpu);
incPC(cpu, info.len);
}
}
return &expect;
}
// For pull opcodes.
auto expect_pull(Reg reg)
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
incSP(cpu);
auto val = mem[getSP(cpu)];
expect_basic(reg, val, expected, info);
}
}
return &expect;
}
// For PLP.
auto expect_PLP()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
incSP(cpu);
setStatus(cpu, mem[getSP(cpu)]);
incPC(cpu, info.len);
}
}
return &expect;
}
// For TSX.
auto expect_TSX()
{
void expect(ref Expected expected, const OpInfo info)
{
ubyte val = cast(ubyte)getSP(expected.cpu);
expect_basic(Reg.X, val, expected, info);
}
return &expect;
}
// For TXS.
auto expect_TXS()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
auto val = getX(cpu);
setSP(cpu, val);
incPC(cpu, info.len);
}
}
return &expect;
}
// For load opcodes.
auto expect_load(Reg reg)
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = expected.mem[info.addr];
expect_basic(reg, val, expected, info);
}
return &expect;
}
// For LAX (except immediate).
auto expect_LAX()
{
void expect(ref Expected expected, const OpInfo info)
{
expect_load(Reg.A)(expected, info);
with(expected) { setX(cpu, getA(cpu)); }
}
return &expect;
}
// For store opcodes.
auto expect_store(Reg reg)
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
mem[info.addr] = getReg(cpu, reg);
incPC(cpu, info.len);
}
}
return &expect;
}
// For SAX.
auto expect_SAX()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
mem[info.addr] = getA(cpu) & getX(cpu);
incPC(cpu, info.len);
}
}
return &expect;
}
// For STZ.
auto expect_STZ()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
mem[info.addr] = 0;
incPC(cpu, info.len);
}
}
return &expect;
}
// For branch opcodes.
auto expect_branch()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected) { setPC(cpu, info.addr); }
}
return &expect;
}
// For BIT.
auto expect_BIT()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
updateFlag(cpu, Flag.N, (info.data & 0x80) != 0);
updateFlag(cpu, Flag.V, (info.data & 0x40) != 0);
updateFlag(cpu, Flag.Z, (info.data & getA(cpu)) == 0);
incPC(cpu, info.len);
}
}
return &expect;
}
// For BIT.
auto expect_BIT_imm()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
updateFlag(cpu, Flag.Z, (info.data & getA(cpu)) == 0);
incPC(cpu, info.len);
}
}
return &expect;
}
// For TRB.
auto expect_TRB()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
updateFlag(cpu, Flag.Z, (info.data & getA(cpu)) == 0);
mem[info.addr] = ~getA(cpu) & info.data;
incPC(cpu, info.len);
}
}
return &expect;
}
auto expect_TSB()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
updateFlag(cpu, Flag.Z, (info.data & getA(cpu)) == 0);
mem[info.addr] = getA(cpu) | info.data;
incPC(cpu, info.len);
}
}
return &expect;
}
// For BRK.
auto expect_BRK()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
ushort ret = cast(ushort)(getPC(cpu) + 2);
mem[getSP(cpu)] = addrHi(ret);
decSP(cpu);
mem[getSP(cpu)] = addrLo(ret);
decSP(cpu);
mem[getSP(cpu)] = info.data;
decSP(cpu);
setPC(cpu, info.addr);
setFlag(cpu, Flag.I);
}
}
return &expect;
}
// For RTI.
auto expect_RTI()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
setStatus(cpu, info.data);
setPC(cpu, info.addr);
incSP(cpu, 3);
}
}
return &expect;
}
// For RTS.
auto expect_RTS()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
setPC(cpu, info.addr);
incSP(cpu, 2);
incPC(cpu, 1);
}
}
return &expect;
}
// For JMP.
auto expect_JMP()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
setPC(cpu, info.addr);
}
}
return &expect;
}
// For JSR.
auto expect_JSR()
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected)
{
auto ret = cast(ushort)(getPC(cpu) + 2);
setPC(cpu, info.addr);
mem[getSP(cpu)] = addrHi(ret);
decSP(cpu);
mem[getSP(cpu)] = addrLo(ret);
decSP(cpu);
}
}
return &expect;
}
void expect_rmw(bool isAcc, ubyte val, ref Expected expected,
const OpInfo info)
{
if (isAcc)
expect_basic(Reg.A, val, expected, info);
else
{
with(expected)
{
mem[info.addr] = val;
setNZ(cpu, val);
incPC(cpu, info.len);
}
}
}
// For DEX, DEY, DEA.
auto expect_DEC_reg(Reg reg)
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = cast(ubyte)(info.data - 1);
expect_basic(reg, val, expected, info);
}
return &expect;
}
// For DEC.
auto expect_DEC()
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = cast(ubyte)(info.data - 1);
expect_rmw(false, val, expected, info);
}
return &expect;
}
// For INX, INY, INA.
auto expect_INC_reg(Reg reg)
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = cast(ubyte)(info.data + 1);
expect_basic(reg, val, expected, info);
}
return &expect;
}
// For INC.
auto expect_INC()
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = cast(ubyte)(info.data + 1);
expect_rmw(false, val, expected, info);
}
return &expect;
}
// For ROL.
auto expect_ROL(bool isAcc)
{
void expect(ref Expected expected, const OpInfo info)
{
auto carry = (getFlag(expected.cpu, Flag.C) ? 1 : 0);
auto val = cast(ubyte)(info.data << 1 | carry);
updateFlag(expected.cpu, Flag.C, (info.data > 0x7F));
expect_rmw(isAcc, val, expected, info);
}
return &expect;
}
// For ASL.
auto expect_ASL(bool isAcc)
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = cast(ubyte)(info.data << 1);
updateFlag(expected.cpu, Flag.C, (info.data > 0x7F));
expect_rmw(isAcc, val, expected, info);
}
return &expect;
}
// For ROR.
auto expect_ROR(bool isAcc)
{
void expect(ref Expected expected, const OpInfo info)
{
auto carry = (getFlag(expected.cpu, Flag.C) ? 0x80 : 0);
auto val = cast(ubyte)(info.data >> 1 | carry);
updateFlag(expected.cpu, Flag.C, ((info.data & 0x01) != 0));
expect_rmw(isAcc, val, expected, info);
}
return &expect;
}
// For LSR.
auto expect_LSR(bool isAcc)
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = cast(ubyte)(info.data >> 1);
updateFlag(expected.cpu, Flag.C, ((info.data & 0x01) != 0));
expect_rmw(isAcc, val, expected, info);
}
return &expect;
}
// For ASO.
auto expect_ASO()
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = cast(ubyte)(info.data << 1);
updateFlag(expected.cpu, Flag.C, (info.data > 0x7F));
setA(expected.cpu, getA(expected.cpu) | val);
expect_rmw(false, val, expected, info);
setNZ(expected.cpu, getA(expected.cpu));
}
return &expect;
}
// For RLA.
auto expect_RLA()
{
void expect(ref Expected expected, const OpInfo info)
{
auto carry = (getFlag(expected.cpu, Flag.C) ? 1 : 0);
auto val = cast(ubyte)(info.data << 1 | carry);
updateFlag(expected.cpu, Flag.C, (info.data > 0x7F));
setA(expected.cpu, getA(expected.cpu) & val);
expect_rmw(false, val, expected, info);
setNZ(expected.cpu, getA(expected.cpu));
}
return &expect;
}
// For LSE.
auto expect_LSE()
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = cast(ubyte)(info.data >> 1);
updateFlag(expected.cpu, Flag.C, ((info.data & 0x01) != 0));
setA(expected.cpu, getA(expected.cpu) ^ val);
expect_rmw(false, val, expected, info);
setNZ(expected.cpu, getA(expected.cpu));
}
return &expect;
}
/*
* Slightly cheating--this is copied from the original implementation,
* but it was independently tested (see test_decimal.d).
*/
void expect_add(bool cmos, ubyte val, ref Expected expected)
{
with(expected)
{
ubyte acc = getA(cpu);
auto d = getFlag(cpu, Flag.D);
auto c = getFlag(cpu, Flag.C);
if (!d)
{
uint sum = acc + val + c;
updateFlag(cpu, Flag.V,
(!((acc ^ val) & 0x80)) && ((val ^ sum) & 0x80));
updateFlag(cpu, Flag.C, (sum > 0xFF));
setNZ(cpu, cast(ubyte)sum);
setA(cpu, cast(ubyte)sum);
}
else
{
int a = acc;
int al = (a & 0x0F) + (val & 0x0F) + c;
if (al >= 0x0A)
al = ((al + 0x06) & 0x0F) + 0x10;
a = (a & 0xF0) + (val & 0xF0) + al;
if (!cmos)
{
updateFlag(cpu, Flag.N, (a & 0xFF) > 0x7F);
updateFlag(cpu, Flag.Z, (cast(ubyte)(acc + val + c)) == 0);
}
updateFlag(cpu, Flag.V,
(!((acc ^ val) & 0x80)) && ((val ^ a) & 0x80));
if (a >= 0xA0)
a = a + 0x60;
updateFlag(cpu, Flag.C, (a >= 0x100));
setA(cpu, cast(ubyte)a);
if (cmos)
setNZ(cpu, cast(ubyte)a);
}
}
}
// For ADC.
auto expect_ADC(bool cmos)
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected) { incPC(cpu, info.len); }
expect_add(cmos, info.data, expected);
}
return &expect;
}
// For RRA.
auto expect_RRA()
{
void expect(ref Expected expected, const OpInfo info)
{
auto carry = (getFlag(expected.cpu, Flag.C) ? 0x80 : 0);
auto val = cast(ubyte)(info.data >> 1 | carry);
updateFlag(expected.cpu, Flag.C, ((info.data & 0x01) != 0));
expect_rmw(false, val, expected, info);
expect_add(false, val, expected);
}
return &expect;
}
/*
* Slightly cheating--this is copied from the original implementation,
* but it was independently tested (see test_decimal.d).
*/
void expect_sub(bool cmos, ubyte val, ref Expected expected)
{
with(expected)
{
ubyte acc = getA(cpu);
auto d = getFlag(cpu, Flag.D);
auto c = getFlag(cpu, Flag.C);
if (!d)
{
uint diff = acc - val - (c ? 0 : 1);
updateFlag(cpu, Flag.V,
((acc ^ diff) & 0x80) && ((acc ^ val) & 0x80));
updateFlag(cpu, Flag.C, (diff < 0x100));
setA(cpu, cast(ubyte)diff);
setNZ(cpu, cast(ubyte)diff);
}
else
{
if (!cmos)
{
int a = acc;
int al = (a & 0x0F) - (val & 0x0F) - !c;
if (al < 0)
al = ((al - 0x06) & 0x0F) - 0x10;
a = (a & 0xF0) - (val & 0xF0) + al;
if (a < 0)
a = a - 0x60;
uint diff = acc - val - !c;
updateFlag(cpu, Flag.V,
((acc ^ diff) & 0x80) && ((acc ^ val) & 0x80));
updateFlag(cpu, Flag.C, (diff < 0x100));
setNZ(cpu, cast(ubyte)diff);
setA(cpu, cast(ubyte)a);
}
else
{
int a = acc;
int al = (a & 0x0F) - (val & 0x0F) - !c;
a = a - val - !c;
if (a < 0)
a = a - 0x60;
if (al < 0)
a = a - 0x06;
uint diff = acc - val - !c;
updateFlag(cpu, Flag.V,
((acc ^ diff) & 0x80) && ((acc ^ val) & 0x80));
updateFlag(cpu, Flag.C, (diff < 0x100));
setNZ(cpu, cast(ubyte)a);
setA(cpu, cast(ubyte)a);
}
}
}
}
// For SBC.
auto expect_SBC(bool cmos)
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected) { incPC(cpu, info.len); }
expect_sub(cmos, info.data, expected);
}
return &expect;
}
// For INS.
auto expect_INS()
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = cast(ubyte)(info.data + 1);
expect_rmw(false, val, expected, info);
expect_sub(false, val, expected);
}
return &expect;
}
void expect_compare(Reg reg, ubyte val, ref Expected expected)
{
with(expected)
{
auto r = getReg(cpu, reg);
updateFlag(cpu, Flag.C, (r >= val));
setNZ(cpu, cast(ubyte)(r - val));
}
}
// For CMP, CPX, CPY.
auto expect_cmp(Reg reg)
{
void expect(ref Expected expected, const OpInfo info)
{
with(expected) { incPC(cpu, info.len); }
expect_compare(reg, info.data, expected);
}
return &expect;
}
// For DCM.
auto expect_DCM()
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = cast(ubyte)(info.data - 1);
expect_rmw(false, val, expected, info);
expect_compare(Reg.A, val, expected);
}
return &expect;
}
// For ORA.
auto expect_ORA()
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = cast(ubyte)(info.data | getA(expected.cpu));
expect_basic(Reg.A, val, expected, info);
}
return &expect;
}
// For AND.
auto expect_AND()
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = cast(ubyte)(info.data & getA(expected.cpu));
expect_basic(Reg.A, val, expected, info);
}
return &expect;
}
// For EOR.
auto expect_EOR()
{
void expect(ref Expected expected, const OpInfo info)
{
auto val = cast(ubyte)(info.data ^ getA(expected.cpu));
expect_basic(Reg.A, val, expected, info);
}
return &expect;
}
// Associates opcodes with test setup functions.
string getMemSetup(T)()
if (isCpu!T)
{
string[] tmp1 = new string[256], tmp2 = new string[256],
tmp3 = new string[256];
tmp1[] = " setup_addr = setup_none();\n";
tmp2[] = " setup_test = setup_none();\n";
tmp3[] = " expect = expect_none();\n";
void get_addr(const(ubyte[]) list, string name, string arg = "")
{
foreach(op; list)
tmp1[op] = "setup_addr = setup_addr_" ~ name ~ "(" ~ arg ~ ");\n";
}
void get_test(const(ubyte[]) list, string name, string arg = "")
{
foreach(op; list)
tmp2[op] = "setup_test = setup_op_" ~ name ~ "(" ~ arg ~ ");\n";
}
void get_expect(const(ubyte[]) list, string name, string arg = "")
{
foreach(op; list)
tmp3[op] = "expect = expect_" ~ name ~ "(" ~ arg ~ ");\n";
}
void get_both(const(ubyte[]) list, string name, string arg = "")
{
get_test(list, name, arg);
get_expect(list, name, arg);
}
get_addr(REG_OPS!T, "implied");
get_addr(PUSH_OPS!T, "implied");
get_addr(PULL_OPS!T, "implied");
get_addr(BRANCH_OPS!T, "branch", "isCMOS!T");
get_addr(IMM_OPS!T, "imm");
get_addr(ZPG_OPS!T, "zpg");
get_addr(ABS_OPS!T, "abs");
get_addr([0x4C], "abs");
get_addr(ZPX_OPS!T, "zpxy", "Reg.X");
get_addr(ZPY_OPS!T, "zpxy", "Reg.Y");
get_addr(ABX_OPS!T, "abxy", "Reg.X");
get_addr(ABY_OPS!T, "abxy", "Reg.Y");
get_addr(IZX_OPS!T, "izx");
get_addr(IZY_OPS!T, "izy");
get_expect([0x18], "flag", "Flag.C, false");
get_expect([0x38], "flag", "Flag.C, true");
get_expect([0xD8], "flag", "Flag.D, false");
get_expect([0xF8], "flag", "Flag.D, true");
get_expect([0x58], "flag", "Flag.I, false");
get_expect([0x78], "flag", "Flag.I, true");
get_expect([0xB8], "flag", "Flag.V, false");
get_both([0x08], "PHP");
get_both([0x28], "PLP");
get_both([0x48], "push", "Reg.A");
get_both([0x68], "pull", "Reg.A");
get_both([0x8A], "transfer", "Reg.X, Reg.A");
get_both([0x98], "transfer", "Reg.Y, Reg.A");
get_both([0xA8], "transfer", "Reg.A, Reg.Y");
get_both([0xAA], "transfer", "Reg.A, Reg.X");
get_test([0x9A, 0xBA], "stack_xfer");
get_expect([0x9A], "TXS");
get_expect([0xBA], "TSX");
get_both([0xA1, 0xA5, 0xA9, 0xAD, 0xB1, 0xB5, 0xB9, 0xBD],
"load", "Reg.A");
get_both([0xA2, 0xA6, 0xAE, 0xB6, 0xBE], "load", "Reg.X");
get_both([0xA0, 0xA4, 0xAC, 0xB4, 0xBC], "load", "Reg.Y");
get_both([0x81, 0x85, 0x8D, 0x91, 0x95, 0x99, 0x9D], "store", "Reg.A");
get_both([0x86, 0x8E, 0x96], "store", "Reg.X");
get_both([0x84, 0x8C, 0x94], "store", "Reg.Y");
get_expect(BRANCH_OPS!T, "branch");
get_both([0x24, 0x2C], "BIT");
get_both([0x00], "BRK");
get_both([0x40], "RTI");
get_both([0x60], "RTS");
get_test([0x6C], "JMP_ind", "isCMOS!T");
get_expect([0x4C, 0x6C], "JMP");
get_both([0x20], "JSR");
get_both([0x88], "DEC_reg", "Reg.Y");
get_both([0xCA], "DEC_reg", "Reg.X");
get_both([0xC8], "INC_reg", "Reg.Y");
get_both([0xE8], "INC_reg", "Reg.X");
get_both([0xE6, 0xEE, 0xF6, 0xFE], "INC");
get_both([0xC6, 0xCE, 0xD6, 0xDE], "DEC");
get_both([0x2A], "ROL", "true");
get_both([0x0A], "ASL", "true");
get_both([0x6A], "ROR", "true");
get_both([0x4A], "LSR", "true");
get_both([0x26, 0x2E, 0x36, 0x3E], "ROL", "false");
get_both([0x06, 0x0E, 0x16, 0x1E], "ASL", "false");
get_both([0x66, 0x6E, 0x76, 0x7E], "ROR", "false");
get_both([0x46, 0x4E, 0x56, 0x5E], "LSR", "false");
get_both([0x61, 0x65, 0x69, 0x6D, 0x71, 0x75, 0x79, 0x7D],
"ADC", "isCMOS!T");
get_both([0xE1, 0xE5, 0xE9, 0xED, 0xF1, 0xF5, 0xF9, 0xFD],
"SBC", "isCMOS!T");
get_both([0xC1, 0xC5, 0xC9, 0xCD, 0xD1, 0xD5, 0xD9, 0xDD], "cmp", "Reg.A");
get_both([0x01, 0x05, 0x09, 0x0D, 0x11, 0x15, 0x19, 0x1D], "ORA");
get_both([0x21, 0x25, 0x29, 0x2D, 0x31, 0x35, 0x39, 0x3D], "AND");
get_both([0x41, 0x45, 0x49, 0x4D, 0x51, 0x55, 0x59, 0x5D], "EOR");
get_both([0xE0, 0xE4, 0xEC], "cmp", "Reg.X");
get_both([0xC0, 0xC4, 0xCC], "cmp", "Reg.Y");
static if (isNMOS!T)
{
get_addr(HLT_OPS!T, "implied");
get_test([0xA3, 0xA7, 0xAF, 0xB3, 0xB7, 0xBF], "load", "Reg.A");
get_expect([0xA3, 0xA7, 0xAF, 0xB3, 0xB7, 0xBF], "LAX");
get_expect([0x1A, 0x3A, 0x5A, 0x7A, 0xDA, 0xEA, 0xFA], "NOP");
get_expect([0x0C, 0x1C, 0x3C, 0x5C, 0x7C, 0xDC, 0xFC], "NOP");
get_expect([0x80, 0x82, 0x89, 0xC2, 0xE2], "NOP");
get_expect([0x04, 0x44, 0x64], "NOP");
get_expect([0x14, 0x34, 0x54, 0x74, 0xD4, 0xF4], "NOP");
get_both([0x83, 0x87, 0x8F, 0x97], "SAX");
get_both([0x03, 0x07, 0x0F, 0x13, 0x17, 0x1B, 0x1F], "ASO");
get_both([0x23, 0x27, 0x2F, 0x33, 0x37, 0x3B, 0x3F], "RLA");
get_both([0x43, 0x47, 0x4F, 0x53, 0x57, 0x5B, 0x5F], "LSE");
get_both([0x63, 0x67, 0x6F, 0x73, 0x77, 0x7B, 0x7F], "RRA");
get_both([0xE3, 0xE7, 0xEF, 0xF3, 0xF7, 0xFB, 0xFF], "INS");
get_both([0xC3, 0xC7, 0xCF, 0xD3, 0xD7, 0xDB, 0xDF], "DCM");
get_both([0xEB], "SBC", "false");
// TODO: implement these opcode tests
get_test([0x0B, 0x2B, 0x4B, 0x6B, 0x8B, 0x93, 0x9B, 0x9C, 0x9E,
0x9F, 0xAB, 0xBB, 0xCB, 0xEB], "abort");
}
else
{
get_addr(NOP1_OPS!T, "implied");
get_addr(ZPI_OPS!T, "zpi");
get_both([0x12], "ORA");
get_both([0x32], "AND");
get_both([0x52], "EOR");
get_both([0x72], "ADC", "true");
get_both([0x92], "store", "Reg.A");
get_both([0xB2], "load", "Reg.A");
get_both([0xD2], "cmp", "Reg.A");
get_both([0xF2], "SBC", "true");
get_both([0x5A], "push", "Reg.Y");
get_both([0xDA], "push", "Reg.X");
get_both([0x7A], "pull", "Reg.Y");
get_both([0xFA], "pull", "Reg.X");
get_expect([0xEA], "NOP");
get_expect(NOP1_OPS!T, "NOP");
get_expect([0x02, 0x22, 0x42, 0x62, 0x82, 0xC2, 0xE2], "NOP");
get_expect([0x44, 0x54, 0xD4, 0xF4, 0xDC, 0xFC], "NOP");
get_both([0x64, 0x74, 0x9C, 0x9E], "STZ");
get_both([0x34, 0x3C], "BIT");
get_test([0x89], "BIT"); get_expect([0x89], "BIT_imm");
get_both([0x14, 0x1C], "TRB");
get_both([0x04, 0x0C], "TSB");
get_test([0x7C], "JMP_inx");
get_expect([0x7C], "JMP");
get_both([0xB2], "load", "Reg.A");
get_both([0x92], "store", "Reg.A");
get_test([0x5C], "5C");
get_expect([0x5C], "NOP");
get_both([0x3A], "DEC_reg", "Reg.A");
get_both([0x1A], "INC_reg", "Reg.A");
}
auto ret = "final switch (opcode)\n{\n";
for (auto i = 0; i < 256; i++)
{
ret ~= " case 0x" ~ to!string(i, 16) ~ ":\n" ~
" " ~ tmp1[i] ~ " " ~ tmp2[i] ~
" " ~ tmp3[i] ~ " break;\n";
}
return ret ~ "\n}";
}
alias void delegate(ubyte, ref Expected, CpuInfo, const ref TestMemory, string)
testreport;
/*
* Runs one opcode. Calls expect for the expected values of the cpu
* registers and memory. Calls report with the expected and actual
* values.
*/
auto run_opcode_test(T)(testexpect expect, testreport report)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
auto testcpu = makeCpu!T(cpu);
auto mem = TestMemory(data);
auto expected = Expected(cpu, mem);
expect(expected, info);
connectMem(testcpu, mem);
runOneOpcode(testcpu);
auto cpuResult = CpuInfo.fromCpu(testcpu);
report(opcode, expected, cpuResult, mem, T.stringof ~ " | " ~ msg);
callNext();
}
return TestSetup(&setup);
}
// Dummy function. Reports nothing.
auto report_none()
{
void report(ubyte opcode, ref Expected expected, CpuInfo cpu,
const ref TestMemory mem, string msg)
{
}
return &report;
}
// Prints the differences between expected and actual cpu/memory.
auto report_debug()
{
void report(ubyte opcode, ref Expected expected, CpuInfo cpu,
const ref TestMemory mem, string msg)
{
import std.stdio;
bool badCpu = (expected.cpu != cpu);
bool badMem = (expected.mem != mem);
if (badCpu || badMem)
writeln(format("[%0.2X] %s", opcode, msg));
if (badCpu)
{
writeln(" expect ", expected.cpu);
writeln(" actual ", cpu);
}
if (badMem)
{
foreach (h; MemDiff(expected.mem, mem))
{
writeln(format(" %0.4X | %s", h.base, formatMemory(h.a, 8)));
writeln(format(" | %s", formatMemory(h.b, 8)));
}
}
if (badCpu || badMem) throw new Exception("BAD");
}
return &report;
}
void test_one_opcode(T)(ubyte opcode, testreport report)
{
TestSetup setup_addr;
TestSetup setup_test;
testexpect expect;
mixin(getMemSetup!T());
auto setup = connect(setup_mask_flags(), setup_addr, setup_test);
auto run = connect(setup, run_opcode_test!T(expect, report));
run.run(opcode);
}
T[] If(alias cond, T)(T[] actions)
{
if (cond)
return actions;
else
return [];
}
/// Bus access pattern for register opcodes.
auto accesses_reg(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc];
assert(REG_OPS!T.canFind(opcode));
cycles = 2;
return [Bus(Action.READ, pc)] ~
If!(isStrict!T)(
[Bus(Action.READ, pc+1)]);
}
/// Bus access pattern for push opcodes.
auto accesses_push(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc];
assert(PUSH_OPS!T.canFind(opcode));
auto sp = getSP(cpu);
cycles = 3;
return [Bus(Action.READ, pc)] ~
If!(isStrict!T)(
[Bus(Action.READ, pc+1)]) ~
[Bus(Action.WRITE, sp)];
}
/// Bus access pattern for pull opcodes.
auto accesses_pull(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc];
assert(PULL_OPS!T.canFind(opcode));
auto sp = getSP(cpu);
auto sp1 = pageWrapAdd(sp, 1);
cycles = 4;
return [Bus(Action.READ, pc)] ~
If!(isStrict!T)(
[Bus(Action.READ, pc+1),
Bus(Action.READ, sp)]) ~
[Bus(Action.READ, sp1)];
}
/// Bus access pattern for immediate mode opcodes.
auto accesses_imm(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc];
assert(IMM_OPS!T.canFind(opcode));
bool decimal = isCMOS!T && getFlag(cpu, Flag.D) &&
BCD_OPS!T.canFind(opcode);
cycles = 2 + decimal;
return [Bus(Action.READ, pc),
Bus(Action.READ, pc+1)] ~
If!decimal(If!(isStrict!T)(
[Bus(Action.READ, pc+2)]));
}
/// Bus access pattern for branch opcodes.
auto accesses_rel(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc], op1 = mem[pc+1];
assert(BRANCH_OPS!T.canFind(opcode));
auto base = cast(ushort)(pc + 2);
bool branch = wouldBranch(cpu, opcode);
ushort wrongPage = pageWrapAdd(base, cast(byte)op1);
bool px = wrongPage != pageCrossAdd(base, cast(byte)op1);
ushort wrongAddr = isNMOS!T ? wrongPage : base;
cycles = 2 + branch + px;
return [Bus(Action.READ, pc),
Bus(Action.READ, pc+1)] ~
If!branch(If!(isStrict!T)(
[Bus(Action.READ, pc+2)] ~
If!px(
[Bus(Action.READ, wrongAddr)])));
}
/// Bus access pattern for zeropage mode opcodes.
auto accesses_zpg(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc], op1 = mem[pc+1];
assert(ZPG_OPS!T.canFind(opcode));
cycles = 2; // + accesses_end
return [Bus(Action.READ, pc),
Bus(Action.READ, pc+1)] ~
accesses_end(cpu, opcode, 2, op1, cycles);
}
/// Bus access pattern for absolute mode opcodes.
auto accesses_abs(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc], op1 = mem[pc+1], op2 = mem[pc+2];
assert(ABS_OPS!T.canFind(opcode));
auto addr = address(op1, op2);
cycles = 3; // + accesses_end
return [Bus(Action.READ, pc),
Bus(Action.READ, pc+1),
Bus(Action.READ, pc+2)] ~
accesses_end(cpu, opcode, 3, addr, cycles);
}
/// Bus access pattern for zeropage,x/y mode opcodes.
auto accesses_zpxy(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc], op1 = mem[pc+1];
bool useX = ZPX_OPS!T.canFind(opcode);
assert(useX || ZPY_OPS!T.canFind(opcode));
auto idx = (useX ? getX(cpu) : getY(cpu));
auto addr = pageWrapAdd(op1, idx);
cycles = 3; // + accesses_end
return [Bus(Action.READ, pc),
Bus(Action.READ, pc+1)] ~
If!(isStrict!T)(
If!(isNMOS!T)(
[Bus(Action.READ, op1)]) ~
If!(isCMOS!T)(
[Bus(Action.READ, pc+2)])) ~ // XXX
accesses_end(cpu, opcode, 2, addr, cycles);
/*
* According to "Understanding the Apple IIe", the extra read on
* the 65C02 (marked XXX above) is the address of the last operand
* byte (pc + 1).
*/
}
/// Bus access pattern for absolute,x/y mode opcodes.
auto accesses_abxy(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc];
auto op1 = mem[pc+1], op2 = mem[pc+2];
bool useX = ABX_OPS!T.canFind(opcode);
assert(useX || ABY_OPS!T.canFind(opcode));
auto idx = useX ? getX(cpu) : getY(cpu);
auto base = address(op1, op2);
auto guess = pageWrapAdd(base, idx);
auto addr = pageCrossAdd(base, idx);
cycles = 3; // + accesses_px + accesses_end
return [Bus(Action.READ, pc),
Bus(Action.READ, pc+1),
Bus(Action.READ, pc+2)] ~
accesses_px(cpu, opcode, 3, guess, addr, cycles) ~
accesses_end(cpu, opcode, 3, addr, cycles);
}
/// Bus access pattern for indirect zeropage,x mode opcodes.
auto accesses_izx(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc], op1 = mem[pc+1];
assert(IZX_OPS!T.canFind(opcode));
auto idx = getX(cpu);
auto ial = pageWrapAdd(op1, idx);
auto iah = pageWrapAdd(ial, 1);
auto addr = address(mem[ial], mem[iah]);
cycles = 5; // + accesses_end
return [Bus(Action.READ, pc),
Bus(Action.READ, pc+1)] ~
If!(isStrict!T)(
If!(isNMOS!T)(
[Bus(Action.READ, op1)]) ~
If!(isCMOS!T)(
[Bus(Action.READ, pc+2)])) ~ // XXX
[Bus(Action.READ, ial),
Bus(Action.READ, iah)] ~
accesses_end(cpu, opcode, 2, addr, cycles);
/*
* According to "Understanding the Apple IIe", the extra read on
* the 65C02 (marked XXX above) is the address of the last operand
* byte (pc + 1).
*/
}
/// Bus access pattern for indirect zeropage,y mode opcodes.
auto accesses_izy(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc], op1 = mem[pc+1];
assert(IZY_OPS!T.canFind(opcode));
auto idx = getY(cpu);
auto ial = op1;
auto iah = pageWrapAdd(ial, 1);
auto base = address(mem[ial], mem[iah]);
auto guess = pageWrapAdd(base, idx);
auto addr = pageCrossAdd(base, idx);
cycles = 4; // + accesses_px + accesses_end
return [Bus(Action.READ, pc),
Bus(Action.READ, pc+1),
Bus(Action.READ, ial),
Bus(Action.READ, iah)] ~
accesses_px(cpu, opcode, 2, guess, addr, cycles) ~
accesses_end(cpu, opcode, 2, addr, cycles);
}
/// Bus access pattern for indirect zeropage mode opcodes.
auto accesses_zpi(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T && isCMOS!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc], op1 = mem[pc+1];
assert(ZPI_OPS!T.canFind(opcode));
auto ial = op1;
auto iah = pageWrapAdd(ial, 1);
auto addr = address(mem[ial], mem[iah]);
cycles = 4; // + accesses_end
return [Bus(Action.READ, pc),
Bus(Action.READ, pc+1),
Bus(Action.READ, ial),
Bus(Action.READ, iah)] ~
accesses_end(cpu, opcode, 2, addr, cycles);
}
/// Bus access pattern for NMOS HLT opcodes.
auto accesses_hlt(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T && isNMOS!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc];
assert(HLT_OPS!T.canFind(opcode));
cycles = 1;
return [Bus(Action.READ, pc)];
}
/// Bus access pattern for 1-cycle NOPs.
auto accesses_nop1(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T && isCMOS!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc];
assert(NOP1_OPS!T.canFind(opcode));
cycles = 1;
return [Bus(Action.READ, pc)];
}
auto accesses_px(T)(T cpu, ubyte opcode, int opLen, ushort guess, ushort right,
ref int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
bool noShortcut = WRITE_OPS!T.canFind(opcode) ||
(isNMOS!T ? (RMW_OPS!T.canFind(opcode))
: (opcode == 0xDE || opcode == 0xFE));
if (guess != right)
{
cycles += 1;
return If!(isStrict!T)(
If!(isNMOS!T)([Bus(Action.READ, guess)]) ~
If!(isCMOS!T)([Bus(Action.READ, pc + opLen)])); // XXX
}
else if (noShortcut)
{
cycles += 1;
return If!(isStrict!T)([Bus(Action.READ, guess)]);
}
else
{
return cast(Bus[])[];
}
/*
* According to "Understanding the Apple IIe", the extra read on
* the 65C02 (marked XXX above) is the address of the last operand
* byte (pc + opLen - 1) for abx/aby, or the address of the high
* byte of the indirect address (op1 + 1) for izy.
*/
}
auto accesses_end(T)(T cpu, ubyte opcode, int opLen, ushort addr,
ref int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
bool rmw = RMW_OPS!T.canFind(opcode);
bool write = !rmw && WRITE_OPS!T.canFind(opcode);
bool read = !rmw && !write;
bool decimal = isCMOS!T && getFlag(cpu, Flag.D) &&
BCD_OPS!T.canFind(opcode);
cycles += (rmw ? 3 : (write ? 1 : (1 + decimal)));
return If!read(
[Bus(Action.READ, addr)] ~
If!decimal(If!(isStrict!T)(
[Bus(Action.READ, pc + opLen)]))) ~
If!write(
[Bus(Action.WRITE, addr)]) ~
If!rmw(
[Bus(Action.READ, addr)] ~
If!(isStrict!T)(
If!(isNMOS!T)(
[Bus(Action.WRITE, addr)]) ~
If!(isCMOS!T)(
[Bus(Action.READ, addr)])) ~
[Bus(Action.WRITE, addr)]);
}
/// Bus access pattern for RTS.
auto accesses_op_RTS(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc];
assert(opcode == 0x60);
auto sp = getSP(cpu);
auto sp1 = pageWrapAdd(sp, 1);
auto sp2 = pageWrapAdd(sp, 2);
auto ret = address(mem[sp1], mem[sp2]);
cycles = 6;
return [Bus(Action.READ, pc)] ~
If!(isStrict!T)(
[Bus(Action.READ, pc+1),
Bus(Action.READ, sp)]) ~
[Bus(Action.READ, sp1),
Bus(Action.READ, sp2)] ~
If!(isStrict!T)(
[Bus(Action.READ, ret)]);
}
/// Bus access pattern for RTI.
auto accesses_op_RTI(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc];
assert(opcode == 0x40);
auto sp = getSP(cpu);
auto sp1 = pageWrapAdd(sp, 1);
auto sp2 = pageWrapAdd(sp, 2);
auto sp3 = pageWrapAdd(sp, 3);
cycles = 6;
return [Bus(Action.READ, pc)] ~
If!(isStrict!T)(
[Bus(Action.READ, pc+1),
Bus(Action.READ, sp)]) ~
[Bus(Action.READ, sp1),
Bus(Action.READ, sp2),
Bus(Action.READ, sp3)];
}
/// Bus access pattern for BRK.
auto accesses_op_BRK(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc];
assert(opcode == 0x00);
auto sp = getSP(cpu);
auto sp1 = pageWrapAdd(sp, -1);
auto sp2 = pageWrapAdd(sp, -2);
cycles = 7;
return [Bus(Action.READ, pc)] ~
If!(isStrict!T)(
[Bus(Action.READ, pc+1)]) ~
[Bus(Action.WRITE, sp),
Bus(Action.WRITE, sp1),
Bus(Action.WRITE, sp2),
Bus(Action.READ, 0xFFFE),
Bus(Action.READ, 0xFFFF)];
}
/// Bus access pattern for JSR
auto accesses_op_JSR(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc];
assert(opcode == 0x20);
auto sp = getSP(cpu);
auto sp1 = pageWrapAdd(sp, -1);
cycles = 6;
return [Bus(Action.READ, pc),
Bus(Action.READ, pc+1)] ~
If!(isStrict!T)(
[Bus(Action.READ, sp)]) ~
[Bus(Action.WRITE, sp),
Bus(Action.WRITE, sp1),
Bus(Action.READ, pc+2)];
}
/// Bus access pattern for JMP absolute
auto accesses_op_JMP_abs(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc];
assert(opcode == 0x4C);
cycles = 3;
return [Bus(Action.READ, pc),
Bus(Action.READ, pc+1),
Bus(Action.READ, pc+2)];
}
/// Bus access pattern for JMP indirect
auto accesses_op_JMP_ind(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc], op1 = mem[pc+1], op2 = mem[pc+2];
assert(opcode == 0x6C);
auto ial = address(op1, op2);
auto iah = (isNMOS!T ? pageWrapAdd(ial, 1)
: pageCrossAdd(ial, 1));
cycles = 5 + isCMOS!T;
return [Bus(Action.READ, pc),
Bus(Action.READ, pc+1),
Bus(Action.READ, pc+2)] ~
If!(isStrict!T)(If!(isCMOS!T)(
[Bus(Action.READ, pc+3)])) ~ // XXX
[Bus(Action.READ, ial),
Bus(Action.READ, iah)];
/*
* According to "Understanding the Apple IIe", the extra read on
* the 65C02 (marked XXX above) is the address of the last operand
* byte (pc + 2).
*/
}
/// Bus access pattern for JMP indirect,x
auto accesses_op_JMP_inx(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T && isCMOS!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc];
assert(opcode == 0x7C);
auto idx = getX(cpu);
auto base = address(mem[pc+1], mem[pc+2]);
auto ial = pageCrossAdd(base, idx);
auto iah = pageCrossAdd(ial, 1);
cycles = 6;
return [Bus(Action.READ, pc),
Bus(Action.READ, pc+1),
Bus(Action.READ, pc+2)] ~
If!(isStrict!T)(
[Bus(Action.READ, pc+3)]) ~ // XXX
[Bus(Action.READ, ial),
Bus(Action.READ, iah)];
/*
* According to "Understanding the Apple IIe", the extra read on
* the 65C02 (marked XXX above) is the address of the last operand
* byte (pc + 2).
*/
}
/// Bus access pattern for CMOS opcode 5C
auto accesses_op_5C(T)(T cpu, ref TestMemory mem, out int cycles)
if (isCpu!T && isCMOS!T)
{
auto pc = getPC(cpu);
auto opcode = mem[pc];
assert(opcode == 0x5C);
auto weird = address(mem[pc+1], 0xFF);
cycles = 8;
return [Bus(Action.READ, pc),
Bus(Action.READ, pc+1)] ~
If!(isStrict!T)(
[Bus(Action.READ, pc+2),
Bus(Action.READ, weird),
Bus(Action.READ, 0xFFFF),
Bus(Action.READ, 0xFFFF),
Bus(Action.READ, 0xFFFF),
Bus(Action.READ, 0xFFFF)]);
}
// Associates opcodes with expected access patterns.
string getExpected(T)()
{
string[] tmp = new string[256];
void add_op(const(ubyte[]) list, string fname)
{
foreach(op; list)
{
tmp[op] = " case 0x" ~ to!string(op, 16) ~ ": " ~
"expected = &" ~ fname ~ "!T; break;";
}
}
add_op(REG_OPS!T, "accesses_reg");
add_op(PUSH_OPS!T, "accesses_push");
add_op(PULL_OPS!T, "accesses_pull");
add_op(BRANCH_OPS!T, "accesses_rel");
add_op(IMM_OPS!T, "accesses_imm");
add_op(ZPG_OPS!T, "accesses_zpg");
add_op(ZPX_OPS!T, "accesses_zpxy");
add_op(ZPY_OPS!T, "accesses_zpxy");
add_op(ABS_OPS!T, "accesses_abs");
add_op(ABX_OPS!T, "accesses_abxy");
add_op(ABY_OPS!T, "accesses_abxy");
add_op(IZX_OPS!T, "accesses_izx");
add_op(IZY_OPS!T, "accesses_izy");
add_op([0x00], "accesses_op_BRK");
add_op([0x20], "accesses_op_JSR");
add_op([0x40], "accesses_op_RTI");
add_op([0x4C], "accesses_op_JMP_abs");
add_op([0x60], "accesses_op_RTS");
add_op([0x6C], "accesses_op_JMP_ind");
static if (isNMOS!T)
add_op(HLT_OPS!T, "accesses_hlt");
else
{
add_op(ZPI_OPS!T, "accesses_zpi");
add_op(NOP1_OPS!T, "accesses_nop1");
add_op([0x7C], "accesses_op_JMP_inx");
add_op([0x5C], "accesses_op_5C");
}
return "final switch (opcode)\n{\n" ~ join(tmp, "\n") ~ "\n}";
}
template timesetup_t(T)
{
alias Bus[] function(T, ref TestMemory, out int) timesetup_t;
}
alias void delegate(int, const Bus[], int, const Bus[], ubyte, string)
busreport;
auto run_timing_test(T)(timesetup_t!T expect, busreport report)
{
auto setup(ubyte opcode, CpuInfo cpu, Block[] data, OpInfo info,
string msg, TestSetup* next)
{
mixin testCallNext;
auto testcpu = makeCpu!T(cpu);
auto mem = TestMemory(data);
int expCycles;
auto expBus = expect(testcpu, mem, expCycles);
expBus = expBus ~ new Bus[8 - expBus.length];
connectMem(testcpu, mem);
auto actualBus = recordBus(testcpu);
auto actualCycles = recordCycles(testcpu);
runOneOpcode(testcpu);
report(actualCycles, actualBus, expCycles, expBus,
opcode, T.stringof ~ " | " ~ msg);
callNext();
}
return TestSetup(&setup);
}
auto report_timing_debug()
{
void report(int actualCycles, const Bus[] actualBus,
int expectCycles, const Bus[] expectBus,
ubyte opcode, string msg)
{
if (actualBus != expectBus)
{
// XXX make error message, throw
}
if (actualCycles != expectCycles)
{
// XXX make error message, throw
}
if (actualBus == expectBus && actualCycles == expectCycles) {}
else
{
write(format("[%0.2X] %s", opcode, msg));
writeln();
writeln(expectCycles, " ", actualCycles);
writeln(expectBus);
writeln(actualBus);
throw new TestException("timing");
}
}
return &report;
}
// Tests the bus access patterns and cycles taken for a given opcode.
void test_opcode_timing(T)(ubyte opcode, busreport report)
{
TestSetup setup_addr;
TestSetup setup_test;
testexpect expect;
timesetup_t!T expected;
mixin(getMemSetup!T());
mixin(getExpected!T());
auto setup = connect(setup_mask_flags(), setup_addr, setup_test);
auto run = connect(setup, run_timing_test!T(expected, report));
run.run(opcode);
}