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. diff --git a/emu/mfdemu/impl/cpu.cpp b/emu/mfdemu/impl/cpu.cpp index 5fcbb0f..a4d1219 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"; @@ -571,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; @@ -1063,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; 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, 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..b058cd2 --- /dev/null +++ b/test/mfdemu/interrupt.cpp @@ -0,0 +1,124 @@ +#define DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#include + +#include "test_cpu.hpp" + +#include + +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(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); + + 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.m_regFL.ie = true; + + /* IRQ */ + cpu.irq = true; + cpu.iclck(); // initial cpu reaction time + cpu.irq = false; + + /* 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(); + } + + REQUIRE(cpu.ira()); // IRA T1 + cpu.iclck(); // IRA T2 + REQUIRE(cpu.ira()); + cpu.iclck(); // IRA done, internal interrupt processing + + // 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(); + } + + 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); + } +} +} // namespace test::mfdemu diff --git a/test/mfdemu/test_cpu.hpp b/test/mfdemu/test_cpu.hpp index 297458d..bd5a9ef 100644 --- a/test/mfdemu/test_cpu.hpp +++ b/test/mfdemu/test_cpu.hpp @@ -18,10 +18,13 @@ 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; u16 &m_ioBusOutput = Cpu::m_ioBusOutput; u16 &m_ioBusAddress = Cpu::m_ioBusAddress; + + std::stack> &m_state = Cpu::m_state; }; } // namespace test::mfdemu