diff --git a/Docs/Interrupts.md b/Docs/Interrupts.md index 8e77fd2..1cab414 100644 --- a/Docs/Interrupts.md +++ b/Docs/Interrupts.md @@ -7,3 +7,21 @@ Non-maskable interrupts are triggered by the interrupt source by firing its `Nmi Maskable interrupts are handled by the processor in the following way: after an instruction execution is finished, and if maskable interrupts are enabled, the list of registered interrupts sources is scanned for instances that have the `IntLineIsActive` property set to _true_. If at least one match is found, the interrupt is serviced the standard way (depends on the current interrupt mode). For interrupt mode 2 the `ValueOnDataBus` of the interrupt source is used as well, so it should have a meaningful value. The `HALT` instruction behaves the expected way: after this instruction is executed the processor will enter a loop in which `NOP` instructions are executed (PC is not incremented) until an interrupt is received; note however that if `AutoSopOnDiPlusHalt` is _true_ a `HALT` instruction may cause the execution to stop (depends on the processor [configuration](Configuration.md)). + +### Interrupt events + +[IZ80Processor](../Main/IZ80Processor.cs) provides the following interrupt related events: + +* _`NonMaskableInterruptServicingStart`_ is fired right before a maskable interrupt is going to be serviced. The execution state is as follows when the event is fired: + + * For interrupt mode 0: the opcode has been already fetched from the data bus and is about to be executed. + + * For interrupt mode 1: PC is already set to 0x0038 and the return address has been pushed to the stack. + + * For interrupt mode 2: PC is already set to the address of the routine to execute and the return address has been pushed to the stack. + +* _`MaskableInterruptServicingStart`_ is fired right before a non-maskable interrupt is going to be serviced. PC is already set to 0x0066 and the return address has been pushed to the stack when this event is fired. + +* _`BeforeRetiInstructionExecution`_ and _`BeforeRetnInstructionExecution`_ are fired before a RETI/RETN instruction is about to be executed, right after the corresponding _`BeforeInstructionExecution`_ event. + +* _`AfterRetiInstructionExecution`_ and _`AfterRetnInstructionExecution`_ are fired after a RETI/RETN instruction is about to be executed, right after the corresponding _`AfterInstructionExecution`_ event. diff --git a/Main.Tests/InterruptSourceForTests.cs b/Main.Tests/InterruptSourceForTests.cs index 759652d..2280804 100644 --- a/Main.Tests/InterruptSourceForTests.cs +++ b/Main.Tests/InterruptSourceForTests.cs @@ -6,8 +6,7 @@ public class InterruptSourceForTests : IZ80InterruptSource { public void FireNmi() { - if(NmiInterruptPulse != null) - NmiInterruptPulse(this, EventArgs.Empty); + NmiInterruptPulse?.Invoke(this, EventArgs.Empty); } public event EventHandler NmiInterruptPulse; diff --git a/Main.Tests/Z80ProcessorTests_InstructionExecution.cs b/Main.Tests/Z80ProcessorTests_InstructionExecution.cs index 1caa31c..589c45f 100644 --- a/Main.Tests/Z80ProcessorTests_InstructionExecution.cs +++ b/Main.Tests/Z80ProcessorTests_InstructionExecution.cs @@ -372,6 +372,138 @@ public void Does_not_auto_stops_when_RET_is_found_with_stack_not_equal_to_initia #region Before and after instruction execution events + [Test] + [TestCase(0x4D)] + [TestCase(0x5D)] + [TestCase(0x6D)] + [TestCase(0x7D)] + public void Fires_before_and_after_reti_instruction_execution_in_proper_order(byte opcode) + { + var beforeExecutionEventRaised = false; + var afterExecutionEventRaised = false; + var beforeRetiExecutionEventRaised = false; + var afterRetiExecutionEventRaised = false; + + var instructionBytes = new byte[] + { + 0xED, opcode + }; + Sut.Memory.SetContents(0, instructionBytes); + + Sut.BeforeInstructionExecution += (sender, e) => + { + Assert.IsFalse(beforeExecutionEventRaised); + Assert.IsFalse(beforeRetiExecutionEventRaised); + Assert.IsFalse(afterExecutionEventRaised); + Assert.IsFalse(afterRetiExecutionEventRaised); + + beforeExecutionEventRaised = true; + }; + + Sut.BeforeRetiInstructionExecution += (sender, e) => + { + Assert.IsTrue(beforeExecutionEventRaised); + Assert.IsFalse(beforeRetiExecutionEventRaised); + Assert.IsFalse(afterExecutionEventRaised); + Assert.IsFalse(afterRetiExecutionEventRaised); + + beforeRetiExecutionEventRaised = true; + }; + + Sut.AfterInstructionExecution += (sender, e) => + { + Assert.IsTrue(beforeExecutionEventRaised); + Assert.IsTrue(beforeRetiExecutionEventRaised); + Assert.IsFalse(afterExecutionEventRaised); + Assert.IsFalse(afterRetiExecutionEventRaised); + + afterExecutionEventRaised = true; + }; + + Sut.AfterRetiInstructionExecution += (sender, e) => + { + Assert.IsTrue(beforeExecutionEventRaised); + Assert.IsTrue(beforeRetiExecutionEventRaised); + Assert.IsTrue(afterExecutionEventRaised); + Assert.IsFalse(afterRetiExecutionEventRaised); + + afterRetiExecutionEventRaised = true; + }; + + Sut.ExecuteNextInstruction(); + + Assert.IsTrue(beforeExecutionEventRaised); + Assert.IsTrue(beforeRetiExecutionEventRaised); + Assert.IsTrue(afterExecutionEventRaised); + Assert.IsTrue(afterRetiExecutionEventRaised); + } + + [Test] + [TestCase(0x45)] + [TestCase(0x55)] + [TestCase(0x65)] + [TestCase(0x75)] + public void Fires_before_and_after_retn_instruction_execution_in_proper_order(byte opcode) + { + var beforeExecutionEventRaised = false; + var afterExecutionEventRaised = false; + var beforeRetnExecutionEventRaised = false; + var afterRetnExecutionEventRaised = false; + + var instructionBytes = new byte[] + { + 0xED, opcode + }; + Sut.Memory.SetContents(0, instructionBytes); + + Sut.BeforeInstructionExecution += (sender, e) => + { + Assert.IsFalse(beforeExecutionEventRaised); + Assert.IsFalse(beforeRetnExecutionEventRaised); + Assert.IsFalse(afterExecutionEventRaised); + Assert.IsFalse(afterRetnExecutionEventRaised); + + beforeExecutionEventRaised = true; + }; + + Sut.BeforeRetnInstructionExecution += (sender, e) => + { + Assert.IsTrue(beforeExecutionEventRaised); + Assert.IsFalse(beforeRetnExecutionEventRaised); + Assert.IsFalse(afterExecutionEventRaised); + Assert.IsFalse(afterRetnExecutionEventRaised); + + beforeRetnExecutionEventRaised = true; + }; + + Sut.AfterInstructionExecution += (sender, e) => + { + Assert.IsTrue(beforeExecutionEventRaised); + Assert.IsTrue(beforeRetnExecutionEventRaised); + Assert.IsFalse(afterExecutionEventRaised); + Assert.IsFalse(afterRetnExecutionEventRaised); + + afterExecutionEventRaised = true; + }; + + Sut.AfterRetnInstructionExecution += (sender, e) => + { + Assert.IsTrue(beforeExecutionEventRaised); + Assert.IsTrue(beforeRetnExecutionEventRaised); + Assert.IsTrue(afterExecutionEventRaised); + Assert.IsFalse(afterRetnExecutionEventRaised); + + afterRetnExecutionEventRaised = true; + }; + + Sut.ExecuteNextInstruction(); + + Assert.IsTrue(beforeExecutionEventRaised); + Assert.IsTrue(beforeRetnExecutionEventRaised); + Assert.IsTrue(afterExecutionEventRaised); + Assert.IsTrue(afterRetnExecutionEventRaised); + } + [Test] public void Fires_before_and_after_instruction_execution_with_proper_opcodes_and_local_state() { @@ -833,8 +965,10 @@ public int Execute(byte firstOpcodeByte) else TimesEachInstructionIsExecuted[firstOpcodeByte] = 1; - if(ExtraBeforeFetchCode != null) - ExtraBeforeFetchCode(firstOpcodeByte); + if (firstOpcodeByte == 0xED) + ProcessorAgent.FetchNextOpcode(); + + ExtraBeforeFetchCode?.Invoke(firstOpcodeByte); InstructionFetchFinished(this, new InstructionFetchFinishedEventArgs() @@ -844,10 +978,9 @@ public int Execute(byte firstOpcodeByte) IsHaltInstruction = (firstOpcodeByte == HALT_opcode) }); - if(ExtraAfterFetchCode != null) - ExtraAfterFetchCode(firstOpcodeByte); + ExtraAfterFetchCode?.Invoke(firstOpcodeByte); - if(TStatesReturner == null) + if (TStatesReturner == null) return 0; else return TStatesReturner(firstOpcodeByte); diff --git a/Main.Tests/Z80ProcessorTests_Interrupts.cs b/Main.Tests/Z80ProcessorTests_Interrupts.cs index 1e8bc98..19d1b82 100644 --- a/Main.Tests/Z80ProcessorTests_Interrupts.cs +++ b/Main.Tests/Z80ProcessorTests_Interrupts.cs @@ -13,6 +13,9 @@ public class Z80ProcessorTests_Interrupts private const byte HALT_opcode = 0x76; private const byte NOP_opcode = 0x00; private const byte RST20h_opcode = 0xE7; + private const byte IM0_opcode = 0x46; + private const byte IM1_opcode = 0x56; + private const byte IM2_opcode = 0x5E; Z80ProcessorForTests Sut { get; set; } Fixture Fixture { get; set; } @@ -463,5 +466,58 @@ public void Halted_processor_awakes_on_interrupt(bool isNmi) } #endregion + + #region Interrupt servicing start events + + [Test] + public void Fires_NonMaskableInterruptServicingStart() + { + Sut.RegisterInterruptSource(InterruptSource1); + + Sut.Memory[0] = NOP_opcode; + + var nonMaskableInterruptServicingStartFired = false; + Sut.NonMaskableInterruptServicingStart += (server, args) => + { + nonMaskableInterruptServicingStartFired = true; + }; + + InterruptSource1.FireNmi(); + + Sut.ExecuteNextInstruction(); + + Assert.True(nonMaskableInterruptServicingStartFired); + } + + [Test] + [TestCase(IM0_opcode)] + [TestCase(IM1_opcode)] + [TestCase(IM2_opcode)] + public void Fires_MaskableInterruptServicingStart(byte imOpcode) + { + Sut.RegisterInterruptSource(InterruptSource1); + + Sut.Memory[0] = 0xED; + Sut.Memory[1] = imOpcode; + Sut.Memory[2] = EI_opcode; + Sut.Memory[3] = NOP_opcode; + + Sut.ExecuteNextInstruction(); //This sets interrupt mode + Sut.ExecuteNextInstruction(); //This runs EI + + var maskableInterruptServicingStartFired = false; + Sut.MaskableInterruptServicingStart += (server, args) => + { + maskableInterruptServicingStartFired = true; + }; + + InterruptSource1.IntLineIsActive = true; + + Sut.ExecuteNextInstruction(); + + Assert.True(maskableInterruptServicingStartFired); + } + + #endregion } } diff --git a/Main/Enums/InterruptType.cs b/Main/Enums/InterruptType.cs new file mode 100644 index 0000000..05d1f7e --- /dev/null +++ b/Main/Enums/InterruptType.cs @@ -0,0 +1,18 @@ +namespace Konamiman.Z80dotNet.Enums +{ + /// + /// Defines the type of a Z80 interrupt + /// + public enum InterruptType + { + /// + /// Maskable interrupt + /// + Maskable = 1, + + /// + /// Non-maskable interrupt + /// + NonMaskable + } +} \ No newline at end of file diff --git a/Main/IZ80Processor.cs b/Main/IZ80Processor.cs index 7ffa76d..2ffe980 100644 --- a/Main/IZ80Processor.cs +++ b/Main/IZ80Processor.cs @@ -395,6 +395,48 @@ public interface IZ80Processor /// event EventHandler AfterInstructionExecution; + /// + /// Triggered when a maskable interrupt is about to be serviced. + /// + /// For IM 0: The opcode has been already fetched from the data bus and is about to be executed. + /// + /// For IM 1: PC is already set to 0x0038 and the return address has been pushed to the stack. + /// + /// For IM 2: PC is already set to the address of the routine to execute and the return address has been pushed to the stack. + /// + event EventHandler MaskableInterruptServicingStart; + + /// + /// Triggered when a non-maskable interrupt is about to be serviced. + /// PC is already set to 0x0066 and the return address has been pushed to the stack + /// when this event is invoked. + /// + event EventHandler NonMaskableInterruptServicingStart; + + /// + /// Triggered before a RETI instruction is about to be executed, + /// right after the corresponding BeforeInstructionExecution event + /// + event EventHandler BeforeRetiInstructionExecution; + + /// + /// Triggered after a RETI instruction has been executed, + /// right after the corresponding AfterInstructionExecution event + /// + event EventHandler AfterRetiInstructionExecution; + + /// + /// Triggered before a RETN instruction is about to be executed, + /// right after the corresponding BeforeInstructionExecution event + /// + event EventHandler BeforeRetnInstructionExecution; + + /// + /// Triggered after a RETN instruction has been executed, + /// right after the corresponding AfterInstructionExecution event + /// + event EventHandler AfterRetnInstructionExecution; + #endregion #region Utils diff --git a/Main/Main.csproj b/Main/Main.csproj index 0ae15ff..d2e188a 100644 --- a/Main/Main.csproj +++ b/Main/Main.csproj @@ -37,6 +37,7 @@ + diff --git a/Main/Properties/AssemblyInfo.cs b/Main/Properties/AssemblyInfo.cs index b2573d7..80df78c 100644 --- a/Main/Properties/AssemblyInfo.cs +++ b/Main/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.6.0")] -[assembly: AssemblyFileVersion("1.0.6.0")] +[assembly: AssemblyVersion("1.0.7.0")] +[assembly: AssemblyFileVersion("1.0.7.0")] diff --git a/Main/Z80Processor.cs b/Main/Z80Processor.cs index 1a5faf7..e8e7d39 100644 --- a/Main/Z80Processor.cs +++ b/Main/Z80Processor.cs @@ -1,4 +1,5 @@ -using System; +using Konamiman.Z80dotNet.Enums; +using System; using System.Collections.Generic; using System.Linq; @@ -18,6 +19,9 @@ public class Z80Processor : IZ80Processor, IZ80ProcessorAgent private const ushort NmiServiceRoutine = 0x66; private const byte NOP_opcode = 0x00; private const byte RST38h_opcode = 0xFF; + private const byte RETI_RETN_prefix = 0xED; + private const byte RETI_opcode = 0x4D; + private const byte RETN_opcode = 0x45; public Z80Processor() { @@ -161,6 +165,7 @@ private int AcceptPendingInterrupt() IsHalted = false; Registers.IFF1 = 0; ExecuteCall(NmiServiceRoutine); + TriggerInterruptEvent(InterruptType.NonMaskable); return 11; } @@ -178,10 +183,12 @@ private int AcceptPendingInterrupt() switch(InterruptMode) { case 0: var opcode = activeIntSource.ValueOnDataBus.GetValueOrDefault(0xFF); + TriggerInterruptEvent(InterruptType.Maskable); InstructionExecutor.Execute(opcode); return 13; case 1: InstructionExecutor.Execute(RST38h_opcode); + TriggerInterruptEvent(InterruptType.Maskable); return 13; case 2: var pointerAddress = NumberUtils.CreateUshort( @@ -191,6 +198,7 @@ private int AcceptPendingInterrupt() lowByte: ReadFromMemoryInternal(pointerAddress), highByte: ReadFromMemoryInternal((ushort)(pointerAddress + 1))); ExecuteCall(callAddress); + TriggerInterruptEvent(InterruptType.Maskable); return 19; } @@ -209,6 +217,23 @@ public void ExecuteCall(ushort address) Registers.PC = address; } + private void TriggerInterruptEvent(InterruptType interruptType) + { + switch (interruptType) + { + case InterruptType.Maskable: + MaskableInterruptServicingStart?.Invoke(this, EventArgs.Empty); + break; + + case InterruptType.NonMaskable: + NonMaskableInterruptServicingStart?.Invoke(this, EventArgs.Empty); + break; + + default: + throw new InvalidOperationException($"Unknown interrupt type: {interruptType}"); + } + } + public void ExecuteRet() { var sp = (ushort)Registers.SP; @@ -261,11 +286,22 @@ private bool InterruptsEnabled void FireAfterInstructionExecutionEvent(int tStates) { + var opcodeBytes = executionContext.OpcodeBytes.ToArray(); + AfterInstructionExecution?.Invoke(this, new AfterInstructionExecutionEventArgs( - executionContext.OpcodeBytes.ToArray(), + opcodeBytes, stopper: this, localUserState: executionContext.LocalUserStateFromPreviousEvent, tStates: tStates)); + + if (opcodeBytes[0] == RETI_RETN_prefix) + { + opcodeBytes[1] &= 0xCF; //To account for mirrored variants + if (opcodeBytes[1] == RETI_opcode) + AfterRetiInstructionExecution?.Invoke(this, EventArgs.Empty); + else if (opcodeBytes[1] == RETN_opcode) + AfterRetnInstructionExecution?.Invoke(this, EventArgs.Empty); + } } void InstructionExecutor_InstructionFetchFinished(object sender, InstructionFetchFinishedEventArgs e) @@ -305,12 +341,22 @@ void FireBeforeInstructionFetchEvent() BeforeInstructionExecutionEventArgs FireBeforeInstructionExecutionEvent() { + var opcodeBytes = executionContext.OpcodeBytes.ToArray(); + var eventArgs = new BeforeInstructionExecutionEventArgs( - executionContext.OpcodeBytes.ToArray(), + opcodeBytes, executionContext.LocalUserStateFromPreviousEvent); - if(BeforeInstructionExecution != null) - BeforeInstructionExecution(this, eventArgs); + BeforeInstructionExecution?.Invoke(this, eventArgs); + + if (opcodeBytes[0] == RETI_RETN_prefix) + { + opcodeBytes[1] &= 0xCF; //To account for mirrored variants + if(opcodeBytes[1] == RETI_opcode) + BeforeRetiInstructionExecution?.Invoke(this, EventArgs.Empty); + else if (opcodeBytes[1] == RETN_opcode) + BeforeRetnInstructionExecution?.Invoke(this, EventArgs.Empty); + } return eventArgs; } @@ -638,6 +684,18 @@ public IClockSynchronizer ClockSynchronizer public event EventHandler AfterInstructionExecution; + public event EventHandler MaskableInterruptServicingStart; + + public event EventHandler NonMaskableInterruptServicingStart; + + public event EventHandler BeforeRetiInstructionExecution; + + public event EventHandler AfterRetiInstructionExecution; + + public event EventHandler BeforeRetnInstructionExecution; + + public event EventHandler AfterRetnInstructionExecution; + #endregion #region Members of IZ80ProcessorAgent diff --git a/README.md b/README.md index 9074257..94e55de 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ For your convenience, you can add Z80.NET to your project [as a NuGet package](h 1. Create an instance of [the Z80Processor class](Main/Z80Processor.cs). 2. Optionally, plug your own implementations of one or more of the [dependencies](Docs/Dependencies.md). 3. [Configure your instance](Docs/Configuration.md) as appropriate. -4. Optionally, register one or more [interrupt sources](Docs/Interrupts.md). +4. Optionally, register one or more [interrupt sources](Docs/Interrupts.md), and capture the related events if you need to. 5. Optionally, capture [the memory access events](Docs/MemoryAccessFlow.md) and/or [the instruction execution events](Docs/InstructionExecutionFlow.md). 6. [Start the simulated processor execution](Docs/HowExecutionWorks.md) by using one of the execution control methods. 7. Execution will stop (and the execution method invoked will then return) when one of [the execution stop conditions is met](Docs/StopConditions.md). You can then check [the processor state](Docs/State.md) and, if desired, resume execution.