From 439b0dcad827632e4de74a43b418f3d5395f1ee5 Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Sun, 6 Sep 2026 22:11:03 +0200 Subject: [PATCH 01/17] emu: removed GTE as separate variant --- source/xebin/emu.d | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/source/xebin/emu.d b/source/xebin/emu.d index 1caa01e..66165d1 100644 --- a/source/xebin/emu.d +++ b/source/xebin/emu.d @@ -157,10 +157,9 @@ enum trb = q{ ubyte tmp = @r; zflag = (a & tmp) == 0; @w(cast(ubyte) (tmp & ~a)) /// enum CpuVariant { mos_6502, /// NMOS - wdc_65c02, /// original CMOS re-design - gte_65sc02, /// e.g. Lynx - rockwell_r65c02, /// Rockwell - wdc_w65c02s, /// modern W65C02S + wdc_65c02, /// original CMOS (also Synertek, GTE, etc.), part of Lynx's Mikey + rockwell_r65c02, /// Rockwell (bit ops) + wdc_w65c02s, /// modern W65C02S (bit ops + WAI/STP)} } /// Basic CMOS instruction set + BCD and JMP (abs) fixes. @@ -813,8 +812,7 @@ unittest } // ... while the CMOS parts see it as an ordinary two-byte NOP. - static foreach (v; [CpuVariant.wdc_65c02, CpuVariant.gte_65sc02, - CpuVariant.rockwell_r65c02, CpuVariant.wdc_w65c02s]) + static foreach (v; [CpuVariant.wdc_65c02, CpuVariant.rockwell_r65c02, CpuVariant.wdc_w65c02s]) {{ auto emu = new Emulator!v(); load(emu, 0x1000, [ubyte(0x02), 0x37, 0xe8]); From dcd5d90a535a47679d34ab6ec670e668c6800556 Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Sun, 6 Sep 2026 23:13:11 +0200 Subject: [PATCH 02/17] Single-step tests runner --- .github/workflows/test.yml | 22 ++++++++++++ .gitignore | 3 ++ dub.sdl | 2 ++ test/singlestep/dub.sdl | 7 ++++ test/singlestep/source/app.d | 67 ++++++++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+) create mode 100644 test/singlestep/dub.sdl create mode 100644 test/singlestep/source/app.d diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f210902..d3affce 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,3 +25,25 @@ jobs: - name: Run tests shell: bash run: dub test + + singlestep: + name: Single Step Tests + runs-on: ubuntu-latest + steps: + - name: Checkout xebin + uses: actions/checkout@v7 + with: + submodules: recursive + + - name: Install D compiler + uses: dlang-community/setup-dlang@v2 + with: + compiler: ldc + + - name: Checkout SingleStepTests/65x02 + uses: actions/checkout@v7 + with: + repository: SingleStepTests/65x02 + path: ext/65x02 + + - run: dub run -b release :singlestep -- --dir="${{ github.workspace }}/ext/65x02" diff --git a/.gitignore b/.gitignore index d846100..e355afb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ /*.obj /libxebin.a /cputest/bin/ +dub.selections.json +/test/singlestep/singlestep +/ext/65x02/ \ No newline at end of file diff --git a/dub.sdl b/dub.sdl index 3564f5c..486e41f 100644 --- a/dub.sdl +++ b/dub.sdl @@ -5,3 +5,5 @@ copyright "Copyright © 2010-2014, 2017, 2023, 2026 Adrian Matoga" license "Zlib" dependency "xasm" path="ext/xasm" + +subPackage "test/singlestep" \ No newline at end of file diff --git a/test/singlestep/dub.sdl b/test/singlestep/dub.sdl new file mode 100644 index 0000000..30ad35a --- /dev/null +++ b/test/singlestep/dub.sdl @@ -0,0 +1,7 @@ +name "singlestep" +description "Runs SingleStepTests/65x02 against xebin.emu" +targetType "executable" +targetName "singlestep" + +dependency "xebin" path="../.." +dependency "std_data_json" version="~>0.18.7" \ No newline at end of file diff --git a/test/singlestep/source/app.d b/test/singlestep/source/app.d new file mode 100644 index 0000000..3163eff --- /dev/null +++ b/test/singlestep/source/app.d @@ -0,0 +1,67 @@ +/** Runs SingleStepTests/65x02 against xebin CPU emulation. + + --- + git clone https://github.com/SingleStepTests/65x02 ext/65x02 + dub run :singlestep + --- + + Author: Adrian Matoga adrian@matoga.info + + zlib License: + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + 3. This notice may not be removed or altered from any source + distribution. +*/ + +import std.file : exists; +import std.getopt; +import std.path : buildPath; +import std.stdio; + +string findSuite(string dir) +{ + if (dir.length) + return dir; + foreach (candidate; ["ext/65x02", "../65x02", "65x02"]) + if (exists(buildPath(candidate, "6502", "v1"))) + return candidate; + return null; +} + +int main(string[] args) +{ + string dir; + auto help = getopt(args, + "d|dir", "root of SingleStepTests/65x02", &dir); + + if (help.helpWanted) + { + defaultGetoptPrinter( + "Runs SingleStepTests/65x02 against xebin CPU emulator.\n", help.options); + return 0; + } + + const root = findSuite(dir); + if (root is null) + { + stderr.writeln("65x02 test data not found. Clone " ~ + "https://github.com/SingleStepTests/65x02 and pass --dir="); + return 2; + } + + return 0; +} From 7870becc869eaa8bfba5daad62eb32d3064a35d9 Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Mon, 7 Sep 2026 00:59:55 +0200 Subject: [PATCH 03/17] singlestep: parse test cases from json --- .github/workflows/test.yml | 1 + .gitignore | 1 + test/singlestep/source/app.d | 133 +++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d3affce..e9bf32a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,4 +46,5 @@ jobs: repository: SingleStepTests/65x02 path: ext/65x02 + - run: dub test :singlestep - run: dub run -b release :singlestep -- --dir="${{ github.workspace }}/ext/65x02" diff --git a/.gitignore b/.gitignore index e355afb..503e707 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ /cputest/bin/ dub.selections.json /test/singlestep/singlestep +/test/singlestep/xebin-singlestep-test-application /ext/65x02/ \ No newline at end of file diff --git a/test/singlestep/source/app.d b/test/singlestep/source/app.d index 3163eff..d8e9eda 100644 --- a/test/singlestep/source/app.d +++ b/test/singlestep/source/app.d @@ -27,11 +27,144 @@ distribution. */ +import std.array : appender; import std.file : exists; +import std.format; import std.getopt; import std.path : buildPath; import std.stdio; +import stdx.data.json; + +// Work around undefined reference to `TaggedAlgebraic.opEquals` +private bool forceJSONValueOpEquals(const JSONValue a, const JSONValue b) +{ + return a == b; +} + +struct Access +{ + ushort addr; + ubyte value; + bool write; + + string toString() const + { + return format("%s %04x %02x", write ? "W" : "R", addr, value); + } +} + +struct State +{ + ushort pc; + ubyte sp, a, x, y, p; + + string toString() const + { + return format("pc=%04x s=%02x a=%02x x=%02x y=%02x p=%02x", + pc, sp, a, x, y, p); + } +} + +struct Cell +{ + ushort addr; + ubyte value; +} + +struct TestCase +{ + string name; + State initial, expected; + Cell[] initialRam, expectedRam; + Access[] cycles; +} + +TestCase[] parseTests(string text) +{ + auto json = parseJSONStream(text); + auto tests = appender!(TestCase[]); + TestCase t; + + void readState(ref State s, ref Cell[] ram) + { + json.readObject((string key) { + switch (key) + { + case "pc": s.pc = cast(ushort) json.readDouble(); break; + case "s": s.sp = cast(ubyte) json.readDouble(); break; + case "a": s.a = cast(ubyte) json.readDouble(); break; + case "x": s.x = cast(ubyte) json.readDouble(); break; + case "y": s.y = cast(ubyte) json.readDouble(); break; + case "p": s.p = cast(ubyte) json.readDouble() & ~0x10; break; // ignore p.b as it isn't stored in CPU + case "ram": + json.readArray({ + Cell c; + size_t i; + json.readArray({ + const n = cast(uint) json.readDouble(); + if (i++ == 0) + c.addr = cast(ushort) n; + else + c.value = cast(ubyte) n; + }); + ram ~= c; + }); + break; + default: json.skipValue(); break; + } + }); + } + + json.readArray({ + t = TestCase.init; + json.readObject((string key) { + switch (key) + { + case "name": t.name = json.readString(); break; + case "initial": readState(t.initial, t.initialRam); break; + case "final": readState(t.expected, t.expectedRam); break; + case "cycles": + json.readArray({ + Access acc; + size_t i; + json.readArray({ + switch (i++) + { + case 0: acc.addr = cast(ushort) json.readDouble(); break; + case 1: acc.value = cast(ubyte) json.readDouble(); break; + default: acc.write = json.readString() == "write"; break; + } + }); + t.cycles ~= acc; + }); + break; + default: json.skipValue(); break; + } + }); + tests ~= t; + }); + return tests.data; +} + +unittest +{ + auto tests = parseTests(`[ + { "name": "b1 28 b5", + "initial": { "pc": 59082, "s": 39, "a": 57, "x": 33, "y": 174, "p": 96, + "ram": [ [59082, 177], [40, 160]]}, + "final": { "pc": 59084, "s": 39, "a": 119, "x": 33, "y": 174, "p": 96, + "ram": [ [40, 160]]}, + "cycles": [ [59082, 177, "read"], [40, 160, "write"]] } + ]`); + assert(tests.length == 1); + assert(tests[0].name == "b1 28 b5"); + assert(tests[0].initial == State(59082, 39, 57, 33, 174, 96)); + assert(tests[0].expected.a == 119); + assert(tests[0].initialRam == [Cell(59082, 177), Cell(40, 160)]); + assert(tests[0].cycles == [Access(59082, 177, false), Access(40, 160, true)]); +} + string findSuite(string dir) { if (dir.length) From a44c2276cfe937ac68f539526ac3083d3e8044cb Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Mon, 7 Sep 2026 01:36:00 +0200 Subject: [PATCH 04/17] singlestep: filter and load test cases from files --- .github/workflows/test.yml | 2 +- Makefile | 3 + test/singlestep/source/app.d | 121 ++++++++++++++++++++++++++++++++++- 3 files changed, 123 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e9bf32a..f414588 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,4 +47,4 @@ jobs: path: ext/65x02 - run: dub test :singlestep - - run: dub run -b release :singlestep -- --dir="${{ github.workspace }}/ext/65x02" + - run: make singlestep diff --git a/Makefile b/Makefile index d29724b..1875a5d 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,9 @@ install: test: dub test +singlestep: + dub run -b release :singlestep -- -o ea + .PHONY: all doc debug dist windist srcdist clean install test .DELETE_ON_ERROR: diff --git a/test/singlestep/source/app.d b/test/singlestep/source/app.d index d8e9eda..326c820 100644 --- a/test/singlestep/source/app.d +++ b/test/singlestep/source/app.d @@ -27,14 +27,21 @@ distribution. */ +import std.algorithm : canFind, filter, map; import std.array : appender; -import std.file : exists; +import std.conv : to; +import std.file : exists, read; import std.format; import std.getopt; +import std.parallelism : parallel; import std.path : buildPath; +import std.range : array, iota, join, split; import std.stdio; +import std.string : strip; +import std.traits : EnumMembers; import stdx.data.json; +import xebin.emu; // Work around undefined reference to `TaggedAlgebraic.opEquals` private bool forceJSONValueOpEquals(const JSONValue a, const JSONValue b) @@ -165,6 +172,74 @@ unittest assert(tests[0].cycles == [Access(59082, 177, false), Access(40, 160, true)]); } +struct Result +{ + ubyte opcode; + size_t total, failed; +} + +Result runOpcode(CpuVariant v)(string path, ubyte opcode) +{ + Result r = { opcode: opcode }; + + foreach (ref t; parseTests(cast(string) read(path))) + { + } + + return r; +} + +struct Target +{ + string dir; + CpuVariant cpu; +} + +immutable Target[] targets = [ + Target("6502", CpuVariant.mos_6502), + Target("synertek65c02", CpuVariant.wdc_65c02), + Target("rockwell65c02", CpuVariant.rockwell_r65c02), + Target("wdc65c02", CpuVariant.wdc_w65c02s), +]; + +size_t runTarget(CpuVariant v)(string dir, const(ubyte)[] opcodes) +{ + auto results = new Result[opcodes.length]; + foreach (i, opcode; opcodes.parallel) + { + const path = buildPath(dir, format("%02x.json", opcode)); + results[i] = exists(path) + ? runOpcode!v(path, opcode) + : Result(opcode); + } + + size_t failed; + return failed; +} + +ubyte[] parseOpcodes(string spec) +{ + if (!spec.length) + return iota(256).map!(i => cast(ubyte) i).array; + bool[256] set; + foreach (part; spec.split(",")) + { + const range = part.split("-").map!(a => a.to!uint(16)).array; + const lo = range[0]; + const hi = range.length > 1 ? range[1] : lo; + foreach (i; lo .. hi + 1) + set[i] = true; + } + return iota(256).filter!(i => set[i]).map!(i => cast(ubyte) i).array; +} + +unittest +{ + assert(parseOpcodes("a9") == [0xa9]); + assert(parseOpcodes("b1-b5,00") == [0x00, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5]); + assert(parseOpcodes("").length == 256); +} + string findSuite(string dir) { if (dir.length) @@ -178,8 +253,13 @@ string findSuite(string dir) int main(string[] args) { string dir; + string cpuSpec; + string opcodeSpec; + auto help = getopt(args, - "d|dir", "root of SingleStepTests/65x02", &dir); + "d|dir", "root of SingleStepTests/65x02", &dir, + "c|cpu", "CPUs to test, comma separated (default: all found).", &cpuSpec, + "o|opcodes", "Opcodes in hex, e.g. a9,1e,b1-b5 (default: all).", &opcodeSpec); if (help.helpWanted) { @@ -196,5 +276,42 @@ int main(string[] args) return 2; } + const opcodes = parseOpcodes(opcodeSpec); + const cpus = cpuSpec.length ? cpuSpec.split(",") : null; + + size_t failed; + size_t ran; + foreach (target; targets) + { + if (cpus !is null && !cpus.canFind(target.dir) && + !cpus.canFind(target.cpu.to!string)) + continue; + const dataDir = buildPath(root, target.dir, "v1"); + if (!exists(dataDir)) + continue; + ++ran; + writefln("%s (%s)", target.cpu, target.dir); + stdout.flush(); + dispatch: switch (target.cpu) + { + static foreach (v; EnumMembers!CpuVariant) + { + case v: + failed += runTarget!v(dataDir, opcodes); + break dispatch; + } + default: + assert(0); + } + } + + if (!ran) + { + const ts = targets.map!(t => [t.dir, t.cpu.to!string]).join.array; + stderr.writefln("Nothing to run. None of [%-(%-s%|, %)] found in [%-(%-s%|, %)]", cpus, ts); + return 2; + } + writefln("%d opcode%s with failures", failed, failed == 1 ? "" : "s"); + return 0; } From a2ab78ca554f48cb8dbbe38f301ced687e251f94 Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Wed, 9 Sep 2026 20:44:04 +0200 Subject: [PATCH 05/17] emu: wrap P, remove bflag, functional 1-step --- Makefile | 2 +- source/xebin/emu.d | 69 +++++++++++--------------- test/singlestep/source/app.d | 93 +++++++++++++++++++++++++++++++++++- 3 files changed, 120 insertions(+), 44 deletions(-) diff --git a/Makefile b/Makefile index 1875a5d..9285cd5 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ test: dub test singlestep: - dub run -b release :singlestep -- -o ea + dub run -b release :singlestep -- -o 00,08,28,40 .PHONY: all doc debug dist windist srcdist clean install test diff --git a/source/xebin/emu.d b/source/xebin/emu.d index 66165d1..5be0eb7 100644 --- a/source/xebin/emu.d +++ b/source/xebin/emu.d @@ -245,12 +245,35 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve ubyte sp = 0xff; bool nflag; bool vflag; - bool bflag; bool dflag; bool iflag; bool zflag; bool cflag; + /// Flags, `NV1BDIZC`. + @property ubyte p() const + { + return cast(ubyte) ( + (nflag ? 0x80 : 0) | + (vflag ? 0x40 : 0) | + 0x20 | + (dflag ? 0x08 : 0) | + (iflag ? 0x04 : 0) | + (zflag ? 0x02 : 0) | + (cflag ? 0x01 : 0)); + } + + /// ditto + @property void p(ubyte value) + { + nflag = (value & 0x80) != 0; + vflag = (value & 0x40) != 0; + dflag = (value & 0x08) != 0; + iflag = (value & 0x04) != 0; + zflag = (value & 0x02) != 0; + cflag = (value & 0x01) != 0; + } + this() { memory = new ubyte[65536]; @@ -429,14 +452,7 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve case 0x00: push((pc + 2) >> 8); push((pc + 2) & 0xff); - push( - (nflag ? 0x80 : 0) | - (vflag ? 0x40 : 0) | - 0x20 | 0x10 | - (dflag ? 0x08 : 0) | - (iflag ? 0x04 : 0) | - (zflag ? 0x02 : 0) | - (cflag ? 0x01 : 0)); + push(p | 0x10); iflag = true; static if (isCmos!cpuVariant) dflag = false; @@ -469,16 +485,7 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve case 0x01: doIndirectX!ora(); break; case 0x05: doAbsoluteZP!ora(); break; case 0x06: doAbsoluteZP!asl(); break; - case 0x08: - push( - (nflag ? 0x80 : 0) | - (vflag ? 0x40 : 0) | - 0x20 | 0x10 | - (dflag ? 0x08 : 0) | - (iflag ? 0x04 : 0) | - (zflag ? 0x02 : 0) | - (cflag ? 0x01 : 0)); - break; + case 0x08: push(p | 0x10); break; case 0x09: doImmediate!ora(); break; case 0x0a: doAccumulator!asl(); break; case 0x0d: doAbsolute!ora(); break; @@ -501,18 +508,7 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve case 0x24: doAbsoluteZP!bit(); break; case 0x25: doAbsoluteZP!and(); break; case 0x26: doAbsoluteZP!rol(); break; - case 0x28: - { - auto p = pop(); - nflag = (p & 0x80) != 0; - vflag = (p & 0x40) != 0; - bflag = (p & 0x10) != 0; - dflag = (p & 0x08) != 0; - iflag = (p & 0x04) != 0; - zflag = (p & 0x02) != 0; - cflag = (p & 0x01) != 0; - } - break; + case 0x28: p = pop(); break; case 0x29: doImmediate!and(); break; case 0x2a: doAccumulator!rol(); break; case 0x2c: doAbsolute!bit(); break; @@ -527,16 +523,7 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve case 0x3d: doAbsolute!and(x); break; case 0x3e: doAbsolute!rol(x); break; case 0x40: - { - auto p = pop(); - nflag = (p & 0x80) != 0; - vflag = (p & 0x40) != 0; - bflag = (p & 0x10) != 0; - dflag = (p & 0x08) != 0; - iflag = (p & 0x04) != 0; - zflag = (p & 0x02) != 0; - cflag = (p & 0x01) != 0; - } + p = pop(); ushort rti = pop(); rti |= cast(ushort) (pop() << 8); pc = cast(ushort) (rti - 1); diff --git a/test/singlestep/source/app.d b/test/singlestep/source/app.d index 326c820..5ee13a6 100644 --- a/test/singlestep/source/app.d +++ b/test/singlestep/source/app.d @@ -30,6 +30,7 @@ import std.algorithm : canFind, filter, map; import std.array : appender; import std.conv : to; +import std.exception : collectExceptionMsg; import std.file : exists, read; import std.format; import std.getopt; @@ -172,18 +173,88 @@ unittest assert(tests[0].cycles == [Access(59082, 177, false), Access(40, 160, true)]); } +struct Trace +{ + ubyte[] ram; + Access[] accesses; + + void instruction(E)(E emu) {} + void fetch(ushort addr, ubyte value) { accesses ~= Access(addr, value, false); } + void read(ushort addr, ubyte value) { accesses ~= Access(addr, value, false); } + void write(ushort addr, ubyte value) { accesses ~= Access(addr, value, true); } + void idle(ushort addr) { accesses ~= Access(addr, ram[addr], false); } + void endInstruction() {} +} + +string runOne(E)(E emu, ref const TestCase t) +{ + foreach (c; t.initialRam) + emu.ram[c.addr] = c.value; + emu.pc = t.initial.pc; + emu.sp = t.initial.sp; + emu.a = t.initial.a; + emu.x = t.initial.x; + emu.y = t.initial.y; + emu.p = t.initial.p; + emu.stopped = false; + emu.instructions = 0; + emu.instructionLimit = 1; + emu.observer.accesses.length = 0; + emu.observer.accesses.assumeSafeAppend(); + + string thrown = collectExceptionMsg(emu.run()); + + const got = State(cast(ushort) (emu.pc + 1), emu.sp, emu.a, emu.x, emu.y, emu.p); + + string[] problems; + if (thrown.length) + problems ~= " threw: " ~ thrown; + if (got != t.expected) + { + problems ~= format(" state: got %s", got); + problems ~= format(" expected %s", t.expected); + } + foreach (c; t.expectedRam) + { + if (emu.ram[c.addr] != c.value) + problems ~= format("memory: $%04x is $%02x, expected $%02x", + c.addr, emu.ram[c.addr], c.value); + } + + emu.ram[] = 0; + + if (!problems.length) + return null; + return format(" \"%s\"\n %-(%s\n %)", t.name, problems); +} + struct Result { ubyte opcode; size_t total, failed; + string[] reports; } Result runOpcode(CpuVariant v)(string path, ubyte opcode) { Result r = { opcode: opcode }; + auto emu = new Emulator!(v, Trace)(); + emu.observer.ram = emu.ram; + emu.stopOnEmptyStackRts = false; + + auto text = strip(cast(string) read(path)); + if (text.length == 0) + return r; - foreach (ref t; parseTests(cast(string) read(path))) + foreach (ref t; parseTests(text)) { + ++r.total; + const diag = runOne(emu, t); + if (diag !is null) { + ++r.failed; + if (r.reports.length < 5) + r.reports ~= diag; + } } return r; @@ -214,6 +285,24 @@ size_t runTarget(CpuVariant v)(string dir, const(ubyte)[] opcodes) } size_t failed; + foreach (ref r; results) + { + if (r.failed) + ++failed; + if (!r.total) + { + writefln(" $%02x no test data", r.opcode); + continue; + } + if (r.failed) + writefln(" $%02x %6d/%-6d %s", r.opcode, r.total - r.failed, r.total, + r.failed ? "FAIL " : "ok"); + foreach (report; r.reports) + writeln(report); + } + return failed; + + return failed; } @@ -313,5 +402,5 @@ int main(string[] args) } writefln("%d opcode%s with failures", failed, failed == 1 ? "" : "s"); - return 0; + return failed ? 1 : 0; } From 346c099b33fe717b15c0c3c83f6c86896584bcbd Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Thu, 10 Sep 2026 01:43:55 +0200 Subject: [PATCH 06/17] emu: fix functional bugs from singlestep tests Fixed adc/sbc decimal mode, jsr, jmp (), multi-cycle nops. Excluded undocumented nmos opcodes from tests for now. --- Makefile | 4 +- source/xebin/emu.d | 346 ++++++++++++++++++++++++----------- test/singlestep/source/app.d | 112 ++++++++---- 3 files changed, 316 insertions(+), 146 deletions(-) diff --git a/Makefile b/Makefile index 9285cd5..cc3314a 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,9 @@ test: dub test singlestep: - dub run -b release :singlestep -- -o 00,08,28,40 + dub run -b release :singlestep -- -c 6502 -u + dub run -b release :singlestep -- -c synertek65c02,rockwell65c02,wdc65c02 + .PHONY: all doc debug dist windist srcdist clean install test diff --git a/source/xebin/emu.d b/source/xebin/emu.d index 5be0eb7..3311956 100644 --- a/source/xebin/emu.d +++ b/source/xebin/emu.d @@ -25,7 +25,11 @@ module xebin.emu; import std.string; -version(unittest) import std.stdio; +version(unittest) +{ + import std.stdio; + import std.format : formattedWrite; +} private ushort makeWord(uint b1, uint b0) { @@ -50,53 +54,55 @@ q{ else { uint al = (a & 0x0f) + (arg & 0x0f) + cflag; - if (al >= 10) - { - tmp += al < 26 ? 6 : -10; - nflag = (tmp & 0x80) != 0; - } - vflag = (~(arg ^ a) & (a ^ tmp) & 0x80) != 0; - if (tmp >= 0xa0) - { - cflag = true; - a = (tmp + 0x60) & 0xff; - } - else - { - cflag = false; - a = tmp & 0xff; - } + if (al >= 0x0a) + al = ((al + 0x06) & 0x0f) + 0x10; + uint sum = (a & 0xf0) + (arg & 0xf0) + al; + nflag = (sum & 0x80) != 0; + vflag = (~(arg ^ a) & (a ^ sum) & 0x80) != 0; + zflag = (tmp & 0xff) == 0; + if (sum >= 0xa0) + sum += 0x60; + cflag = sum >= 0x100; + a = cast(ubyte) sum; static if (isCmos!cpuVariant) setNZ(a); - else - setNZ(tmp & 0xff); } }; enum sbc = q{ - ubyte operand = @r; - ubyte arg = cast(ubyte) ~operand; - ubyte oa = a; - uint tmp = oa + arg + cflag; - ubyte bin = tmp & 0xff; + const ubyte operand = @r; + const ubyte oa = a; + const bool carryIn = cflag; + const uint tmp = oa + cast(ubyte) ~operand + carryIn; + const ubyte bin = tmp & 0xff; setNZ(bin); - vflag = (~(arg ^ oa) & (oa ^ bin) & 0x80) != 0; + vflag = ((oa ^ operand) & (oa ^ bin) & 0x80) != 0; + cflag = tmp >= 0x100; if (!dflag) a = bin; else { - int al = (oa & 0x0f) - (operand & 0x0f) + cflag - 1; - if (al < 0) - al = ((al - 0x06) & 0x0f) - 0x10; - int res = (oa & 0xf0) - (operand & 0xf0) + al; - if (res < 0) - res -= 0x60; - a = cast(ubyte) res; + int al = (oa & 0x0f) - (operand & 0x0f) + carryIn - 1; static if (isCmos!cpuVariant) - setNZ(a); + { + int res = oa - operand + carryIn - 1; + if (res < 0) + res -= 0x60; + if (al < 0) + res -= 0x06; + setNZ(a = cast(ubyte) res); + } + else + { + if (al < 0) + al = ((al - 0x06) & 0x0f) - 0x10; + int res = (oa & 0xf0) - (operand & 0xf0) + al; + if (res < 0) + res -= 0x60; + a = cast(ubyte) res; + } } - cflag = tmp >= 0x100; }; enum cmp = q{ ubyte tmp = ld(addr); setNZ(a - tmp); cflag = a >= tmp; }; @@ -287,7 +293,7 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve ushort dpeek(uint addr) { - return makeWord(memory[addr + 1], memory[addr]); + return makeWord(memory[(addr + 1) & 0xffff], memory[addr]); } void push(uint b) @@ -499,10 +505,10 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve case 0x1d: doAbsolute!ora(x); break; case 0x1e: doAbsolute!asl(x); break; case 0x20: - push((pc + 2) >> 8); - push((pc + 2) & 0xff); - pc = fetchWord(); - --pc; + const lo = fetchByte(); + push((pc + 1) >> 8); + push((pc + 1) & 0xff); + pc = cast(ushort) (makeWord(fetchByte(), lo) - 1); break; case 0x21: doIndirectX!and(); break; case 0x24: doAbsoluteZP!bit(); break; @@ -564,10 +570,19 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve case 0x68: setNZ(a = pop()); break; case 0x69: doImmediate!adc(); break; case 0x6a: doAccumulator!ror(); break; - case 0x6c: - ushort ad = fetchWord(); - pc = dpeek(ad); - --pc; + case 0x6c: { + const ptr = fetchWord(); + const wrapped = + cast(ushort) ((ptr & 0xff00) | ((ptr + 1) & 0xff)); + const lo = ld(ptr); + static if (isCmos!cpuVariant) + { + pc = cast(ushort) (makeWord( + ld(cast(ushort) (ptr + 1)), lo) - 1); + } + else + pc = cast(ushort) (makeWord(ld(wrapped), lo) - 1); + } break; case 0x6d: doAbsolute!adc(); break; case 0x6e: doAbsolute!ror(); break; @@ -701,12 +716,20 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve else { case 0x07 + n * 0x10: - case 0x87 + n * 0x10: + case 0x87 + n * 0x10: fetchByte(); break dispatch; case 0x0f + n * 0x10: - case 0x8f + n * 0x10: break dispatch; + case 0x8f + n * 0x10: fetchByte(); fetchByte(); break dispatch; } } case 0xcb: // TODO: interrupts + static if (hasWaiStp!cpuVariant) + { + stopped = true; + observer.endInstruction(); + return; + } + else + break; case 0xdb: static if (hasWaiStp!cpuVariant) { @@ -715,7 +738,10 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve return; } else + { + fetchByte(); break; + } case 0x03: case 0x13: case 0x23: case 0x33: case 0x43: case 0x53: case 0x63: case 0x73: case 0x83: case 0x93: case 0xa3: case 0xb3: @@ -810,87 +836,86 @@ unittest }} } -private version(unittest) { +private version(unittest): - // TODO: build tests from source and extract the values from there - enum ushort entryPoint = 0x0400; - enum ushort testCaseVar = 0x0200; +// TODO: build tests from source and extract the values from there +enum ushort entryPoint = 0x0400; +enum ushort testCaseVar = 0x0200; - enum long instructionsChunk = 1_000_000; - enum long instructionsLimit = 500_000_000; +enum long instructionsChunk = 1_000_000; +enum long instructionsLimit = 500_000_000; - bool isStuckSelfLoop(E)(E emu, ushort address) +bool isStuckSelfLoop(E)(E emu, ushort address) +{ + const opcode = emu.ld(address); + if (opcode == 0x4c) // jmp * + return emu.dpeek(address + 1) == address; + if (emu.ld(cast(ushort) (address + 1)) != 0xfe) // not a branch to self + return false; + switch (opcode) { - const opcode = emu.ld(address); - if (opcode == 0x4c) // jmp * - return emu.dpeek(address + 1) == address; - if (emu.ld(cast(ushort) (address + 1)) != 0xfe) // not a branch to self - return false; - switch (opcode) - { - case 0x10: return !emu.nflag; // bpl - case 0x30: return emu.nflag; // bmi - case 0x50: return !emu.vflag; // bvc - case 0x70: return emu.vflag; // bvs - case 0x90: return !emu.cflag; // bcc - case 0xb0: return emu.cflag; // bcs - case 0xd0: return !emu.zflag; // bne - case 0xf0: return emu.zflag; // beq - case 0x80: return true; // bra (65C02) - default: return false; - } + case 0x10: return !emu.nflag; // bpl + case 0x30: return emu.nflag; // bmi + case 0x50: return !emu.vflag; // bvc + case 0x70: return emu.vflag; // bvs + case 0x90: return !emu.cflag; // bcc + case 0xb0: return emu.cflag; // bcs + case 0xd0: return !emu.zflag; // bne + case 0xf0: return emu.zflag; // beq + case 0x80: return true; // bra (65C02) + default: return false; } +} - ushort runToSelfLoop(E)(E emu, immutable(ubyte)[] image) +ushort runToSelfLoop(E)(E emu, immutable(ubyte)[] image) +{ + foreach (i, b; image) + emu.st(cast(ushort) i, b); + emu.sp = 0xff; + emu.pc = entryPoint; + emu.instructions = 0; + emu.stopOnEmptyStackRts = false; + + bool started; + while (emu.instructions < instructionsLimit) { - foreach (i, b; image) - emu.st(cast(ushort) i, b); - emu.sp = 0xff; - emu.pc = entryPoint; - emu.instructions = 0; - emu.stopOnEmptyStackRts = false; - - bool started; - while (emu.instructions < instructionsLimit) + emu.instructionLimit = emu.instructions + instructionsChunk; + if (started) + emu.resume(); + else { - emu.instructionLimit = emu.instructions + instructionsChunk; - if (started) - emu.resume(); - else - { - emu.run(); - started = true; - } - foreach (ushort candidate; [cast(ushort) (emu.pc + 1), emu.pc]) - if (emu.isStuckSelfLoop(candidate)) - return candidate; + emu.run(); + started = true; } - throw new Exception(format( - "did not settle within %d instructions (pc=$%04X, test_case=%d)", - instructionsLimit, emu.pc, emu.ld(testCaseVar))); + foreach (ushort candidate; [cast(ushort) (emu.pc + 1), emu.pc]) + if (emu.isStuckSelfLoop(candidate)) + return candidate; } + throw new Exception(format( + "did not settle within %d instructions (pc=$%04X, test_case=%d)", + instructionsLimit, emu.pc, emu.ld(testCaseVar))); +} - bool check(E)(E emu, string what, immutable(ubyte)[] image, - ushort successAddress) +bool check(E)(E emu, string what, immutable(ubyte)[] image, + ushort successAddress) +{ + writef("%-34s ", what ~ ":"); + stdout.flush(); + try { - writef("%-34s ", what ~ ":"); - stdout.flush(); - try + const trap = runToSelfLoop(emu, image); + if (trap == successAddress) { - const trap = runToSelfLoop(emu, image); - if (trap == successAddress) - { - writefln("PASS (success trap $%04X, %d instructions)", trap, emu.instructions); - return true; - } - writefln("FAIL: stopped at $%04X, expected $%04X, test_case=%d", - trap, successAddress, emu.ld(testCaseVar)); - writeln(" look the address up in the suite's .lst to identify the check"); + writefln("PASS (success trap $%04X, %d instructions)", trap, emu.instructions); + return true; } - catch (Exception e) - writefln("FAIL: %s", e.msg); - return false; + writefln("FAIL: stopped at $%04X, expected $%04X, test_case=%d", + trap, successAddress, emu.ld(testCaseVar)); + writeln(" look the address up in the suite's .lst to identify the check"); } + catch (Exception e) + writefln("FAIL: %s", e.msg); + return false; } unittest @@ -918,3 +943,102 @@ unittest writeln(ok ? "all CPU tests passed" : "CPU TESTS FAILED"); assert(ok); } + +struct State +{ + ushort pc; + ubyte sp, a, x, y, p; + + void toString(W)(scope W writer) const + { + writer.formattedWrite!"pc=%04x s=%02x a=%02x x=%02x y=%02x p=%02x"( + pc, sp, a, x, y, p); + } +} + +void checkInstruction(CpuVariant v)(State initial, const(int[2])[] ram, + State expected, const(int[2])[] changed, + string file = __FILE__, size_t line = __LINE__) +{ + import std.algorithm : joiner; + import std.array : join, split; + + assert((expected.p & 0x10) == 0); + + auto emu = new Emulator!(v, NoObserver)(); + emu.stopOnEmptyStackRts = false; + + ubyte[ushort] want; + foreach (cell; ram) + { + emu.ram[cell[0]] = cast(ubyte) cell[1]; + want[cast(ushort) cell[0]] = cast(ubyte) cell[1]; + } + foreach (cell; changed) + want[cast(ushort) cell[0]] = cast(ubyte) cell[1]; + + emu.pc = initial.pc; + emu.sp = initial.sp; + emu.a = initial.a; + emu.x = initial.x; + emu.y = initial.y; + emu.p = initial.p; + emu.instructionLimit = 1; + + const opcode = emu.ram[initial.pc]; + emu.run(); + + const where = format("%s(%d): %s $%02x", file, line, v, opcode); + const got = State(cast(ushort) (emu.pc + 1), emu.sp, emu.a, emu.x, emu.y, emu.p); + + assert(got == expected, format("%s: state\n got %s\n expected %s", where, got, expected)); + foreach (addr, value; want) + assert(emu.ram[addr] == value, format("%s: $%04x is $%02x, expected $%02x", + where, addr, emu.ram[addr], value)); +} + +unittest +{ + debug writeln("unittest emu singlestep"); + + // mos_6502, opcode $20: "20 55 13" + checkInstruction!(CpuVariant.mos_6502)( + State(0x017b, 0x7d, 0x9e, 0x89, 0x34, 0xe6), [[0x017b, 0x20], [0x017c, 0x55], [0x017d, 0x13], [0x0155, 0xad]], + State(0x0155, 0x7b, 0x9e, 0x89, 0x34, 0xe6), [[0x017c, 0x7d], [0x017d, 0x01]]); + + // mos_6502, opcode $61: "61 91 cd" + checkInstruction!(CpuVariant.mos_6502)( + State(0xf372, 0x4c, 0xb3, 0x50, 0xd1, 0x6b), [[0xf372, 0x61], [0xf373, 0x91], [0xf374, 0xcd], [0x0091, 0xdc], [0x00e1, 0x1d], [0x00e2, 0x2c], [0x2c1d, 0x46]], + State(0xf374, 0x4c, 0x60, 0x50, 0xd1, 0x29), []); + + // mos_6502, opcode $6c: "6c ff 70" + checkInstruction!(CpuVariant.mos_6502)( + State(0x2887, 0x47, 0x23, 0x66, 0xb2, 0x24), [[0x2887, 0x6c], [0x2888, 0xff], [0x2889, 0x70], [0x70ff, 0x9d], [0x7000, 0x98], [0x989d, 0x23]], + State(0x989d, 0x47, 0x23, 0x66, 0xb2, 0x24), []); + + // wdc_65c02, opcode $07: "07 28 a5" + checkInstruction!(CpuVariant.wdc_65c02)( + State(0x2f7b, 0x32, 0x42, 0x71, 0x5d, 0x2f), [[0x2f7b, 0x07], [0x2f7c, 0x28], [0x2f7d, 0xa5], [0x0028, 0xba]], + State(0x2f7d, 0x32, 0x42, 0x71, 0x5d, 0x2f), []); + + // wdc_65c02, opcode $0f: "0f 7c f5" + checkInstruction!(CpuVariant.wdc_65c02)( + State(0x8da5, 0x9c, 0x88, 0x57, 0x65, 0xe8), [[0x8da5, 0x0f], [0x8da6, 0x7c], [0x8da7, 0xf5], [0x8da8, 0xe1]], + State(0x8da8, 0x9c, 0x88, 0x57, 0x65, 0xe8), []); + + // wdc_65c02, opcode $cb: "cb 4a 20" + checkInstruction!(CpuVariant.wdc_65c02)( + State(0x7487, 0x15, 0xd2, 0xf9, 0x06, 0x22), [[0x7487, 0xcb], [0x7488, 0x4a], [0x7489, 0x20]], + State(0x7488, 0x15, 0xd2, 0xf9, 0x06, 0x22), []); + + // wdc_65c02, opcode $db: "db cf 42" + checkInstruction!(CpuVariant.wdc_65c02)( + State(0x6aeb, 0x6f, 0x61, 0x9e, 0xd5, 0x27), [[0x6aeb, 0xdb], [0x6aec, 0xcf], [0x6aed, 0x42], [0x00cf, 0x8b], [0x006d, 0x85]], + State(0x6aed, 0x6f, 0x61, 0x9e, 0xd5, 0x27), []); + + // wdc_65c02, opcode $f1: "f1 3" + checkInstruction!(CpuVariant.wdc_65c02)( + State(0x73e6, 0x45, 0xf3, 0xfc, 0x7e, 0xe8), [[0x00ff, 0x15], [0x1638, 0x1f], [0x00fe, 0xba], [0x73e7, 0xfe], [0x73e6, 0xf1]], + State(0x73e8, 0x45, 0xcd, 0xfc, 0x7e, 0xa9), []); + +} diff --git a/test/singlestep/source/app.d b/test/singlestep/source/app.d index 5ee13a6..ba52c50 100644 --- a/test/singlestep/source/app.d +++ b/test/singlestep/source/app.d @@ -56,9 +56,9 @@ struct Access ubyte value; bool write; - string toString() const + void toString(W)(scope W writer) const { - return format("%s %04x %02x", write ? "W" : "R", addr, value); + writer.formattedWrite!"%s %04x %02x"(write ? "W" : "R", addr, value); } } @@ -67,10 +67,16 @@ struct State ushort pc; ubyte sp, a, x, y, p; - string toString() const + void toString(W)(scope W writer, FormatSpec!char fmt) const { - return format("pc=%04x s=%02x a=%02x x=%02x y=%02x p=%02x", - pc, sp, a, x, y, p); + if (fmt.spec == 's') + writer.formattedWrite!"pc=%04x s=%02x a=%02x x=%02x y=%02x p=%02x"( + pc, sp, a, x, y, p); + else if (fmt.spec == 'S') + writer.formattedWrite!"State(0x%04x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x)"( + pc, sp, a, x, y, p); + else + assert(0); } } @@ -78,6 +84,11 @@ struct Cell { ushort addr; ubyte value; + + void toString(W)(scope W writer) const + { + writer.formattedWrite!"[0x%04x, 0x%02x]"(addr, value); + } } struct TestCase @@ -173,19 +184,6 @@ unittest assert(tests[0].cycles == [Access(59082, 177, false), Access(40, 160, true)]); } -struct Trace -{ - ubyte[] ram; - Access[] accesses; - - void instruction(E)(E emu) {} - void fetch(ushort addr, ubyte value) { accesses ~= Access(addr, value, false); } - void read(ushort addr, ubyte value) { accesses ~= Access(addr, value, false); } - void write(ushort addr, ubyte value) { accesses ~= Access(addr, value, true); } - void idle(ushort addr) { accesses ~= Access(addr, ram[addr], false); } - void endInstruction() {} -} - string runOne(E)(E emu, ref const TestCase t) { foreach (c; t.initialRam) @@ -199,8 +197,6 @@ string runOne(E)(E emu, ref const TestCase t) emu.stopped = false; emu.instructions = 0; emu.instructionLimit = 1; - emu.observer.accesses.length = 0; - emu.observer.accesses.assumeSafeAppend(); string thrown = collectExceptionMsg(emu.run()); @@ -233,13 +229,30 @@ struct Result ubyte opcode; size_t total, failed; string[] reports; + string[] unittests; } -Result runOpcode(CpuVariant v)(string path, ubyte opcode) +string emitUnittest(CpuVariant v, ref const TestCase t) +{ + ubyte[ushort] before; + foreach (c; t.initialRam) + before[c.addr] = c.value; + auto changed = t.expectedRam.filter!(c => before.get(c.addr, 0) != c.value).array; + + return format( + "\t// %s, opcode $%02x: \"%s\"\n" ~ + "\tcheckInstruction!(CpuVariant.%s)(\n" ~ + "\t\t%S, %s,\n" ~ + "\t\t%S, %s);\n", + v, t.cycles.length ? t.cycles[0].value : 0, t.name, v, + t.initial, t.initialRam, + t.expected, changed); +} + +Result runOpcode(CpuVariant v)(string path, ubyte opcode, bool emitUnittests) { Result r = { opcode: opcode }; - auto emu = new Emulator!(v, Trace)(); - emu.observer.ram = emu.ram; + auto emu = new Emulator!v(); emu.stopOnEmptyStackRts = false; auto text = strip(cast(string) read(path)); @@ -254,6 +267,8 @@ Result runOpcode(CpuVariant v)(string path, ubyte opcode) ++r.failed; if (r.reports.length < 5) r.reports ~= diag; + if (emitUnittests && r.unittests.length < 5) + r.unittests ~= emitUnittest(v, t); } } @@ -273,14 +288,14 @@ immutable Target[] targets = [ Target("wdc65c02", CpuVariant.wdc_w65c02s), ]; -size_t runTarget(CpuVariant v)(string dir, const(ubyte)[] opcodes) +size_t runTarget(CpuVariant v)(string dir, const(ubyte)[] opcodes, bool emitUnittests) { auto results = new Result[opcodes.length]; foreach (i, opcode; opcodes.parallel) { const path = buildPath(dir, format("%02x.json", opcode)); results[i] = exists(path) - ? runOpcode!v(path, opcode) + ? runOpcode!v(path, opcode, emitUnittests) : Result(opcode); } @@ -299,6 +314,8 @@ size_t runTarget(CpuVariant v)(string dir, const(ubyte)[] opcodes) r.failed ? "FAIL " : "ok"); foreach (report; r.reports) writeln(report); + foreach (ut; r.unittests) + writeln(ut); } return failed; @@ -306,12 +323,12 @@ size_t runTarget(CpuVariant v)(string dir, const(ubyte)[] opcodes) return failed; } -ubyte[] parseOpcodes(string spec) +ubyte[] parseOpcodes(string spec, bool skipUndocumented) { - if (!spec.length) - return iota(256).map!(i => cast(ubyte) i).array; bool[256] set; - foreach (part; spec.split(",")) + if (!spec.length) + set[] = true; + else foreach (part; spec.split(",")) { const range = part.split("-").map!(a => a.to!uint(16)).array; const lo = range[0]; @@ -319,6 +336,29 @@ ubyte[] parseOpcodes(string spec) foreach (i; lo .. hi + 1) set[i] = true; } + if (skipUndocumented) { + static immutable undocumentedOpcodes = [ + 0x02, 0x03, 0x04, 0x07, 0x0B, 0x0C, 0x0F, + 0x12, 0x13, 0x14, 0x17, 0x1A, 0x1B, 0x1C, 0x1F, + 0x22, 0x23, 0x27, 0x2B, 0x2F, + 0x32, 0x33, 0x34, 0x37, 0x3A, 0x3B, 0x3C, 0x3F, + 0x42, 0x43, 0x44, 0x47, 0x4B, 0x4F, + 0x52, 0x53, 0x54, 0x57, 0x5A, 0x5B, 0x5C, 0x5F, + 0x62, 0x63, 0x64, 0x67, 0x6B, 0x6F, + 0x72, 0x73, 0x74, 0x77, 0x7A, 0x7B, 0x7C, 0x7F, + 0x80, 0x82, 0x83, 0x87, 0x89, 0x8B, 0x8F, + 0x92, 0x93, 0x97, 0x9B, 0x9C, 0x9E, 0x9F, + 0xA3, 0xA7, 0xAB, 0xAF, + 0xB2, 0xB3, 0xB7, 0xBB, 0xBF, + 0xC2, 0xC3, 0xC7, 0xCB, 0xCF, + 0xD2, 0xD3, 0xD4, 0xD7, 0xDA, 0xDB, 0xDC, 0xDF, + 0xE2, 0xE3, 0xE7, 0xEB, 0xEF, + 0xF2, 0xF3, 0xF4, 0xF7, 0xFA, 0xFB, 0xFC, 0xFF + ]; + static assert(undocumentedOpcodes.length == 105); + foreach (o; undocumentedOpcodes) + set[o] = false; + } return iota(256).filter!(i => set[i]).map!(i => cast(ubyte) i).array; } @@ -344,11 +384,15 @@ int main(string[] args) string dir; string cpuSpec; string opcodeSpec; + bool skipUndocumented; + bool emitUnittests; auto help = getopt(args, - "d|dir", "root of SingleStepTests/65x02", &dir, - "c|cpu", "CPUs to test, comma separated (default: all found).", &cpuSpec, - "o|opcodes", "Opcodes in hex, e.g. a9,1e,b1-b5 (default: all).", &opcodeSpec); + "d|dir", "root of SingleStepTests/65x02", &dir, + "c|cpu", "CPUs to test, comma separated (default: all found).", &cpuSpec, + "o|opcodes", "Opcodes in hex, e.g. a9,1e,b1-b5 (default: all).", &opcodeSpec, + "u|skip-undocumented", "Skip undocumented NMOS 6502 opcodes.", &skipUndocumented, + "t|unittest", "Emit unittests for failing cases.", &emitUnittests); if (help.helpWanted) { @@ -365,7 +409,7 @@ int main(string[] args) return 2; } - const opcodes = parseOpcodes(opcodeSpec); + const opcodes = parseOpcodes(opcodeSpec, skipUndocumented); const cpus = cpuSpec.length ? cpuSpec.split(",") : null; size_t failed; @@ -386,7 +430,7 @@ int main(string[] args) static foreach (v; EnumMembers!CpuVariant) { case v: - failed += runTarget!v(dataDir, opcodes); + failed += runTarget!v(dataDir, opcodes, emitUnittests); break dispatch; } default: From 3a0db53cb5c9c472992a7eeceebb617171d839a7 Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Thu, 10 Sep 2026 06:22:50 +0200 Subject: [PATCH 07/17] emu: All documented instructions cycle exact. --- source/xebin/emu.d | 725 ++++++++++++++++++++++++++--------- test/singlestep/source/app.d | 38 +- 2 files changed, 584 insertions(+), 179 deletions(-) diff --git a/source/xebin/emu.d b/source/xebin/emu.d index 3311956..764e9e8 100644 --- a/source/xebin/emu.d +++ b/source/xebin/emu.d @@ -36,14 +36,24 @@ private ushort makeWord(uint b1, uint b0) return cast(ushort) ((b1 << 8) | b0); } -string substOperand(string expr, string read, string writeOpen) +enum bool readsOperand(string expr) = expr.indexOf("@r") >= 0; +enum bool writesOperand(string expr) = expr.indexOf("@w(") >= 0; +enum indexAlways = "@i;"; +enum bool alwaysIndexed(string expr) = expr.indexOf(indexAlways) >= 0; + +string substOperand(string expr, string read, string writeOpen, string modifyOpen, string idleRead) { - return expr.replace("@w(", writeOpen).replace("@r", read); + return expr + .replace(indexAlways, "") + .replace("@d", idleRead) + .replace("@m(", modifyOpen) + .replace("@w(", writeOpen) + .replace("@r", read); } enum adc = q{ - uint arg = ld(addr); + uint arg = @r; uint tmp = a + arg + cflag; if (!dflag) { @@ -64,8 +74,10 @@ q{ sum += 0x60; cflag = sum >= 0x100; a = cast(ubyte) sum; - static if (isCmos!cpuVariant) + static if (isCmos!cpuVariant) { + @d; setNZ(a); + } } }; @@ -86,6 +98,7 @@ q{ int al = (oa & 0x0f) - (operand & 0x0f) + carryIn - 1; static if (isCmos!cpuVariant) { + @d; int res = oa - operand + carryIn - 1; if (res < 0) res -= 0x60; @@ -105,20 +118,37 @@ q{ } }; -enum cmp = q{ ubyte tmp = ld(addr); setNZ(a - tmp); cflag = a >= tmp; }; -enum cpx = q{ ubyte tmp = ld(addr); setNZ(x - tmp); cflag = x >= tmp; }; -enum cpy = q{ ubyte tmp = ld(addr); setNZ(y - tmp); cflag = y >= tmp; }; -enum lda = q{ setNZ(a = ld(addr)); }; -enum ldx = q{ setNZ(x = ld(addr)); }; -enum ldy = q{ setNZ(y = ld(addr)); }; -enum ora = q{ setNZ(a |= ld(addr)); }; -enum and = q{ setNZ(a &= ld(addr)); }; -enum eor = q{ setNZ(a ^= ld(addr)); }; -enum inc = q{ setNZ(st(addr, ld(addr) + 1)); }; -enum dec = q{ setNZ(st(addr, ld(addr) - 1)); }; +enum cmp = q{ ubyte tmp = @r; setNZ(a - tmp); cflag = a >= tmp; }; +enum cpx = q{ ubyte tmp = @r; setNZ(x - tmp); cflag = x >= tmp; }; +enum cpy = q{ ubyte tmp = @r; setNZ(y - tmp); cflag = y >= tmp; }; +enum lda = q{ setNZ(a = @r); }; +enum ldx = q{ setNZ(x = @r); }; +enum ldy = q{ setNZ(y = @r); }; +enum ora = q{ setNZ(a |= @r); }; +enum and = q{ setNZ(a &= @r); }; +enum eor = q{ setNZ(a ^= @r); }; +enum sta = q{ @w(a); }; +enum stx = q{ @w(x); }; +enum sty = q{ @w(y); }; +enum stz = q{ @w(0); }; +enum inc = indexAlways ~ +q{ + ubyte tmp = @r; + @m(tmp); + setNZ(++tmp); + @w(tmp); +}; +enum dec = indexAlways ~ +q{ + ubyte tmp = @r; + @m(tmp); + setNZ(--tmp); + @w(tmp); +}; enum asl = q{ ubyte tmp = @r; + @m(tmp); cflag = (tmp & 0x80) != 0; tmp <<= 1; setNZ(tmp); @@ -127,6 +157,7 @@ q{ enum rol = q{ ubyte tmp = @r; + @m(tmp); bool nc = (tmp & 0x80) != 0; tmp = cast(ubyte) ((tmp << 1) | cflag); cflag = nc; @@ -136,6 +167,7 @@ q{ enum lsr = q{ ubyte tmp = @r; + @m(tmp); cflag = (tmp & 1) != 0; tmp >>>= 1; setNZ(tmp); @@ -144,6 +176,7 @@ q{ enum ror = q{ ubyte tmp = @r; + @m(tmp); bool nc = (tmp & 1) != 0; tmp = cast(ubyte) ((tmp >>> 1) | (cflag ? 0x80 : 0)); cflag = nc; @@ -157,8 +190,9 @@ q{ nflag = (tmp & 0x80) != 0; vflag = (tmp & 0x40) != 0; }; -enum tsb = q{ ubyte tmp = @r; zflag = (a & tmp) == 0; @w(cast(ubyte) (tmp | a)); }; -enum trb = q{ ubyte tmp = @r; zflag = (a & tmp) == 0; @w(cast(ubyte) (tmp & ~a)); }; +enum tsb = q{ ubyte tmp = @r; @m(tmp); zflag = (a & tmp) == 0; @w(cast(ubyte) (tmp | a)); }; +enum trb = q{ ubyte tmp = @r; @m(tmp); zflag = (a & tmp) == 0; @w(cast(ubyte) (tmp & ~a)); }; +enum nop = q{ ubyte discarded = @r; }; // NOPs with fancy addressing modes /// enum CpuVariant { @@ -224,6 +258,8 @@ struct UniformTicks /// class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserver) { + enum cpu = cpuVariant; + /// Observer policy instance; configure it before running. Observer observer; @@ -324,6 +360,39 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve return makeWord(hi, lo); } + private void idleRead(int addr) + { + observer.idle(cast(ushort) addr); + } + + private void idleFetch() + { + idleRead(pc + 1); + } + + private void idlePop() + { + idleRead(0x100 + sp); + } + + private void indexPenalty(string expr)(ushort base, ubyte index) + { + const ushort fixed = cast(ushort) (base + index); + const bool crossed = (fixed & 0xff00) != (base & 0xff00); + static if (isCmos!cpuVariant) + { + enum always = alwaysIndexed!expr || // INC/DEC + (writesOperand!expr && !readsOperand!expr); // STA + if (crossed || always) + idleRead(pc); + } + else + { + if (crossed || writesOperand!expr) + idleRead((base & 0xff00) | (fixed & 0xff)); + } + } + private ushort readWord(ushort addr, ushort hiAddr) { const lo = ld(addr); @@ -331,46 +400,79 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve return makeWord(hi, lo); } + private void modifyCycle(ushort addr, ubyte value) + { + static if (isCmos!cpuVariant) + ld(addr); + else + st(addr, value); + } + + private static void noModify(ubyte value) {} + void doAccumulator(string expr)() { - mixin(substOperand(expr, "a", "a = (")); + idleFetch(); + mixin(substOperand(expr, "a", "a = (", "noModify(", "")); } void doImmediate(string expr)() { - ++pc; - alias pc addr; - mixin(substOperand(expr, "ld(addr)", "st(addr, ")); + static assert(!writesOperand!expr); + mixin(substOperand(expr, "fetchByte()", "", "noModify(", "idleFetch()")); + } + + private void doAbsolute(string expr)() + { + const addr = fetchWord(); + mixin(substOperand(expr, "ld(addr)", "st(addr, ", "modifyCycle(addr, ", "idleRead(addr)")); + } + + private void doAbsoluteIndexed(string expr)(ubyte index) + { + const base = fetchWord(); + indexPenalty!expr(base, index); + const addr = cast(ushort) (base + index); + mixin(substOperand(expr, "ld(addr)", "st(addr, ", "modifyCycle(addr, ", "idleRead(addr)")); + } + + private void doNopAbsolute(bool indexCycle)() + { + fetchWord(); + static if (indexCycle) + idleRead(pc); } - void doAbsolute(string expr)(ubyte index = 0) + private void doZeroPage(string expr)() { - ushort addr = fetchWord(); - addr += index; - mixin(substOperand(expr, "ld(addr)", "st(addr, ")); + const ubyte addr = fetchByte(); + mixin(substOperand(expr, "ld(addr)", "st(addr, ", "modifyCycle(addr, ", "idleRead(addr)")); } - void doAbsoluteZP(string expr)(ubyte index = 0) + private void doZeroPageIndexed(string expr)(ubyte index) { - ubyte addr = fetchByte(); - addr += index; - mixin(substOperand(expr, "ld(addr)", "st(addr, ")); + const ubyte base = fetchByte(); + idleRead(base); + const ubyte addr = cast(ubyte) (base + index); + mixin(substOperand(expr, "ld(addr)", "st(addr, ", "modifyCycle(addr, ", "idleRead(addr)")); } void doIndirectY(string expr)() { - const ushort zp = fetchByte(); - ushort addr = readWord(zp, cast(ushort) ((zp + 1) & 0xff)); - addr += y; - mixin(substOperand(expr, "ld(addr)", "st(addr, ")); + const ushort zp = fetchByte(); + const base = readWord(zp, cast(ushort) ((zp + 1) & 0xff)); + indexPenalty!expr(base, y); + const addr = cast(ushort) (base + y); + mixin(substOperand(expr, "ld(addr)", "st(addr, ", "modifyCycle(addr, ", "idleRead(addr)")); } void doIndirectX(string expr)() { - ushort zp = fetchByte(); - zp = (zp + x) & 0xff; - const addr = readWord(zp, cast(ushort) ((zp + 1) & 0xff)); - mixin(substOperand(expr, "ld(addr)", "st(addr, ")); + const ushort zp = fetchByte(); + idleRead(zp); + const ptr = (zp + x) & 0xff; + const addr = readWord(cast(ushort) ptr, cast(ushort) ((ptr + 1) & 0xff)); + mixin(substOperand(expr, "ld(addr)", "st(addr, ", "modifyCycle(addr, ", "idleRead(addr)")); } static if (isCmos!cpuVariant) @@ -378,7 +480,7 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve { const ushort zp = fetchByte(); const addr = readWord(zp, cast(ushort) ((zp + 1) & 0xff)); - mixin(substOperand(expr, "ld(addr)", "st(addr, ")); + mixin(substOperand(expr, "ld(addr)", "st(addr, ", "modifyCycle(addr, ", "idleRead(addr)")); } static if (isCmos!cpuVariant) @@ -386,6 +488,7 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve { const ubyte addr = fetchByte(); const ubyte value = ld(addr); + modifyCycle(addr, value); static if (set) st(addr, value | mask); else @@ -397,20 +500,29 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve { const ubyte zp = fetchByte(); const bool isSet = (ld(zp) & mask) != 0; + idleRead(zp); const byte offs = fetchByte(); - if (isSet == branchIfSet) - pc = cast(ushort) (pc + offs); + if (isSet != branchIfSet) + return; + const next = cast(ushort) (pc + 1); + idleRead(next); + const target = cast(ushort) (next + offs); + if ((target & 0xff00) != (next & 0xff00)) + idleRead(next); + pc = cast(ushort) (target - 1); } void doBranch(string pred)() { byte offs = fetchByte(); - if (mixin(pred)) - { - pc++; - pc += offs; - pc--; - } + if (!mixin(pred)) + return; + const next = cast(ushort) (pc + 1); + idleRead(next); + const target = cast(ushort) (next + offs); + if ((target & 0xff00) != (next & 0xff00)) + idleRead((next & 0xff00) | (target & 0xff)); + pc = cast(ushort) (target - 1); } void setNZ(uint res) @@ -456,14 +568,14 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve dispatch: switch (instr) { case 0x00: - push((pc + 2) >> 8); - push((pc + 2) & 0xff); + fetchByte(); + push((pc + 1) >> 8); + push((pc + 1) & 0xff); push(p | 0x10); iflag = true; static if (isCmos!cpuVariant) dflag = false; - pc = dpeek(0xfffe); - --pc; + pc = cast(ushort) (readWord(0xfffe, 0xffff) - 1); break; // Host escape: $02 followed by a selector byte. // Two-byte encoding on all 6502 variants: @@ -489,32 +601,33 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve } break; case 0x01: doIndirectX!ora(); break; - case 0x05: doAbsoluteZP!ora(); break; - case 0x06: doAbsoluteZP!asl(); break; - case 0x08: push(p | 0x10); break; + case 0x05: doZeroPage!ora(); break; + case 0x06: doZeroPage!asl(); break; + case 0x08: idleFetch(); push(p | 0x10); break; case 0x09: doImmediate!ora(); break; case 0x0a: doAccumulator!asl(); break; case 0x0d: doAbsolute!ora(); break; case 0x0e: doAbsolute!asl(); break; case 0x10: doBranch!"!nflag"(); break; case 0x11: doIndirectY!ora(); break; - case 0x15: doAbsoluteZP!ora(x); break; - case 0x16: doAbsoluteZP!asl(x); break; - case 0x18: cflag = false; break; - case 0x19: doAbsolute!ora(y); break; - case 0x1d: doAbsolute!ora(x); break; - case 0x1e: doAbsolute!asl(x); break; + case 0x15: doZeroPageIndexed!ora(x); break; + case 0x16: doZeroPageIndexed!asl(x); break; + case 0x18: idleFetch(); cflag = false; break; + case 0x19: doAbsoluteIndexed!ora(y); break; + case 0x1d: doAbsoluteIndexed!ora(x); break; + case 0x1e: doAbsoluteIndexed!asl(x); break; case 0x20: const lo = fetchByte(); + idlePop(); push((pc + 1) >> 8); push((pc + 1) & 0xff); pc = cast(ushort) (makeWord(fetchByte(), lo) - 1); break; case 0x21: doIndirectX!and(); break; - case 0x24: doAbsoluteZP!bit(); break; - case 0x25: doAbsoluteZP!and(); break; - case 0x26: doAbsoluteZP!rol(); break; - case 0x28: p = pop(); break; + case 0x24: doZeroPage!bit(); break; + case 0x25: doZeroPage!and(); break; + case 0x26: doZeroPage!rol(); break; + case 0x28: idleFetch(); idlePop(); p = pop(); break; case 0x29: doImmediate!and(); break; case 0x2a: doAccumulator!rol(); break; case 0x2c: doAbsolute!bit(); break; @@ -522,22 +635,24 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve case 0x2e: doAbsolute!rol(); break; case 0x30: doBranch!"nflag"(); break; case 0x31: doIndirectY!and(); break; - case 0x35: doAbsoluteZP!and(x); break; - case 0x36: doAbsoluteZP!rol(x); break; - case 0x38: cflag = true; break; - case 0x39: doAbsolute!and(y); break; - case 0x3d: doAbsolute!and(x); break; - case 0x3e: doAbsolute!rol(x); break; + case 0x35: doZeroPageIndexed!and(x); break; + case 0x36: doZeroPageIndexed!rol(x); break; + case 0x38: idleFetch(); cflag = true; break; + case 0x39: doAbsoluteIndexed!and(y); break; + case 0x3d: doAbsoluteIndexed!and(x); break; + case 0x3e: doAbsoluteIndexed!rol(x); break; case 0x40: + idleFetch(); + idlePop(); p = pop(); ushort rti = pop(); rti |= cast(ushort) (pop() << 8); pc = cast(ushort) (rti - 1); break; case 0x41: doIndirectX!eor(); break; - case 0x45: doAbsoluteZP!eor(); break; - case 0x46: doAbsoluteZP!lsr(); break; - case 0x48: push(a); break; + case 0x45: doZeroPage!eor(); break; + case 0x46: doZeroPage!lsr(); break; + case 0x48: idleFetch(); push(a); break; case 0x49: doImmediate!eor(); break; case 0x4a: doAccumulator!lsr(); break; case 0x4c: @@ -548,15 +663,18 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve case 0x4e: doAbsolute!lsr(); break; case 0x50: doBranch!"!vflag"(); break; case 0x51: doIndirectY!eor(); break; - case 0x55: doAbsoluteZP!eor(x); break; - case 0x56: doAbsoluteZP!lsr(x); break; - case 0x58: iflag = false; break; - case 0x59: doAbsolute!eor(y); break; - case 0x5d: doAbsolute!eor(x); break; - case 0x5e: doAbsolute!lsr(x); break; + case 0x55: doZeroPageIndexed!eor(x); break; + case 0x56: doZeroPageIndexed!lsr(x); break; + case 0x58: idleFetch(); iflag = false; break; + case 0x59: doAbsoluteIndexed!eor(y); break; + case 0x5d: doAbsoluteIndexed!eor(x); break; + case 0x5e: doAbsoluteIndexed!lsr(x); break; case 0x60: + idleFetch(); + idlePop(); ushort ad = pop(); ad |= cast(ushort) (pop() << 8); + idleRead(ad); if (stopOnEmptyStackRts && sp == 0xff) { observer.endInstruction(); @@ -565,9 +683,9 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve pc = ad; break; case 0x61: doIndirectX!adc(); break; - case 0x65: doAbsoluteZP!adc(); break; - case 0x66: doAbsoluteZP!ror(); break; - case 0x68: setNZ(a = pop()); break; + case 0x65: doZeroPage!adc(); break; + case 0x66: doZeroPage!ror(); break; + case 0x68: idleFetch(); idlePop(); setNZ(a = pop()); break; case 0x69: doImmediate!adc(); break; case 0x6a: doAccumulator!ror(); break; case 0x6c: { @@ -577,6 +695,7 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve const lo = ld(ptr); static if (isCmos!cpuVariant) { + idleRead(wrapped); pc = cast(ushort) (makeWord( ld(cast(ushort) (ptr + 1)), lo) - 1); } @@ -588,122 +707,125 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve case 0x6e: doAbsolute!ror(); break; case 0x70: doBranch!"vflag"(); break; case 0x71: doIndirectY!adc(); break; - case 0x75: doAbsoluteZP!adc(x); break; - case 0x76: doAbsoluteZP!ror(x); break; - case 0x78: iflag = true; break; - case 0x79: doAbsolute!adc(y); break; - case 0x7d: doAbsolute!adc(x); break; - case 0x7e: doAbsolute!ror(x); break; - case 0x81: doIndirectX!"st(addr, a);"(); break; - case 0x84: doAbsoluteZP!"st(addr, y);"(); break; - case 0x85: doAbsoluteZP!"st(addr, a);"(); break; - case 0x86: doAbsoluteZP!"st(addr, x);"(); break; - case 0x88: setNZ(--y); break; - case 0x8a: setNZ(a = x); break; - case 0x8c: doAbsolute!"st(addr, y);"(); break; - case 0x8d: doAbsolute!"st(addr, a);"(); break; - case 0x8e: doAbsolute!"st(addr, x);"(); break; + case 0x75: doZeroPageIndexed!adc(x); break; + case 0x76: doZeroPageIndexed!ror(x); break; + case 0x78: idleFetch(); iflag = true; break; + case 0x79: doAbsoluteIndexed!adc(y); break; + case 0x7d: doAbsoluteIndexed!adc(x); break; + case 0x7e: doAbsoluteIndexed!ror(x); break; + case 0x81: doIndirectX!sta(); break; + case 0x84: doZeroPage!sty(); break; + case 0x85: doZeroPage!sta(); break; + case 0x86: doZeroPage!stx(); break; + case 0x88: idleFetch(); setNZ(--y); break; + case 0x8a: idleFetch(); setNZ(a = x); break; + case 0x8c: doAbsolute!sty(); break; + case 0x8d: doAbsolute!sta(); break; + case 0x8e: doAbsolute!stx(); break; case 0x90: doBranch!"!cflag"(); break; - case 0x91: doIndirectY!"st(addr, a);"(); break; - case 0x94: doAbsoluteZP!"st(addr, y);"(x); break; - case 0x95: doAbsoluteZP!"st(addr, a);"(x); break; - case 0x96: doAbsoluteZP!"st(addr, x);"(y); break; - case 0x98: setNZ(a = y); break; - case 0x99: doAbsolute!"st(addr, a);"(y); break; - case 0x9a: sp = x; break; - case 0x9d: doAbsolute!"st(addr, a);"(x); break; + case 0x91: doIndirectY!sta(); break; + case 0x94: doZeroPageIndexed!sty(x); break; + case 0x95: doZeroPageIndexed!sta(x); break; + case 0x96: doZeroPageIndexed!stx(y); break; + case 0x98: idleFetch(); setNZ(a = y); break; + case 0x99: doAbsoluteIndexed!sta(y); break; + case 0x9a: idleFetch(); sp = x; break; + case 0x9d: doAbsoluteIndexed!sta(x); break; case 0xa0: doImmediate!ldy(); break; case 0xa1: doIndirectX!lda(); break; case 0xa2: doImmediate!ldx(); break; - case 0xa4: doAbsoluteZP!ldy(); break; - case 0xa5: doAbsoluteZP!lda(); break; - case 0xa6: doAbsoluteZP!ldx(); break; - case 0xa8: setNZ(y = a); break; + case 0xa4: doZeroPage!ldy(); break; + case 0xa5: doZeroPage!lda(); break; + case 0xa6: doZeroPage!ldx(); break; + case 0xa8: idleFetch(); setNZ(y = a); break; case 0xa9: doImmediate!lda(); break; - case 0xaa: setNZ(x = a); break; + case 0xaa: idleFetch(); setNZ(x = a); break; case 0xac: doAbsolute!ldy(); break; case 0xad: doAbsolute!lda(); break; case 0xae: doAbsolute!ldx(); break; case 0xb0: doBranch!"cflag"(); break; case 0xb1: doIndirectY!lda(); break; - case 0xb4: doAbsoluteZP!ldy(x); break; - case 0xb5: doAbsoluteZP!lda(x); break; - case 0xb6: doAbsoluteZP!ldx(y); break; - case 0xb8: vflag = false; break; - case 0xb9: doAbsolute!lda(y); break; - case 0xba: setNZ(x = sp); break; - case 0xbc: doAbsolute!ldy(x); break; - case 0xbd: doAbsolute!lda(x); break; - case 0xbe: doAbsolute!ldx(y); break; + case 0xb4: doZeroPageIndexed!ldy(x); break; + case 0xb5: doZeroPageIndexed!lda(x); break; + case 0xb6: doZeroPageIndexed!ldx(y); break; + case 0xb8: idleFetch(); vflag = false; break; + case 0xb9: doAbsoluteIndexed!lda(y); break; + case 0xba: idleFetch(); setNZ(x = sp); break; + case 0xbc: doAbsoluteIndexed!ldy(x); break; + case 0xbd: doAbsoluteIndexed!lda(x); break; + case 0xbe: doAbsoluteIndexed!ldx(y); break; case 0xc0: doImmediate!cpy(); break; case 0xc1: doIndirectX!cmp(); break; - case 0xc4: doAbsoluteZP!cpy(); break; - case 0xc5: doAbsoluteZP!cmp(); break; - case 0xc6: doAbsoluteZP!dec(); break; - case 0xc8: setNZ(++y); break; + case 0xc4: doZeroPage!cpy(); break; + case 0xc5: doZeroPage!cmp(); break; + case 0xc6: doZeroPage!dec(); break; + case 0xc8: idleFetch(); setNZ(++y); break; case 0xc9: doImmediate!cmp(); break; - case 0xca: setNZ(--x); break; + case 0xca: idleFetch(); setNZ(--x); break; case 0xcc: doAbsolute!cpy(); break; case 0xcd: doAbsolute!cmp(); break; case 0xce: doAbsolute!dec(); break; case 0xd0: doBranch!"!zflag"(); break; case 0xd1: doIndirectY!cmp(); break; - case 0xd5: doAbsoluteZP!cmp(x); break; - case 0xd6: doAbsoluteZP!dec(x); break; - case 0xd8: dflag = false; break; - case 0xd9: doAbsolute!cmp(y); break; - case 0xdd: doAbsolute!cmp(x); break; - case 0xde: doAbsolute!dec(x); break; + case 0xd5: doZeroPageIndexed!cmp(x); break; + case 0xd6: doZeroPageIndexed!dec(x); break; + case 0xd8: idleFetch(); dflag = false; break; + case 0xd9: doAbsoluteIndexed!cmp(y); break; + case 0xdd: doAbsoluteIndexed!cmp(x); break; + case 0xde: doAbsoluteIndexed!dec(x); break; case 0xe0: doImmediate!cpx(); break; case 0xe1: doIndirectX!sbc(); break; - case 0xe4: doAbsoluteZP!cpx(); break; - case 0xe5: doAbsoluteZP!sbc(); break; - case 0xe6: doAbsoluteZP!inc(); break; - case 0xe8: setNZ(++x); break; + case 0xe4: doZeroPage!cpx(); break; + case 0xe5: doZeroPage!sbc(); break; + case 0xe6: doZeroPage!inc(); break; + case 0xe8: idleFetch(); setNZ(++x); break; case 0xe9: doImmediate!sbc(); break; - case 0xea: break; + case 0xea: idleFetch(); break; case 0xed: doAbsolute!sbc(); break; case 0xec: doAbsolute!cpx(); break; case 0xee: doAbsolute!inc(); break; case 0xf0: doBranch!"zflag"(); break; case 0xf1: doIndirectY!sbc(); break; - case 0xf5: doAbsoluteZP!sbc(x); break; - case 0xf6: doAbsoluteZP!inc(x); break; - case 0xf8: dflag = true; break; - case 0xf9: doAbsolute!sbc(y); break; - case 0xfd: doAbsolute!sbc(x); break; - case 0xfe: doAbsolute!inc(x); break; + case 0xf5: doZeroPageIndexed!sbc(x); break; + case 0xf6: doZeroPageIndexed!inc(x); break; + case 0xf8: idleFetch(); dflag = true; break; + case 0xf9: doAbsoluteIndexed!sbc(y); break; + case 0xfd: doAbsoluteIndexed!sbc(x); break; + case 0xfe: doAbsoluteIndexed!inc(x); break; static if (isCmos!cpuVariant) { - case 0x1a: setNZ(a = cast(ubyte) (a + 1)); break; - case 0x3a: setNZ(a = cast(ubyte) (a - 1)); break; - case 0x5a: push(y); break; - case 0x7a: setNZ(y = pop()); break; - case 0xda: push(x); break; - case 0xfa: setNZ(x = pop()); break; + case 0x1a: idleFetch(); setNZ(a = cast(ubyte) (a + 1)); break; + case 0x3a: idleFetch(); setNZ(a = cast(ubyte) (a - 1)); break; + case 0x5a: idleFetch(); push(y); break; + case 0x7a: idleFetch(); idlePop(); setNZ(y = pop()); break; + case 0xda: idleFetch(); push(x); break; + case 0xfa: idleFetch(); idlePop(); setNZ(x = pop()); break; case 0x80: doBranch!"true"(); break; case 0x89: zflag = (a & fetchByte()) == 0; break; - case 0x64: doAbsoluteZP!"st(addr, 0);"(); break; - case 0x74: doAbsoluteZP!"st(addr, 0);"(x); break; - case 0x9c: doAbsolute!"st(addr, 0);"(); break; - case 0x9e: doAbsolute!"st(addr, 0);"(x); break; + case 0x64: doZeroPage!stz(); break; + case 0x74: doZeroPageIndexed!stz(x); break; + case 0x9c: doAbsolute!stz(); break; + case 0x9e: doAbsoluteIndexed!stz(x); break; case 0x12: doIndirectZP!ora(); break; case 0x32: doIndirectZP!and(); break; case 0x52: doIndirectZP!eor(); break; case 0x72: doIndirectZP!adc(); break; - case 0x92: doIndirectZP!"st(addr, a);"(); break; + case 0x92: doIndirectZP!sta(); break; case 0xb2: doIndirectZP!lda(); break; case 0xd2: doIndirectZP!cmp(); break; case 0xf2: doIndirectZP!sbc(); break; - case 0x04: doAbsoluteZP!tsb(); break; + case 0x04: doZeroPage!tsb(); break; case 0x0c: doAbsolute!tsb(); break; - case 0x14: doAbsoluteZP!trb(); break; + case 0x14: doZeroPage!trb(); break; case 0x1c: doAbsolute!trb(); break; - case 0x34: doAbsoluteZP!bit(x); break; - case 0x3c: doAbsolute!bit(x); break; - case 0x7c: - pc = dpeek(cast(ushort) (fetchWord() + x)); - --pc; + case 0x34: doZeroPageIndexed!bit(x); break; + case 0x3c: doAbsoluteIndexed!bit(x); break; + case 0x7c: { + const base = fetchWord(); + idleRead(pc - 1); + const ptr = cast(ushort) (base + x); + pc = cast(ushort) (readWord(ptr, cast(ushort) (ptr + 1)) - 1); break; + } static foreach (n; 0 .. 8) { static if (hasBitOps!cpuVariant) @@ -712,13 +834,20 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve case 0x87 + n * 0x10: doBitSetReset!(1 << n, true)(); break dispatch; case 0x0f + n * 0x10: doBitBranch!(1 << n, false)(); break dispatch; case 0x8f + n * 0x10: doBitBranch!(1 << n, true)(); break dispatch; + } + else static if (n & 1) + { + case 0x07 + n * 0x10: + case 0x87 + n * 0x10: doZeroPageIndexed!nop(x); break dispatch; + case 0x0f + n * 0x10: + case 0x8f + n * 0x10: doNopAbsolute!true(); break dispatch; } else { case 0x07 + n * 0x10: - case 0x87 + n * 0x10: fetchByte(); break dispatch; + case 0x87 + n * 0x10: doZeroPage!nop(); break dispatch; case 0x0f + n * 0x10: - case 0x8f + n * 0x10: fetchByte(); fetchByte(); break dispatch; + case 0x8f + n * 0x10: doNopAbsolute!false(); break dispatch; } } case 0xcb: // TODO: interrupts @@ -729,7 +858,10 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve return; } else + { + idleFetch(); break; + } case 0xdb: static if (hasWaiStp!cpuVariant) { @@ -739,7 +871,7 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve } else { - fetchByte(); + doZeroPageIndexed!nop(x); break; } case 0x03: case 0x13: case 0x23: case 0x33: @@ -752,11 +884,14 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve case 0xeb: case 0xfb: break; case 0x22: case 0x42: case 0x62: case 0x82: - case 0xc2: case 0xe2: case 0x44: case 0x54: - case 0xd4: case 0xf4: + case 0xc2: case 0xe2: fetchByte(); break; + case 0x44: + doZeroPage!nop(); break; + case 0x54: case 0xd4: case 0xf4: + doZeroPageIndexed!nop(x); break; case 0x5c: case 0xdc: case 0xfc: - fetchByte(); fetchByte(); break; + doNopAbsolute!true(); break; } default: throw new Exception( @@ -956,8 +1091,26 @@ struct State } } +struct BusTrace +{ + const(ubyte)[] ram; + string[] cycles; + + void instruction(E)(E emu) {} + void fetch(ushort addr, ubyte value) { note('R', addr, value); } + void read(ushort addr, ubyte value) { note('R', addr, value); } + void write(ushort addr, ubyte value) { note('W', addr, value); } + void idle(ushort addr) { note('R', addr, ram[addr]); } + void endInstruction() {} + + private void note(char kind, ushort addr, ubyte value) + { + cycles ~= format("%s %04x %02x", kind, addr, value); + } +} + void checkInstruction(CpuVariant v)(State initial, const(int[2])[] ram, - State expected, const(int[2])[] changed, + State expected, const(int[2])[] changed, string trace, string file = __FILE__, size_t line = __LINE__) { import std.algorithm : joiner; @@ -965,7 +1118,8 @@ void checkInstruction(CpuVariant v)(State initial, const(int[2])[] ram, assert((expected.p & 0x10) == 0); - auto emu = new Emulator!(v, NoObserver)(); + auto emu = new Emulator!(v, BusTrace)(); + emu.observer.ram = emu.ram; emu.stopOnEmptyStackRts = false; ubyte[ushort] want; @@ -995,50 +1149,273 @@ void checkInstruction(CpuVariant v)(State initial, const(int[2])[] ram, foreach (addr, value; want) assert(emu.ram[addr] == value, format("%s: $%04x is $%02x, expected $%02x", where, addr, emu.ram[addr], value)); + + const gotTrace = emu.observer.cycles.join(" "); + const expectedTrace = trace.split().join(" "); + assert(gotTrace == expectedTrace, + format("%s: %d cycles, expected %d\n got %s\n expected %s", where, + emu.observer.cycles.length, expectedTrace.split().length / 3, + gotTrace, expectedTrace)); } unittest { debug writeln("unittest emu singlestep"); + // mos_6502, opcode $00: "00 3f f7" + checkInstruction!(CpuVariant.mos_6502)( + State(0x8b82, 0x51, 0xcb, 0x75, 0xa2, 0x6a), [[0x8b82, 0x00], [0x8b83, 0x3f], [0x8b84, 0xf7], [0xfffe, 0xd4], [0xffff, 0x25], [0x25d4, 0xed]], + State(0x25d4, 0x4e, 0xcb, 0x75, 0xa2, 0x6e), [[0x014f, 0x7a], [0x0150, 0x84], [0x0151, 0x8b]], + "R 8b82 00 R 8b83 3f W 0151 8b W 0150 84 W 014f 7a R fffe d4 R ffff 25"); + + // mos_6502, opcode $01: "01 bd b0" + checkInstruction!(CpuVariant.mos_6502)( + State(0xd5c4, 0x61, 0xa7, 0xf7, 0xf8, 0x28), [[0xd5c4, 0x01], [0xd5c5, 0xbd], [0xd5c6, 0xb0], [0x00bd, 0x58], [0x00b4, 0x81], [0x00b5, 0xeb], [0xeb81, 0x13]], + State(0xd5c6, 0x61, 0xb7, 0xf7, 0xf8, 0xa8), [], + "R d5c4 01 R d5c5 bd R 00bd 58 R 00b4 81 R 00b5 eb R eb81 13"); + + // mos_6502, opcode $06: "06 89 7c" + checkInstruction!(CpuVariant.mos_6502)( + State(0xec81, 0x56, 0xaa, 0xdd, 0xba, 0x29), [[0xec81, 0x06], [0xec82, 0x89], [0xec83, 0x7c], [0x0089, 0x42]], + State(0xec83, 0x56, 0xaa, 0xdd, 0xba, 0xa8), [[0x0089, 0x84]], + "R ec81 06 R ec82 89 R 0089 42 W 0089 42 W 0089 84"); + // mos_6502, opcode $20: "20 55 13" checkInstruction!(CpuVariant.mos_6502)( State(0x017b, 0x7d, 0x9e, 0x89, 0x34, 0xe6), [[0x017b, 0x20], [0x017c, 0x55], [0x017d, 0x13], [0x0155, 0xad]], - State(0x0155, 0x7b, 0x9e, 0x89, 0x34, 0xe6), [[0x017c, 0x7d], [0x017d, 0x01]]); + State(0x0155, 0x7b, 0x9e, 0x89, 0x34, 0xe6), [[0x017c, 0x7d], [0x017d, 0x01]], + "R 017b 20 R 017c 55 R 017d 13 W 017d 01 W 017c 7d R 017d 01"); // mos_6502, opcode $61: "61 91 cd" + // ADC (zp,x) BCD: B3+46+1 == 113+46+1 == (1)60 checkInstruction!(CpuVariant.mos_6502)( State(0xf372, 0x4c, 0xb3, 0x50, 0xd1, 0x6b), [[0xf372, 0x61], [0xf373, 0x91], [0xf374, 0xcd], [0x0091, 0xdc], [0x00e1, 0x1d], [0x00e2, 0x2c], [0x2c1d, 0x46]], - State(0xf374, 0x4c, 0x60, 0x50, 0xd1, 0x29), []); + State(0xf374, 0x4c, 0x60, 0x50, 0xd1, 0x29), [], + "R f372 61 R f373 91 R 0091 dc R 00e1 1d R 00e2 2c R 2c1d 46"); // mos_6502, opcode $6c: "6c ff 70" + // JMP ($70ff) bug checkInstruction!(CpuVariant.mos_6502)( State(0x2887, 0x47, 0x23, 0x66, 0xb2, 0x24), [[0x2887, 0x6c], [0x2888, 0xff], [0x2889, 0x70], [0x70ff, 0x9d], [0x7000, 0x98], [0x989d, 0x23]], - State(0x989d, 0x47, 0x23, 0x66, 0xb2, 0x24), []); + State(0x989d, 0x47, 0x23, 0x66, 0xb2, 0x24), [], + "R 2887 6c R 2888 ff R 2889 70 R 70ff 9d R 7000 98"); + + // mos_6502, opcode $08: "08 60 be" + // PHP does idle fetch cycle + checkInstruction!(CpuVariant.mos_6502)( + State(0x2f81, 0x26, 0x87, 0x6a, 0xb4, 0x2b), [[0x2f81, 0x08], [0x2f82, 0x60], [0x2f83, 0xbe]], + State(0x2f82, 0x25, 0x87, 0x6a, 0xb4, 0x2b), [[0x0126, 0x3b]], + "R 2f81 08 R 2f82 60 W 0126 3b"); + + // mos_6502, opcode $10: "10 b3 8d" + // BPL taken + checkInstruction!(CpuVariant.mos_6502)( + State(0x90ca, 0x8c, 0x3c, 0x86, 0x68, 0x25), [[0x90ca, 0x10], [0x90cb, 0xb3], [0x90cc, 0x8d], [0x907f, 0xe7]], + State(0x907f, 0x8c, 0x3c, 0x86, 0x68, 0x25), [], + "R 90ca 10 R 90cb b3 R 90cc 8d"); + + // mos_6502, opcode $10: "10 3c 3a" + // BPL taken across page + checkInstruction!(CpuVariant.mos_6502)( + State(0x77e1, 0xed, 0xec, 0x04, 0xfb, 0x2a), [[0x77e1, 0x10], [0x77e2, 0x3c], [0x77e3, 0x3a], [0x771f, 0x88], [0x781f, 0xc7]], + State(0x781f, 0xed, 0xec, 0x04, 0xfb, 0x2a), [], + "R 77e1 10 R 77e2 3c R 77e3 3a R 771f 88"); + + // mos_6502, opcode $18: "18 c9 9b" + checkInstruction!(CpuVariant.mos_6502)( + State(0x09a8, 0x26, 0xd7, 0x79, 0xbf, 0xec), [[0x09a8, 0x18], [0x09a9, 0xc9], [0x09aa, 0x9b]], + State(0x09a9, 0x26, 0xd7, 0x79, 0xbf, 0xec), [], + "R 09a8 18 R 09a9 c9"); + + // mos_6502, opcode $28: "28 c6 97" + // PLP does idle fetch and idle pop + checkInstruction!(CpuVariant.mos_6502)( + State(0xa532, 0xa3, 0x9e, 0x77, 0x6e, 0xad), [[0xa532, 0x28], [0xa533, 0xc6], [0xa534, 0x97], [0x01a3, 0x30], [0x01a4, 0x94]], + State(0xa533, 0xa4, 0x9e, 0x77, 0x6e, 0xa4), [], + "R a532 28 R a533 c6 R 01a3 30 R 01a4 94"); + + // mos_6502, opcode $38: "38 08 67" + checkInstruction!(CpuVariant.mos_6502)( + State(0x85d6, 0x43, 0xa8, 0xd2, 0xb7, 0xe1), [[0x85d6, 0x38], [0x85d7, 0x08], [0x85d8, 0x67]], + State(0x85d7, 0x43, 0xa8, 0xd2, 0xb7, 0xe1), [], + "R 85d6 38 R 85d7 08"); + + // mos_6502, opcode $40: "40 9c 2c" + // RTI does idle fetch and idle pop + checkInstruction!(CpuVariant.mos_6502)( + State(0x8771, 0x6e, 0xa2, 0x81, 0x7e, 0x63), [[0x8771, 0x40], [0x8772, 0x9c], [0x8773, 0x2c], [0x016e, 0x98], [0x016f, 0x9c], [0x0170, 0xaa], [0x0171, 0x65], [0x65aa, 0x0e]], + State(0x65aa, 0x71, 0xa2, 0x81, 0x7e, 0xac), [], + "R 8771 40 R 8772 9c R 016e 98 R 016f 9c R 0170 aa R 0171 65"); + + // mos_6502, opcode $48: "48 e0 36" + // PHA does idle fetch + checkInstruction!(CpuVariant.mos_6502)( + State(0x5063, 0x9f, 0x3e, 0x33, 0x4e, 0xac), [[0x5063, 0x48], [0x5064, 0xe0], [0x5065, 0x36]], + State(0x5064, 0x9e, 0x3e, 0x33, 0x4e, 0xac), [[0x019f, 0x3e]], + "R 5063 48 R 5064 e0 W 019f 3e"); + + // mos_6502, opcode $58: "58 71 bb" + checkInstruction!(CpuVariant.mos_6502)( + State(0x448a, 0xf5, 0xb6, 0xd3, 0x96, 0xa9), [[0x448a, 0x58], [0x448b, 0x71], [0x448c, 0xbb]], + State(0x448b, 0xf5, 0xb6, 0xd3, 0x96, 0xa9), [], + "R 448a 58 R 448b 71"); + + // mos_6502, opcode $60: "60 14 e2" + // RTS does idle fetch, idle pop, and idle PC-1 read + checkInstruction!(CpuVariant.mos_6502)( + State(0x4147, 0xcb, 0xe7, 0x14, 0x64, 0xa6), [[0x4147, 0x60], [0x4148, 0x14], [0x4149, 0xe2], [0x01cb, 0x98], [0x01cc, 0xdc], [0x01cd, 0x1d], [0x1ddc, 0x5d], [0x1ddd, 0xda]], + State(0x1ddd, 0xcd, 0xe7, 0x14, 0x64, 0xa6), [], + "R 4147 60 R 4148 14 R 01cb 98 R 01cc dc R 01cd 1d R 1ddc 5d"); + + // mos_6502, opcode $68: "68 b4 82" + // PLA does idle fetch and idle pop + checkInstruction!(CpuVariant.mos_6502)( + State(0x3021, 0xa4, 0x32, 0xb2, 0x4a, 0x2e), [[0x3021, 0x68], [0x3022, 0xb4], [0x3023, 0x82], [0x01a4, 0xe0], [0x01a5, 0x36]], + State(0x3022, 0xa5, 0x36, 0xb2, 0x4a, 0x2c), [], + "R 3021 68 R 3022 b4 R 01a4 e0 R 01a5 36"); + + // mos_6502, opcode $78: "78 ac 45" + checkInstruction!(CpuVariant.mos_6502)( + State(0x3eb3, 0xa0, 0xa2, 0x8c, 0x01, 0xa4), [[0x3eb3, 0x78], [0x3eb4, 0xac], [0x3eb5, 0x45]], + State(0x3eb4, 0xa0, 0xa2, 0x8c, 0x01, 0xa4), [], + "R 3eb3 78 R 3eb4 ac"); + + // mos_6502, opcode $91: "91 bb 6e" + // STA (zp),y w/ index penalty + checkInstruction!(CpuVariant.mos_6502)( + State(0x1c86, 0x95, 0x3f, 0x11, 0x1c, 0x2b), [[0x1c86, 0x91], [0x1c87, 0xbb], [0x1c88, 0x6e], [0x00bb, 0x8d], [0x00bc, 0x59], [0x59a9, 0xd5]], + State(0x1c88, 0x95, 0x3f, 0x11, 0x1c, 0x2b), [[0x59a9, 0x3f]], + "R 1c86 91 R 1c87 bb R 00bb 8d R 00bc 59 R 59a9 d5 W 59a9 3f"); // wdc_65c02, opcode $07: "07 28 a5" + // NOP absolute checkInstruction!(CpuVariant.wdc_65c02)( State(0x2f7b, 0x32, 0x42, 0x71, 0x5d, 0x2f), [[0x2f7b, 0x07], [0x2f7c, 0x28], [0x2f7d, 0xa5], [0x0028, 0xba]], - State(0x2f7d, 0x32, 0x42, 0x71, 0x5d, 0x2f), []); + State(0x2f7d, 0x32, 0x42, 0x71, 0x5d, 0x2f), [], + "R 2f7b 07 R 2f7c 28 R 0028 ba"); // wdc_65c02, opcode $0f: "0f 7c f5" + // NOP absolute checkInstruction!(CpuVariant.wdc_65c02)( State(0x8da5, 0x9c, 0x88, 0x57, 0x65, 0xe8), [[0x8da5, 0x0f], [0x8da6, 0x7c], [0x8da7, 0xf5], [0x8da8, 0xe1]], - State(0x8da8, 0x9c, 0x88, 0x57, 0x65, 0xe8), []); + State(0x8da8, 0x9c, 0x88, 0x57, 0x65, 0xe8), [], + "R 8da5 0f R 8da6 7c R 8da7 f5"); + + // wdc_65c02, opcode $17: "17 ab f6" + // NOP absolute indexed + checkInstruction!(CpuVariant.wdc_65c02)( + State(0xf0f6, 0xac, 0xf6, 0xd2, 0x9b, 0xa6), [[0xf0f6, 0x17], [0xf0f7, 0xab], [0xf0f8, 0xf6], [0x00ab, 0x03], [0x007d, 0xdb]], + State(0xf0f8, 0xac, 0xf6, 0xd2, 0x9b, 0xa6), [], + "R f0f6 17 R f0f7 ab R 00ab 03 R 007d db"); + + // wdc_65c02, opcode $1a: "1a ea 57" + // INC @ does idle fetch + checkInstruction!(CpuVariant.wdc_65c02)( + State(0x1abb, 0x3b, 0x3d, 0x6b, 0x7e, 0xa1), [[0x1abb, 0x1a], [0x1abc, 0xea], [0x1abd, 0x57]], + State(0x1abc, 0x3b, 0x3e, 0x6b, 0x7e, 0x21), [], + "R 1abb 1a R 1abc ea"); + + // wdc_65c02, opcode $44: "44 3c d4" + // NOP zp + checkInstruction!(CpuVariant.wdc_65c02)( + State(0xd5c9, 0xa0, 0x1e, 0xdc, 0x6b, 0x22), [[0xd5c9, 0x44], [0xd5ca, 0x3c], [0xd5cb, 0xd4], [0x003c, 0x99]], + State(0xd5cb, 0xa0, 0x1e, 0xdc, 0x6b, 0x22), [], + "R d5c9 44 R d5ca 3c R 003c 99"); + + // wdc_65c02, opcode $6c: "6c 42 fc" + // JMP (abs) always takes an extra cycle + checkInstruction!(CpuVariant.wdc_65c02)( + State(0x96ff, 0xc4, 0x96, 0xf7, 0x9b, 0xaf), [[0x96ff, 0x6c], [0x9700, 0x42], [0x9701, 0xfc], [0xfc42, 0xb1], [0xfc43, 0x3f], [0x3fb1, 0x0a]], + State(0x3fb1, 0xc4, 0x96, 0xf7, 0x9b, 0xaf), [], + "R 96ff 6c R 9700 42 R 9701 fc R fc42 b1 R fc43 3f R fc43 3f"); + + // wdc_65c02, opcode $5c: "5c 83 a9" + // NOP abs,X + checkInstruction!(CpuVariant.wdc_65c02)( + State(0x7e40, 0x80, 0xdb, 0xc0, 0x1f, 0x6f), [[0x7e40, 0x5c], [0x7e41, 0x83], [0x7e42, 0xa9], [0x7e43, 0xef]], + State(0x7e43, 0x80, 0xdb, 0xc0, 0x1f, 0x6f), [], + "R 7e40 5c R 7e41 83 R 7e42 a9 R 7e42 a9"); + + // wdc_65c02, opcode $7a: "7a 24 4c" + // PLY does idle fetch and idle pop + checkInstruction!(CpuVariant.wdc_65c02)( + State(0xeeed, 0x9d, 0x46, 0xf3, 0x49, 0xab), [[0xeeed, 0x7a], [0xeeee, 0x24], [0xeeef, 0x4c], [0x019d, 0x9e], [0x019e, 0x80]], + State(0xeeee, 0x9e, 0x46, 0xf3, 0x80, 0xa9), [], + "R eeed 7a R eeee 24 R 019d 9e R 019e 80"); + + // wdc_65c02, opcode $91: "91 c9 f2" + // STA (zp),y always does idle read at PC + checkInstruction!(CpuVariant.wdc_65c02)( + State(0x9d92, 0x6e, 0x1f, 0xf4, 0x4b, 0xef), [[0x9d92, 0x91], [0x9d93, 0xc9], [0x9d94, 0xf2], [0x00c9, 0x80], [0x00ca, 0xd4]], + State(0x9d94, 0x6e, 0x1f, 0xf4, 0x4b, 0xef), [[0xd4cb, 0x1f]], + "R 9d92 91 R 9d93 c9 R 00c9 80 R 00ca d4 R 9d93 c9 W d4cb 1f"); + + // wdc_65c02, opcode $7c: "7c 32 9f" + // JMP (abs,x) + checkInstruction!(CpuVariant.wdc_65c02)( + State(0x83c8, 0xf1, 0xe2, 0x6f, 0x8c, 0xe4), [[0x83c8, 0x7c], [0x83c9, 0x32], [0x83ca, 0x9f], [0x9fa1, 0x76], [0x9fa2, 0x2b], [0x2b76, 0x76]], + State(0x2b76, 0xf1, 0xe2, 0x6f, 0x8c, 0xe4), [], + "R 83c8 7c R 83c9 32 R 83ca 9f R 83c9 32 R 9fa1 76 R 9fa2 2b"); // wdc_65c02, opcode $cb: "cb 4a 20" + // NOP imp checkInstruction!(CpuVariant.wdc_65c02)( State(0x7487, 0x15, 0xd2, 0xf9, 0x06, 0x22), [[0x7487, 0xcb], [0x7488, 0x4a], [0x7489, 0x20]], - State(0x7488, 0x15, 0xd2, 0xf9, 0x06, 0x22), []); + State(0x7488, 0x15, 0xd2, 0xf9, 0x06, 0x22), [], + "R 7487 cb R 7488 4a"); // wdc_65c02, opcode $db: "db cf 42" + // NOP zp,x checkInstruction!(CpuVariant.wdc_65c02)( State(0x6aeb, 0x6f, 0x61, 0x9e, 0xd5, 0x27), [[0x6aeb, 0xdb], [0x6aec, 0xcf], [0x6aed, 0x42], [0x00cf, 0x8b], [0x006d, 0x85]], - State(0x6aed, 0x6f, 0x61, 0x9e, 0xd5, 0x27), []); + State(0x6aed, 0x6f, 0x61, 0x9e, 0xd5, 0x27), [], + "R 6aeb db R 6aec cf R 00cf 8b R 006d 85"); // wdc_65c02, opcode $f1: "f1 3" + // SBC (zp,x) BCD checkInstruction!(CpuVariant.wdc_65c02)( State(0x73e6, 0x45, 0xf3, 0xfc, 0x7e, 0xe8), [[0x00ff, 0x15], [0x1638, 0x1f], [0x00fe, 0xba], [0x73e7, 0xfe], [0x73e6, 0xf1]], - State(0x73e8, 0x45, 0xcd, 0xfc, 0x7e, 0xa9), []); + State(0x73e8, 0x45, 0xcd, 0xfc, 0x7e, 0xa9), [], + "R 73e6 f1 R 73e7 fe R 00fe ba R 00ff 15 R 73e7 fe R 1638 1f R 1638 1f"); + // wdc_65c02, opcode $69: "69 47 e1" + // ADC #imm BCD + // SingleStepTests expect a dummy read at $0056, but that looks suspicious + // and probably it should be pc+1. TODO: check on real hardware. + checkInstruction!(CpuVariant.wdc_65c02)( + State(0x1378, 0xaf, 0x73, 0x19, 0xe9, 0x29), [[0x1378, 0x69], [0x1379, 0x47], [0x137a, 0xe1], [0x0056, 0x6d]], + State(0x137a, 0xaf, 0x21, 0x19, 0xe9, 0x69), [], + "R 1378 69 R 1379 47 R 137a e1"); + + // wdc_w65c02s, opcode $04: "04 a7 8c" + // TSB zp + checkInstruction!(CpuVariant.wdc_w65c02s)( + State(0xe8ee, 0x21, 0xa6, 0xa5, 0xc5, 0xee), [[0xe8ee, 0x04], [0xe8ef, 0xa7], [0xe8f0, 0x8c], [0x00a7, 0x6f]], + State(0xe8f0, 0x21, 0xa6, 0xa5, 0xc5, 0xec), [[0x00a7, 0xef]], + "R e8ee 04 R e8ef a7 R 00a7 6f R 00a7 6f W 00a7 ef"); + + // wdc_w65c02s, opcode $07: "07 1f 9e" + // RMB0 + checkInstruction!(CpuVariant.wdc_w65c02s)( + State(0x5c3f, 0xbf, 0x34, 0x0c, 0x45, 0xe1), [[0x5c3f, 0x07], [0x5c40, 0x1f], [0x5c41, 0x9e], [0x001f, 0xaa]], + State(0x5c41, 0xbf, 0x34, 0x0c, 0x45, 0xe1), [], + "R 5c3f 07 R 5c40 1f R 001f aa R 001f aa W 001f aa"); + + // wdc_w65c02s, opcode $0f: "0f 1" + checkInstruction!(CpuVariant.wdc_w65c02s)( + State(0xfeb7, 0x2f, 0xe9, 0x9d, 0x86, 0x68), [[0xfeba, 0xe9], [0xfeb9, 0x54], [0x0035, 0x3c], [0xfeb8, 0x35], [0xfeb7, 0x0f]], + State(0xff0e, 0x2f, 0xe9, 0x9d, 0x86, 0x68), [], + "R feb7 0f R feb8 35 R 0035 3c R 0035 3c R feb9 54 R feba e9 R feba e9"); + + // wdc_w65c02s, opcode $0f: "0f 2" + checkInstruction!(CpuVariant.wdc_w65c02s)( + State(0x40fc, 0x72, 0xd2, 0x47, 0x46, 0x67), [[0x40fe, 0x8a], [0x00af, 0x3d], [0x40fd, 0xaf], [0x40fc, 0x0f]], + State(0x40ff, 0x72, 0xd2, 0x47, 0x46, 0x67), [], + "R 40fc 0f R 40fd af R 00af 3d R 00af 3d R 40fe 8a"); + + // wdc_w65c02s, opcode $1f: "1f 1" + checkInstruction!(CpuVariant.wdc_w65c02s)( + State(0x1a2f, 0xdc, 0x03, 0x6a, 0x49, 0xe7), [[0x1a32, 0x2e], [0x1a31, 0x48], [0x0023, 0xdc], [0x1a30, 0x23], [0x1a2f, 0x1f]], + State(0x1a7a, 0xdc, 0x03, 0x6a, 0x49, 0xe7), [], + "R 1a2f 1f R 1a30 23 R 0023 dc R 0023 dc R 1a31 48 R 1a32 2e"); } diff --git a/test/singlestep/source/app.d b/test/singlestep/source/app.d index ba52c50..28ec7a3 100644 --- a/test/singlestep/source/app.d +++ b/test/singlestep/source/app.d @@ -184,6 +184,19 @@ unittest assert(tests[0].cycles == [Access(59082, 177, false), Access(40, 160, true)]); } +struct BusTrace +{ + const(ubyte)[] ram; + Access[] accesses; + + void instruction(E)(E emu) {} + void fetch(ushort addr, ubyte value) { accesses ~= Access(addr, value, false); } + void read(ushort addr, ubyte value) { accesses ~= Access(addr, value, false); } + void write(ushort addr, ubyte value) { accesses ~= Access(addr, value, true); } + void idle(ushort addr) { accesses ~= Access(addr, ram[addr], false); } + void endInstruction() {} +} + string runOne(E)(E emu, ref const TestCase t) { foreach (c; t.initialRam) @@ -197,6 +210,13 @@ string runOne(E)(E emu, ref const TestCase t) emu.stopped = false; emu.instructions = 0; emu.instructionLimit = 1; + emu.observer.accesses.length = 0; + emu.observer.accesses.assumeSafeAppend(); + + // TODO: What SingleStepTests expect as the idle read address on decimal + // correction cycle in ADC/SBC #imm looks suspiciously wrong. + // Let's ignore this cycle for now, but check on real hardware some day. + bool bcdImmediate = isCmos!(E.cpu) && (emu.p & 0x08) && (emu.ram[emu.pc] & 0x7f) == 0x69; string thrown = collectExceptionMsg(emu.run()); @@ -216,6 +236,11 @@ string runOne(E)(E emu, ref const TestCase t) problems ~= format("memory: $%04x is $%02x, expected $%02x", c.addr, emu.ram[c.addr], c.value); } + if (emu.observer.accesses[0 .. $ - bcdImmediate] != t.cycles[0 .. $ - bcdImmediate]) + { + problems ~= format("cycles: got %d [%(%s, %)]", emu.observer.accesses.length, emu.observer.accesses); + problems ~= format(" expected %d [%(%s, %)]", t.cycles.length, t.cycles); + } emu.ram[] = 0; @@ -243,16 +268,19 @@ string emitUnittest(CpuVariant v, ref const TestCase t) "\t// %s, opcode $%02x: \"%s\"\n" ~ "\tcheckInstruction!(CpuVariant.%s)(\n" ~ "\t\t%S, %s,\n" ~ - "\t\t%S, %s);\n", + "\t\t%S, %s,\n" ~ + "\t\t\"%-(%s %)\");\n", v, t.cycles.length ? t.cycles[0].value : 0, t.name, v, t.initial, t.initialRam, - t.expected, changed); + t.expected, changed, + t.cycles); } Result runOpcode(CpuVariant v)(string path, ubyte opcode, bool emitUnittests) { Result r = { opcode: opcode }; - auto emu = new Emulator!v(); + auto emu = new Emulator!(v, BusTrace)(); + emu.observer.ram = emu.ram; emu.stopOnEmptyStackRts = false; auto text = strip(cast(string) read(path)); @@ -265,9 +293,9 @@ Result runOpcode(CpuVariant v)(string path, ubyte opcode, bool emitUnittests) const diag = runOne(emu, t); if (diag !is null) { ++r.failed; - if (r.reports.length < 5) + if (r.reports.length < 2) r.reports ~= diag; - if (emitUnittests && r.unittests.length < 5) + if (emitUnittests && r.unittests.length < 2) r.unittests ~= emitUnittest(v, t); } } From e605819b22b81150de8c8039ed5344c66f258fbd Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Thu, 10 Sep 2026 06:56:44 +0200 Subject: [PATCH 08/17] emu: lax & sax --- Makefile | 1 + source/xebin/emu.d | 93 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/Makefile b/Makefile index cc3314a..773cd40 100644 --- a/Makefile +++ b/Makefile @@ -52,6 +52,7 @@ test: singlestep: dub run -b release :singlestep -- -c 6502 -u + dub run -b release :singlestep -- -c 6502 -o 83,87,8f,97,a3,a7,af,b3,b7,bf dub run -b release :singlestep -- -c synertek65c02,rockwell65c02,wdc65c02 diff --git a/source/xebin/emu.d b/source/xebin/emu.d index 764e9e8..51eb0d4 100644 --- a/source/xebin/emu.d +++ b/source/xebin/emu.d @@ -194,6 +194,9 @@ enum tsb = q{ ubyte tmp = @r; @m(tmp); zflag = (a & tmp) == 0; @w(cast(ubyte) (t enum trb = q{ ubyte tmp = @r; @m(tmp); zflag = (a & tmp) == 0; @w(cast(ubyte) (tmp & ~a)); }; enum nop = q{ ubyte discarded = @r; }; // NOPs with fancy addressing modes +enum lax = q{ setNZ(a = x = @r); }; +enum sax = q{ @w(a & x); }; + /// enum CpuVariant { mos_6502, /// NMOS @@ -893,6 +896,19 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve case 0x5c: case 0xdc: case 0xfc: doNopAbsolute!true(); break; } + else // NMOS + { + case 0x83: doIndirectX!sax(); break; + case 0x87: doZeroPage!sax(); break; + case 0x8f: doAbsolute!sax(); break; + case 0x97: doZeroPageIndexed!sax(y); break; + case 0xa3: doIndirectX!lax(); break; + case 0xa7: doZeroPage!lax(); break; + case 0xaf: doAbsolute!lax(); break; + case 0xb3: doIndirectY!lax(); break; + case 0xb7: doZeroPageIndexed!lax(y); break; + case 0xbf: doAbsoluteIndexed!lax(y); break; + } default: throw new Exception( format("Unimplemented instruction %02X", instr)); @@ -1287,6 +1303,83 @@ unittest State(0x1c88, 0x95, 0x3f, 0x11, 0x1c, 0x2b), [[0x59a9, 0x3f]], "R 1c86 91 R 1c87 bb R 00bb 8d R 00bc 59 R 59a9 d5 W 59a9 3f"); + // mos_6502, opcode $83: "83 1b c1" + // SAX (zp,x) + checkInstruction!(CpuVariant.mos_6502)( + State(0xb525, 0x86, 0xbe, 0x60, 0x8a, 0x6c), [[0xb525, 0x83], [0xb526, 0x1b], [0xb527, 0xc1], [0x001b, 0x4c], [0x007b, 0x42], [0x007c, 0x3a]], + State(0xb527, 0x86, 0xbe, 0x60, 0x8a, 0x6c), [[0x3a42, 0x20]], + "R b525 83 R b526 1b R 001b 4c R 007b 42 R 007c 3a W 3a42 20"); + + // mos_6502, opcode $87: "87 3c 74" + // SAX zp + checkInstruction!(CpuVariant.mos_6502)( + State(0xad56, 0x91, 0xc2, 0xa4, 0x70, 0x6d), [[0xad56, 0x87], [0xad57, 0x3c], [0xad58, 0x74]], + State(0xad58, 0x91, 0xc2, 0xa4, 0x70, 0x6d), [[0x003c, 0x80]], + "R ad56 87 R ad57 3c W 003c 80"); + + // mos_6502, opcode $8f: "8f 5f a0" + // SAX abs + checkInstruction!(CpuVariant.mos_6502)( + State(0x5f95, 0x48, 0xc5, 0x8a, 0xc9, 0x64), [[0x5f95, 0x8f], [0x5f96, 0x5f], [0x5f97, 0xa0], [0x5f98, 0x44]], + State(0x5f98, 0x48, 0xc5, 0x8a, 0xc9, 0x64), [[0xa05f, 0x80]], + "R 5f95 8f R 5f96 5f R 5f97 a0 W a05f 80"); + + // mos_6502, opcode $97: "97 8b 67" + // SAX zp,y wraps within zero page + checkInstruction!(CpuVariant.mos_6502)( + State(0x901d, 0x42, 0xbf, 0x1a, 0xe3, 0xe0), [[0x901d, 0x97], [0x901e, 0x8b], [0x901f, 0x67], [0x008b, 0xc9]], + State(0x901f, 0x42, 0xbf, 0x1a, 0xe3, 0xe0), [[0x006e, 0x1a]], + "R 901d 97 R 901e 8b R 008b c9 W 006e 1a"); + + // mos_6502, opcode $a3: "a3 4e d0" + // LAX (zp,x) + checkInstruction!(CpuVariant.mos_6502)( + State(0xe472, 0xba, 0x61, 0xe7, 0x4c, 0x2d), [[0xe472, 0xa3], [0xe473, 0x4e], [0xe474, 0xd0], [0x004e, 0x72], [0x0035, 0xe6], [0x0036, 0x20], [0x20e6, 0x8f]], + State(0xe474, 0xba, 0x8f, 0x8f, 0x4c, 0xad), [], + "R e472 a3 R e473 4e R 004e 72 R 0035 e6 R 0036 20 R 20e6 8f"); + + // mos_6502, opcode $a7: "a7 f0 99" + // LAX zp + checkInstruction!(CpuVariant.mos_6502)( + State(0x777f, 0x5b, 0xe3, 0x52, 0xd9, 0xab), [[0x777f, 0xa7], [0x7780, 0xf0], [0x7781, 0x99], [0x00f0, 0x0c]], + State(0x7781, 0x5b, 0x0c, 0x0c, 0xd9, 0x29), [], + "R 777f a7 R 7780 f0 R 00f0 0c"); + + // mos_6502, opcode $af: "af 6d 59" + // LAX abs + checkInstruction!(CpuVariant.mos_6502)( + State(0xff9a, 0xee, 0x7c, 0x08, 0x7c, 0xe9), [[0xff9a, 0xaf], [0xff9b, 0x6d], [0xff9c, 0x59], [0x596d, 0xe7], [0xff9d, 0x5c]], + State(0xff9d, 0xee, 0xe7, 0xe7, 0x7c, 0xe9), [], + "R ff9a af R ff9b 6d R ff9c 59 R 596d e7"); + + // mos_6502, opcode $b3: "b3 eb bb" + // LAX (zp),y across page: dummy read at the unfixed address + checkInstruction!(CpuVariant.mos_6502)( + State(0x0139, 0x9c, 0x69, 0xf5, 0x81, 0x6c), [[0x0139, 0xb3], [0x013a, 0xeb], [0x013b, 0xbb], [0x00eb, 0xd6], [0x00ec, 0x8c], [0x8c57, 0xd4], [0x8d57, 0xab]], + State(0x013b, 0x9c, 0xab, 0xab, 0x81, 0xec), [], + "R 0139 b3 R 013a eb R 00eb d6 R 00ec 8c R 8c57 d4 R 8d57 ab"); + + // mos_6502, opcode $b7: "b7 6b 71" + // LAX zp,y + checkInstruction!(CpuVariant.mos_6502)( + State(0xe5a5, 0x68, 0x9f, 0x61, 0x54, 0x64), [[0xe5a5, 0xb7], [0xe5a6, 0x6b], [0xe5a7, 0x71], [0x006b, 0xba], [0x00bf, 0xe1]], + State(0xe5a7, 0x68, 0xe1, 0xe1, 0x54, 0xe4), [], + "R e5a5 b7 R e5a6 6b R 006b ba R 00bf e1"); + + // mos_6502, opcode $bf: "bf 1f f7" + // LAX abs,y, no page crossing + checkInstruction!(CpuVariant.mos_6502)( + State(0x0b88, 0x64, 0xfa, 0x4f, 0x2c, 0xa5), [[0x0b88, 0xbf], [0x0b89, 0x1f], [0x0b8a, 0xf7], [0xf74b, 0x62], [0x0b8b, 0xa9]], + State(0x0b8b, 0x64, 0x62, 0x62, 0x2c, 0x25), [], + "R 0b88 bf R 0b89 1f R 0b8a f7 R f74b 62"); + + // mos_6502, opcode $bf: "bf 54 1c" + // LAX abs,y across page + checkInstruction!(CpuVariant.mos_6502)( + State(0xa814, 0x1b, 0x7c, 0x4a, 0xdc, 0xa0), [[0xa814, 0xbf], [0xa815, 0x54], [0xa816, 0x1c], [0x1c30, 0x09], [0x1d30, 0xd3], [0xa817, 0x4f]], + State(0xa817, 0x1b, 0xd3, 0xd3, 0xdc, 0xa0), [], + "R a814 bf R a815 54 R a816 1c R 1c30 09 R 1d30 d3"); + // wdc_65c02, opcode $07: "07 28 a5" // NOP absolute checkInstruction!(CpuVariant.wdc_65c02)( From 09d48ac84e2ec5f966f36f1d024bccb5d54c06eb Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Thu, 10 Sep 2026 07:06:39 +0200 Subject: [PATCH 09/17] emu: NMOS unofficial NOPs --- Makefile | 2 +- source/xebin/emu.d | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 773cd40..4ee07fc 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ test: singlestep: dub run -b release :singlestep -- -c 6502 -u - dub run -b release :singlestep -- -c 6502 -o 83,87,8f,97,a3,a7,af,b3,b7,bf + dub run -b release :singlestep -- -c 6502 -o 83,87,8f,97,a3,a7,af,b3,b7,bf,1a,3a,5a,7a,da,fa,80,82,89,c2,e2,04,44,64,14,34,54,74,d4,f4,0c,1c,3c,5c,7c,dc,fc dub run -b release :singlestep -- -c synertek65c02,rockwell65c02,wdc65c02 diff --git a/source/xebin/emu.d b/source/xebin/emu.d index 51eb0d4..71d6657 100644 --- a/source/xebin/emu.d +++ b/source/xebin/emu.d @@ -908,6 +908,18 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve case 0xb3: doIndirectY!lax(); break; case 0xb7: doZeroPageIndexed!lax(y); break; case 0xbf: doAbsoluteIndexed!lax(y); break; + case 0x1a: case 0x3a: case 0x5a: case 0x7a: case 0xda: case 0xfa: + idleFetch(); break; + case 0x80: case 0x82: case 0x89: case 0xc2: case 0xe2: + doImmediate!nop(); break; + case 0x04: case 0x44: case 0x64: + doZeroPage!nop(); break; + case 0x14: case 0x34: case 0x54: case 0x74: case 0xd4: case 0xf4: + doZeroPageIndexed!nop(x); break; + case 0x0c: + doAbsolute!nop(); break; + case 0x1c: case 0x3c: case 0x5c: case 0x7c: case 0xdc: case 0xfc: + doAbsoluteIndexed!nop(x); break; } default: throw new Exception( From 929d410e0565e0c14efc9f543244101563211f14 Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Thu, 10 Sep 2026 07:22:25 +0200 Subject: [PATCH 10/17] emu: JAM --- Makefile | 2 +- source/xebin/emu.d | 20 +++++++++++++++++--- test/singlestep/source/app.d | 17 ++++++++++++++--- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 4ee07fc..ac51c11 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ test: singlestep: dub run -b release :singlestep -- -c 6502 -u - dub run -b release :singlestep -- -c 6502 -o 83,87,8f,97,a3,a7,af,b3,b7,bf,1a,3a,5a,7a,da,fa,80,82,89,c2,e2,04,44,64,14,34,54,74,d4,f4,0c,1c,3c,5c,7c,dc,fc + dub run -b release :singlestep -- -c 6502 -o 83,87,8f,97,a3,a7,af,b3,b7,bf,1a,3a,5a,7a,da,fa,80,82,89,c2,e2,04,44,64,14,34,54,74,d4,f4,0c,1c,3c,5c,7c,dc,fc,02,12,22,32,42,52,62,72,92,b2,d2,f2 dub run -b release :singlestep -- -c synertek65c02,rockwell65c02,wdc65c02 diff --git a/source/xebin/emu.d b/source/xebin/emu.d index 71d6657..9eebcbf 100644 --- a/source/xebin/emu.d +++ b/source/xebin/emu.d @@ -378,6 +378,17 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve idleRead(0x100 + sp); } + private void jam() + { + idleRead(0xffff); + idleRead(0xfffe); + idleRead(0xfffe); + idleRead(0xffff); + --pc; + stopped = true; + observer.endInstruction(); + } + private void indexPenalty(string expr)(ushort base, ubyte index) { const ushort fixed = cast(ushort) (base + index); @@ -595,9 +606,7 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve static if (isCmos!cpuVariant) {} else { - --pc; // JAM: report the address of the $02 itself - stopped = true; - observer.endInstruction(); + jam(); return; } } @@ -920,6 +929,11 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve doAbsolute!nop(); break; case 0x1c: case 0x3c: case 0x5c: case 0x7c: case 0xdc: case 0xfc: doAbsoluteIndexed!nop(x); break; + case 0x12: case 0x22: case 0x32: case 0x42: case 0x52: case 0x62: + case 0x72: case 0x92: case 0xb2: case 0xd2: case 0xf2: + fetchByte(); + jam(); + return; } default: throw new Exception( diff --git a/test/singlestep/source/app.d b/test/singlestep/source/app.d index 28ec7a3..ed3211e 100644 --- a/test/singlestep/source/app.d +++ b/test/singlestep/source/app.d @@ -27,7 +27,7 @@ distribution. */ -import std.algorithm : canFind, filter, map; +import std.algorithm : all, canFind, filter, map; import std.array : appender; import std.conv : to; import std.exception : collectExceptionMsg; @@ -236,10 +236,21 @@ string runOne(E)(E emu, ref const TestCase t) problems ~= format("memory: $%04x is $%02x, expected $%02x", c.addr, emu.ram[c.addr], c.value); } - if (emu.observer.accesses[0 .. $ - bcdImmediate] != t.cycles[0 .. $ - bcdImmediate]) + + // A jammed CPU repeats its last bus cycle forever and the suite records + // an arbitrary number of those repetitions. The emulator stops after the + // first one, so only that prefix is compared, and the rest of the expected + // trace must be copies of the cycle it stopped on. + const(Access)[] expectedCycles = t.cycles; + const(Access)[] got_ = emu.observer.accesses; + if (emu.stopped && got_.length && got_.length < t.cycles.length && + t.cycles[got_.length .. $].all!(c => c == got_[$ - 1])) + expectedCycles = t.cycles[0 .. got_.length]; + + if (emu.observer.accesses[0 .. $ - bcdImmediate] != expectedCycles[0 .. $ - bcdImmediate]) { problems ~= format("cycles: got %d [%(%s, %)]", emu.observer.accesses.length, emu.observer.accesses); - problems ~= format(" expected %d [%(%s, %)]", t.cycles.length, t.cycles); + problems ~= format(" expected %d [%(%s, %)]", expectedCycles.length, expectedCycles); } emu.ram[] = 0; From d3dfdf6d34545d8574354d44b90328cc44bcd303 Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Thu, 10 Sep 2026 07:40:25 +0200 Subject: [PATCH 11/17] emu: alu + rmw combos --- Makefile | 2 +- source/xebin/emu.d | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ac51c11..021a335 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ test: singlestep: dub run -b release :singlestep -- -c 6502 -u - dub run -b release :singlestep -- -c 6502 -o 83,87,8f,97,a3,a7,af,b3,b7,bf,1a,3a,5a,7a,da,fa,80,82,89,c2,e2,04,44,64,14,34,54,74,d4,f4,0c,1c,3c,5c,7c,dc,fc,02,12,22,32,42,52,62,72,92,b2,d2,f2 + dub run -b release :singlestep -- -c 6502 -o 83,87,8f,97,a3,a7,af,b3,b7,bf,1a,3a,5a,7a,da,fa,80,82,89,c2,e2,04,44,64,14,34,54,74,d4,f4,0c,1c,3c,5c,7c,dc,fc,02,12,22,32,42,52,62,72,92,b2,d2,f2,03,07,0f,13,17,1b,1f,23,27,2f,33,37,3b,3f,43,47,4f,53,57,5b,5f,63,67,6f,73,77,7b,7f,c3,c7,cf,d3,d7,db,df,e3,e7,ef,f3,f7,fb,ff dub run -b release :singlestep -- -c synertek65c02,rockwell65c02,wdc65c02 diff --git a/source/xebin/emu.d b/source/xebin/emu.d index 9eebcbf..a5a0bd8 100644 --- a/source/xebin/emu.d +++ b/source/xebin/emu.d @@ -196,6 +196,14 @@ enum nop = q{ ubyte discarded = @r; }; // NOPs with fancy addressing modes enum lax = q{ setNZ(a = x = @r); }; enum sax = q{ @w(a & x); }; +enum combo(string rmw, string alu) = + rmw.replace("tmp", "modified") ~ alu.replace("@r", "modified"); +enum slo = combo!(asl, ora); +enum rla = combo!(rol, and); +enum sre = combo!(lsr, eor); +enum rra = combo!(ror, adc); +enum dcp = combo!(dec, cmp); +enum isc = combo!(inc, sbc); /// enum CpuVariant { @@ -934,6 +942,48 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve fetchByte(); jam(); return; + case 0x03: doIndirectX!slo(); break; + case 0x07: doZeroPage!slo(); break; + case 0x0f: doAbsolute!slo(); break; + case 0x13: doIndirectY!slo(); break; + case 0x17: doZeroPageIndexed!slo(x); break; + case 0x1b: doAbsoluteIndexed!slo(y); break; + case 0x1f: doAbsoluteIndexed!slo(x); break; + case 0x23: doIndirectX!rla(); break; + case 0x27: doZeroPage!rla(); break; + case 0x2f: doAbsolute!rla(); break; + case 0x33: doIndirectY!rla(); break; + case 0x37: doZeroPageIndexed!rla(x); break; + case 0x3b: doAbsoluteIndexed!rla(y); break; + case 0x3f: doAbsoluteIndexed!rla(x); break; + case 0x43: doIndirectX!sre(); break; + case 0x47: doZeroPage!sre(); break; + case 0x4f: doAbsolute!sre(); break; + case 0x53: doIndirectY!sre(); break; + case 0x57: doZeroPageIndexed!sre(x); break; + case 0x5b: doAbsoluteIndexed!sre(y); break; + case 0x5f: doAbsoluteIndexed!sre(x); break; + case 0x63: doIndirectX!rra(); break; + case 0x67: doZeroPage!rra(); break; + case 0x6f: doAbsolute!rra(); break; + case 0x73: doIndirectY!rra(); break; + case 0x77: doZeroPageIndexed!rra(x); break; + case 0x7b: doAbsoluteIndexed!rra(y); break; + case 0x7f: doAbsoluteIndexed!rra(x); break; + case 0xc3: doIndirectX!dcp(); break; + case 0xc7: doZeroPage!dcp(); break; + case 0xcf: doAbsolute!dcp(); break; + case 0xd3: doIndirectY!dcp(); break; + case 0xd7: doZeroPageIndexed!dcp(x); break; + case 0xdb: doAbsoluteIndexed!dcp(y); break; + case 0xdf: doAbsoluteIndexed!dcp(x); break; + case 0xe3: doIndirectX!isc(); break; + case 0xe7: doZeroPage!isc(); break; + case 0xef: doAbsolute!isc(); break; + case 0xf3: doIndirectY!isc(); break; + case 0xf7: doZeroPageIndexed!isc(x); break; + case 0xfb: doAbsoluteIndexed!isc(y); break; + case 0xff: doAbsoluteIndexed!isc(x); break; } default: throw new Exception( From 6c3aeea006e9c1ccb0bc00a150b5a8065e3b2fff Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Thu, 10 Sep 2026 07:50:03 +0200 Subject: [PATCH 12/17] emu: undocumented imnmediates --- Makefile | 2 +- source/xebin/emu.d | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 021a335..45721d1 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ test: singlestep: dub run -b release :singlestep -- -c 6502 -u - dub run -b release :singlestep -- -c 6502 -o 83,87,8f,97,a3,a7,af,b3,b7,bf,1a,3a,5a,7a,da,fa,80,82,89,c2,e2,04,44,64,14,34,54,74,d4,f4,0c,1c,3c,5c,7c,dc,fc,02,12,22,32,42,52,62,72,92,b2,d2,f2,03,07,0f,13,17,1b,1f,23,27,2f,33,37,3b,3f,43,47,4f,53,57,5b,5f,63,67,6f,73,77,7b,7f,c3,c7,cf,d3,d7,db,df,e3,e7,ef,f3,f7,fb,ff + dub run -b release :singlestep -- -c 6502 -o 83,87,8f,97,a3,a7,af,b3,b7,bf,1a,3a,5a,7a,da,fa,80,82,89,c2,e2,04,44,64,14,34,54,74,d4,f4,0c,1c,3c,5c,7c,dc,fc,02,12,22,32,42,52,62,72,92,b2,d2,f2,03,07,0f,13,17,1b,1f,23,27,2f,33,37,3b,3f,43,47,4f,53,57,5b,5f,63,67,6f,73,77,7b,7f,c3,c7,cf,d3,d7,db,df,e3,e7,ef,f3,f7,fb,ff,0b,2b,4b,6b,cb,eb dub run -b release :singlestep -- -c synertek65c02,rockwell65c02,wdc65c02 diff --git a/source/xebin/emu.d b/source/xebin/emu.d index a5a0bd8..9c908a8 100644 --- a/source/xebin/emu.d +++ b/source/xebin/emu.d @@ -204,6 +204,39 @@ enum sre = combo!(lsr, eor); enum rra = combo!(ror, adc); enum dcp = combo!(dec, cmp); enum isc = combo!(inc, sbc); +enum anc = q{ setNZ(a &= @r); cflag = nflag; }; +enum alr = q{ a &= @r; cflag = (a & 1) != 0; setNZ(a >>>= 1); }; +enum arr = +q{ + const ubyte anded = a & @r; + const ubyte rotated = cast(ubyte) ((anded >>> 1) | (cflag ? 0x80 : 0)); + if (!dflag) + { + setNZ(rotated); + cflag = (rotated & 0x40) != 0; + vflag = ((rotated ^ (rotated << 1)) & 0x40) != 0; + a = rotated; + } + else + { + nflag = cflag; + zflag = rotated == 0; + vflag = ((anded ^ rotated) & 0x40) != 0; + uint fixed = rotated; + if ((anded & 0x0f) + (anded & 0x01) > 0x05) + fixed = (fixed & 0xf0) | ((fixed + 0x06) & 0x0f); + cflag = (anded & 0xf0) + (anded & 0x10) > 0x50; + if (cflag) + fixed += 0x60; + a = cast(ubyte) fixed; + } +}; +enum sbx = q{ + const ubyte tmp = @r; + const ubyte ax = a & x; + cflag = ax >= tmp; + setNZ(x = cast(ubyte) (ax - tmp)); +}; /// enum CpuVariant { @@ -942,6 +975,11 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve fetchByte(); jam(); return; + case 0x0b: case 0x2b: doImmediate!anc(); break; + case 0x4b: doImmediate!alr(); break; + case 0x6b: doImmediate!arr(); break; + case 0xcb: doImmediate!sbx(); break; + case 0xeb: doImmediate!sbc(); break; case 0x03: doIndirectX!slo(); break; case 0x07: doZeroPage!slo(); break; case 0x0f: doAbsolute!slo(); break; From c54e3f474fe47486cf0e5387dbf9bf30dca8849f Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Thu, 10 Sep 2026 08:09:37 +0200 Subject: [PATCH 13/17] emu: all undocumented NMOS opcodes --- Makefile | 4 +--- source/xebin/emu.d | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 45721d1..8a8e137 100644 --- a/Makefile +++ b/Makefile @@ -51,9 +51,7 @@ test: dub test singlestep: - dub run -b release :singlestep -- -c 6502 -u - dub run -b release :singlestep -- -c 6502 -o 83,87,8f,97,a3,a7,af,b3,b7,bf,1a,3a,5a,7a,da,fa,80,82,89,c2,e2,04,44,64,14,34,54,74,d4,f4,0c,1c,3c,5c,7c,dc,fc,02,12,22,32,42,52,62,72,92,b2,d2,f2,03,07,0f,13,17,1b,1f,23,27,2f,33,37,3b,3f,43,47,4f,53,57,5b,5f,63,67,6f,73,77,7b,7f,c3,c7,cf,d3,d7,db,df,e3,e7,ef,f3,f7,fb,ff,0b,2b,4b,6b,cb,eb - dub run -b release :singlestep -- -c synertek65c02,rockwell65c02,wdc65c02 + dub run -b release :singlestep .PHONY: all doc debug dist windist srcdist clean install test diff --git a/source/xebin/emu.d b/source/xebin/emu.d index 9c908a8..c1911fc 100644 --- a/source/xebin/emu.d +++ b/source/xebin/emu.d @@ -237,6 +237,9 @@ enum sbx = q{ cflag = ax >= tmp; setNZ(x = cast(ubyte) (ax - tmp)); }; +enum ane = q{ setNZ(a = (a | magicConstant) & x & @r); }; +enum laxImmediate = q{ setNZ(a = x = (a | magicConstant) & @r); }; +enum las = q{ setNZ(a = x = (sp &= @r)); }; /// enum CpuVariant { @@ -323,6 +326,8 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve bool stopOnEmptyStackRts = true; bool stopped; + /// For ANE and LAX #imm. $EE matches SingleStepTests and MOS 6510. + ubyte magicConstant = 0xee; ubyte a; ubyte x; @@ -491,6 +496,28 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve mixin(substOperand(expr, "ld(addr)", "st(addr, ", "modifyCycle(addr, ", "idleRead(addr)")); } + // SHA/SHX/SHY/TAS + // TODO: revisit if RDY is implemented + private void unstableStore(ushort base, ubyte index, uint value) + { + const ushort fixed = cast(ushort) (base + index); + idleRead((base & 0xff00) | (fixed & 0xff)); + const ubyte data = cast(ubyte) (value & ((base >> 8) + 1)); + const bool crossed = (fixed & 0xff00) != (base & 0xff00); + st(crossed ? cast(ushort) ((data << 8) | (fixed & 0xff)) : fixed, data); + } + + private void doUnstableStoreAbsoluteIndexed(ubyte index, uint value) + { + unstableStore(fetchWord(), index, value); + } + + private void doUnstableStoreIndirectY(uint value) + { + const ushort zp = fetchByte(); + unstableStore(readWord(zp, cast(ushort) ((zp + 1) & 0xff)), y, value); + } + private void doNopAbsolute(bool indexCycle)() { fetchWord(); @@ -975,6 +1002,14 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve fetchByte(); jam(); return; + case 0x8b: doImmediate!ane(); break; + case 0xab: doImmediate!laxImmediate(); break; + case 0x9f: doUnstableStoreAbsoluteIndexed(y, a & x); break; + case 0x93: doUnstableStoreIndirectY(a & x); break; + case 0x9e: doUnstableStoreAbsoluteIndexed(y, x); break; + case 0x9c: doUnstableStoreAbsoluteIndexed(x, y); break; + case 0x9b: sp = a & x; doUnstableStoreAbsoluteIndexed(y, sp); break; + case 0xbb: doAbsoluteIndexed!las(y); break; case 0x0b: case 0x2b: doImmediate!anc(); break; case 0x4b: doImmediate!alr(); break; case 0x6b: doImmediate!arr(); break; @@ -1101,6 +1136,28 @@ unittest }} } +unittest +{ + debug writeln("unittest magic constant"); + + // ANE #$FF with A = 0 and X = $FF leaves exactly the leaked bits in A. + static ubyte ane(ubyte magic) + { + auto emu = new Emulator!(CpuVariant.mos_6502)(); + emu.magicConstant = magic; + emu.ram[0x1000 .. 0x1002] = [ubyte(0x8b), 0xff]; + emu.pc = 0x1000; + emu.a = 0; + emu.x = 0xff; + emu.instructionLimit = 1; + emu.run(); + return emu.a; + } + assert(ane(0xee) == 0xee); + assert(ane(0xff) == 0xff); + assert(ane(0x00) == 0x00); +} + private version(unittest): // TODO: build tests from source and extract the values from there From e30c148b5731af99fe5dca22ecb96d72d1051cdf Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Thu, 10 Sep 2026 08:16:32 +0200 Subject: [PATCH 14/17] emu: sprinkle with private --- source/xebin/emu.d | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/source/xebin/emu.d b/source/xebin/emu.d index c1911fc..ab427e2 100644 --- a/source/xebin/emu.d +++ b/source/xebin/emu.d @@ -36,6 +36,8 @@ private ushort makeWord(uint b1, uint b0) return cast(ushort) ((b1 << 8) | b0); } +private: + enum bool readsOperand(string expr) = expr.indexOf("@r") >= 0; enum bool writesOperand(string expr) = expr.indexOf("@w(") >= 0; enum indexAlways = "@i;"; @@ -241,6 +243,8 @@ enum ane = q{ setNZ(a = (a | magicConstant) & x & @r); }; enum laxImmediate = q{ setNZ(a = x = (a | magicConstant) & @r); }; enum las = q{ setNZ(a = x = (sp &= @r)); }; +public: + /// enum CpuVariant { mos_6502, /// NMOS @@ -395,14 +399,14 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve return memory[addr]; } - ubyte fetchByte() + private ubyte fetchByte() { ++pc; observer.fetch(pc, memory[pc]); return memory[pc]; } - ushort fetchWord() + private ushort fetchWord() { const lo = fetchByte(); const hi = fetchByte(); @@ -470,13 +474,13 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve private static void noModify(ubyte value) {} - void doAccumulator(string expr)() + private void doAccumulator(string expr)() { idleFetch(); mixin(substOperand(expr, "a", "a = (", "noModify(", "")); } - void doImmediate(string expr)() + private void doImmediate(string expr)() { static assert(!writesOperand!expr); mixin(substOperand(expr, "fetchByte()", "", "noModify(", "idleFetch()")); @@ -539,16 +543,16 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve mixin(substOperand(expr, "ld(addr)", "st(addr, ", "modifyCycle(addr, ", "idleRead(addr)")); } - void doIndirectY(string expr)() + private void doIndirectY(string expr)() { - const ushort zp = fetchByte(); + const ushort zp = fetchByte(); const base = readWord(zp, cast(ushort) ((zp + 1) & 0xff)); indexPenalty!expr(base, y); const addr = cast(ushort) (base + y); mixin(substOperand(expr, "ld(addr)", "st(addr, ", "modifyCycle(addr, ", "idleRead(addr)")); } - void doIndirectX(string expr)() + private void doIndirectX(string expr)() { const ushort zp = fetchByte(); idleRead(zp); @@ -558,7 +562,7 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve } static if (isCmos!cpuVariant) - void doIndirectZP(string expr)() + private void doIndirectZP(string expr)() { const ushort zp = fetchByte(); const addr = readWord(zp, cast(ushort) ((zp + 1) & 0xff)); @@ -566,7 +570,7 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve } static if (isCmos!cpuVariant) - void doBitSetReset(ubyte mask, bool set)() + private void doBitSetReset(ubyte mask, bool set)() { const ubyte addr = fetchByte(); const ubyte value = ld(addr); @@ -578,7 +582,7 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve } static if (hasBitOps!cpuVariant) - void doBitBranch(ubyte mask, bool branchIfSet)() + private void doBitBranch(ubyte mask, bool branchIfSet)() { const ubyte zp = fetchByte(); const bool isSet = (ld(zp) & mask) != 0; @@ -594,7 +598,7 @@ class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserve pc = cast(ushort) (target - 1); } - void doBranch(string pred)() + private void doBranch(string pred)() { byte offs = fetchByte(); if (!mixin(pred)) From 9ba1f038382ac3eef265d2946791ce522dfba588 Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Thu, 3 Sep 2026 17:06:03 +0200 Subject: [PATCH 15/17] disasm: 65c02 --- Makefile | 3 +- README.asciidoc | 13 +- source/app.d | 32 +- source/xebin/cpu.d | 42 ++ source/xebin/disasm.d | 767 ++++++++++++++++++++++++++--------- source/xebin/emu.d | 133 +++--- source/xebin/trace.d | 27 +- test/singlestep/source/app.d | 20 +- 8 files changed, 760 insertions(+), 277 deletions(-) create mode 100644 source/xebin/cpu.d diff --git a/Makefile b/Makefile index 8a8e137..195b445 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ VERSION = 1.1.0 -SOURCES = $(addprefix source/xebin/,flashpack.d binary.d disasm.d vm.d xasm.d) \ - source/app.d +SOURCES = $(wildcard source/xebin/*.d) source/app.d ASCIIDOC = asciidoc -o $@ -a doctime ASCIIDOC_POSTPROCESS = ZIP = 7z a -mx=9 -tzip $@ diff --git a/README.asciidoc b/README.asciidoc index 7d605a6..11ec258 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -119,7 +119,7 @@ Disassembling binary file ~~~~~~~~~~~~~~~~~~~~~~~~~ ---------------------------- -$ xebin d[isasm] [-o=fn] [input_files] +$ xebin d[isasm] [-c=cpu] [-o=fn] [input_files] ---------------------------- Each block is disassembled as code, preceded by an +ORG+ directive with its load @@ -131,10 +131,13 @@ run and init addresses. Addresses outside the loaded blocks are defined with +EQ the beginning of the output, and a reference pointing into the middle of an instruction is printed as an offset from that instruction's label, e.g. +L2103+1+. -Opcode names and argument syntax are taken from http://atari800.sourceforge.net/[Atari 800] -emulator. Bytes that do not decode into an official 6502 instruction, as well as an -instruction truncated by the end of a block, are emitted as +DTA+, so that the whole -output can be fed back to http://atariarea.krap.pl/x-asm/[xasm] and assembled into +Option +-c=cpu+ or +--cpu=cpu+ selects the instruction set: +6502+ (NMOS, the default), ++65c02+ (WDC 65C02), +r65c02+ (Rockwell R65C02, adding +RMB+/+SMB+/+BBR+/+BBS+) or ++w65c02s+ (adding +WAI+ and +STP+). + +Bytes that do not decode into a documented instruction of the selected CPU, as well as +an instruction truncated by the end of a block, are emitted as +DTA+, so that the whole +output can be fed back to https://github.com/pfusik/xasm[xasm] and assembled into the original file. Example output: diff --git a/source/app.d b/source/app.d index 6d90922..089a5af 100644 --- a/source/app.d +++ b/source/app.d @@ -44,6 +44,29 @@ bool verbose; string outputFile; bool disableOs; bool removeHeader; +Cpu cpu = Cpu.mos_6502; + +void parseCpu(string option, string value) +{ + switch (value.toLower()) + { + case "6502": + cpu = Cpu.mos_6502; + break; + case "65c02": + cpu = Cpu.wdc_65c02; + break; + case "r65c02": + cpu = Cpu.rockwell_r65c02; + break; + case "w65c02s": + cpu = Cpu.wdc_w65c02s; + break; + default: + throw new Exception("Unknown CPU `" ~ value + ~ "'. Available CPUs: 6502, 65c02, r65c02, w65c02s"); + } +} immutable VERSION_STRING = "1.1.0"; @@ -205,7 +228,7 @@ void disassembly(string[] args) if (args.length > 3) of.writeln("; ", file.name, ":"); - foreach (line; BinaryFileReader(file).readFile().disassemble) + foreach (line; BinaryFileReader(file).readFile().disassemble(cpu)) { of.writeln(line); } @@ -225,7 +248,7 @@ void run(string[] args) { auto blocks = BinaryFileReader(InputFiles(args).front).readFile(); if (tracing(Trace.cpu)) - runEmulator!(Emulator!(CpuVariant.mos_6502, CpuTracer))(blocks); + runEmulator!(Emulator!(Cpu.mos_6502, CpuTracer))(blocks); else runEmulator!(Emulator!())(blocks); } @@ -244,7 +267,7 @@ void printHelp(string[] args) " r[emove] [-n=pos] [-o=fn] [-v] remove block from file\n" ~ " i[nsert] [-n=pos] [-a=ad] [-o=fn] [-v] insert block into file\n" ~ " o[ptimize] [-o=fn] [-i] optimize file\n +/ - " d[isasm] [-o=fn] disassemble blocks\n" ~ + " d[isasm] [-c=cpu] [-o=fn] disassemble blocks\n" ~ " r[un] [--trace=what] run in a simple emulator\n" ~ " p[ack] [-a=ad] [-s] [-o=fn] [-v] pack using FlashPack algorithm\n" ~ " u[npack] [-o=fn] [-v] unpack FlashPack'd file\n" ~ @@ -256,6 +279,8 @@ void printHelp(string[] args) " (use '-' for end address)\n" ~ " -n|--position=pos which block to extract (indexed from 0)\n" ~ " -r|--raw remove header from extracted block\n" ~ + " -c|--cpu=type disassemble for the given CPU: 6502 (default),\n" ~ + " 65c02, r65c02 or w65c02s\n" ~ // " -i|--ignore-order do not preserve order of block if it makes\n" ~ // " optimized file shorter\n" ~ // " -n|--position=pos set block position for extract, delete, insert\n" ~ @@ -370,6 +395,7 @@ int main(string[] args) config.caseSensitive, config.noBundling, "s|disable-os", &disableOs, + "c|cpu", &parseCpu, "trace", &parseTraces, "a|address", &strAddr, "n|position", &position, diff --git a/source/xebin/cpu.d b/source/xebin/cpu.d new file mode 100644 index 0000000..a10cdb2 --- /dev/null +++ b/source/xebin/cpu.d @@ -0,0 +1,42 @@ +/** CPU versions and features + + Author: Adrian Matoga adrian@matoga.info + + zlib License: + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + 3. This notice may not be removed or altered from any source + distribution. +*/ +module xebin.cpu; + +/// +enum Cpu { + mos_6502, /// NMOS + wdc_65c02, /// original CMOS (also Synertek, GTE, etc.), part of Lynx's Mikey + rockwell_r65c02, /// Rockwell (bit ops) + wdc_w65c02s, /// modern W65C02S (bit ops + WAI/STP) +} + +/// Basic CMOS instruction set + BCD and JMP (abs) fixes. +enum bool isCmos(Cpu v) = v != Cpu.mos_6502; + +/// RMBn/SMBn/BBRn/BBSn. Rockwell's addition, carried over into the W65C02S. +enum bool hasBitOps(Cpu v) = + v == Cpu.rockwell_r65c02 || v == Cpu.wdc_w65c02s; + +/// WAI and STP, added by the W65C02S; NOPs everywhere else. +enum bool hasWaiStp(Cpu v) = v == Cpu.wdc_w65c02s; diff --git a/source/xebin/disasm.d b/source/xebin/disasm.d index f0a826f..a713d34 100644 --- a/source/xebin/disasm.d +++ b/source/xebin/disasm.d @@ -1,4 +1,4 @@ -/* Simple 6502 disassembler. +/* Simple 65(c)02 disassembler. Author: Adrian Matoga adrian@matoga.info @@ -24,40 +24,82 @@ module xebin.disasm; -import std.algorithm : map, sort, uniq, equal; +import std.algorithm : any, equal, sort, uniq; import std.array : array, appender; -import std.format : formattedWrite; -import std.range : chunks, assumeSorted, SortedRange; -import std.string; +import std.format : format, formattedWrite; +import std.range : assumeSorted, SortedRange; +import std.traits : EnumMembers; import xebin.binary; +public import xebin.cpu : Cpu; +import xebin.cpu : isCmos, hasBitOps, hasWaiStp; -/// -string disassembleOne(const(ubyte[]) memory, ref ushort addr) +/// Disassembles the instruction at `addr`, advancing `addr` past it. +string disassembleOne(const(ubyte[]) memory, ref ushort addr, + Cpu cpu = Cpu.mos_6502) { auto app = appender!string; - string instr = instructions[memory[addr]]; - const len = opLengths[memory[addr]]; + const op = opcodes(cpu)[memory[addr]]; + const len = instructionLength(op.mode); app.formattedWrite("%04X ", addr); foreach (i; 0 .. len) app.formattedWrite("%02X ", memory[(addr + i) & 0xffff]); foreach (i; len .. 3) app.put(" "); - if (instr[0] == '@') - instr = instr[1 .. $]; app.put(" "); - foreach (char c; instr) + app.put(op.mnem); + const b1 = memory[(addr + 1) & 0xffff]; + const b2 = memory[(addr + 2) & 0xffff]; + final switch (op.mode) with (Mode) { - if (c == '0') - app.formattedWrite("$%04X", - (addr + 2 + cast(byte) memory[(addr + 1) & 0xffff]) & 0xffff); - else if (c == '1') - app.formattedWrite("$%02X", memory[(addr + 1) & 0xffff]); - else if (c == '2') - app.formattedWrite("$%04X", memory[(addr + 1) & 0xffff] | - (memory[(addr + 2) & 0xffff] << 8)); - else - app.put(c); + case implied: + break; + case accumulator: + app.put(" @"); + break; + case immediate: + app.formattedWrite(" #$%02X", b1); + break; + case zeroPage: + app.formattedWrite(" $%02X", b1); + break; + case zeroPageX: + app.formattedWrite(" $%02X,X", b1); + break; + case zeroPageY: + app.formattedWrite(" $%02X,Y", b1); + break; + case indirectX: + app.formattedWrite(" ($%02X,X)", b1); + break; + case indirectY: + app.formattedWrite(" ($%02X),Y", b1); + break; + case zeroPageIndirect: + app.formattedWrite(" ($%02X)", b1); + break; + case absolute: + app.formattedWrite(" $%04X", b1 | (b2 << 8)); + break; + case absoluteX: + app.formattedWrite(" $%04X,X", b1 | (b2 << 8)); + break; + case absoluteY: + app.formattedWrite(" $%04X,Y", b1 | (b2 << 8)); + break; + case indirect: + app.formattedWrite(" ($%04X)", b1 | (b2 << 8)); + break; + case absoluteIndirectX: + app.formattedWrite(" ($%04X,X)", b1 | (b2 << 8)); + break; + case relative: + app.formattedWrite(" $%04X", (addr + 2 + cast(byte) b1) & 0xffff); + break; + case zeroPageRelative: + app.formattedWrite(" $%02X,$%04X", b1, + (addr + 3 + cast(byte) b2) & 0xffff); + break; } addr += len; return app.data; @@ -65,7 +107,6 @@ string disassembleOne(const(ubyte[]) memory, ref ushort addr) unittest { - import std.stdio; ushort addr = 0xFFFD; ubyte[0x10000] memory; memory[0xFFFD .. $] = [ 0xa9, 0x42, 0x8d ]; @@ -74,19 +115,67 @@ unittest assert(disassembleOne(memory, addr) == "FFFF 8D AD DE STA $DEAD"); assert(disassembleOne(memory, addr) == "0002 D0 FB BNE $FFFF"); assert(disassembleOne(memory, addr) == "0004 60 RTS"); - assert(disassembleOne(memory, addr) == "0005 02 CIM"); + assert(disassembleOne(memory, addr) == "0005 02 JAM"); } -/// -auto disassemble(BinaryBlock[] blocks) +unittest { - return Disassembler(blocks); + // NMOS undocumented instructions come out under their common names + ushort addr = 0x1000; + ubyte[0x10000] memory; + memory[0x1000 .. 0x1005] = [ 0x07, 0x12, 0xbf, 0x34, 0x12 ]; + assert(disassembleOne(memory, addr) == "1000 07 12 SLO $12"); + assert(disassembleOne(memory, addr) == "1002 BF 34 12 LAX $1234,Y"); +} + +unittest +{ + // 65C02 + ushort addr = 0x2000; + ubyte[0x10000] memory; + memory[0x2000 .. 0x200c] = + [ 0x64, 0x12, 0xb2, 0x80, 0x7c, 0x00, 0x30, 0x80, 0xf7, 0x1a, 0xcb, 0xcb ]; + with (Cpu) + { + assert(disassembleOne(memory, addr, wdc_65c02) == "2000 64 12 STZ $12"); + assert(disassembleOne(memory, addr, wdc_65c02) == "2002 B2 80 LDA ($80)"); + assert(disassembleOne(memory, addr, wdc_65c02) == "2004 7C 00 30 JMP ($3000,X)"); + assert(disassembleOne(memory, addr, wdc_65c02) == "2007 80 F7 BRA $2000"); + assert(disassembleOne(memory, addr, wdc_65c02) == "2009 1A INC @"); + // WAI exists on the W65C02S only; elsewhere its slot is a NOP + assert(disassembleOne(memory, addr, wdc_65c02) == "200A CB NOP"); + assert(disassembleOne(memory, addr, wdc_w65c02s) == "200B CB WAI"); + } +} + +unittest +{ + // Rockwell bit instructions: zp + relative in one operand + ushort addr = 0x2000; + ubyte[0x10000] memory; + memory[0x2000 .. 0x2005] = [ 0x87, 0x12, 0x2f, 0x12, 0xfb ]; + with (Cpu) + { + assert(disassembleOne(memory, addr, rockwell_r65c02) == "2000 87 12 SMB0 $12"); + assert(disassembleOne(memory, addr, rockwell_r65c02) == "2002 2F 12 FB BBR2 $12,$2000"); + // NOPs on plain 65C02 + addr = 0x2000; + assert(disassembleOne(memory, addr, wdc_65c02) == "2000 87 12 NOP $12"); + assert(disassembleOne(memory, addr, wdc_65c02) == "2002 2F 12 FB NOP $FB12"); + } } -private version(unittest) string[] disassembleToStrings(BinaryBlock[] blocks) +/// Returns an `opApply` iterator that emits disassembly one line at a time as `const(char)[]`. +auto disassemble(in BinaryBlock[] blocks, Cpu cpu = Cpu.mos_6502) +{ + return Disassembler(blocks, cpu); +} + +private version(unittest) +string[] disassembleToStrings(in BinaryBlock[] blocks, Cpu cpu = Cpu.mos_6502) { auto app = appender!(string[]); - foreach (l; blocks.disassemble) + foreach (l; blocks.disassemble(cpu)) { app.put(l.idup); } @@ -142,18 +231,46 @@ unittest ])); } +unittest +{ + // 65C02 with labels in both operands of BBRn + auto bb = [ + BinaryBlock(0x3000, cast(ubyte[]) x"80 02 64 12 0f 12 fb 7c 00 30"), + ]; + assert(bb.disassembleToStrings(Cpu.rockwell_r65c02).equal([ + "L0012\tEQU $0012", + "\tORG $3000", + "L3000\tBRA L3004", + "L3002\tSTZ L0012", + "L3004\tBBR0 L0012,L3002", + "\tJMP (L3000,X)" + ])); + // $0f becomes data on other chips + assert(bb.disassembleToStrings(Cpu.wdc_65c02).equal([ + "L0012\tEQU $0012", + "L00FB\tEQU $00FB", + "\tORG $3000", + "L3000\tBRA L3004", + "\tSTZ L0012", + "L3004\tDTA $0F", + "\tORA (L00FB)", + "\tJMP (L3000,X)" + ])); +} + private: struct Disassembler { - this(BinaryBlock[] blocks) + this(in BinaryBlock[] blocks, Cpu cpu) { m_blocks = blocks; + m_cpu = cpu; m_instrSpans = { auto app = appender!(Span[]); foreach (block; blocks) { - foreach (addr, atype, bytes; instructionSplitter(block)) + foreach (addr, kind, bytes; instructionSplitter(block, cpu)) app.put(Span(addr, cast(ushort) (addr + bytes.length - 1))); } return app.data.sort; @@ -170,22 +287,35 @@ struct Disassembler put(block.runAddress); if (block.contains(initAd)) put(block.initAddress); - foreach (addr, atype, const bytes; instructionSplitter(block)) + foreach (addr, kind, const bytes; instructionSplitter(block, cpu)) { - switch (atype) with(AddrType) + switch (kind) with (ItemKind) { - case relative: - put(cast(ushort) (addr + 2 + cast(byte) bytes[1])); - break; - case zeropage: - put(bytes[1]); - break; - case word: - put(bytes[1 .. $].peek!ushort); - break; - case dta_a: + case dataAddr: put(bytes[].peek!ushort); break; + case code: + final switch (opcodes(cpu)[bytes[0]].mode) with (Mode) + { + case relative: + put(cast(ushort) (addr + 2 + cast(byte) bytes[1])); + break; + case zeroPage: case zeroPageX: case zeroPageY: + case indirectX: case indirectY: case zeroPageIndirect: + put(bytes[1]); + break; + case absolute: case absoluteX: case absoluteY: + case indirect: case absoluteIndirectX: + put(bytes[1 .. $].peek!ushort); + break; + case zeroPageRelative: + put(bytes[1]); + put(cast(ushort) (addr + 3 + cast(byte) bytes[2])); + break; + case implied: case accumulator: case immediate: + break; + } + break; default: } } @@ -233,22 +363,19 @@ struct Disassembler app.formattedWrite("L%04X+%d", a, addr - a); } - foreach (l; m_labeledAddresses) + void putAbsolute(ushort addr) { - const inblock = { - foreach (block; m_blocks) - { - if (Span(block.addr, block.end).overlaps(Span(l, l))) - { - return true; - } - } - return false; - }(); - if (!inblock) + if (addr < 0x100) + app.put("A:"); + putAddr(addr); + } + + foreach (la; m_labeledAddresses) + { + if (!m_blocks.any!(b => Span(b.addr, b.end).overlaps(Span(la, la)))) { - declareLabel(l); - if (auto res = put("EQU $%04X", l, l)) + declareLabel(la); + if (auto res = put("EQU $%04X", la, la)) return res; } } @@ -257,48 +384,93 @@ struct Disassembler { if (auto res = put("\tORG $%04X", block.addr)) return res; - foreach (addr, atype, bytes; instructionSplitter(block)) + foreach (addr, kind, bytes; instructionSplitter(block, m_cpu)) { auto r = m_labeledAddresses.equalRange(addr); if (!r.empty) app.formattedWrite("L%04X", addr); app.put('\t'); - if (atype == AddrType.dta_a) + if (kind == ItemKind.dataAddr) { app.put("DTA A("); putAddr(bytes[].peek!ushort); app.put(')'); } + else if (kind == ItemKind.data) + { + app.formattedWrite("DTA $%02X", bytes[0]); + } else { - const instr = instructions[bytes[0]]; - if (instr[0] == '@' || bytes.length < opLengths[bytes[0]]) - { - app.formattedWrite("DTA $%02X", bytes[0]); - } - else + const op = opcodes(m_cpu)[bytes[0]]; + app.put(op.mnem); + final switch (op.mode) with (Mode) { - foreach (char c; instr) - { - if (c == '0') - putAddr(cast(ushort) (addr + 2 + cast(byte) bytes[1])); - else if (c == '1') - { - if (atype == AddrType.immediate) - app.formattedWrite("$%02X", bytes[1]); - else - putAddr(bytes[1]); - } - else if (c == '2') - { - const a = peek!ushort(bytes[1 .. $]); - if (a < 0x100 && app.data[$ - 1] == ' ') - app.put("A:"); - putAddr(a); - } - else - app.put(c); - } + case implied: + break; + case accumulator: + app.put(" @"); + break; + case immediate: + app.formattedWrite(" #$%02X", bytes[1]); + break; + case zeroPage: + app.put(' '); + putAddr(bytes[1]); + break; + case zeroPageX: + app.put(' '); + putAddr(bytes[1]); + app.put(",X"); + break; + case zeroPageY: + app.put(' '); + putAddr(bytes[1]); + app.put(",Y"); + break; + case indirectX: + app.put(" ("); + putAddr(bytes[1]); + app.put(",X)"); + break; + case indirectY: + app.put(" ("); + putAddr(bytes[1]); + app.put("),Y"); + break; + case zeroPageIndirect: + app.put(" ("); + putAddr(bytes[1]); + app.put(')'); + break; + case absolute: case absoluteX: case absoluteY: + app.put(' '); + putAbsolute(bytes[1 .. $].peek!ushort); + if (op.mode == absoluteX) + app.put(",X"); + else if (op.mode == absoluteY) + app.put(",Y"); + break; + case indirect: + app.put(" ("); + putAddr(bytes[1 .. $].peek!ushort); + app.put(')'); + break; + case absoluteIndirectX: + app.put(" ("); + putAddr(bytes[1 .. $].peek!ushort); + app.put(",X)"); + break; + case relative: + app.put(' '); + putAddr(cast(ushort) (addr + 2 + cast(byte) bytes[1])); + break; + case zeroPageRelative: + app.put(' '); + putAddr(bytes[1]); + app.put(','); + putAddr(cast(ushort) (addr + 3 + cast(byte) bytes[2])); + break; } } if (auto res = dg(app.data)) @@ -311,7 +483,8 @@ struct Disassembler } private: - BinaryBlock[] m_blocks; + const(BinaryBlock)[] m_blocks; + Cpu m_cpu; SortedRange!(Span[]) m_instrSpans; SortedRange!(ushort[]) m_labeledAddresses; @@ -349,114 +522,347 @@ T peek(T, R)(auto ref R r) return stdpeek!(T, Endian.littleEndian, R)(r); } -static immutable(string[256]) instructions = -[ // shamelessly stolen from Atari800 - "BRK", "ORA (1,X)", "@CIM", "@ASO (1,X)", "@NOP 1", "ORA 1", "ASL 1", "@ASO 1", - "PHP", "ORA #1", "ASL @", "@ANC #1", "@NOP 2", "ORA 2", "ASL 2", "@ASO 2", - - "BPL 0", "ORA (1),Y", "@CIM", "@ASO (1),Y", "@NOP 1,X", "ORA 1,X", "ASL 1,X", "@ASO 1,X", - "CLC", "ORA 2,Y", "@NOP !", "@ASO 2,Y", "@NOP 2,X", "ORA 2,X", "ASL 2,X", "@ASO 2,X", - - "JSR 2", "AND (1,X)", "@CIM", "@RLA (1,X)", "BIT 1", "AND 1", "ROL 1", "@RLA 1", - "PLP", "AND #1", "ROL @", "@ANC #1", "BIT 2", "AND 2", "ROL 2", "@RLA 2", - - "BMI 0", "AND (1),Y", "@CIM", "@RLA (1),Y", "@NOP 1,X", "AND 1,X", "ROL 1,X", "@RLA 1,X", - "SEC", "AND 2,Y", "@NOP !", "@RLA 2,Y", "@NOP 2,X", "AND 2,X", "ROL 2,X", "@RLA 2,X", - - - "RTI", "EOR (1,X)", "@CIM", "@LSE (1,X)", "@NOP 1", "EOR 1", "LSR 1", "@LSE 1", - "PHA", "EOR #1", "LSR @", "@ALR #1", "JMP 2", "EOR 2", "LSR 2", "@LSE 2", - - "BVC 0", "EOR (1),Y", "@CIM", "@LSE (1),Y", "@NOP 1,X", "EOR 1,X", "LSR 1,X", "@LSE 1,X", - "CLI", "EOR 2,Y", "@NOP !", "@LSE 2,Y", "@NOP 2,X", "EOR 2,X", "LSR 2,X", "@LSE 2,X", - - "RTS", "ADC (1,X)", "@CIM", "@RRA (1,X)", "@NOP 1", "ADC 1", "ROR 1", "@RRA 1", - "PLA", "ADC #1", "ROR @", "@ARR #1", "JMP (2)", "ADC 2", "ROR 2", "@RRA 2", - - "BVS 0", "ADC (1),Y", "@CIM", "@RRA (1),Y", "@NOP 1,X", "ADC 1,X", "ROR 1,X", "@RRA 1,X", - "SEI", "ADC 2,Y", "@NOP !", "@RRA 2,Y", "@NOP 2,X", "ADC 2,X", "ROR 2,X", "@RRA 2,X", - +enum Mode : ubyte +{ + implied, + accumulator, + immediate, + zeroPage, + zeroPageX, + zeroPageY, + indirectX, + indirectY, + zeroPageIndirect, + absolute, + absoluteX, + absoluteY, + indirect, // JMP + absoluteIndirectX, // JMP ($1234,X) + relative, + zeroPageRelative, // BBRn/BBSn $12,$3456 +} - "@NOP #1", "STA (1,X)", "@NOP #1", "@SAX (1,X)", "STY 1", "STA 1", "STX 1", "@SAX 1", - "DEY", "@NOP #1", "TXA", "@ANE #1", "STY 2", "STA 2", "STX 2", "@SAX 2", +uint instructionLength(Mode mode) @safe pure nothrow @nogc +{ + final switch (mode) with (Mode) + { + case implied: case accumulator: + return 1; + case immediate: case zeroPage: case zeroPageX: case zeroPageY: + case indirectX: case indirectY: case zeroPageIndirect: case relative: + return 2; + case absolute: case absoluteX: case absoluteY: case indirect: + case absoluteIndirectX: case zeroPageRelative: + return 3; + } +} - "BCC 0", "STA (1),Y", "@CIM", "@SHA (1),Y", "STY 1,X", "STA 1,X", "STX 1,Y", "@SAX 1,Y", - "TYA", "STA 2,Y", "TXS", "@SHS 2,Y", "@SHY 2,X", "STA 2,X", "@SHX 2,Y", "@SHA 2,Y", +struct Op +{ + string mnem = "JAM"; + Mode mode = Mode.implied; + // code in trace, data in block disassembly + bool documented = false; +} - "LDY #1", "LDA (1,X)", "LDX #1", "@LAX (1,X)", "LDY 1", "LDA 1", "LDX 1", "@LAX 1", - "TAY", "LDA #1", "TAX", "@ANX #1", "LDY 2", "LDA 2", "LDX 2", "@LAX 2", +Op[256] buildOpcodes(Cpu cpu)() @safe pure +{ + Op[256] t; + bool[256] used; - "BCS 0", "LDA (1),Y", "@CIM", "@LAX (1),Y", "LDY 1,X", "LDA 1,X", "LDX 1,Y", "@LAX 1,X", - "CLV", "LDA 2,Y", "TSX", "@LAS 2,Y", "LDY 2,X", "LDA 2,X", "LDX 2,Y", "@LAX 2,Y", + void def(int opcode, string mnem, Mode mode = Mode.implied, bool documented = true) + { + assert(!used[opcode], format!"opcode %02X defined twice"(opcode)); + used[opcode] = true; + t[opcode] = Op(mnem, mode, documented); + } + foreach (i, m; ["ORA", "AND", "EOR", "ADC", "STA", "LDA", "CMP", "SBC"]) + { + const b = cast(int) i * 0x20; + def(b + 0x01, m, Mode.indirectX); + def(b + 0x05, m, Mode.zeroPage); + if (m != "STA") + def(b + 0x09, m, Mode.immediate); + def(b + 0x0d, m, Mode.absolute); + def(b + 0x11, m, Mode.indirectY); + def(b + 0x15, m, Mode.zeroPageX); + def(b + 0x19, m, Mode.absoluteY); + def(b + 0x1d, m, Mode.absoluteX); + static if (isCmos!cpu) + def(b + 0x12, m, Mode.zeroPageIndirect); + } - "CPY #1", "CMP (1,X)", "@NOP #1", "@DCM (1,X)", "CPY 1", "CMP 1", "DEC 1", "@DCM 1", - "INY", "CMP #1", "DEX", "@SBX #1", "CPY 2", "CMP 2", "DEC 2", "@DCM 2", + foreach (i, m; ["ASL", "ROL", "LSR", "ROR"]) + { + const b = cast(int) i * 0x20; + def(b + 0x06, m, Mode.zeroPage); + def(b + 0x0a, m, Mode.accumulator); + def(b + 0x0e, m, Mode.absolute); + def(b + 0x16, m, Mode.zeroPageX); + def(b + 0x1e, m, Mode.absoluteX); + } + def(0xc6, "DEC", Mode.zeroPage); + def(0xd6, "DEC", Mode.zeroPageX); + def(0xce, "DEC", Mode.absolute); + def(0xde, "DEC", Mode.absoluteX); + def(0xe6, "INC", Mode.zeroPage); + def(0xf6, "INC", Mode.zeroPageX); + def(0xee, "INC", Mode.absolute); + def(0xfe, "INC", Mode.absoluteX); + + foreach (i, m; ["BPL", "BMI", "BVC", "BVS", "BCC", "BCS", "BNE", "BEQ"]) + def(cast(int) i * 0x20 + 0x10, m, Mode.relative); + + def(0xa2, "LDX", Mode.immediate); + def(0xa6, "LDX", Mode.zeroPage); + def(0xb6, "LDX", Mode.zeroPageY); + def(0xae, "LDX", Mode.absolute); + def(0xbe, "LDX", Mode.absoluteY); + def(0xa0, "LDY", Mode.immediate); + def(0xa4, "LDY", Mode.zeroPage); + def(0xb4, "LDY", Mode.zeroPageX); + def(0xac, "LDY", Mode.absolute); + def(0xbc, "LDY", Mode.absoluteX); + def(0x86, "STX", Mode.zeroPage); + def(0x96, "STX", Mode.zeroPageY); + def(0x8e, "STX", Mode.absolute); + def(0x84, "STY", Mode.zeroPage); + def(0x94, "STY", Mode.zeroPageX); + def(0x8c, "STY", Mode.absolute); + def(0xe0, "CPX", Mode.immediate); + def(0xe4, "CPX", Mode.zeroPage); + def(0xec, "CPX", Mode.absolute); + def(0xc0, "CPY", Mode.immediate); + def(0xc4, "CPY", Mode.zeroPage); + def(0xcc, "CPY", Mode.absolute); + + def(0x24, "BIT", Mode.zeroPage); + def(0x2c, "BIT", Mode.absolute); + + def(0x00, "BRK"); + def(0x20, "JSR", Mode.absolute); + def(0x40, "RTI"); + def(0x4c, "JMP", Mode.absolute); + def(0x60, "RTS"); + def(0x6c, "JMP", Mode.indirect); + + def(0x08, "PHP"); + def(0x28, "PLP"); + def(0x48, "PHA"); + def(0x68, "PLA"); + def(0x18, "CLC"); + def(0x38, "SEC"); + def(0x58, "CLI"); + def(0x78, "SEI"); + def(0xb8, "CLV"); + def(0xd8, "CLD"); + def(0xf8, "SED"); + def(0x88, "DEY"); + def(0xc8, "INY"); + def(0xca, "DEX"); + def(0xe8, "INX"); + def(0x8a, "TXA"); + def(0xaa, "TAX"); + def(0x98, "TYA"); + def(0xa8, "TAY"); + def(0x9a, "TXS"); + def(0xba, "TSX"); + def(0xea, "NOP"); + + static if (!isCmos!cpu) + { + foreach (i, m; ["SLO", "RLA", "SRE", "RRA", "", "", "DCP", "ISC"]) + { + if (!m.length) + continue; + const b = cast(int) i * 0x20; + def(b + 0x03, m, Mode.indirectX, false); + def(b + 0x07, m, Mode.zeroPage, false); + def(b + 0x0f, m, Mode.absolute, false); + def(b + 0x13, m, Mode.indirectY, false); + def(b + 0x17, m, Mode.zeroPageX, false); + def(b + 0x1b, m, Mode.absoluteY, false); + def(b + 0x1f, m, Mode.absoluteX, false); + } + def(0x83, "SAX", Mode.indirectX, false); + def(0x87, "SAX", Mode.zeroPage, false); + def(0x8f, "SAX", Mode.absolute, false); + def(0x97, "SAX", Mode.zeroPageY, false); + def(0x93, "SHA", Mode.indirectY, false); + def(0x9f, "SHA", Mode.absoluteY, false); + def(0x9b, "TAS", Mode.absoluteY, false); + def(0x9c, "SHY", Mode.absoluteX, false); + def(0x9e, "SHX", Mode.absoluteY, false); + def(0xa3, "LAX", Mode.indirectX, false); + def(0xa7, "LAX", Mode.zeroPage, false); + def(0xaf, "LAX", Mode.absolute, false); + def(0xb3, "LAX", Mode.indirectY, false); + def(0xb7, "LAX", Mode.zeroPageY, false); + def(0xbf, "LAX", Mode.absoluteY, false); + def(0xab, "LXA", Mode.immediate, false); + def(0xbb, "LAS", Mode.absoluteY, false); + def(0x0b, "ANC", Mode.immediate, false); + def(0x2b, "ANC", Mode.immediate, false); + def(0x4b, "ALR", Mode.immediate, false); + def(0x6b, "ARR", Mode.immediate, false); + def(0x8b, "ANE", Mode.immediate, false); + def(0xcb, "SBX", Mode.immediate, false); + def(0xeb, "SBC", Mode.immediate, false); + foreach (op; [0x1a, 0x3a, 0x5a, 0x7a, 0xda, 0xfa]) + def(op, "NOP", Mode.implied, false); + foreach (op; [0x80, 0x82, 0x89, 0xc2, 0xe2]) + def(op, "NOP", Mode.immediate, false); + foreach (op; [0x04, 0x44, 0x64]) + def(op, "NOP", Mode.zeroPage, false); + foreach (op; [0x14, 0x34, 0x54, 0x74, 0xd4, 0xf4]) + def(op, "NOP", Mode.zeroPageX, false); + def(0x0c, "NOP", Mode.absolute, false); + foreach (op; [0x1c, 0x3c, 0x5c, 0x7c, 0xdc, 0xfc]) + def(op, "NOP", Mode.absoluteX, false); + foreach (op; [0x02, 0x12, 0x22, 0x32, 0x42, 0x52, 0x62, 0x72, + 0x92, 0xb2, 0xd2, 0xf2]) + def(op, "JAM", Mode.implied, false); + } + else + { + def(0x1a, "INC", Mode.accumulator); + def(0x3a, "DEC", Mode.accumulator); + def(0x5a, "PHY"); + def(0x7a, "PLY"); + def(0xda, "PHX"); + def(0xfa, "PLX"); + def(0x80, "BRA", Mode.relative); + def(0x89, "BIT", Mode.immediate); + def(0x34, "BIT", Mode.zeroPageX); + def(0x3c, "BIT", Mode.absoluteX); + def(0x04, "TSB", Mode.zeroPage); + def(0x0c, "TSB", Mode.absolute); + def(0x14, "TRB", Mode.zeroPage); + def(0x1c, "TRB", Mode.absolute); + def(0x64, "STZ", Mode.zeroPage); + def(0x74, "STZ", Mode.zeroPageX); + def(0x9c, "STZ", Mode.absolute); + def(0x9e, "STZ", Mode.absoluteX); + def(0x7c, "JMP", Mode.absoluteIndirectX); + + foreach (n; 0 .. 8) + { + const digit = cast(char) ('0' + n); + static if (hasBitOps!cpu) + { + def(0x07 + n * 0x10, "RMB" ~ digit, Mode.zeroPage); + def(0x87 + n * 0x10, "SMB" ~ digit, Mode.zeroPage); + def(0x0f + n * 0x10, "BBR" ~ digit, Mode.zeroPageRelative); + def(0x8f + n * 0x10, "BBS" ~ digit, Mode.zeroPageRelative); + } + else + { + const zmode = n % 2 ? Mode.zeroPageX : Mode.zeroPage; + const amode = n % 2 ? Mode.absoluteX : Mode.absolute; + def(0x07 + n * 0x10, "NOP", zmode, false); + def(0x87 + n * 0x10, "NOP", zmode, false); + def(0x0f + n * 0x10, "NOP", amode, false); + def(0x8f + n * 0x10, "NOP", amode, false); + } + } - "BNE 0", "CMP (1),Y", "@ESCRTS #1", "@DCM (1),Y", "NOP 1,X", "CMP 1,X", "DEC 1,X", "@DCM 1,X", - "CLD", "CMP 2,Y", "@NOP !", "@DCM 2,Y", "@NOP 2,X", "CMP 2,X", "DEC 2,X", "@DCM 2,X", + static if (hasWaiStp!cpu) + { + def(0xcb, "WAI"); + def(0xdb, "STP"); + } + else + { + def(0xcb, "NOP", Mode.implied, false); + def(0xdb, "NOP", Mode.zeroPageX, false); + } + foreach (op; [0x03, 0x13, 0x23, 0x33, 0x43, 0x53, 0x63, 0x73, + 0x83, 0x93, 0xa3, 0xb3, 0xc3, 0xd3, 0xe3, 0xf3, + 0x0b, 0x1b, 0x2b, 0x3b, 0x4b, 0x5b, 0x6b, 0x7b, + 0x8b, 0x9b, 0xab, 0xbb, 0xeb, 0xfb]) + def(op, "NOP", Mode.implied, false); + foreach (op; [0x02, 0x22, 0x42, 0x62, 0x82, 0xc2, 0xe2]) + def(op, "NOP", Mode.immediate, false); + def(0x44, "NOP", Mode.zeroPage, false); + foreach (op; [0x54, 0xd4, 0xf4]) + def(op, "NOP", Mode.zeroPageX, false); + foreach (op; [0x5c, 0xdc, 0xfc]) + def(op, "NOP", Mode.absolute, false); + } - "CPX #1", "@SBC (1,X)", "@NOP #1", "@INS (1,X)", "CPX 1", "SBC 1", "INC 1", "@INS 1", - "INX", "SBC #1", "NOP", "@SBC #1 !", "CPX 2", "SBC 2", "INC 2", "@INS 2", + foreach (op, u; used) + assert(u, format!"opcode %02X not defined"(op)); - "BEQ 0", "@SBC (1),Y", "@ESCAPE #1", "@INS (1),Y", "@NOP 1,X", "SBC 1,X", "INC 1,X", "@INS 1,X", - "SED", "SBC 2,Y", "@NOP !", "@INS 2,Y", "@NOP 2,X", "SBC 2,X", "INC 2,X", "@INS 2,X" -]; + return t; +} -enum AddrType +template opcodeTable(Cpu cpu) { - none, - immediate, - zeropage, - word, - relative, - dta_a, + static immutable Op[256] opcodeTable = buildOpcodes!cpu(); } -static immutable(AddrType[256]) addrTypes = - instructions[].map!((instr) +ref immutable(Op[256]) opcodes(Cpu cpu) @safe pure nothrow @nogc +{ + final switch (cpu) + { + static foreach (v; EnumMembers!Cpu) { - foreach (char c; instr) - { - switch (c) - { - case '@': - return AddrType.none; - case '#': - return AddrType.immediate; - case '0': - return AddrType.relative; - case '1': - return AddrType.zeropage; - case '2': - return AddrType.word; - default: - } - } - return AddrType.none; - }).array; + case v: + return opcodeTable!v; + } + } +} -static assert(addrTypes[0x0d] == AddrType.word); +static assert(opcodeTable!(Cpu.mos_6502)[0xad] == Op("LDA", Mode.absolute, true)); +static assert(opcodeTable!(Cpu.mos_6502)[0x6b] == Op("ARR", Mode.immediate, false)); +static assert(opcodeTable!(Cpu.wdc_65c02)[0x64] == Op("STZ", Mode.zeroPage, true)); +static assert(opcodeTable!(Cpu.wdc_65c02)[0x0f] == Op("NOP", Mode.absolute, false)); +static assert(opcodeTable!(Cpu.rockwell_r65c02)[0x0f] == Op("BBR0", Mode.zeroPageRelative, true)); +static assert(opcodeTable!(Cpu.rockwell_r65c02)[0xcb] == Op("NOP", Mode.implied, false)); +static assert(opcodeTable!(Cpu.wdc_w65c02s)[0xcb] == Op("WAI", Mode.implied, true)); -static immutable(ubyte[256]) opLengths = - addrTypes[].map!((atype) +unittest +{ + import std.algorithm : canFind; + import xebin.emu : Emulator; + + static foreach (v; EnumMembers!Cpu) + { + import std.stdio; + foreach (op; 0 .. 256) { - final switch (atype) with (AddrType) - { - case none: return 1; - case word: return 3; - case immediate: case relative: case zeropage: case dta_a: - return 2; - } - }).array; + const entry = opcodes(v)[op]; + // anything that redirects pc cannot be measured this way + if (entry.mode == Mode.relative || entry.mode == Mode.zeroPageRelative + || ["JMP", "JSR", "RTS", "RTI", "BRK", "WAI", "STP"].canFind(entry.mnem)) + continue; + auto emu = new Emulator!v(); + emu.ram[0x8000 .. 0x8003] = [cast(ubyte) op, 0x34, 0x12]; + emu.pc = 0x8000; + emu.instructionLimit = 1; + emu.run(); + assert(emu.pc == 0x8000 + instructionLength(entry.mode) - 1, + format!"%s: opcode %02X"(v, op)); + } + } +} + +enum ItemKind +{ + code, // documented instruction + data, // reserved opcode or truncated instruction (1 byte) + dataAddr, // run/init vector (2 bytes) +} -auto instructionSplitter(BinaryBlock block) +auto instructionSplitter(const BinaryBlock block, Cpu cpu) { static struct Splitter { - BinaryBlock block; - int opApply(scope int delegate(ushort addr, AddrType atype, const(ubyte)[] bytes) dg) const + const BinaryBlock block; + Cpu cpu; + int opApply(scope int delegate(ushort addr, ItemKind kind, const(ubyte)[] bytes) dg) const { ushort addr = block.addr; const(ubyte)[] data = block.data; @@ -464,26 +870,25 @@ auto instructionSplitter(BinaryBlock block) { if ((addr == runAd || addr == initAd) && data.length >= 2) { - if (auto res = dg(addr, AddrType.dta_a, data[0 .. 2])) + if (auto res = dg(addr, ItemKind.dataAddr, data[0 .. 2])) return res; addr += 2; data = data[2 .. $]; continue; } - const opcode = data[0]; - const atype = addrTypes[opcode]; - const len = opLengths[opcode]; - if (len <= data.length) + const op = opcodes(cpu)[data[0]]; + const len = instructionLength(op.mode); + if (op.documented && len <= data.length) { - if (auto res = dg(addr, atype, data[0 .. len])) + if (auto res = dg(addr, ItemKind.code, data[0 .. len])) return res; addr += len; data = data[len .. $]; continue; } - if (auto res = dg(addr, AddrType.none, data[0 .. 1])) + if (auto res = dg(addr, ItemKind.data, data[0 .. 1])) return res; addr += 1; data = data[1 .. $]; @@ -491,5 +896,5 @@ auto instructionSplitter(BinaryBlock block) return 0; } } - return Splitter(block); + return Splitter(block, cpu); } diff --git a/source/xebin/emu.d b/source/xebin/emu.d index ab427e2..04ec4a5 100644 --- a/source/xebin/emu.d +++ b/source/xebin/emu.d @@ -25,6 +25,8 @@ module xebin.emu; import std.string; +public import xebin.cpu; + version(unittest) { import std.stdio; @@ -245,24 +247,6 @@ enum las = q{ setNZ(a = x = (sp &= @r)); }; public: -/// -enum CpuVariant { - mos_6502, /// NMOS - wdc_65c02, /// original CMOS (also Synertek, GTE, etc.), part of Lynx's Mikey - rockwell_r65c02, /// Rockwell (bit ops) - wdc_w65c02s, /// modern W65C02S (bit ops + WAI/STP)} -} - -/// Basic CMOS instruction set + BCD and JMP (abs) fixes. -enum bool isCmos(CpuVariant v) = v != CpuVariant.mos_6502; - -/// Rockwell's RMBn/SMBn/BBRn/BBSn, carried over into the W65C02S. -enum bool hasBitOps(CpuVariant v) = - v == CpuVariant.rockwell_r65c02 || v == CpuVariant.wdc_w65c02s; - -/// WAI and STP, added by the W65C02S; NOPs everywhere else. -enum bool hasWaiStp(CpuVariant v) = v == CpuVariant.wdc_w65c02s; - /// Observer policy interface with no-op implementation. struct NoObserver { @@ -307,9 +291,10 @@ struct UniformTicks } /// -class Emulator(CpuVariant cpuVariant = CpuVariant.mos_6502, Observer = NoObserver) +final class Emulator(Cpu cpuVariant = Cpu.mos_6502, Observer = NoObserver) { - enum cpu = cpuVariant; + /// The variant being emulated, for observers that care (e.g. a disassembler). + enum Cpu cpu = cpuVariant; /// Observer policy instance; configure it before running. Observer observer; @@ -1119,7 +1104,7 @@ unittest // With no handler the escape falls back to what the silicon would do: // NMOS jams, reporting the address of the $02 rather than the selector. { - auto emu = new Emulator!(CpuVariant.mos_6502)(); + auto emu = new Emulator!(Cpu.mos_6502)(); load(emu, 0x1000, [ubyte(0x02), 0x37, 0xe8]); emu.instructionLimit = 2; emu.run(); @@ -1129,7 +1114,7 @@ unittest } // ... while the CMOS parts see it as an ordinary two-byte NOP. - static foreach (v; [CpuVariant.wdc_65c02, CpuVariant.rockwell_r65c02, CpuVariant.wdc_w65c02s]) + static foreach (v; [Cpu.wdc_65c02, Cpu.rockwell_r65c02, Cpu.wdc_w65c02s]) {{ auto emu = new Emulator!v(); load(emu, 0x1000, [ubyte(0x02), 0x37, 0xe8]); @@ -1147,7 +1132,7 @@ unittest // ANE #$FF with A = 0 and X = $FF leaves exactly the leaked bits in A. static ubyte ane(ubyte magic) { - auto emu = new Emulator!(CpuVariant.mos_6502)(); + auto emu = new Emulator!(Cpu.mos_6502)(); emu.magicConstant = magic; emu.ram[0x1000 .. 0x1002] = [ubyte(0x8b), 0xff]; emu.pc = 0x1000; @@ -1261,7 +1246,7 @@ unittest // Built with wdc_op=1 (WAI/STP skipped), rkwl_wdc_op=1 (BBR/BBS/RMB/SMB // fully tested) and skip_nop=0 (every undefined opcode tested as a NOP). - ok &= check(new Emulator!(CpuVariant.wdc_w65c02s), + ok &= check(new Emulator!(Cpu.wdc_w65c02s), "65C02 extended opcodes test", cast(immutable(ubyte)[]) read("ext/6502_65C02_functional_tests/bin_files/65C02_extended_opcodes_test.bin"), 0x24f1); @@ -1300,7 +1285,7 @@ struct BusTrace } } -void checkInstruction(CpuVariant v)(State initial, const(int[2])[] ram, +void checkInstruction(Cpu v)(State initial, const(int[2])[] ram, State expected, const(int[2])[] changed, string trace, string file = __FILE__, size_t line = __LINE__) { @@ -1354,294 +1339,294 @@ unittest debug writeln("unittest emu singlestep"); // mos_6502, opcode $00: "00 3f f7" - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x8b82, 0x51, 0xcb, 0x75, 0xa2, 0x6a), [[0x8b82, 0x00], [0x8b83, 0x3f], [0x8b84, 0xf7], [0xfffe, 0xd4], [0xffff, 0x25], [0x25d4, 0xed]], State(0x25d4, 0x4e, 0xcb, 0x75, 0xa2, 0x6e), [[0x014f, 0x7a], [0x0150, 0x84], [0x0151, 0x8b]], "R 8b82 00 R 8b83 3f W 0151 8b W 0150 84 W 014f 7a R fffe d4 R ffff 25"); // mos_6502, opcode $01: "01 bd b0" - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0xd5c4, 0x61, 0xa7, 0xf7, 0xf8, 0x28), [[0xd5c4, 0x01], [0xd5c5, 0xbd], [0xd5c6, 0xb0], [0x00bd, 0x58], [0x00b4, 0x81], [0x00b5, 0xeb], [0xeb81, 0x13]], State(0xd5c6, 0x61, 0xb7, 0xf7, 0xf8, 0xa8), [], "R d5c4 01 R d5c5 bd R 00bd 58 R 00b4 81 R 00b5 eb R eb81 13"); // mos_6502, opcode $06: "06 89 7c" - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0xec81, 0x56, 0xaa, 0xdd, 0xba, 0x29), [[0xec81, 0x06], [0xec82, 0x89], [0xec83, 0x7c], [0x0089, 0x42]], State(0xec83, 0x56, 0xaa, 0xdd, 0xba, 0xa8), [[0x0089, 0x84]], "R ec81 06 R ec82 89 R 0089 42 W 0089 42 W 0089 84"); // mos_6502, opcode $20: "20 55 13" - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x017b, 0x7d, 0x9e, 0x89, 0x34, 0xe6), [[0x017b, 0x20], [0x017c, 0x55], [0x017d, 0x13], [0x0155, 0xad]], State(0x0155, 0x7b, 0x9e, 0x89, 0x34, 0xe6), [[0x017c, 0x7d], [0x017d, 0x01]], "R 017b 20 R 017c 55 R 017d 13 W 017d 01 W 017c 7d R 017d 01"); // mos_6502, opcode $61: "61 91 cd" // ADC (zp,x) BCD: B3+46+1 == 113+46+1 == (1)60 - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0xf372, 0x4c, 0xb3, 0x50, 0xd1, 0x6b), [[0xf372, 0x61], [0xf373, 0x91], [0xf374, 0xcd], [0x0091, 0xdc], [0x00e1, 0x1d], [0x00e2, 0x2c], [0x2c1d, 0x46]], State(0xf374, 0x4c, 0x60, 0x50, 0xd1, 0x29), [], "R f372 61 R f373 91 R 0091 dc R 00e1 1d R 00e2 2c R 2c1d 46"); // mos_6502, opcode $6c: "6c ff 70" // JMP ($70ff) bug - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x2887, 0x47, 0x23, 0x66, 0xb2, 0x24), [[0x2887, 0x6c], [0x2888, 0xff], [0x2889, 0x70], [0x70ff, 0x9d], [0x7000, 0x98], [0x989d, 0x23]], State(0x989d, 0x47, 0x23, 0x66, 0xb2, 0x24), [], "R 2887 6c R 2888 ff R 2889 70 R 70ff 9d R 7000 98"); // mos_6502, opcode $08: "08 60 be" // PHP does idle fetch cycle - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x2f81, 0x26, 0x87, 0x6a, 0xb4, 0x2b), [[0x2f81, 0x08], [0x2f82, 0x60], [0x2f83, 0xbe]], State(0x2f82, 0x25, 0x87, 0x6a, 0xb4, 0x2b), [[0x0126, 0x3b]], "R 2f81 08 R 2f82 60 W 0126 3b"); // mos_6502, opcode $10: "10 b3 8d" // BPL taken - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x90ca, 0x8c, 0x3c, 0x86, 0x68, 0x25), [[0x90ca, 0x10], [0x90cb, 0xb3], [0x90cc, 0x8d], [0x907f, 0xe7]], State(0x907f, 0x8c, 0x3c, 0x86, 0x68, 0x25), [], "R 90ca 10 R 90cb b3 R 90cc 8d"); // mos_6502, opcode $10: "10 3c 3a" // BPL taken across page - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x77e1, 0xed, 0xec, 0x04, 0xfb, 0x2a), [[0x77e1, 0x10], [0x77e2, 0x3c], [0x77e3, 0x3a], [0x771f, 0x88], [0x781f, 0xc7]], State(0x781f, 0xed, 0xec, 0x04, 0xfb, 0x2a), [], "R 77e1 10 R 77e2 3c R 77e3 3a R 771f 88"); // mos_6502, opcode $18: "18 c9 9b" - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x09a8, 0x26, 0xd7, 0x79, 0xbf, 0xec), [[0x09a8, 0x18], [0x09a9, 0xc9], [0x09aa, 0x9b]], State(0x09a9, 0x26, 0xd7, 0x79, 0xbf, 0xec), [], "R 09a8 18 R 09a9 c9"); // mos_6502, opcode $28: "28 c6 97" // PLP does idle fetch and idle pop - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0xa532, 0xa3, 0x9e, 0x77, 0x6e, 0xad), [[0xa532, 0x28], [0xa533, 0xc6], [0xa534, 0x97], [0x01a3, 0x30], [0x01a4, 0x94]], State(0xa533, 0xa4, 0x9e, 0x77, 0x6e, 0xa4), [], "R a532 28 R a533 c6 R 01a3 30 R 01a4 94"); // mos_6502, opcode $38: "38 08 67" - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x85d6, 0x43, 0xa8, 0xd2, 0xb7, 0xe1), [[0x85d6, 0x38], [0x85d7, 0x08], [0x85d8, 0x67]], State(0x85d7, 0x43, 0xa8, 0xd2, 0xb7, 0xe1), [], "R 85d6 38 R 85d7 08"); // mos_6502, opcode $40: "40 9c 2c" // RTI does idle fetch and idle pop - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x8771, 0x6e, 0xa2, 0x81, 0x7e, 0x63), [[0x8771, 0x40], [0x8772, 0x9c], [0x8773, 0x2c], [0x016e, 0x98], [0x016f, 0x9c], [0x0170, 0xaa], [0x0171, 0x65], [0x65aa, 0x0e]], State(0x65aa, 0x71, 0xa2, 0x81, 0x7e, 0xac), [], "R 8771 40 R 8772 9c R 016e 98 R 016f 9c R 0170 aa R 0171 65"); // mos_6502, opcode $48: "48 e0 36" // PHA does idle fetch - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x5063, 0x9f, 0x3e, 0x33, 0x4e, 0xac), [[0x5063, 0x48], [0x5064, 0xe0], [0x5065, 0x36]], State(0x5064, 0x9e, 0x3e, 0x33, 0x4e, 0xac), [[0x019f, 0x3e]], "R 5063 48 R 5064 e0 W 019f 3e"); // mos_6502, opcode $58: "58 71 bb" - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x448a, 0xf5, 0xb6, 0xd3, 0x96, 0xa9), [[0x448a, 0x58], [0x448b, 0x71], [0x448c, 0xbb]], State(0x448b, 0xf5, 0xb6, 0xd3, 0x96, 0xa9), [], "R 448a 58 R 448b 71"); // mos_6502, opcode $60: "60 14 e2" // RTS does idle fetch, idle pop, and idle PC-1 read - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x4147, 0xcb, 0xe7, 0x14, 0x64, 0xa6), [[0x4147, 0x60], [0x4148, 0x14], [0x4149, 0xe2], [0x01cb, 0x98], [0x01cc, 0xdc], [0x01cd, 0x1d], [0x1ddc, 0x5d], [0x1ddd, 0xda]], State(0x1ddd, 0xcd, 0xe7, 0x14, 0x64, 0xa6), [], "R 4147 60 R 4148 14 R 01cb 98 R 01cc dc R 01cd 1d R 1ddc 5d"); // mos_6502, opcode $68: "68 b4 82" // PLA does idle fetch and idle pop - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x3021, 0xa4, 0x32, 0xb2, 0x4a, 0x2e), [[0x3021, 0x68], [0x3022, 0xb4], [0x3023, 0x82], [0x01a4, 0xe0], [0x01a5, 0x36]], State(0x3022, 0xa5, 0x36, 0xb2, 0x4a, 0x2c), [], "R 3021 68 R 3022 b4 R 01a4 e0 R 01a5 36"); // mos_6502, opcode $78: "78 ac 45" - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x3eb3, 0xa0, 0xa2, 0x8c, 0x01, 0xa4), [[0x3eb3, 0x78], [0x3eb4, 0xac], [0x3eb5, 0x45]], State(0x3eb4, 0xa0, 0xa2, 0x8c, 0x01, 0xa4), [], "R 3eb3 78 R 3eb4 ac"); // mos_6502, opcode $91: "91 bb 6e" // STA (zp),y w/ index penalty - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x1c86, 0x95, 0x3f, 0x11, 0x1c, 0x2b), [[0x1c86, 0x91], [0x1c87, 0xbb], [0x1c88, 0x6e], [0x00bb, 0x8d], [0x00bc, 0x59], [0x59a9, 0xd5]], State(0x1c88, 0x95, 0x3f, 0x11, 0x1c, 0x2b), [[0x59a9, 0x3f]], "R 1c86 91 R 1c87 bb R 00bb 8d R 00bc 59 R 59a9 d5 W 59a9 3f"); // mos_6502, opcode $83: "83 1b c1" // SAX (zp,x) - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0xb525, 0x86, 0xbe, 0x60, 0x8a, 0x6c), [[0xb525, 0x83], [0xb526, 0x1b], [0xb527, 0xc1], [0x001b, 0x4c], [0x007b, 0x42], [0x007c, 0x3a]], State(0xb527, 0x86, 0xbe, 0x60, 0x8a, 0x6c), [[0x3a42, 0x20]], "R b525 83 R b526 1b R 001b 4c R 007b 42 R 007c 3a W 3a42 20"); // mos_6502, opcode $87: "87 3c 74" // SAX zp - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0xad56, 0x91, 0xc2, 0xa4, 0x70, 0x6d), [[0xad56, 0x87], [0xad57, 0x3c], [0xad58, 0x74]], State(0xad58, 0x91, 0xc2, 0xa4, 0x70, 0x6d), [[0x003c, 0x80]], "R ad56 87 R ad57 3c W 003c 80"); // mos_6502, opcode $8f: "8f 5f a0" // SAX abs - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x5f95, 0x48, 0xc5, 0x8a, 0xc9, 0x64), [[0x5f95, 0x8f], [0x5f96, 0x5f], [0x5f97, 0xa0], [0x5f98, 0x44]], State(0x5f98, 0x48, 0xc5, 0x8a, 0xc9, 0x64), [[0xa05f, 0x80]], "R 5f95 8f R 5f96 5f R 5f97 a0 W a05f 80"); // mos_6502, opcode $97: "97 8b 67" // SAX zp,y wraps within zero page - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x901d, 0x42, 0xbf, 0x1a, 0xe3, 0xe0), [[0x901d, 0x97], [0x901e, 0x8b], [0x901f, 0x67], [0x008b, 0xc9]], State(0x901f, 0x42, 0xbf, 0x1a, 0xe3, 0xe0), [[0x006e, 0x1a]], "R 901d 97 R 901e 8b R 008b c9 W 006e 1a"); // mos_6502, opcode $a3: "a3 4e d0" // LAX (zp,x) - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0xe472, 0xba, 0x61, 0xe7, 0x4c, 0x2d), [[0xe472, 0xa3], [0xe473, 0x4e], [0xe474, 0xd0], [0x004e, 0x72], [0x0035, 0xe6], [0x0036, 0x20], [0x20e6, 0x8f]], State(0xe474, 0xba, 0x8f, 0x8f, 0x4c, 0xad), [], "R e472 a3 R e473 4e R 004e 72 R 0035 e6 R 0036 20 R 20e6 8f"); // mos_6502, opcode $a7: "a7 f0 99" // LAX zp - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x777f, 0x5b, 0xe3, 0x52, 0xd9, 0xab), [[0x777f, 0xa7], [0x7780, 0xf0], [0x7781, 0x99], [0x00f0, 0x0c]], State(0x7781, 0x5b, 0x0c, 0x0c, 0xd9, 0x29), [], "R 777f a7 R 7780 f0 R 00f0 0c"); // mos_6502, opcode $af: "af 6d 59" // LAX abs - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0xff9a, 0xee, 0x7c, 0x08, 0x7c, 0xe9), [[0xff9a, 0xaf], [0xff9b, 0x6d], [0xff9c, 0x59], [0x596d, 0xe7], [0xff9d, 0x5c]], State(0xff9d, 0xee, 0xe7, 0xe7, 0x7c, 0xe9), [], "R ff9a af R ff9b 6d R ff9c 59 R 596d e7"); // mos_6502, opcode $b3: "b3 eb bb" // LAX (zp),y across page: dummy read at the unfixed address - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x0139, 0x9c, 0x69, 0xf5, 0x81, 0x6c), [[0x0139, 0xb3], [0x013a, 0xeb], [0x013b, 0xbb], [0x00eb, 0xd6], [0x00ec, 0x8c], [0x8c57, 0xd4], [0x8d57, 0xab]], State(0x013b, 0x9c, 0xab, 0xab, 0x81, 0xec), [], "R 0139 b3 R 013a eb R 00eb d6 R 00ec 8c R 8c57 d4 R 8d57 ab"); // mos_6502, opcode $b7: "b7 6b 71" // LAX zp,y - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0xe5a5, 0x68, 0x9f, 0x61, 0x54, 0x64), [[0xe5a5, 0xb7], [0xe5a6, 0x6b], [0xe5a7, 0x71], [0x006b, 0xba], [0x00bf, 0xe1]], State(0xe5a7, 0x68, 0xe1, 0xe1, 0x54, 0xe4), [], "R e5a5 b7 R e5a6 6b R 006b ba R 00bf e1"); // mos_6502, opcode $bf: "bf 1f f7" // LAX abs,y, no page crossing - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0x0b88, 0x64, 0xfa, 0x4f, 0x2c, 0xa5), [[0x0b88, 0xbf], [0x0b89, 0x1f], [0x0b8a, 0xf7], [0xf74b, 0x62], [0x0b8b, 0xa9]], State(0x0b8b, 0x64, 0x62, 0x62, 0x2c, 0x25), [], "R 0b88 bf R 0b89 1f R 0b8a f7 R f74b 62"); // mos_6502, opcode $bf: "bf 54 1c" // LAX abs,y across page - checkInstruction!(CpuVariant.mos_6502)( + checkInstruction!(Cpu.mos_6502)( State(0xa814, 0x1b, 0x7c, 0x4a, 0xdc, 0xa0), [[0xa814, 0xbf], [0xa815, 0x54], [0xa816, 0x1c], [0x1c30, 0x09], [0x1d30, 0xd3], [0xa817, 0x4f]], State(0xa817, 0x1b, 0xd3, 0xd3, 0xdc, 0xa0), [], "R a814 bf R a815 54 R a816 1c R 1c30 09 R 1d30 d3"); // wdc_65c02, opcode $07: "07 28 a5" // NOP absolute - checkInstruction!(CpuVariant.wdc_65c02)( + checkInstruction!(Cpu.wdc_65c02)( State(0x2f7b, 0x32, 0x42, 0x71, 0x5d, 0x2f), [[0x2f7b, 0x07], [0x2f7c, 0x28], [0x2f7d, 0xa5], [0x0028, 0xba]], State(0x2f7d, 0x32, 0x42, 0x71, 0x5d, 0x2f), [], "R 2f7b 07 R 2f7c 28 R 0028 ba"); // wdc_65c02, opcode $0f: "0f 7c f5" // NOP absolute - checkInstruction!(CpuVariant.wdc_65c02)( + checkInstruction!(Cpu.wdc_65c02)( State(0x8da5, 0x9c, 0x88, 0x57, 0x65, 0xe8), [[0x8da5, 0x0f], [0x8da6, 0x7c], [0x8da7, 0xf5], [0x8da8, 0xe1]], State(0x8da8, 0x9c, 0x88, 0x57, 0x65, 0xe8), [], "R 8da5 0f R 8da6 7c R 8da7 f5"); // wdc_65c02, opcode $17: "17 ab f6" // NOP absolute indexed - checkInstruction!(CpuVariant.wdc_65c02)( + checkInstruction!(Cpu.wdc_65c02)( State(0xf0f6, 0xac, 0xf6, 0xd2, 0x9b, 0xa6), [[0xf0f6, 0x17], [0xf0f7, 0xab], [0xf0f8, 0xf6], [0x00ab, 0x03], [0x007d, 0xdb]], State(0xf0f8, 0xac, 0xf6, 0xd2, 0x9b, 0xa6), [], "R f0f6 17 R f0f7 ab R 00ab 03 R 007d db"); // wdc_65c02, opcode $1a: "1a ea 57" // INC @ does idle fetch - checkInstruction!(CpuVariant.wdc_65c02)( + checkInstruction!(Cpu.wdc_65c02)( State(0x1abb, 0x3b, 0x3d, 0x6b, 0x7e, 0xa1), [[0x1abb, 0x1a], [0x1abc, 0xea], [0x1abd, 0x57]], State(0x1abc, 0x3b, 0x3e, 0x6b, 0x7e, 0x21), [], "R 1abb 1a R 1abc ea"); // wdc_65c02, opcode $44: "44 3c d4" // NOP zp - checkInstruction!(CpuVariant.wdc_65c02)( + checkInstruction!(Cpu.wdc_65c02)( State(0xd5c9, 0xa0, 0x1e, 0xdc, 0x6b, 0x22), [[0xd5c9, 0x44], [0xd5ca, 0x3c], [0xd5cb, 0xd4], [0x003c, 0x99]], State(0xd5cb, 0xa0, 0x1e, 0xdc, 0x6b, 0x22), [], "R d5c9 44 R d5ca 3c R 003c 99"); // wdc_65c02, opcode $6c: "6c 42 fc" // JMP (abs) always takes an extra cycle - checkInstruction!(CpuVariant.wdc_65c02)( + checkInstruction!(Cpu.wdc_65c02)( State(0x96ff, 0xc4, 0x96, 0xf7, 0x9b, 0xaf), [[0x96ff, 0x6c], [0x9700, 0x42], [0x9701, 0xfc], [0xfc42, 0xb1], [0xfc43, 0x3f], [0x3fb1, 0x0a]], State(0x3fb1, 0xc4, 0x96, 0xf7, 0x9b, 0xaf), [], "R 96ff 6c R 9700 42 R 9701 fc R fc42 b1 R fc43 3f R fc43 3f"); // wdc_65c02, opcode $5c: "5c 83 a9" // NOP abs,X - checkInstruction!(CpuVariant.wdc_65c02)( + checkInstruction!(Cpu.wdc_65c02)( State(0x7e40, 0x80, 0xdb, 0xc0, 0x1f, 0x6f), [[0x7e40, 0x5c], [0x7e41, 0x83], [0x7e42, 0xa9], [0x7e43, 0xef]], State(0x7e43, 0x80, 0xdb, 0xc0, 0x1f, 0x6f), [], "R 7e40 5c R 7e41 83 R 7e42 a9 R 7e42 a9"); // wdc_65c02, opcode $7a: "7a 24 4c" // PLY does idle fetch and idle pop - checkInstruction!(CpuVariant.wdc_65c02)( + checkInstruction!(Cpu.wdc_65c02)( State(0xeeed, 0x9d, 0x46, 0xf3, 0x49, 0xab), [[0xeeed, 0x7a], [0xeeee, 0x24], [0xeeef, 0x4c], [0x019d, 0x9e], [0x019e, 0x80]], State(0xeeee, 0x9e, 0x46, 0xf3, 0x80, 0xa9), [], "R eeed 7a R eeee 24 R 019d 9e R 019e 80"); // wdc_65c02, opcode $91: "91 c9 f2" // STA (zp),y always does idle read at PC - checkInstruction!(CpuVariant.wdc_65c02)( + checkInstruction!(Cpu.wdc_65c02)( State(0x9d92, 0x6e, 0x1f, 0xf4, 0x4b, 0xef), [[0x9d92, 0x91], [0x9d93, 0xc9], [0x9d94, 0xf2], [0x00c9, 0x80], [0x00ca, 0xd4]], State(0x9d94, 0x6e, 0x1f, 0xf4, 0x4b, 0xef), [[0xd4cb, 0x1f]], "R 9d92 91 R 9d93 c9 R 00c9 80 R 00ca d4 R 9d93 c9 W d4cb 1f"); // wdc_65c02, opcode $7c: "7c 32 9f" // JMP (abs,x) - checkInstruction!(CpuVariant.wdc_65c02)( + checkInstruction!(Cpu.wdc_65c02)( State(0x83c8, 0xf1, 0xe2, 0x6f, 0x8c, 0xe4), [[0x83c8, 0x7c], [0x83c9, 0x32], [0x83ca, 0x9f], [0x9fa1, 0x76], [0x9fa2, 0x2b], [0x2b76, 0x76]], State(0x2b76, 0xf1, 0xe2, 0x6f, 0x8c, 0xe4), [], "R 83c8 7c R 83c9 32 R 83ca 9f R 83c9 32 R 9fa1 76 R 9fa2 2b"); // wdc_65c02, opcode $cb: "cb 4a 20" // NOP imp - checkInstruction!(CpuVariant.wdc_65c02)( + checkInstruction!(Cpu.wdc_65c02)( State(0x7487, 0x15, 0xd2, 0xf9, 0x06, 0x22), [[0x7487, 0xcb], [0x7488, 0x4a], [0x7489, 0x20]], State(0x7488, 0x15, 0xd2, 0xf9, 0x06, 0x22), [], "R 7487 cb R 7488 4a"); // wdc_65c02, opcode $db: "db cf 42" // NOP zp,x - checkInstruction!(CpuVariant.wdc_65c02)( + checkInstruction!(Cpu.wdc_65c02)( State(0x6aeb, 0x6f, 0x61, 0x9e, 0xd5, 0x27), [[0x6aeb, 0xdb], [0x6aec, 0xcf], [0x6aed, 0x42], [0x00cf, 0x8b], [0x006d, 0x85]], State(0x6aed, 0x6f, 0x61, 0x9e, 0xd5, 0x27), [], "R 6aeb db R 6aec cf R 00cf 8b R 006d 85"); // wdc_65c02, opcode $f1: "f1 3" // SBC (zp,x) BCD - checkInstruction!(CpuVariant.wdc_65c02)( + checkInstruction!(Cpu.wdc_65c02)( State(0x73e6, 0x45, 0xf3, 0xfc, 0x7e, 0xe8), [[0x00ff, 0x15], [0x1638, 0x1f], [0x00fe, 0xba], [0x73e7, 0xfe], [0x73e6, 0xf1]], State(0x73e8, 0x45, 0xcd, 0xfc, 0x7e, 0xa9), [], "R 73e6 f1 R 73e7 fe R 00fe ba R 00ff 15 R 73e7 fe R 1638 1f R 1638 1f"); @@ -1650,39 +1635,39 @@ unittest // ADC #imm BCD // SingleStepTests expect a dummy read at $0056, but that looks suspicious // and probably it should be pc+1. TODO: check on real hardware. - checkInstruction!(CpuVariant.wdc_65c02)( + checkInstruction!(Cpu.wdc_65c02)( State(0x1378, 0xaf, 0x73, 0x19, 0xe9, 0x29), [[0x1378, 0x69], [0x1379, 0x47], [0x137a, 0xe1], [0x0056, 0x6d]], State(0x137a, 0xaf, 0x21, 0x19, 0xe9, 0x69), [], "R 1378 69 R 1379 47 R 137a e1"); // wdc_w65c02s, opcode $04: "04 a7 8c" // TSB zp - checkInstruction!(CpuVariant.wdc_w65c02s)( + checkInstruction!(Cpu.wdc_w65c02s)( State(0xe8ee, 0x21, 0xa6, 0xa5, 0xc5, 0xee), [[0xe8ee, 0x04], [0xe8ef, 0xa7], [0xe8f0, 0x8c], [0x00a7, 0x6f]], State(0xe8f0, 0x21, 0xa6, 0xa5, 0xc5, 0xec), [[0x00a7, 0xef]], "R e8ee 04 R e8ef a7 R 00a7 6f R 00a7 6f W 00a7 ef"); // wdc_w65c02s, opcode $07: "07 1f 9e" // RMB0 - checkInstruction!(CpuVariant.wdc_w65c02s)( + checkInstruction!(Cpu.wdc_w65c02s)( State(0x5c3f, 0xbf, 0x34, 0x0c, 0x45, 0xe1), [[0x5c3f, 0x07], [0x5c40, 0x1f], [0x5c41, 0x9e], [0x001f, 0xaa]], State(0x5c41, 0xbf, 0x34, 0x0c, 0x45, 0xe1), [], "R 5c3f 07 R 5c40 1f R 001f aa R 001f aa W 001f aa"); // wdc_w65c02s, opcode $0f: "0f 1" - checkInstruction!(CpuVariant.wdc_w65c02s)( + checkInstruction!(Cpu.wdc_w65c02s)( State(0xfeb7, 0x2f, 0xe9, 0x9d, 0x86, 0x68), [[0xfeba, 0xe9], [0xfeb9, 0x54], [0x0035, 0x3c], [0xfeb8, 0x35], [0xfeb7, 0x0f]], State(0xff0e, 0x2f, 0xe9, 0x9d, 0x86, 0x68), [], "R feb7 0f R feb8 35 R 0035 3c R 0035 3c R feb9 54 R feba e9 R feba e9"); // wdc_w65c02s, opcode $0f: "0f 2" - checkInstruction!(CpuVariant.wdc_w65c02s)( + checkInstruction!(Cpu.wdc_w65c02s)( State(0x40fc, 0x72, 0xd2, 0x47, 0x46, 0x67), [[0x40fe, 0x8a], [0x00af, 0x3d], [0x40fd, 0xaf], [0x40fc, 0x0f]], State(0x40ff, 0x72, 0xd2, 0x47, 0x46, 0x67), [], "R 40fc 0f R 40fd af R 00af 3d R 00af 3d R 40fe 8a"); // wdc_w65c02s, opcode $1f: "1f 1" - checkInstruction!(CpuVariant.wdc_w65c02s)( + checkInstruction!(Cpu.wdc_w65c02s)( State(0x1a2f, 0xdc, 0x03, 0x6a, 0x49, 0xe7), [[0x1a32, 0x2e], [0x1a31, 0x48], [0x0023, 0xdc], [0x1a30, 0x23], [0x1a2f, 0x1f]], State(0x1a7a, 0xdc, 0x03, 0x6a, 0x49, 0xe7), [], "R 1a2f 1f R 1a30 23 R 0023 dc R 0023 dc R 1a31 48 R 1a32 2e"); diff --git a/source/xebin/trace.d b/source/xebin/trace.d index 0fc786d..c1725d8 100644 --- a/source/xebin/trace.d +++ b/source/xebin/trace.d @@ -56,7 +56,7 @@ struct CpuTracer emu.zflag ? "Z" : "-", emu.cflag ? "C" : "-"); ushort addr = emu.pc; - line.put(disassembleOne(emu.ram, addr)); + line.put(disassembleOne(emu.ram, addr, E.cpu)); } /// Opcode and operand fetches: shown as disassembly, don't trace. @@ -103,7 +103,7 @@ unittest debug writeln("unittest CpuTracer"); - auto emu = new Emulator!(CpuVariant.mos_6502, CpuTracer)(); + auto emu = new Emulator!(Cpu.mos_6502, CpuTracer)(); string[] lines; emu.observer.sink = delegate void(const(char)[] l) { lines ~= l.idup; }; @@ -123,3 +123,26 @@ unittest assert(lines[1].canFind("W 0080 42")); assert(!lines[0].canFind("W "), "no access on an immediate load"); } + +unittest +{ + import xebin.emu; + + debug writeln("unittest CpuTracer 65C02"); + + auto emu = new Emulator!(Cpu.wdc_65c02, CpuTracer)(); + string[] lines; + emu.observer.sink = delegate void(const(char)[] l) { lines ~= l.idup; }; + + // stz $80 + emu.ram[0x2000 .. 0x2002] = [ubyte(0x64), 0x80]; + emu.ram[0x0080] = 0xff; + emu.pc = 0x2000; + emu.instructionLimit = 1; + emu.run(); + + assert(lines.length == 1); + // the disassembly follows the emulated variant's instruction set + assert(lines[0].canFind("STZ $80")); + assert(lines[0].canFind("W 0080 00")); +} diff --git a/test/singlestep/source/app.d b/test/singlestep/source/app.d index ed3211e..5d6f75b 100644 --- a/test/singlestep/source/app.d +++ b/test/singlestep/source/app.d @@ -268,7 +268,7 @@ struct Result string[] unittests; } -string emitUnittest(CpuVariant v, ref const TestCase t) +string emitUnittest(Cpu v, ref const TestCase t) { ubyte[ushort] before; foreach (c; t.initialRam) @@ -277,7 +277,7 @@ string emitUnittest(CpuVariant v, ref const TestCase t) return format( "\t// %s, opcode $%02x: \"%s\"\n" ~ - "\tcheckInstruction!(CpuVariant.%s)(\n" ~ + "\tcheckInstruction!(Cpu.%s)(\n" ~ "\t\t%S, %s,\n" ~ "\t\t%S, %s,\n" ~ "\t\t\"%-(%s %)\");\n", @@ -287,7 +287,7 @@ string emitUnittest(CpuVariant v, ref const TestCase t) t.cycles); } -Result runOpcode(CpuVariant v)(string path, ubyte opcode, bool emitUnittests) +Result runOpcode(Cpu v)(string path, ubyte opcode, bool emitUnittests) { Result r = { opcode: opcode }; auto emu = new Emulator!(v, BusTrace)(); @@ -317,17 +317,17 @@ Result runOpcode(CpuVariant v)(string path, ubyte opcode, bool emitUnittests) struct Target { string dir; - CpuVariant cpu; + Cpu cpu; } immutable Target[] targets = [ - Target("6502", CpuVariant.mos_6502), - Target("synertek65c02", CpuVariant.wdc_65c02), - Target("rockwell65c02", CpuVariant.rockwell_r65c02), - Target("wdc65c02", CpuVariant.wdc_w65c02s), + Target("6502", Cpu.mos_6502), + Target("synertek65c02", Cpu.wdc_65c02), + Target("rockwell65c02", Cpu.rockwell_r65c02), + Target("wdc65c02", Cpu.wdc_w65c02s), ]; -size_t runTarget(CpuVariant v)(string dir, const(ubyte)[] opcodes, bool emitUnittests) +size_t runTarget(Cpu v)(string dir, const(ubyte)[] opcodes, bool emitUnittests) { auto results = new Result[opcodes.length]; foreach (i, opcode; opcodes.parallel) @@ -466,7 +466,7 @@ int main(string[] args) stdout.flush(); dispatch: switch (target.cpu) { - static foreach (v; EnumMembers!CpuVariant) + static foreach (v; EnumMembers!Cpu) { case v: failed += runTarget!v(dataDir, opcodes, emitUnittests); From 29804bbefb5009552ad3ae35b02345487a078711 Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Thu, 10 Sep 2026 16:29:04 +0200 Subject: [PATCH 16/17] cli: select cpu for run --- source/app.d | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/source/app.d b/source/app.d index 089a5af..1c6e148 100644 --- a/source/app.d +++ b/source/app.d @@ -247,10 +247,18 @@ private void runEmulator(E)(BinaryBlock[] blocks) void run(string[] args) { auto blocks = BinaryFileReader(InputFiles(args).front).readFile(); - if (tracing(Trace.cpu)) - runEmulator!(Emulator!(Cpu.mos_6502, CpuTracer))(blocks); - else - runEmulator!(Emulator!())(blocks); + final switch (cpu) + { + static foreach (c; EnumMembers!Cpu) + { + case c: + if (tracing(Trace.cpu)) + runEmulator!(Emulator!(c, CpuTracer))(blocks); + else + runEmulator!(Emulator!(c))(blocks); + return; + } + } } void printHelp(string[] args) @@ -268,7 +276,7 @@ void printHelp(string[] args) " i[nsert] [-n=pos] [-a=ad] [-o=fn] [-v] insert block into file\n" ~ " o[ptimize] [-o=fn] [-i] optimize file\n +/ " d[isasm] [-c=cpu] [-o=fn] disassemble blocks\n" ~ - " r[un] [--trace=what] run in a simple emulator\n" ~ + " r[un] [-c=cpu] [--trace=what] run in a simple emulator\n" ~ " p[ack] [-a=ad] [-s] [-o=fn] [-v] pack using FlashPack algorithm\n" ~ " u[npack] [-o=fn] [-v] unpack FlashPack'd file\n" ~ " h[elp] print this message\n" ~ @@ -279,8 +287,8 @@ void printHelp(string[] args) " (use '-' for end address)\n" ~ " -n|--position=pos which block to extract (indexed from 0)\n" ~ " -r|--raw remove header from extracted block\n" ~ - " -c|--cpu=type disassemble for the given CPU: 6502 (default),\n" ~ - " 65c02, r65c02 or w65c02s\n" ~ + " -c|--cpu=type disassemble or run for the given CPU: 6502\n" ~ + " (default), 65c02, r65c02 or w65c02s\n" ~ // " -i|--ignore-order do not preserve order of block if it makes\n" ~ // " optimized file shorter\n" ~ // " -n|--position=pos set block position for extract, delete, insert\n" ~ From 7a811cf73996df72f6ea3bdd5256557ab2b06328 Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Thu, 10 Sep 2026 16:36:06 +0200 Subject: [PATCH 17/17] update README --- README.asciidoc | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.asciidoc b/README.asciidoc index 11ec258..b0fe6fd 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -164,17 +164,20 @@ Running binary file ~~~~~~~~~~~~~~~~~~~ ---------------------------- -$ xebin r[un] [--trace=what] [input_file] +$ xebin r[un] [-c=cpu] [--trace=what] [input_file] ---------------------------- The file is loaded into memory of a simple built-in emulator and run: each +INIT+ routine is called as soon as the block setting it is loaded, and the +RUN+ routine is called once the whole file is loaded. Only the first input file is used. -Only the CPU (NMOS 6502) and a few OS entry points are emulated. There is neither OS +Only the CPU and a few OS entry points are emulated. There is neither OS ROM nor any hardware, so this is useful for programs that compute and do character I/O, and not for anything that touches ANTIC, POKEY or GTIA. +Option +-c=cpu+ or +--cpu=cpu+ selects the emulated CPU, with the same choices as +for +disasm+: +6502+ (NMOS, the default), +65c02+, +r65c02+ or +w65c02s+. + +CIOV+ ($E456) is served by the host: - IOCB #0 is the console: get record reads a line from +stdin+, put record and put @@ -270,7 +273,7 @@ unreleased:: Added +run+ command, which loads and executes a binary file in a built-in emulator, with OS calls served by the host. The emulator implements all 65x02 variants (NMOS 6502, 65C02, Rockwell R65C02 and W65C02S) -with cycle-exact timing; the +run+ command uses the NMOS 6502. +with cycle-exact timing. Added +--trace+ option, which reports what the +run+ command loads, executes and asks CIO for. Disassembler now creates labels for addresses referenced inside the disassembled code.