diff --git a/EightBitNet.sln b/EightBitNet.sln index 39b0494..8ee3e74 100644 --- a/EightBitNet.sln +++ b/EightBitNet.sln @@ -41,6 +41,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "M6502.Symbols", "M6502\M650 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SM83.HarteTest", "LR35902\SM83.HarteTest\SM83.HarteTest.csproj", "{9A85562F-986F-472B-AEAE-AAAFF0B02B48}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Z80.HarteTest", "Z80\Z80.HarteTest\Z80.HarteTest.csproj", "{4238F9DF-E58B-456D-86B4-A92381BC471D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -123,6 +125,10 @@ Global {9A85562F-986F-472B-AEAE-AAAFF0B02B48}.Debug|Any CPU.Build.0 = Debug|Any CPU {9A85562F-986F-472B-AEAE-AAAFF0B02B48}.Release|Any CPU.ActiveCfg = Release|Any CPU {9A85562F-986F-472B-AEAE-AAAFF0B02B48}.Release|Any CPU.Build.0 = Release|Any CPU + {4238F9DF-E58B-456D-86B4-A92381BC471D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4238F9DF-E58B-456D-86B4-A92381BC471D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4238F9DF-E58B-456D-86B4-A92381BC471D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4238F9DF-E58B-456D-86B4-A92381BC471D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Z80/Disassembler.cs b/Z80/Disassembler.cs index 4fd40f1..8ae9e86 100644 --- a/Z80/Disassembler.cs +++ b/Z80/Disassembler.cs @@ -102,7 +102,7 @@ namespace Z80 _ => throw new ArgumentOutOfRangeException(nameof(which)), }; - private string Disassemble(Z80 cpu, ushort pc) + public string Disassemble(Z80 cpu, ushort pc) { var opCode = this.Bus.Peek(pc); diff --git a/Z80/Z80.HarteTest/Checker.cs b/Z80/Z80.HarteTest/Checker.cs new file mode 100644 index 0000000..20738cb --- /dev/null +++ b/Z80/Z80.HarteTest/Checker.cs @@ -0,0 +1,436 @@ +namespace Z80.HarteTest +{ + using EightBit; + using System.Collections.Generic; + using System.Diagnostics; + + internal sealed class Checker + { + private TestRunner Runner { get; } + + private Disassembler Disassembler { get; } + + private bool CycleCountMismatch { get; set; } + + public int Cycles { get; private set; } + + public bool Valid { get; private set; } + + public bool Invalid => !this.Valid; + + public bool Unimplemented => this.Invalid && this.CycleCountMismatch && (this.Cycles == 1); + + public bool Implemented => !this.Unimplemented; + + public List Messages { get; } = []; + + private List ActualCycles { get; } = []; + + public Checker(TestRunner runner) + { + this.Runner = runner; + this.Disassembler = new(this.Runner); + } + + public void Check(Test test) + { + var cpu = this.Runner.CPU; + + this.Reset(); + + this.Runner.RaisePOWER(); + this.InitialiseState(test); + var pc = cpu.PC.Word; + + this.Cycles = cpu.Step(); + this.Runner.LowerPOWER(); + + this.Valid = this.CheckState(test); + + if (this.Unimplemented) + { + this.Messages.Add("Unimplemented"); + return; + } + + Debug.Assert(this.Implemented); + if (this.Invalid) + { + this.AddDisassembly(pc); + + var final = test.Final ?? throw new InvalidOperationException("Final test state cannot be null"); + + this.Raise("PC", final.PC, cpu.PC); + this.Raise("SP", final.SP, cpu.SP); + + this.Raise("A", final.A, cpu.A); + this.Raise("F", final.F, cpu.F); + this.Raise("B", final.B, cpu.B); + this.Raise("C", final.C, cpu.C); + this.Raise("D", final.D, cpu.D); + this.Raise("E", final.E, cpu.E); + this.Raise("H", final.H, cpu.H); + this.Raise("L", final.L, cpu.L); + + cpu.Exx(); + cpu.ExxAF(); + + this.Raise("'AF", final.AF_, cpu.AF); + this.Raise("'BC", final.BC_, cpu.BC); + this.Raise("'DE", final.DE_, cpu.DE); + this.Raise("'HL", final.HL_, cpu.HL); + + this.Raise("I", final.I, cpu.IV); + this.Raise("R", final.R, cpu.REFRESH); + + this.Raise("IM", final.IM, (ushort)cpu.IM); + + this.Raise("IFF1", final.IFF1, cpu.IFF1); + this.Raise("IFF2", final.IFF2, cpu.IFF2); + + this.Raise("WZ", final.WZ, cpu.MEMPTR); + + this.Raise("IX", final.IX, cpu.IX); + this.Raise("IY", final.IY, cpu.IY); + + if (test.Cycles is null) + { + throw new InvalidOperationException("test cycles cannot be null"); + } + + this.Messages.Add($"Stepped cycles: {this.Cycles}, expected events: {test.Cycles.Count}, actual events: {this.ActualCycles.Count}"); + + this.DumpCycles(test.AvailableCycles(), this.ActualCycles); + } + } + + private void Reset() + { + this.Messages.Clear(); + this.ActualCycles.Clear(); + + this.CycleCountMismatch = false; + this.Cycles = 0; + this.Valid = false; + } + + private bool Check(string what, ushort expected, Register16 actual) => this.Check(what, expected, actual.Word); + + private bool Check(string what, ushort expected, ushort actual) + { + var success = actual == expected; + if (!success) + { + this.Raise(what, expected, actual); + } + return success; + } + + private bool Check(string what, byte expected, bool actual) => this.Check(what, expected, (byte)(actual ? 1 : 0)); + + private bool Check(string what, byte expected, byte actual) + { + var success = actual == expected; + if (!success) + { + this.Raise(what, expected, actual); + } + return success; + } + + private bool Check(string what, string? expected, string? actual) + { + ArgumentNullException.ThrowIfNull(expected); + ArgumentNullException.ThrowIfNull(actual); + var success = actual == expected; + if (!success) + { + this.Raise(what, expected, actual); + } + return success; + } + + private bool Check(string what, ushort address, byte expected, byte actual) + { + var success = actual == expected; + if (!success) + { + this.Raise($"{what}: {address}", expected, actual); + } + return success; + } + + private void AddDisassembly(ushort address) + { + string message; + try + { + message = this.Disassemble(address); + } + catch (InvalidOperationException error) + { + message = $"Disassembly problem: {error.Message}"; + } + + this.Messages.Add(message); + } + + private string Disassemble(ushort address) => this.Disassembler.Disassemble(this.Runner.CPU, address); + + private bool CheckState(Test test) + { + var runner = this.Runner; + var cpu = runner.CPU; + + var expectedCycles = test.AvailableCycles(); + var actualCycles = this.ActualCycles; + + var actualIDX = 0; + foreach (var expectedCycle in expectedCycles) + { + if (actualIDX >= actualCycles.Count) + { + this.CycleCountMismatch = true; + return false; // more expected cycles than actual + } + + var actualCycle = actualCycles[actualIDX++]; + + var interestingCycleData = expectedCycle.Value is not null; + if (interestingCycleData) + { + var expectedAddress = expectedCycle.Address; + var actualAddress = actualCycle.Address; + _ = this.Check("Cycle address", expectedAddress, actualAddress); + + var expectedValue = (byte)expectedCycle.Value; + var actualValue = (byte)actualCycle.Value; + _ = this.Check("Cycle value", expectedValue, actualValue); + + var expectedAction = expectedCycle.Type; + var actualAction = actualCycle.Type; + _ = this.Check("Cycle action", expectedAction, actualAction); + } + } + + if (actualIDX < actualCycles.Count) + { + this.CycleCountMismatch = true; + return false; // less expected cycles than actual + } + + if (this.Messages.Count > 0) + { + return false; + } + + var final = test.Final ?? throw new InvalidOperationException("Final state cannot be null"); + var pc_good = this.Check("PC", final.PC, cpu.PC); + var sp_good = this.Check("SP", final.SP, cpu.SP); + + var a_good = this.Check("A", final.A, cpu.A); + var f_good = this.Check("F", final.F, cpu.F); + var b_good = this.Check("B", final.B, cpu.B); + var c_good = this.Check("C", final.C, cpu.C); + var d_good = this.Check("D", final.D, cpu.D); + var e_good = this.Check("E", final.E, cpu.E); + var h_good = this.Check("H", final.H, cpu.H); + var l_good = this.Check("L", final.L, cpu.L); + + cpu.Exx(); + cpu.ExxAF(); + + var af_a_good = this.Check("'AF", final.AF_, cpu.AF); + var bc_a_good = this.Check("'BC", final.BC_, cpu.BC); + var de_a_good = this.Check("'DE", final.DE_, cpu.DE); + var hl_a_good = this.Check("'HL", final.HL_, cpu.HL); + + var i_good = this.Check("I", final.I, cpu.IV); + var r_good = this.Check("R", final.R, cpu.REFRESH); + + var im_good = this.Check("IM", final.IM, (byte)cpu.IM); + + var iff1_good = this.Check("IFF1", final.IFF1, cpu.IFF1); + var iff2_good = this.Check("IFF2", final.IFF2, cpu.IFF2); + + var wz_good = this.Check("WZ", final.WZ, cpu.MEMPTR); + + var ix_good = this.Check("IX", final.IX, cpu.IX); + var iy_good = this.Check("IY", final.IY, cpu.IY); + + if (!f_good) + { + this.Messages.Add($"Expected flags: {Disassembler.AsFlags(final.F)}"); + this.Messages.Add($"Actual flags : {Disassembler.AsFlags(cpu.F)}"); + } + + if (final.RAM is null) + { + throw new InvalidOperationException("Expected RAM cannot be null"); + } + + var ramProblem = false; + foreach (var entry in final.RAM) + { + if (entry.Length != 2) + { + throw new InvalidOperationException("RAM entry length must be 2"); + } + + var address = (ushort)entry[0]; + var value = (byte)entry[1]; + + var ramGood = this.Check("RAM", address, value, runner.Peek(address)); + if (!ramGood && !ramProblem) + { + ramProblem = true; + } + } + + return + pc_good && sp_good + && a_good && f_good + && b_good && c_good + && d_good && e_good + && h_good && l_good + && af_a_good + && bc_a_good + && de_a_good + && hl_a_good + && i_good && r_good + && im_good + && iff1_good && iff2_good + && wz_good + && ix_good && iy_good; + } + + private void Raise(string what, byte expected, bool actual) => this.Raise(what, expected, (byte)(actual ? 1 : 0)); + + private void Raise(string what, byte expected, byte actual) => this.Messages.Add($"{what}: expected: {expected:X2}, actual: {actual:X2}"); + + private void Raise(string what, ushort expected, Register16 actual) => this.Raise(what, expected, actual.Word); + + private void Raise(string what, ushort expected, ushort actual) => this.Messages.Add($"{what}: expected: {expected:X4}, actual: {actual:X4}"); + + private void Raise(string what, string expected, string actual) => this.Messages.Add($"{what}: expected: {expected}, actual: {actual}"); + + public void Initialise() + { + this.Runner.CPU.Ticked += this.CPU_Ticked; + } + + private void CPU_Ticked(object? sender, EventArgs e) + { + var read = this.Runner.CPU.RD == EightBit.PinLevel.Low ? "r" : "-"; + var write = this.Runner.CPU.WR == EightBit.PinLevel.Low ? "w" : "-"; + var memory = this.Runner.CPU.MREQ == EightBit.PinLevel.Low ? "m" : "-"; + var io = this.Runner.CPU.IORQ == EightBit.PinLevel.Low ? "i" : "-"; + this.AddActualCycle(this.Runner.Address, this.Runner.Data, $"{read}{write}{memory}{io}"); + } + + private void InitialiseState(Test test) + { + var initial = test.Initial ?? throw new InvalidOperationException("Test cannot have an invalid initial state"); + this.InitialiseState(initial); + } + + private void InitialiseState(State state) + { + var runner = this.Runner; + var cpu = runner.CPU; + + cpu.PC.Word = state.PC; + cpu.SP.Word = state.SP; + + cpu.AF.Word = state.AF_; + cpu.BC.Word = state.BC_; + cpu.DE.Word = state.DE_; + cpu.HL.Word = state.HL_; + + cpu.Exx(); + cpu.ExxAF(); + + cpu.A = state.A; + cpu.F = state.F; + + cpu.B = state.B; + cpu.C = state.C; + + cpu.D = state.D; + cpu.E = state.E; + + cpu.H = state.H; + cpu.L = state.L; + + cpu.IV = state.I; + cpu.REFRESH = state.R; + + cpu.IM = state.IM; + + cpu.IFF1 = state.IFF1 != 0; + cpu.IFF2 = state.IFF2 != 0; + + cpu.MEMPTR.Word = state.WZ; + + cpu.IX.Word = state.IX; + cpu.IY.Word = state.IY; + + var initialRAM = state.RAM ?? throw new InvalidOperationException("Initial test state cannot have invalid RAM"); + foreach (var entry in initialRAM) + { + var count = entry.Length; + if (count != 2) + { + throw new InvalidOperationException("RAM entry length must be 2"); + } + + var address = (ushort)entry[0]; + var value = (byte)entry[1]; + runner.Poke(address, value); + } + } + + private void AddActualCycle(EightBit.Register16 address, byte value, string action) => this.AddActualCycle(address.Word, value, action); + + private void AddActualCycle(ushort address, byte value, string action) => this.ActualCycles.Add(new Cycle(address, value, action)); + + private string ExpandCycle(string prefix, ushort address, byte? value, string? action) + { + ArgumentNullException.ThrowIfNull(action); + return value is null + ? $"{prefix}: Address: {address:X4}, action: {action}" + : $"{prefix}: Address: {address:X4}, value: {value:X2}, action: {action}"; + } + + private string ExpandCycle(string prefix, Cycle cycle) => this.ExpandCycle(prefix, cycle.Address, cycle.Value, cycle.Type); + + private void DumpCycles(IEnumerable expected, IEnumerable actual) + { + List expectedCycles = [.. expected]; + List actualCycles = [.. actual]; + + var until = Math.Max(expectedCycles.Count, actualCycles.Count); + for (var i = 0; i < until; i++) + { + var expectedCycle = i < expectedCycles.Count ? expectedCycles[i] : null; + var actualCycle = i < actualCycles.Count ? actualCycles[i] : null; + var message = ""; + if (expectedCycle is not null) + { + message += this.ExpandCycle("Expected", expectedCycle); + message += " "; + } + if (actualCycle is not null) + { + if ((expectedCycle is not null) && (expectedCycle.Value is null)) + { + actualCycle.Value = null; + } + message += this.ExpandCycle("Actual ", actualCycle); + } + Debug.Assert(!string.IsNullOrEmpty(message), "Message should not be empty"); + this.Messages.Add(message); + } + } + } +} diff --git a/Z80/Z80.HarteTest/Cycle.cs b/Z80/Z80.HarteTest/Cycle.cs new file mode 100644 index 0000000..23c8638 --- /dev/null +++ b/Z80/Z80.HarteTest/Cycle.cs @@ -0,0 +1,45 @@ +namespace Z80.HarteTest +{ + using System.Diagnostics; + + // Cycle-by-cycle breakdown of bus activity + [DebuggerDisplay("Cycle = Address:{Address}, Value:{Value}, Type:{Type}")] + internal sealed class Cycle + { + public ushort Address { get; set; } + + public byte? Value { get; set; } + + // Type is a combination of "r(ead)", "w(rite)", "(m)emory" or "(i)o" + public string Type { get; set; } + + public Cycle(ushort address, byte? value, string type) + { + this.Address = address; + this.Value = value; + this.Type = type; + } + + public Cycle(List input) + { + ArgumentNullException.ThrowIfNull(input); + + this.Type = string.Empty; + + if (input.Count != 3) + { + throw new ArgumentOutOfRangeException(nameof(input), input, "Cycles can only have three elements"); + } + + this.Address = AsElement(input[0]).GetUInt16(); + if (input[1] is not null) + { + this.Value = AsElement(input[1]).GetByte(); + } + + this.Type = AsElement(input[2]).GetString(); + } + + private static System.Text.Json.JsonElement AsElement(object part) => (System.Text.Json.JsonElement)part; + } +} diff --git a/Z80/Z80.HarteTest/OpcodeTestSuite.cs b/Z80/Z80.HarteTest/OpcodeTestSuite.cs new file mode 100644 index 0000000..b742108 --- /dev/null +++ b/Z80/Z80.HarteTest/OpcodeTestSuite.cs @@ -0,0 +1,41 @@ +namespace Z80.HarteTest +{ + using System.Runtime.CompilerServices; + using System.Text.Json; + using System.Text.Json.Serialization; + + internal sealed class OpcodeTestSuite(string path) : IDisposable + { + private static readonly JsonSerializerOptions SerializerOptions = new() + { + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + }; + private bool _disposed; + + public string Path { get; } = path; + + private readonly FileStream _stream = File.Open(path, FileMode.Open); + + public ConfiguredCancelableAsyncEnumerable TestsAsync => JsonSerializer.DeserializeAsyncEnumerable(this._stream, SerializerOptions).ConfigureAwait(false); + + private void Dispose(bool disposing) + { + if (!this._disposed) + { + if (disposing) + { + this._stream.Dispose(); + } + + this._disposed = true; + } + } + + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Z80/Z80.HarteTest/Port.cs b/Z80/Z80.HarteTest/Port.cs new file mode 100644 index 0000000..67d0d1a --- /dev/null +++ b/Z80/Z80.HarteTest/Port.cs @@ -0,0 +1,38 @@ +namespace Z80.HarteTest +{ + // Cycle-by-cycle breakdown of bus activity + internal sealed class Port + { + public ushort Address { get; set; } + + public byte Value { get; set; } + + // Type can be one of "r(ead)" or "w(rite)" + public string Type { get; set; } + + public Port(ushort address, byte value, string type) + { + this.Address = address; + this.Value = value; + this.Type = type; + } + + public Port(List input) + { + ArgumentNullException.ThrowIfNull(input); + + this.Type = string.Empty; + + if (input.Count != 3) + { + throw new ArgumentOutOfRangeException(nameof(input), input, "Ports can only have three elements"); + } + + this.Address = AsElement(input[0]).GetUInt16(); + this.Value = AsElement(input[1]).GetByte(); + this.Type = AsElement(input[2]).GetString(); + } + + private static System.Text.Json.JsonElement AsElement(object part) => (System.Text.Json.JsonElement)part; + } +} diff --git a/Z80/Z80.HarteTest/ProcessorTestSuite.cs b/Z80/Z80.HarteTest/ProcessorTestSuite.cs new file mode 100644 index 0000000..060a204 --- /dev/null +++ b/Z80/Z80.HarteTest/ProcessorTestSuite.cs @@ -0,0 +1,19 @@ +namespace Z80.HarteTest +{ + internal sealed class ProcessorTestSuite(string location) + { + public string Location { get; set; } = location; + + public IEnumerable OpcodeTests() + { + foreach (var filename in Directory.EnumerateFiles(this.Location, "*.json")) + { + var fileInformation = new FileInfo(filename); + if (fileInformation.Length > 0) + { + yield return new OpcodeTestSuite(filename); + } + } + } + } +} diff --git a/Z80/Z80.HarteTest/Program.cs b/Z80/Z80.HarteTest/Program.cs new file mode 100644 index 0000000..0e77e5b --- /dev/null +++ b/Z80/Z80.HarteTest/Program.cs @@ -0,0 +1,87 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// + +namespace Z80.HarteTest +{ + public static class Program + { + public static async Task Main(string[] _) + { + var directory = @"C:\github\spectrum\libraries\EightBit\modules\z80\v1"; + + await ProcessTestSuiteAsync(directory).ConfigureAwait(false); + } + + private static async Task ProcessTestSuiteAsync(string directory) + { + var startTime = DateTime.Now; + + var unimplemented_opcode_count = 0; + var invalid_opcode_count = 0; + + var runner = new TestRunner(); + runner.Initialize(); + + var checker = new Checker(runner); + checker.Initialise(); + + var testSuite = new ProcessorTestSuite(directory); + foreach (var opcode in testSuite.OpcodeTests()) + { + Console.WriteLine($"Processing: {Path.GetFileName(opcode.Path)}"); + + List testNames = []; + var tests = opcode.TestsAsync; + await foreach (var test in tests) + { + if (test is null) + { + throw new InvalidOperationException("Test cannot be null"); + } + + checker.Check(test); + if (checker.Invalid) + { + ++invalid_opcode_count; + + // Was it just unimplemented? + if (checker.Unimplemented) + { + ++unimplemented_opcode_count; + } + + // Let's see if we had any successes! + if (testNames.Count > 0) + { + Console.WriteLine("**** The follow test variations succeeded"); + foreach (var testName in testNames) + { + Console.WriteLine($"****** {testName}"); + } + } + + // OK, we've attempted an implementation, how did it fail? + foreach (var message in checker.Messages) + { + Console.WriteLine($"**** {message}"); + } + + // I'm not really interested in the remaining tests for this opcode + break; + } + + testNames.Add(test.Name); + } + } + + var finishTime = DateTime.Now; + var elapsedTime = finishTime - startTime; + + Console.Write($"Elapsed time: {elapsedTime.TotalSeconds} seconds"); + Console.Write($", unimplemented opcode count: {unimplemented_opcode_count}"); + Console.Write($", invalid opcode count: {invalid_opcode_count - unimplemented_opcode_count}"); + Console.WriteLine(); + } + } +} \ No newline at end of file diff --git a/Z80/Z80.HarteTest/State.cs b/Z80/Z80.HarteTest/State.cs new file mode 100644 index 0000000..82a75bd --- /dev/null +++ b/Z80/Z80.HarteTest/State.cs @@ -0,0 +1,55 @@ +namespace Z80.HarteTest +{ + internal sealed class State + { + public ushort PC { get; set; } + + public ushort SP { get; set; } + + // A, B, C, etc. are stored as 8-bit values + + public byte A { get; set; } + public byte F { get; set; } + + public byte B { get; set; } + public byte C { get; set; } + + public byte D { get; set; } + public byte E { get; set; } + + public byte H { get; set; } + public byte L { get; set; } + + // af_ etc. are the "shadow registers" + + public ushort AF_ { get; set; } + public ushort BC_ { get; set; } + public ushort DE_ { get; set; } + public ushort HL_ { get; set; } + + public byte I { get; set; } + public byte R { get; set; } + + public byte IM { get; set; } + + // EI refers to if Enable Interrupt was the last-emulated instruction. You can probably ignore this. + public byte EI { get; set; } + + // Used to track specific behavior during interrupt depending on if CMOS or not and previously-executed instructions. You can probably ignore this. + public int P { get; set; } + + // Used to track if the last-modified opcode modified flag registers (with a few exceptions). This is important because CCF will behave differently depending on this + public int Q { get; set; } + + public byte IFF1 { get; set; } + public byte IFF2 { get; set; } + + public ushort WZ { get; set; } + + public ushort IX { get; set; } + public ushort IY { get; set; } + + // Address, value pairs to initialize RAM + public int[][]? RAM { get; set; } + } +} diff --git a/Z80/Z80.HarteTest/Test.cs b/Z80/Z80.HarteTest/Test.cs new file mode 100644 index 0000000..5016881 --- /dev/null +++ b/Z80/Z80.HarteTest/Test.cs @@ -0,0 +1,41 @@ +namespace Z80.HarteTest +{ + internal sealed class Test + { + public string? Name { get; set; } + + public State? Initial { get; set; } + + public State? Final { get; set; } + + public List>? Cycles { get; set; } + + public List>? Ports { get; set; } + + public IEnumerable AvailableCycles() + { + if (this.Cycles is null) + { + throw new InvalidOperationException("Cycles have not been initialised"); + } + + foreach (var cycle in this.Cycles) + { + yield return new Cycle(cycle); + } + } + + public IEnumerable AvailablePorts() + { + if (this.Ports is null) + { + throw new InvalidOperationException("Ports have not been initialised"); + } + + foreach (var port in this.Ports) + { + yield return new Port(port); + } + } + } +} diff --git a/Z80/Z80.HarteTest/TestRunner.cs b/Z80/Z80.HarteTest/TestRunner.cs new file mode 100644 index 0000000..e568f08 --- /dev/null +++ b/Z80/Z80.HarteTest/TestRunner.cs @@ -0,0 +1,43 @@ +namespace Z80.HarteTest +{ + using EightBit; + + internal sealed class TestRunner : Bus + { + private readonly MemoryMapping _mapping; + + private readonly InputOutput ports = new(); + + public Ram RAM { get; } = new(0x10000); + public Z80 CPU { get; } + + public TestRunner() + { + this.CPU = new(this, this.ports); + this._mapping = new(this.RAM, 0x0000, (ushort)Mask.Sixteen, AccessLevel.ReadWrite); + } + + public override MemoryMapping Mapping(ushort _) => this._mapping; + + public override void Initialize() + { + } + + public override void LowerPOWER() + { + this.CPU.LowerPOWER(); + base.LowerPOWER(); + } + + + public override void RaisePOWER() + { + base.RaisePOWER(); + this.CPU.RaisePOWER(); + this.CPU.RaiseRESET(); + this.CPU.RaiseINT(); + this.CPU.RaiseHALT(); + this.CPU.RaiseNMI(); + } + } +} diff --git a/Z80/Z80.HarteTest/Z80.HarteTest.csproj b/Z80/Z80.HarteTest/Z80.HarteTest.csproj new file mode 100644 index 0000000..46bea42 --- /dev/null +++ b/Z80/Z80.HarteTest/Z80.HarteTest.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + diff --git a/Z80/Z80.csproj b/Z80/Z80.csproj index a6bed09..5dfccc6 100644 --- a/Z80/Z80.csproj +++ b/Z80/Z80.csproj @@ -20,10 +20,13 @@ + + +