From 5cea8025cebea28285c28566f7f4fa5cdcc5c131 Mon Sep 17 00:00:00 2001
From: Irmen de Jong <irmen@razorvine.net>
Date: Sat, 21 Sep 2019 23:16:55 +0200
Subject: [PATCH] Added irq() and nmi() to the MPU

---
 CHANGES.txt                        |  4 ++++
 py65/devices/mpu6502.py            | 22 ++++++++++++++++++++
 py65/tests/devices/test_mpu6502.py | 32 ++++++++++++++++++++++++++++++
 3 files changed, 58 insertions(+)

diff --git a/CHANGES.txt b/CHANGES.txt
index b430d10..701bea8 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -9,6 +9,10 @@
    Py65 now requires Python 3.4 or later.  On Python 2, Py65 now requires
    Python 2.7.
 
+ - added ``irq()`` and ``nmi()`` methods to the ``MPU`` class, so that
+   interrupts can be simulated. Patch by Irmen de Jong.
+
+
 1.1.0 (2018-07-01)
 ------------------
 
diff --git a/py65/devices/mpu6502.py b/py65/devices/mpu6502.py
index 4166d70..b211570 100644
--- a/py65/devices/mpu6502.py
+++ b/py65/devices/mpu6502.py
@@ -74,6 +74,28 @@ class MPU:
         self.p = self.BREAK | self.UNUSED
         self.processorCycles = 0
 
+    def irq(self):
+        # triggers a normal IRQ
+        # this is very similar to the BRK instruction
+        if self.p & self.INTERRUPT:
+            return
+        self.stPushWord(self.pc)
+        self.p &= ~self.BREAK
+        self.stPush(self.p | self.UNUSED)
+        self.p |= self.INTERRUPT
+        self.pc = self.WordAt(self.IRQ)
+        self.processorCycles += 7
+
+    def nmi(self):
+        # triggers a NMI IRQ in the processor
+        # this is very similar to the BRK instruction
+        self.stPushWord(self.pc)
+        self.p &= ~self.BREAK
+        self.stPush(self.p | self.UNUSED)
+        self.p |= self.INTERRUPT
+        self.pc = self.WordAt(self.NMI)
+        self.processorCycles += 7
+
     # Helpers for addressing modes
 
     def ByteAt(self, addr):
diff --git a/py65/tests/devices/test_mpu6502.py b/py65/tests/devices/test_mpu6502.py
index 8cfd6a3..fc03a24 100644
--- a/py65/tests/devices/test_mpu6502.py
+++ b/py65/tests/devices/test_mpu6502.py
@@ -1905,6 +1905,38 @@ class Common6502Tests:
 
         self.assertEqual(mpu.BREAK | mpu.UNUSED | mpu.INTERRUPT, mpu.p)
 
+    # IRQ and NMI handling (very similar to BRK)
+
+    def test_irq_pushes_pc_and_correct_status_then_sets_pc_to_irq_vector(self):
+        mpu = self._make_mpu()
+        mpu.p = mpu.UNUSED
+        self._write(mpu.memory, 0xFFFA, (0x88, 0x77))
+        self._write(mpu.memory, 0xFFFE, (0xCD, 0xAB))
+        mpu.pc = 0xC123
+        mpu.irq()
+        self.assertEqual(0xABCD, mpu.pc)
+        self.assertEqual(0xC1, mpu.memory[0x1FF])  # PCH
+        self.assertEqual(0x23, mpu.memory[0x1FE])  # PCL
+        self.assertEqual(mpu.UNUSED, mpu.memory[0x1FD])  # Status
+        self.assertEqual(0xFC, mpu.sp)
+        self.assertEqual(mpu.UNUSED | mpu.INTERRUPT, mpu.p)
+        self.assertEqual(7, mpu.processorCycles)
+
+    def test_nmi_pushes_pc_and_correct_status_then_sets_pc_to_nmi_vector(self):
+        mpu = self._make_mpu()
+        mpu.p = mpu.UNUSED
+        self._write(mpu.memory, 0xFFFA, (0x88, 0x77))
+        self._write(mpu.memory, 0xFFFE, (0xCD, 0xAB))
+        mpu.pc = 0xC123
+        mpu.nmi()
+        self.assertEqual(0x7788, mpu.pc)
+        self.assertEqual(0xC1, mpu.memory[0x1FF])  # PCH
+        self.assertEqual(0x23, mpu.memory[0x1FE])  # PCL
+        self.assertEqual(mpu.UNUSED, mpu.memory[0x1FD])  # Status
+        self.assertEqual(0xFC, mpu.sp)
+        self.assertEqual(mpu.UNUSED | mpu.INTERRUPT, mpu.p)
+        self.assertEqual(7, mpu.processorCycles)
+
     # BVC
 
     def test_bvc_overflow_clear_branches_relative_forward(self):