mirror of
https://github.com/MoleskiCoder/EightBitNet.git
synced 2026-03-11 05:41:49 +00:00
Add single stepping Z80 testing code
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
436
Z80/Z80.HarteTest/Checker.cs
Normal file
436
Z80/Z80.HarteTest/Checker.cs
Normal file
@@ -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<string> Messages { get; } = [];
|
||||
|
||||
private List<Cycle> 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<Cycle> expected, IEnumerable<Cycle> actual)
|
||||
{
|
||||
List<Cycle> expectedCycles = [.. expected];
|
||||
List<Cycle> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Z80/Z80.HarteTest/Cycle.cs
Normal file
45
Z80/Z80.HarteTest/Cycle.cs
Normal file
@@ -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<object> 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;
|
||||
}
|
||||
}
|
||||
41
Z80/Z80.HarteTest/OpcodeTestSuite.cs
Normal file
41
Z80/Z80.HarteTest/OpcodeTestSuite.cs
Normal file
@@ -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<Test?> TestsAsync => JsonSerializer.DeserializeAsyncEnumerable<Test>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Z80/Z80.HarteTest/Port.cs
Normal file
38
Z80/Z80.HarteTest/Port.cs
Normal file
@@ -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<object> 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;
|
||||
}
|
||||
}
|
||||
19
Z80/Z80.HarteTest/ProcessorTestSuite.cs
Normal file
19
Z80/Z80.HarteTest/ProcessorTestSuite.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Z80.HarteTest
|
||||
{
|
||||
internal sealed class ProcessorTestSuite(string location)
|
||||
{
|
||||
public string Location { get; set; } = location;
|
||||
|
||||
public IEnumerable<OpcodeTestSuite> OpcodeTests()
|
||||
{
|
||||
foreach (var filename in Directory.EnumerateFiles(this.Location, "*.json"))
|
||||
{
|
||||
var fileInformation = new FileInfo(filename);
|
||||
if (fileInformation.Length > 0)
|
||||
{
|
||||
yield return new OpcodeTestSuite(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
Z80/Z80.HarteTest/Program.cs
Normal file
87
Z80/Z80.HarteTest/Program.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
// <copyright file="Program.cs" company="Adrian Conlon">
|
||||
// Copyright (c) Adrian Conlon. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
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<string?> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
55
Z80/Z80.HarteTest/State.cs
Normal file
55
Z80/Z80.HarteTest/State.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
41
Z80/Z80.HarteTest/Test.cs
Normal file
41
Z80/Z80.HarteTest/Test.cs
Normal file
@@ -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<List<object>>? Cycles { get; set; }
|
||||
|
||||
public List<List<object>>? Ports { get; set; }
|
||||
|
||||
public IEnumerable<Cycle> 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<Port> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Z80/Z80.HarteTest/TestRunner.cs
Normal file
43
Z80/Z80.HarteTest/TestRunner.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Z80/Z80.HarteTest/Z80.HarteTest.csproj
Normal file
15
Z80/Z80.HarteTest/Z80.HarteTest.csproj
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\EightBit\EightBit.csproj" />
|
||||
<ProjectReference Include="..\Z80.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -20,10 +20,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Z80.FuseTest\**" />
|
||||
<Compile Remove="Z80.HarteTest\**" />
|
||||
<Compile Remove="Z80.Test\**" />
|
||||
<EmbeddedResource Remove="Z80.FuseTest\**" />
|
||||
<EmbeddedResource Remove="Z80.HarteTest\**" />
|
||||
<EmbeddedResource Remove="Z80.Test\**" />
|
||||
<None Remove="Z80.FuseTest\**" />
|
||||
<None Remove="Z80.HarteTest\**" />
|
||||
<None Remove="Z80.Test\**" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user