From 70416b57542fb9b6a9f260df28caa688d9de83ed Mon Sep 17 00:00:00 2001 From: Marie Date: Fri, 17 Oct 2025 01:14:16 +0200 Subject: [PATCH 1/7] tests: begin implementing emu interrupt tests --- test/mfdemu/CMakeLists.txt | 3 ++- test/mfdemu/interrupt.cpp | 48 ++++++++++++++++++++++++++++++++++++++ test/mfdemu/test_cpu.hpp | 1 + 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 test/mfdemu/interrupt.cpp diff --git a/test/mfdemu/CMakeLists.txt b/test/mfdemu/CMakeLists.txt index b484034..233126f 100644 --- a/test/mfdemu/CMakeLists.txt +++ b/test/mfdemu/CMakeLists.txt @@ -1,7 +1,8 @@ add_executable(emu-test main.cpp arithmetic.cpp gio.cpp + interrupt.cpp ) target_link_libraries(emu-test PRIVATE emu shared) -add_test(NAME emu-test COMMAND emu-test --ni) \ No newline at end of file +add_test(NAME emu-test COMMAND emu-test --ni) diff --git a/test/mfdemu/interrupt.cpp b/test/mfdemu/interrupt.cpp new file mode 100644 index 0000000..302eb4f --- /dev/null +++ b/test/mfdemu/interrupt.cpp @@ -0,0 +1,48 @@ +#define DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#include + +#include "test_cpu.hpp" + +#include + +using namespace ::mfdemu::impl; + +namespace test::mfdemu { +TEST_SUITE("Interrupt") { + TEST_CASE("hardware interrupt") { + std::vector main_memory(64 * 1024); + main_memory[0x0000] = 0x10; + main_memory[0x0001] = 0x00; + main_memory[0x0002] = 0x00; + main_memory[0x0003] = 0x00; + main_memory[0xFFFC] = 0x00; + main_memory[0xFFFD] = 0x00; + main_memory[0xFFFE] = 0x00; + main_memory[0xFFFF] = 0x00; + auto main_memory_dev = std::make_shared(false, 64 * 1024); + main_memory_dev->setData(std::move(main_memory)); + REQUIRE(main_memory_dev != nullptr); + + CpuTest cpu; + cpu.connectAddressDevice(main_memory_dev); + cpu.reset = true; + cpu.iclck(); + cpu.reset = false; + + /* run the cpu for some time */ + for(u8 i = 0; i < 16; i++) { + cpu.iclck(); + } + + cpu.irq = true; + cpu.iclck(); /* initial cpu reaction time */ + cpu.iclck(); /* buffer cycle */ + cpu.iclck(); /* start of IRA (IRA T1) */ + REQUIRE(cpu.ira()); + cpu.iclck(); /* IRA T2 */ + REQUIRE(cpu.ira()); + cpu.iclck(); /* IRA done, internal interrupt processing */ + } + TEST_CASE("software interrupt") {} +} +} // namespace test::mfdemu diff --git a/test/mfdemu/test_cpu.hpp b/test/mfdemu/test_cpu.hpp index 297458d..c093be9 100644 --- a/test/mfdemu/test_cpu.hpp +++ b/test/mfdemu/test_cpu.hpp @@ -18,6 +18,7 @@ class CpuTest : public Cpu { u16 &m_regSP = Cpu::m_regSP; u16 &m_regIP = Cpu::m_regIP; u16 &m_regAR = Cpu::m_regAR; + u16 &m_regIID = Cpu::m_regIID; CpuFlags &m_regFL = Cpu::m_regFL; u16 &m_ioBusInput = Cpu::m_ioBusInput; From a216ff46266775ee29437121d009574e34c56dce Mon Sep 17 00:00:00 2001 From: Marie Date: Fri, 17 Oct 2025 01:33:58 +0200 Subject: [PATCH 2/7] tests: implement hardware interrupt test --- test/mfdemu/interrupt.cpp | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/test/mfdemu/interrupt.cpp b/test/mfdemu/interrupt.cpp index 302eb4f..a36a4a1 100644 --- a/test/mfdemu/interrupt.cpp +++ b/test/mfdemu/interrupt.cpp @@ -11,12 +11,12 @@ namespace test::mfdemu { TEST_SUITE("Interrupt") { TEST_CASE("hardware interrupt") { std::vector main_memory(64 * 1024); - main_memory[0x0000] = 0x10; - main_memory[0x0001] = 0x00; - main_memory[0x0002] = 0x00; - main_memory[0x0003] = 0x00; - main_memory[0xFFFC] = 0x00; - main_memory[0xFFFD] = 0x00; + main_memory[0x1234] = 0x10; + main_memory[0x1235] = 0x00; + main_memory[0x1236] = 0x00; + main_memory[0x1237] = 0x00; + main_memory[0xFFFC] = 0x12; + main_memory[0xFFFD] = 0x34; main_memory[0xFFFE] = 0x00; main_memory[0xFFFF] = 0x00; auto main_memory_dev = std::make_shared(false, 64 * 1024); @@ -34,14 +34,29 @@ TEST_SUITE("Interrupt") { cpu.iclck(); } + cpu.m_regFL.ie = true; + + /* IRQ */ cpu.irq = true; - cpu.iclck(); /* initial cpu reaction time */ - cpu.iclck(); /* buffer cycle */ - cpu.iclck(); /* start of IRA (IRA T1) */ + cpu.iclck(); // initial cpu reaction time + cpu.irq = false; + + /* buffer cycle */ + cpu.iclck(); + + /* IRA */ + cpu.iclck(); // IRA T1 REQUIRE(cpu.ira()); - cpu.iclck(); /* IRA T2 */ + cpu.iclck(); // IRA T2 REQUIRE(cpu.ira()); - cpu.iclck(); /* IRA done, internal interrupt processing */ + cpu.iclck(); // IRA done, internal interrupt processing + + // run until the cpu is about to enter the interrupt vector + while(cpu.m_regFL.ie) { + cpu.iclck(); + } + + REQUIRE_EQ(cpu.m_regIP, 0x1234); } TEST_CASE("software interrupt") {} } From 9b247c355da121405e15a6e64d802b91b59af5f2 Mon Sep 17 00:00:00 2001 From: Marie Date: Fri, 17 Oct 2025 01:35:01 +0200 Subject: [PATCH 3/7] emu: add required buffer cycle to hardware interrupt --- emu/mfdemu/impl/cpu.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/emu/mfdemu/impl/cpu.cpp b/emu/mfdemu/impl/cpu.cpp index 5fcbb0f..12d8725 100644 --- a/emu/mfdemu/impl/cpu.cpp +++ b/emu/mfdemu/impl/cpu.cpp @@ -545,16 +545,20 @@ void Cpu::execReset() { void Cpu::execHardInterrupt() { switch(m_stateStep) { case 0: - logDebug() << "starting interrupt acknowledge\n"; - m_pinIRA = true; + logDebug() << "hardware interrupt buffer cycle\n"; m_stateStep = 1; break; case 1: - logDebug() << "setting IID\n"; - m_regIID = m_ioBusInput & 0xFF; + logDebug() << "starting interrupt acknowledge\n"; + m_pinIRA = true; m_stateStep = 2; break; case 2: + logDebug() << "setting IID\n"; + m_regIID = m_ioBusInput & 0xFF; + m_stateStep = 3; + break; + case 3: logDebug() << "terminated interrupt acknowledge\n"; m_pinIRA = false; finishState(); From c53ef6454e2ef721d0bb8feaed96e9a47ce9cae1 Mon Sep 17 00:00:00 2001 From: Marie Date: Thu, 29 Jan 2026 16:46:13 +0100 Subject: [PATCH 4/7] remove interrupt buffer cycle instead of waiting for a single cycle before starting the IRA sequence, the CPU will simply finish executing the current instruction/wait until it has reached the INST_FETCH state again before starting the interrupt acknowledge (or the interrupt processing in case of software interrupts). --- DESIGN | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/DESIGN b/DESIGN index 6beced5..1dbd806 100644 --- a/DESIGN +++ b/DESIGN @@ -508,8 +508,10 @@ CONTENTS The CPU checks if the IRQ line is high at the ench of each clock cycle. So if IRQ is high at the end of T1, an interrupt was requested. - During T2, the cpu is already in the process of accepting and processing - the interrupt. This cycle is called the "buffer cycle" + After the CPU has received an interrupt request, it will set its internal IQ + flag and wait for the execution of the current instruction to finish. After + finishing, the IQ will be cleared and the Interrupt acknowledge sequence will + be started. Interrupt acknowledge: @@ -524,7 +526,7 @@ CONTENTS GIO ─────────────────────────────🮤 DATA IN 🮥─ 🮡──────────🮠 - In the next cycle after the buffer cycle, the cpu will set the IRA line high + In the first cycle of the IRA sequence, the cpu will set the IRA line high for two clock pulses. During the first pulse, nothing else happens. During the second pulse the CPU reads a byte of whatever data currently is on the I/O Bus. From e8e53977065f19c0b2d60c6fc748e68c31d64391 Mon Sep 17 00:00:00 2001 From: Marie Date: Thu, 29 Jan 2026 16:48:23 +0100 Subject: [PATCH 5/7] emu: wait for INST_FETCH state before processing IRQ --- emu/mfdemu/impl/cpu.cpp | 24 ++++++++++++++---------- emu/mfdemu/impl/cpu.hpp | 4 +++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/emu/mfdemu/impl/cpu.cpp b/emu/mfdemu/impl/cpu.cpp index 12d8725..6797f06 100644 --- a/emu/mfdemu/impl/cpu.cpp +++ b/emu/mfdemu/impl/cpu.cpp @@ -70,6 +70,12 @@ void Cpu::iclck() { this->gioWrite(); break; case CpuState::INST_FETCH: + if(m_regFL.iq) { + this->newState(CpuState::HARD_INTERRUPT); + this->execHardInterrupt(); + this->m_regFL.iq = false; + break; + } this->fetchInst(); break; case CpuState::INST_EXEC: @@ -86,8 +92,9 @@ void Cpu::iclck() { break; } + // Let the current instruction finished before acknowledging the interrupt if(irq && m_regFL.ie) { - newState(CpuState::HARD_INTERRUPT); + m_regFL.iq = true; } } @@ -531,6 +538,7 @@ void Cpu::execReset() { .nf = false, .ie = false, .rt = false, + .iq = false, }; logDebug() << "\nreset, IP = " << std::hex << m_regIP << std::dec << "\n"; @@ -545,20 +553,16 @@ void Cpu::execReset() { void Cpu::execHardInterrupt() { switch(m_stateStep) { case 0: - logDebug() << "hardware interrupt buffer cycle\n"; - m_stateStep = 1; - break; - case 1: logDebug() << "starting interrupt acknowledge\n"; m_pinIRA = true; - m_stateStep = 2; + m_stateStep = 1; break; - case 2: + case 1: logDebug() << "setting IID\n"; m_regIID = m_ioBusInput & 0xFF; - m_stateStep = 3; + m_stateStep = 2; break; - case 3: + case 2: logDebug() << "terminated interrupt acknowledge\n"; m_pinIRA = false; finishState(); @@ -575,7 +579,7 @@ void Cpu::execInterrupt() { logDebug() << "saving IP\n"; m_regSP -= 2; m_addressBusAddress = m_regSP; - m_addressBusOutput = m_regIP; + m_addressBusOutput = NEXT_IP_VALUE; m_stateStep = 1; newState(CpuState::ABUS_WRITE); break; diff --git a/emu/mfdemu/impl/cpu.hpp b/emu/mfdemu/impl/cpu.hpp index 15d074e..522e864 100644 --- a/emu/mfdemu/impl/cpu.hpp +++ b/emu/mfdemu/impl/cpu.hpp @@ -38,6 +38,8 @@ struct CpuFlags { bool nf; bool ie; bool rt; + /** @brief interrupt queued; not program-accessible. */ + bool iq; }; struct AddressingMode { @@ -56,7 +58,7 @@ struct Operand { class Cpu { public: enum class CpuState : u8 { - ABUS_READ, + ABUS_READ = 0, ABUS_READ_INDIRECT, ABUS_WRITE, ABUS_WRITE_INDIRECT, From 4e63a63380193635ae41726c9a9af6f536e5f631 Mon Sep 17 00:00:00 2001 From: Marie Date: Thu, 29 Jan 2026 16:48:50 +0100 Subject: [PATCH 6/7] emu: implement INT and IRET --- emu/mfdemu/impl/cpu.cpp | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/emu/mfdemu/impl/cpu.cpp b/emu/mfdemu/impl/cpu.cpp index 6797f06..a4d1219 100644 --- a/emu/mfdemu/impl/cpu.cpp +++ b/emu/mfdemu/impl/cpu.cpp @@ -1071,11 +1071,32 @@ void Cpu::execInstINC() { m_stateStep = EXEC_INST_STEP_INC_IP; } -/** @todo: implement */ -void Cpu::execInstINT() {} +void Cpu::execInstINT() { + if(!m_regFL.ie) { + finishState(); + return; + } + m_regIID = m_operand1.value; + newState(CpuState::INTERRUPT); +} -/** @todo: implement */ -void Cpu::execInstIRET() {} +void Cpu::execInstIRET() { + switch(m_stateStep) { + case 0: + m_regIID = 0; + m_addressBusAddress = m_regSP; + m_stateStep = 1; + newState(CpuState::ABUS_READ); + break; + case 1: + m_regSP += 2; + m_regIP = m_addressBusInput; + finishState(); + break; + default: + shared::panic("invalid state: execInstIRET reached an invalid state step"); + } +} void Cpu::execInstJMP() { constexpr u8 MOVE_TO_STASH = 16; From a23b1c27607b1f2abf040f0f6431996122e1f2e5 Mon Sep 17 00:00:00 2001 From: Marie Date: Thu, 29 Jan 2026 16:49:50 +0100 Subject: [PATCH 7/7] tests: update interrupt test * Adjust & cleanup test logic. * Add IRET and INT testing. --- test/mfdemu/interrupt.cpp | 97 +++++++++++++++++++++++++++++++-------- test/mfdemu/test_cpu.hpp | 2 + 2 files changed, 81 insertions(+), 18 deletions(-) diff --git a/test/mfdemu/interrupt.cpp b/test/mfdemu/interrupt.cpp index a36a4a1..b058cd2 100644 --- a/test/mfdemu/interrupt.cpp +++ b/test/mfdemu/interrupt.cpp @@ -8,17 +8,41 @@ using namespace ::mfdemu::impl; namespace test::mfdemu { +static void test(CpuTest &cpu, u16 iid, u16 iv, u16 ret) { + // run until the cpu is about to enter the interrupt vector + while(cpu.m_regFL.ie) { + cpu.iclck(); + } + + CHECK_EQ(cpu.m_regIID, iid); + REQUIRE_EQ(cpu.m_regIP, iv); + + // Test IRET + for(u8 i = 0; i < 16; i++) { + cpu.iclck(); + } + + CHECK_EQ(cpu.m_regIID, 0x0000); + REQUIRE_EQ(cpu.m_regIP, ret); +} + +static void setMemory(std::vector &mem) { + mem[0x0000] = 0x10; + mem[0x0001] = 0x00; + mem[0x0002] = 0x00; + mem[0x0003] = 0x00; + mem[0x1234] = 0x0f; + mem[0x1235] = 0x00; + mem[0xFFFC] = 0x12; + mem[0xFFFD] = 0x34; + mem[0xFFFE] = 0x00; + mem[0xFFFF] = 0x00; +} + TEST_SUITE("Interrupt") { TEST_CASE("hardware interrupt") { - std::vector main_memory(64 * 1024); - main_memory[0x1234] = 0x10; - main_memory[0x1235] = 0x00; - main_memory[0x1236] = 0x00; - main_memory[0x1237] = 0x00; - main_memory[0xFFFC] = 0x12; - main_memory[0xFFFD] = 0x34; - main_memory[0xFFFE] = 0x00; - main_memory[0xFFFF] = 0x00; + std::vector main_memory(64UL * 1024); + setMemory(main_memory); auto main_memory_dev = std::make_shared(false, 64 * 1024); main_memory_dev->setData(std::move(main_memory)); REQUIRE(main_memory_dev != nullptr); @@ -41,23 +65,60 @@ TEST_SUITE("Interrupt") { cpu.iclck(); // initial cpu reaction time cpu.irq = false; - /* buffer cycle */ - cpu.iclck(); + /* wait for CPU to switch state & IRA T1. + * Might take a moment since its finishing off the current instruction. + * This will also include T1 of the IRA. + */ + while(cpu.m_regFL.iq) { + cpu.iclck(); + } - /* IRA */ - cpu.iclck(); // IRA T1 - REQUIRE(cpu.ira()); - cpu.iclck(); // IRA T2 + REQUIRE(cpu.ira()); // IRA T1 + cpu.iclck(); // IRA T2 REQUIRE(cpu.ira()); cpu.iclck(); // IRA done, internal interrupt processing - // run until the cpu is about to enter the interrupt vector + // TODO: Provide an IO device to properly test IRA sequence + const u16 fake_iid = 0x0012; + cpu.m_regIID = fake_iid; + test(cpu, fake_iid, 0x1234, 0x004); + } + TEST_CASE("software interrupt") { + std::vector main_memory(64UL * 1024); + setMemory(main_memory); + main_memory[0x0000] = 0x0e; + main_memory[0x0003] = 0x12; + auto main_memory_dev = std::make_shared(false, 64 * 1024); + main_memory_dev->setData(std::move(main_memory)); + REQUIRE(main_memory_dev != nullptr); + + CpuTest cpu; + cpu.connectAddressDevice(main_memory_dev); + + cpu.reset = true; + cpu.iclck(); + cpu.reset = false; + + /* last thing reset does is clear all flags. */ + cpu.m_regFL.ie = true; while(cpu.m_regFL.ie) { cpu.iclck(); } - REQUIRE_EQ(cpu.m_regIP, 0x1234); + cpu.m_regFL.ie = true; + + /* run until interrupts disabled. means we are processing one. */ + while(cpu.m_regFL.ie) { + cpu.iclck(); + } + + for(u8 i = 0; i < 3; i++) { + cpu.iclck(); + } + + // TODO: Provide an IO device to properly test IRA sequence + const u16 fake_iid = 0x0012; + test(cpu, fake_iid, 0x1234, 0x004); } - TEST_CASE("software interrupt") {} } } // namespace test::mfdemu diff --git a/test/mfdemu/test_cpu.hpp b/test/mfdemu/test_cpu.hpp index c093be9..bd5a9ef 100644 --- a/test/mfdemu/test_cpu.hpp +++ b/test/mfdemu/test_cpu.hpp @@ -24,5 +24,7 @@ class CpuTest : public Cpu { u16 &m_ioBusInput = Cpu::m_ioBusInput; u16 &m_ioBusOutput = Cpu::m_ioBusOutput; u16 &m_ioBusAddress = Cpu::m_ioBusAddress; + + std::stack> &m_state = Cpu::m_state; }; } // namespace test::mfdemu