From e9603b0e081b70ca2abbcb8659a8915c9352c776 Mon Sep 17 00:00:00 2001 From: Adrian Conlon Date: Sun, 17 May 2026 20:48:22 +0100 Subject: [PATCH] Add Z80 interrupt and pin tests --- EightBitNet.sln | 10 +- Z80/Z80.UnitTests/Board.cs | 43 ++++++++ Z80/Z80.UnitTests/HaltTests.cs | 88 +++++++++++++++ Z80/Z80.UnitTests/IffTests.cs | 116 +++++++++++++++++++ Z80/Z80.UnitTests/IntTests.cs | 147 +++++++++++++++++++++++++ Z80/Z80.UnitTests/NmiTests.cs | 110 ++++++++++++++++++ Z80/Z80.UnitTests/ResetTests.cs | 86 +++++++++++++++ Z80/Z80.UnitTests/Z80.UnitTests.csproj | 43 ++++++++ Z80/Z80.csproj | 3 + 9 files changed, 644 insertions(+), 2 deletions(-) create mode 100644 Z80/Z80.UnitTests/Board.cs create mode 100644 Z80/Z80.UnitTests/HaltTests.cs create mode 100644 Z80/Z80.UnitTests/IffTests.cs create mode 100644 Z80/Z80.UnitTests/IntTests.cs create mode 100644 Z80/Z80.UnitTests/NmiTests.cs create mode 100644 Z80/Z80.UnitTests/ResetTests.cs create mode 100644 Z80/Z80.UnitTests/Z80.UnitTests.csproj diff --git a/EightBitNet.sln b/EightBitNet.sln index 2b854bd..defccd0 100644 --- a/EightBitNet.sln +++ b/EightBitNet.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.9.34902.65 +# Visual Studio Version 18 +VisualStudioVersion = 18.5.11801.241 oobstable MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EightBit", "EightBit\EightBit.csproj", "{B9DB62BA-F1C6-41B4-BE0F-85A1D4E64A33}" EndProject @@ -45,6 +45,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Z80.HarteTest", "Z80\Z80.Ha EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EightBit.UnitTest", "EightBit\EightBit.UnitTest\EightBit.UnitTest.csproj", "{A8B3D819-0E84-4586-ADB1-D57AB1A8165B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Z80.UnitTests", "Z80\Z80.UnitTests\Z80.UnitTests.csproj", "{DB313D5B-9DC3-4844-9916-925678967112}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -135,6 +137,10 @@ Global {A8B3D819-0E84-4586-ADB1-D57AB1A8165B}.Debug|Any CPU.Build.0 = Debug|Any CPU {A8B3D819-0E84-4586-ADB1-D57AB1A8165B}.Release|Any CPU.ActiveCfg = Release|Any CPU {A8B3D819-0E84-4586-ADB1-D57AB1A8165B}.Release|Any CPU.Build.0 = Release|Any CPU + {DB313D5B-9DC3-4844-9916-925678967112}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB313D5B-9DC3-4844-9916-925678967112}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB313D5B-9DC3-4844-9916-925678967112}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB313D5B-9DC3-4844-9916-925678967112}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Z80/Z80.UnitTests/Board.cs b/Z80/Z80.UnitTests/Board.cs new file mode 100644 index 0000000..3d3340a --- /dev/null +++ b/Z80/Z80.UnitTests/Board.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace Z80.UnitTests +{ + using EightBit; + + public sealed class Board : Bus + { + private readonly Ram ram = new(0x10000); + private readonly InputOutput ports = new(); + private readonly MemoryMapping mapping; + + public Board() + { + this.CPU = new(this, this.ports); + this.mapping = new(this.ram, 0x0000, (ushort)Mask.Sixteen, AccessLevel.ReadWrite); + } + + public Z80 CPU { get; } + + public override void Initialize() + { + } + + public override MemoryMapping Mapping(ushort absolute) => this.mapping; + + public override void RaisePOWER() + { + base.RaisePOWER(); + this.CPU.RaisePOWER(); + this.CPU.RaiseRESET(); + this.CPU.RaiseINT(); + this.CPU.RaiseNMI(); + } + + public override void LowerPOWER() + { + this.CPU.LowerPOWER(); + base.LowerPOWER(); + } + } +} diff --git a/Z80/Z80.UnitTests/HaltTests.cs b/Z80/Z80.UnitTests/HaltTests.cs new file mode 100644 index 0000000..d8ad88b --- /dev/null +++ b/Z80/Z80.UnitTests/HaltTests.cs @@ -0,0 +1,88 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace Z80.UnitTests +{ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class HaltTests + { + private readonly Board board = new(); + private readonly Z80 cpu; + + public HaltTests() => this.cpu = this.board.CPU; + + [TestInitialize] + public void Initialise() + { + this.board.RaisePOWER(); + this.cpu.PC.Joined = 0x0000; + this.cpu.SP.Joined = 0xFF00; + } + + [TestCleanup] + public void Cleanup() => this.board.LowerPOWER(); + + [TestMethod] + public void Halt_Instruction_Lowers_Halt_Pin() + { + this.board.Poke(0x0000, 0x76); // HALT + Assert.IsTrue(this.cpu.HALT.Raised(), "HALT pin should be inactive before HALT instruction"); + this.cpu.Step(); + Assert.IsTrue(this.cpu.HALT.Lowered(), "HALT pin should be active after HALT instruction"); + } + + [TestMethod] + public void Halt_State_Does_Not_Advance_PC() + { + this.board.Poke(0x0000, 0x76); // HALT + this.board.Poke(0x0001, 0x00); // NOP (should not execute while halted) + this.cpu.Step(); // execute HALT — PC advances to 0x0001 + var pcAfterHalt = this.cpu.PC.Joined; + this.cpu.Step(); // CPU is halted — PC must not advance + Assert.AreEqual(pcAfterHalt, this.cpu.PC.Joined, "PC must not advance while CPU is halted"); + } + + [TestMethod] + public void Halt_Nmi_Raises_Halt_Pin() + { + this.board.Poke(0x0000, 0x76); // HALT + this.cpu.Step(); // enter HALT + Assert.IsTrue(this.cpu.HALT.Lowered(), "CPU must be halted before NMI test"); + + this.cpu.LowerNMI(); + this.cpu.Step(); // NMI exits HALT and jumps to 0x0066 + Assert.IsTrue(this.cpu.HALT.Raised(), "HALT pin must be raised after NMI"); + } + + [TestMethod] + public void Halt_Int_Raises_Halt_Pin_When_IFF1_Enabled() + { + this.board.Poke(0x0000, 0x76); // HALT + this.cpu.IFF1 = true; + this.cpu.IM = 1; + this.cpu.Step(); // enter HALT + Assert.IsTrue(this.cpu.HALT.Lowered(), "CPU must be halted before INT test"); + + this.cpu.LowerINT(); + this.cpu.Step(); // INT exits HALT (IM1 → jumps to 0x0038) + Assert.IsTrue(this.cpu.HALT.Raised(), "HALT pin must be raised after INT with IFF1 enabled"); + } + + [TestMethod] + public void Halt_Int_Does_Not_Exit_When_IFF1_Disabled() + { + this.board.Poke(0x0000, 0x76); // HALT + this.cpu.IFF1 = false; + this.cpu.Step(); // enter HALT + var pcAfterHalt = this.cpu.PC.Joined; + Assert.IsTrue(this.cpu.HALT.Lowered(), "CPU must be halted before INT test"); + + this.cpu.LowerINT(); // INT pending, but IFF1=false so it will be dismissed + this.cpu.Step(); + Assert.IsTrue(this.cpu.HALT.Lowered(), "CPU must remain halted when IFF1 is disabled"); + Assert.AreEqual(pcAfterHalt, this.cpu.PC.Joined, "PC must not change when INT is masked"); + } + } +} diff --git a/Z80/Z80.UnitTests/IffTests.cs b/Z80/Z80.UnitTests/IffTests.cs new file mode 100644 index 0000000..27d6ecf --- /dev/null +++ b/Z80/Z80.UnitTests/IffTests.cs @@ -0,0 +1,116 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace Z80.UnitTests +{ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class IffTests + { + private readonly Board board = new(); + private readonly Z80 cpu; + + public IffTests() => this.cpu = this.board.CPU; + + [TestInitialize] + public void Initialise() + { + this.board.RaisePOWER(); + this.cpu.PC.Joined = 0x0000; + this.cpu.SP.Joined = 0xFF00; + } + + [TestCleanup] + public void Cleanup() => this.board.LowerPOWER(); + + [TestMethod] + public void DI_Clears_IFF1_And_IFF2() + { + this.cpu.IFF1 = true; + this.cpu.IFF2 = true; + this.board.Poke(0x0000, 0xF3); // DI + this.cpu.Step(); + Assert.IsFalse(this.cpu.IFF1, "DI must clear IFF1"); + Assert.IsFalse(this.cpu.IFF2, "DI must clear IFF2"); + } + + [TestMethod] + public void EI_Sets_IFF1_And_IFF2() + { + this.cpu.IFF1 = false; + this.cpu.IFF2 = false; + this.board.Poke(0x0000, 0xFB); // EI + this.cpu.Step(); + Assert.IsTrue(this.cpu.IFF1, "EI must set IFF1"); + Assert.IsTrue(this.cpu.IFF2, "EI must set IFF2"); + } + + [TestMethod] + public void DI_Prevents_INT() + { + this.board.Poke(0x0000, 0x00); // NOP + this.cpu.IFF1 = false; + this.cpu.IM = 1; + this.cpu.LowerINT(); + this.cpu.Step(); // pending cleared (IFF1=false), then NOP executes + Assert.AreEqual(0x0001, this.cpu.PC.Joined, "INT must be blocked when IFF1 is clear"); + } + + [TestMethod] + public void EI_Followed_By_INT_Fires_On_Next_LowerINT() + { + // After EI, the previous INT pending was already consumed without firing + // (because IFF1 was false when it was checked). INT must produce a new + // falling edge to retrigger. The INT pin is still low, so raise it first. + this.board.Poke(0x0000, 0xFB); // EI + this.board.Poke(0x0001, 0x00); // NOP + this.cpu.IFF1 = false; + this.cpu.IM = 1; + + this.cpu.LowerINT(); // pending=true, but IFF1=false + this.cpu.Step(); // pending cleared (IFF1=false), EI runs → IFF1=true + Assert.IsTrue(this.cpu.IFF1, "IFF1 must be true after EI"); + Assert.AreEqual(0x0001, this.cpu.PC.Joined, "No interrupt must fire during EI step"); + + this.cpu.Step(); // NOP at 0x0001 — still no interrupt (pending was cleared) + Assert.AreEqual(0x0002, this.cpu.PC.Joined, "No interrupt must fire on instruction after EI"); + + // Produce a new falling edge on INT (raise then lower) to retrigger + this.cpu.RaiseINT(); + this.cpu.LowerINT(); + this.cpu.Step(); + Assert.AreEqual(0x0038, this.cpu.PC.Joined, "INT must fire after new falling edge post-EI"); + } + + [TestMethod] + public void Im_Set_Mode_0() + { + this.cpu.IM = 2; + this.board.Poke(0x0000, 0xED); // IM 0 prefix + this.board.Poke(0x0001, 0x46); // IM 0 (ED 46) + this.cpu.Step(); + Assert.AreEqual(0, this.cpu.IM, "IM 0 instruction must set interrupt mode to 0"); + } + + [TestMethod] + public void Im_Set_Mode_1() + { + this.cpu.IM = 0; + this.board.Poke(0x0000, 0xED); // IM 1 prefix + this.board.Poke(0x0001, 0x56); // IM 1 (ED 56) + this.cpu.Step(); + Assert.AreEqual(1, this.cpu.IM, "IM 1 instruction must set interrupt mode to 1"); + } + + [TestMethod] + public void Im_Set_Mode_2() + { + this.cpu.IM = 0; + this.board.Poke(0x0000, 0xED); // IM 2 prefix + this.board.Poke(0x0001, 0x5E); // IM 2 (ED 5E) + this.cpu.Step(); + Assert.AreEqual(2, this.cpu.IM, "IM 2 instruction must set interrupt mode to 2"); + } + } +} diff --git a/Z80/Z80.UnitTests/IntTests.cs b/Z80/Z80.UnitTests/IntTests.cs new file mode 100644 index 0000000..bd383da --- /dev/null +++ b/Z80/Z80.UnitTests/IntTests.cs @@ -0,0 +1,147 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace Z80.UnitTests +{ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class IntTests + { + private readonly Board board = new(); + private readonly Z80 cpu; + + public IntTests() => this.cpu = this.board.CPU; + + [TestInitialize] + public void Initialise() + { + this.board.RaisePOWER(); + this.cpu.PC.Joined = 0x0200; + this.cpu.SP.Joined = 0xFF00; + this.cpu.IFF1 = true; + } + + [TestCleanup] + public void Cleanup() => this.board.LowerPOWER(); + + // ── IM 1 ───────────────────────────────────────────────────────────── + + [TestMethod] + public void Int_IM1_Jumps_To_0x0038() + { + this.cpu.IM = 1; + this.cpu.LowerINT(); + this.cpu.Step(); + Assert.AreEqual(0x0038, this.cpu.PC.Joined, "IM 1 INT must jump to 0x0038"); + } + + [TestMethod] + public void Int_IM1_Saves_PC_On_Stack() + { + this.cpu.PC.Joined = 0x1234; + this.cpu.IM = 1; + this.cpu.LowerINT(); + this.cpu.Step(); + + var low = this.board.Peek(this.cpu.SP.Joined); + var high = this.board.Peek((ushort)(this.cpu.SP.Joined + 1)); + var savedPc = (ushort)((high << 8) | low); + Assert.AreEqual(0x1234, savedPc, "IM 1 INT must save PC on the stack"); + } + + [TestMethod] + public void Int_IM1_Clears_IFF1_And_IFF2() + { + this.cpu.IFF2 = true; + this.cpu.IM = 1; + this.cpu.LowerINT(); + this.cpu.Step(); + Assert.IsFalse(this.cpu.IFF1, "INT must clear IFF1"); + Assert.IsFalse(this.cpu.IFF2, "INT must clear IFF2"); + } + + [TestMethod] + public void Int_Blocked_When_IFF1_Disabled() + { + this.board.Poke(0x0200, 0x00); // NOP + this.cpu.IFF1 = false; + this.cpu.IM = 1; + this.cpu.LowerINT(); // INT pending, but IFF1 = false + this.cpu.Step(); + Assert.AreEqual(0x0201, this.cpu.PC.Joined, "INT must not fire when IFF1 is disabled"); + } + + // ── IM 2 ───────────────────────────────────────────────────────────── + + [TestMethod] + public void Int_IM2_Jumps_Via_Vector_Table() + { + // Vector table: address = (IV << 8) | bus_data_byte + // IV = 0x10, bus byte = 0x20 → table entry at 0x1020 + // Table entry (little-endian) = 0x5678 + this.cpu.IV = 0x10; + this.board.Poke(0x1020, 0x78); + this.board.Poke(0x1021, 0x56); + + this.cpu.IM = 2; + this.board.Data = 0x20; // interrupt device places vector byte on data bus + this.cpu.LowerINT(); + this.cpu.Step(); + Assert.AreEqual(0x5678, this.cpu.PC.Joined, "IM 2 INT must read jump address from vector table"); + } + + [TestMethod] + public void Int_IM2_Saves_PC_On_Stack() + { + this.cpu.PC.Joined = 0xABCD; + this.cpu.IV = 0x10; + this.board.Poke(0x1020, 0x78); + this.board.Poke(0x1021, 0x56); + + this.cpu.IM = 2; + this.board.Data = 0x20; + this.cpu.LowerINT(); + this.cpu.Step(); + + var low = this.board.Peek(this.cpu.SP.Joined); + var high = this.board.Peek((ushort)(this.cpu.SP.Joined + 1)); + var savedPc = (ushort)((high << 8) | low); + Assert.AreEqual(0xABCD, savedPc, "IM 2 INT must save PC on the stack"); + } + + // ── IM 0 ───────────────────────────────────────────────────────────── + + [TestMethod] + public void Int_IM0_Executes_Instruction_From_Data_Bus() + { + // RST 7 (0xFF) placed on data bus by interrupt device → jumps to 0x0038 + this.cpu.PC.Joined = 0x0200; + this.cpu.IM = 0; + this.board.Data = 0xFF; // RST 7 + this.cpu.LowerINT(); + this.cpu.Step(); + Assert.AreEqual(0x0038, this.cpu.PC.Joined, "IM 0 INT must execute RST 7 from data bus"); + } + + // ── RETI ───────────────────────────────────────────────────────────── + + [TestMethod] + public void Reti_Restores_PC_From_Stack() + { + // RETI (ED 4D) at 0x0038 — return to saved PC + this.board.Poke(0x0038, 0xED); + this.board.Poke(0x0039, 0x4D); + + this.cpu.PC.Joined = 0x0200; + this.cpu.IM = 1; + this.cpu.LowerINT(); + this.cpu.Step(); // handles INT, jumps to 0x0038, saves 0x0200 + + Assert.AreEqual(0x0038, this.cpu.PC.Joined); + + this.cpu.Step(); // executes RETI + Assert.AreEqual(0x0200, this.cpu.PC.Joined, "RETI must restore PC from stack"); + } + } +} diff --git a/Z80/Z80.UnitTests/NmiTests.cs b/Z80/Z80.UnitTests/NmiTests.cs new file mode 100644 index 0000000..ab42205 --- /dev/null +++ b/Z80/Z80.UnitTests/NmiTests.cs @@ -0,0 +1,110 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace Z80.UnitTests +{ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class NmiTests + { + private readonly Board board = new(); + private readonly Z80 cpu; + + public NmiTests() => this.cpu = this.board.CPU; + + [TestInitialize] + public void Initialise() + { + this.board.RaisePOWER(); + this.cpu.PC.Joined = 0x0100; + this.cpu.SP.Joined = 0xFF00; + } + + [TestCleanup] + public void Cleanup() => this.board.LowerPOWER(); + + [TestMethod] + public void Nmi_Jumps_To_0x0066() + { + this.cpu.LowerNMI(); + this.cpu.Step(); + Assert.AreEqual(0x0066, this.cpu.PC.Joined, "NMI must jump to 0x0066"); + } + + [TestMethod] + public void Nmi_Saves_PC_On_Stack() + { + this.cpu.PC.Joined = 0x1234; + this.cpu.LowerNMI(); + this.cpu.Step(); + + // PushShort pushes High first then Low, so: + // mem[SP-1] = PC.High, mem[SP-2] = PC.Low + var low = this.board.Peek(this.cpu.SP.Joined); // mem[SP] = PC.Low + var high = this.board.Peek((ushort)(this.cpu.SP.Joined + 1)); // mem[SP+1] = PC.High + var savedPc = (ushort)((high << 8) | low); + Assert.AreEqual(0x1234, savedPc, "NMI must save PC on the stack"); + } + + [TestMethod] + public void Nmi_Clears_IFF1() + { + this.cpu.IFF1 = true; + this.cpu.LowerNMI(); + this.cpu.Step(); + Assert.IsFalse(this.cpu.IFF1, "NMI must clear IFF1"); + } + + [TestMethod] + public void Nmi_Preserves_IFF1_In_IFF2() + { + this.cpu.IFF1 = true; + this.cpu.IFF2 = false; + this.cpu.LowerNMI(); + this.cpu.Step(); + Assert.IsTrue(this.cpu.IFF2, "NMI must copy IFF1 into IFF2 before clearing IFF1"); + Assert.IsFalse(this.cpu.IFF1, "NMI must clear IFF1"); + } + + [TestMethod] + public void Nmi_Fires_Even_When_IFF1_Disabled() + { + this.cpu.IFF1 = false; + this.cpu.LowerNMI(); + this.cpu.Step(); + Assert.AreEqual(0x0066, this.cpu.PC.Joined, "NMI must fire regardless of IFF1"); + } + + [TestMethod] + public void Nmi_Exits_Halt_State() + { + this.board.Poke(0x0100, 0x76); // HALT + this.cpu.Step(); // enter HALT + Assert.IsTrue(this.cpu.HALT.Lowered(), "CPU must be halted before NMI test"); + + this.cpu.LowerNMI(); + this.cpu.Step(); // NMI exits HALT + Assert.IsTrue(this.cpu.HALT.Raised(), "NMI must raise HALT pin"); + Assert.AreEqual(0x0066, this.cpu.PC.Joined, "NMI must jump to 0x0066 from HALT state"); + } + + [TestMethod] + public void Retn_Restores_IFF1_From_IFF2() + { + // RETN (ED 45) at 0x0066 + this.board.Poke(0x0066, 0xED); + this.board.Poke(0x0067, 0x45); + + this.cpu.IFF1 = true; + this.cpu.LowerNMI(); + this.cpu.Step(); // handles NMI: IFF2=true, IFF1=false, jumps to 0x0066 + + Assert.IsFalse(this.cpu.IFF1, "IFF1 must be false after NMI"); + Assert.IsTrue(this.cpu.IFF2, "IFF2 must be true after NMI"); + + this.cpu.Step(); // executes RETN at 0x0066: IFF1 = IFF2 = true + Assert.IsTrue(this.cpu.IFF1, "RETN must restore IFF1 from IFF2"); + } + } +} diff --git a/Z80/Z80.UnitTests/ResetTests.cs b/Z80/Z80.UnitTests/ResetTests.cs new file mode 100644 index 0000000..2d9a17a --- /dev/null +++ b/Z80/Z80.UnitTests/ResetTests.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace Z80.UnitTests +{ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class ResetTests + { + private readonly Board board = new(); + private readonly Z80 cpu; + + public ResetTests() => this.cpu = this.board.CPU; + + [TestInitialize] + public void Initialise() + { + this.board.RaisePOWER(); + this.cpu.PC.Joined = 0x1234; + this.cpu.SP.Joined = 0x8000; + } + + [TestCleanup] + public void Cleanup() => this.board.LowerPOWER(); + + [TestMethod] + public void Reset_Sets_PC_To_Zero() + { + this.cpu.LowerRESET(); + this.cpu.Step(); + Assert.AreEqual(0x0000, this.cpu.PC.Joined, "RESET must set PC to 0x0000"); + } + + [TestMethod] + public void Reset_Clears_IFF1_And_IFF2() + { + this.cpu.IFF1 = true; + this.cpu.IFF2 = true; + this.cpu.LowerRESET(); + this.cpu.Step(); + Assert.IsFalse(this.cpu.IFF1, "RESET must clear IFF1"); + Assert.IsFalse(this.cpu.IFF2, "RESET must clear IFF2"); + } + + [TestMethod] + public void Reset_Sets_IM_To_Zero() + { + this.cpu.IM = 2; + this.cpu.LowerRESET(); + this.cpu.Step(); + Assert.AreEqual(0, this.cpu.IM, "RESET must set interrupt mode to 0"); + } + + [TestMethod] + public void Reset_Clears_I_And_R_Registers() + { + this.cpu.IV = 0xAB; + this.cpu.REFRESH = 0xCD; + this.cpu.LowerRESET(); + this.cpu.Step(); + Assert.AreEqual(0x00, this.cpu.IV, "RESET must clear I register"); + Assert.AreEqual(0x00, (byte)this.cpu.REFRESH, "RESET must clear R register"); + } + + [TestMethod] + public void Reset_Deasserts_Reset_Pin() + { + this.cpu.LowerRESET(); // asserts RESET — fires LoweredRESET event + Assert.IsTrue(this.cpu.RESET.Lowered(), "RESET pin must be asserted before step"); + this.cpu.Step(); + Assert.IsTrue(this.cpu.RESET.Raised(), "HandleRESET must deassert the RESET pin"); + } + + [TestMethod] + public void Reset_Has_Priority_Over_NMI() + { + // Assert both RESET and NMI simultaneously; RESET is checked first in PoweredStep + this.cpu.LowerRESET(); + this.cpu.LowerNMI(); + this.cpu.Step(); + // RESET handler runs, sets PC=0 — NMI (0x0066) must not win + Assert.AreEqual(0x0000, this.cpu.PC.Joined, "RESET must take priority over pending NMI"); + } + } +} diff --git a/Z80/Z80.UnitTests/Z80.UnitTests.csproj b/Z80/Z80.UnitTests/Z80.UnitTests.csproj new file mode 100644 index 0000000..ed83bc3 --- /dev/null +++ b/Z80/Z80.UnitTests/Z80.UnitTests.csproj @@ -0,0 +1,43 @@ + + + + net10.0 + latest + enable + enable + true + False + False + Exe + true + True + latest-all + true + + + + 9999 + + + + 9999 + + + + + + + + + + + + + + + + + + + + diff --git a/Z80/Z80.csproj b/Z80/Z80.csproj index 0385b9f..7eb2bad 100644 --- a/Z80/Z80.csproj +++ b/Z80/Z80.csproj @@ -22,12 +22,15 @@ + + +