diff --git a/Z80/inc/Z80.h b/Z80/inc/Z80.h index 63fc52b..9dd86aa 100644 --- a/Z80/inc/Z80.h +++ b/Z80/inc/Z80.h @@ -66,10 +66,10 @@ namespace EightBit { int execute() final; int step() final; - [[nodiscard]] register16_t& AF() final; - [[nodiscard]] register16_t& BC() final; - [[nodiscard]] register16_t& DE() final; - [[nodiscard]] register16_t& HL() final; + [[nodiscard]] register16_t& AF() final { return m_accumulatorFlags[m_accumulatorFlagsSet]; } + [[nodiscard]] register16_t& BC() final { return m_registers[m_registerSet][BC_IDX]; } + [[nodiscard]] register16_t& DE() final { return m_registers[m_registerSet][DE_IDX]; } + [[nodiscard]] register16_t& HL() final { return m_registers[m_registerSet][HL_IDX]; } [[nodiscard]] auto& IX() { return m_ix; } [[nodiscard]] auto& IXH() { return IX().high; } @@ -111,15 +111,14 @@ namespace EightBit { bool requestingRead() { return lowered(RD()); } bool requestingWrite() { return lowered(WR()); } - DECLARE_PIN_INPUT(NMI) - DECLARE_PIN_OUTPUT(M1) - // ** From the Z80 CPU User Manual // RFSH.Refresh(output, active Low). RFSH, together with MREQ, indicates that the lower // seven bits of the system’s address bus can be used as a refresh address to the system’s // dynamic memories. DECLARE_PIN_OUTPUT(RFSH) + DECLARE_PIN_INPUT(NMI) + DECLARE_PIN_OUTPUT(M1) DECLARE_PIN_OUTPUT(MREQ) DECLARE_PIN_OUTPUT(IORQ) DECLARE_PIN_OUTPUT(RD) @@ -152,6 +151,19 @@ namespace EightBit { } private: + + DEFINE_PIN_ACTIVATOR_LOW(RFSH) + DEFINE_PIN_ACTIVATOR_LOW(M1) + DEFINE_PIN_ACTIVATOR_LOW(MREQ) + DEFINE_PIN_ACTIVATOR_LOW(IORQ) + DEFINE_PIN_ACTIVATOR_LOW(RD) + DEFINE_PIN_ACTIVATOR_LOW(WR) + + auto readBusDataM1() { + _ActivateM1 m1(*this); + return BUS().DATA(); + } + enum { BC_IDX, DE_IDX, HL_IDX }; std::array, 2> m_registers; @@ -180,56 +192,29 @@ namespace EightBit { void handleNMI(); - [[nodiscard]] uint16_t displacedAddress() { - assert(m_displaced); - return MEMPTR().word = (m_prefixDD ? IX() : IY()).word + m_displacement; + [[nodiscard]] uint16_t displacedAddress(); + void fetchDisplacement(); + [[nodiscard]] uint8_t fetchOpCode(); + + typedef std::function addresser_t; + typedef std::function reader_t; + + void loadAccumulatorIndirect(addresser_t addresser) { + (MEMPTR() = BUS().ADDRESS() = addresser())++; + A() = memoryRead(); } - void fetchDisplacement() { - m_displacement = fetchByte(); + void storeAccumulatorIndirect(addresser_t addresser) { + (MEMPTR() = BUS().ADDRESS() = addresser())++; + MEMPTR().high = BUS().DATA() = A(); + memoryWrite(); } - // ** From the Z80 CPU User Manual - // Figure 5 depicts the timing during an M1 (op code fetch) cycle. The Program Counter is - // placed on the address bus at the beginning of the M1 cycle. One half clock cycle later, the - // MREQ signal goes active. At this time, the address to memory has had time to stabilize so - // that the falling edge of MREQ can be used directly as a chip enable clock to dynamic - // memories. The RD line also goes active to indicate that the memory read data should be - // enabled onto the CPU data bus. The CPU samples the data from the memory space on the - // data bus with the rising edge of the clock of state T3, and this same edge is used by the - // CPU to turn off the RD and MREQ signals. As a result, the data is sampled by the CPU - // before the RD signal becomes inactive. Clock states T3 and T4 of a fetch cycle are used to - // refresh dynamic memories. The CPU uses this time to decode and execute the fetched - // instruction so that no other concurrent operation can be performed. - // - // When a software HALT instruction is executed, the CPU executes NOPs until an interrupt - // is received(either a nonmaskable or a maskable interrupt while the interrupt flip-flop is - // enabled). The two interrupt lines are sampled with the rising clock edge during each T4 - // state as depicted in Figure 11.If a nonmaskable interrupt is received or a maskable interrupt - // is received and the interrupt enable flip-flop is set, then the HALT state is exited on - // the next rising clock edge.The following cycle is an interrupt acknowledge cycle corresponding - // to the type of interrupt that was received.If both are received at this time, then - // the nonmaskable interrupt is acknowledged because it is the highest priority.The purpose - // of executing NOP instructions while in the HALT state is to keep the memory refresh signals - // active.Each cycle in the HALT state is a normal M1(fetch) cycle except that the data - // received from the memory is ignored and an NOP instruction is forced internally to the - // CPU.The HALT acknowledge signal is active during this time indicating that the processor - // is in the HALT state - uint8_t fetchInitialOpCode() { + void readInternalRegister(reader_t reader) { + F() = adjustSZXY(F(), A() = reader()); + F() = clearBit(F(), NF | HC); + F() = setBit(F(), PF, IFF2()); tick(); - lowerM1(); - auto returned = IntelProcessor::memoryRead(PC()); - if (UNLIKELY(lowered(HALT()))) - returned = 0; // NOP - else - PC()++; - raiseM1(); - BUS().ADDRESS() = { REFRESH(), IV() }; - lowerRFSH(); - lowerMREQ(); - raiseMREQ(); - raiseRFSH(); - return returned; } [[nodiscard]] auto& HL2() { diff --git a/Z80/src/Z80.cpp b/Z80/src/Z80.cpp index 6a85001..0b8c9b9 100644 --- a/Z80/src/Z80.cpp +++ b/Z80/src/Z80.cpp @@ -41,54 +41,58 @@ DEFINE_PIN_LEVEL_CHANGERS(IORQ, Z80); DEFINE_PIN_LEVEL_CHANGERS(RD, Z80); DEFINE_PIN_LEVEL_CHANGERS(WR, Z80); -EightBit::register16_t& EightBit::Z80::AF() { - return m_accumulatorFlags[m_accumulatorFlagsSet]; -} - -EightBit::register16_t& EightBit::Z80::BC() { - return m_registers[m_registerSet][BC_IDX]; -} - -EightBit::register16_t& EightBit::Z80::DE() { - return m_registers[m_registerSet][DE_IDX]; -} - -EightBit::register16_t& EightBit::Z80::HL() { - return m_registers[m_registerSet][HL_IDX]; -} - void EightBit::Z80::memoryWrite() { - WritingMemory.fire(EventArgs::empty()); - tick(2); - lowerMREQ(); + + class _Writer final { + Z80& m_parent; + public: + _Writer(Z80& parent) : m_parent(parent) { + m_parent.WritingMemory.fire(EventArgs::empty()); + m_parent.tick(2); + m_parent.lowerMREQ(); + } + + ~_Writer() { + m_parent.raiseMREQ(); + m_parent.WrittenMemory.fire(EventArgs::empty()); + } + }; + + _Writer writer(*this); IntelProcessor::memoryWrite(); - raiseMREQ(); - WrittenMemory.fire(EventArgs::empty()); } uint8_t EightBit::Z80::memoryRead() { - ReadingMemory.fire(EventArgs::empty()); - tick(2); - lowerMREQ(); - const auto returned = IntelProcessor::memoryRead(); - raiseMREQ(); - ReadMemory.fire(EventArgs::empty()); - return returned; + + class _Reader final { + Z80& m_parent; + public: + _Reader(Z80& parent) : m_parent(parent) { + m_parent.ReadingMemory.fire(EventArgs::empty()); + m_parent.tick(2); + m_parent.lowerMREQ(); + } + + ~_Reader() { + m_parent.raiseMREQ(); + m_parent.ReadMemory.fire(EventArgs::empty()); + } + }; + + _Reader reader(*this); + return IntelProcessor::memoryRead(); } void EightBit::Z80::busWrite() { tick(); - lowerWR(); + _ActivateWR writer(*this); IntelProcessor::busWrite(); - raiseWR(); } uint8_t EightBit::Z80::busRead() { tick(); - lowerRD(); - const auto returned = IntelProcessor::busRead(); - raiseRD(); - return returned; + _ActivateRD reader(*this); + return IntelProcessor::busRead(); } void EightBit::Z80::handleRESET() { @@ -104,19 +108,17 @@ void EightBit::Z80::handleNMI() { raiseHALT(); IFF2() = IFF1(); IFF1() = false; - lowerM1(); - const auto discarded = BUS().DATA(); - raiseM1(); + readBusDataM1(); restart(0x66); } void EightBit::Z80::handleINT() { IntelProcessor::handleINT(); - lowerM1(); - lowerIORQ(); - const auto data = BUS().DATA(); - raiseIORQ(); - raiseM1(); + uint8_t data; + { + _ActivateIORQ iorq(*this); + data = readBusDataM1(); + } di(); tick(5); switch (IM()) { @@ -665,15 +667,23 @@ void EightBit::Z80::portWrite(const uint8_t port) { } void EightBit::Z80::portWrite() { - WritingIO.fire(EventArgs::empty()); - lowerIORQ(); + + class _Writer final { + Z80& m_parent; + public: + _Writer(Z80& parent) : m_parent(parent) { + m_parent.WritingIO.fire(EventArgs::empty()); + } + + ~_Writer() { + m_parent.WrittenIO.fire(EventArgs::empty()); + m_parent.tick(3); + } + }; + + _Writer writer(*this); + _ActivateIORQ iorq(*this); busWrite(); - raiseIORQ(); - WrittenIO.fire(EventArgs::empty()); - tick(3); // I guess this means the extra three ticks on - // port writing are just a quirk of the - // individual instructions, rather than a - // fundamental aspect of port IO. Hmmm. } uint8_t EightBit::Z80::portRead(const uint8_t port) { @@ -683,12 +693,80 @@ uint8_t EightBit::Z80::portRead(const uint8_t port) { } uint8_t EightBit::Z80::portRead() { - ReadingIO.fire(EventArgs::empty()); - lowerIORQ(); - const auto returned = busRead(); - raiseIORQ(); - ReadIO.fire(EventArgs::empty()); - tick(3); + + class _Reader final { + Z80& m_parent; + public: + _Reader(Z80& parent) : m_parent(parent) { + m_parent.ReadingIO.fire(EventArgs::empty()); + } + + ~_Reader() { + m_parent.ReadIO.fire(EventArgs::empty()); + m_parent.tick(3); + } + }; + + _Reader reader(*this); + _ActivateIORQ iorq(*this); + return busRead(); +} + +// + +uint16_t EightBit::Z80::displacedAddress() { + assert(m_displaced); + return MEMPTR().word = (m_prefixDD ? IX() : IY()).word + m_displacement; +} + +void EightBit::Z80::fetchDisplacement() { + m_displacement = fetchByte(); +} + +// ** From the Z80 CPU User Manual + +// Figure 5 depicts the timing during an M1 (op code fetch) cycle. The Program Counter is +// placed on the address bus at the beginning of the M1 cycle. One half clock cycle later, the +// MREQ signal goes active. At this time, the address to memory has had time to stabilize so +// that the falling edge of MREQ can be used directly as a chip enable clock to dynamic +// memories. The RD line also goes active to indicate that the memory read data should be +// enabled onto the CPU data bus. The CPU samples the data from the memory space on the +// data bus with the rising edge of the clock of state T3, and this same edge is used by the +// CPU to turn off the RD and MREQ signals. As a result, the data is sampled by the CPU +// before the RD signal becomes inactive. Clock states T3 and T4 of a fetch cycle are used to +// refresh dynamic memories. The CPU uses this time to decode and execute the fetched +// instruction so that no other concurrent operation can be performed. + +// When a software HALT instruction is executed, the CPU executes NOPs until an interrupt +// is received(either a nonmaskable or a maskable interrupt while the interrupt flip-flop is +// enabled). The two interrupt lines are sampled with the rising clock edge during each T4 +// state as depicted in Figure 11.If a nonmaskable interrupt is received or a maskable interrupt +// is received and the interrupt enable flip-flop is set, then the HALT state is exited on +// the next rising clock edge.The following cycle is an interrupt acknowledge cycle corresponding +// to the type of interrupt that was received.If both are received at this time, then +// the nonmaskable interrupt is acknowledged because it is the highest priority.The purpose +// of executing NOP instructions while in the HALT state is to keep the memory refresh signals +// active.Each cycle in the HALT state is a normal M1(fetch) cycle except that the data +// received from the memory is ignored and an NOP instruction is forced internally to the +// CPU.The HALT acknowledge signal is active during this time indicating that the processor +// is in the HALT state +uint8_t EightBit::Z80::fetchOpCode() { + tick(); + uint8_t returned; + { + _ActivateM1 m1(*this); + const auto halted = lowered(HALT()); + returned = IntelProcessor::memoryRead(PC()); + if (UNLIKELY(halted)) + returned = 0; // NOP + else + PC()++; + } + BUS().ADDRESS() = { REFRESH(), IV() }; + { + _ActivateRFSH rfsh(*this); + _ActivateMREQ mreq(*this); + } return returned; } @@ -713,7 +791,7 @@ int EightBit::Z80::step() { } } if (!handled) - IntelProcessor::execute(fetchInitialOpCode()); + IntelProcessor::execute(fetchOpCode()); } ExecutedInstruction.fire(*this); return cycles(); @@ -905,15 +983,10 @@ void EightBit::Z80::executeED(const int x, const int y, const int z, const int p tick(); break; case 2: // LD A,I - F() = adjustSZXY(F(), A() = IV()); - F() = clearBit(F(), NF | HC); - F() = setBit(F(), PF, IFF2()); - tick(); + readInternalRegister([this]() { return IV(); }); break; case 3: // LD A,R - F() = adjustSZXY(F(), A() = REFRESH()); - F() = clearBit(F(), NF | HC); - F() = setBit(F(), PF, IFF2()); + readInternalRegister([this]() { return REFRESH(); }); tick(); break; case 4: // RRD @@ -1082,23 +1155,17 @@ void EightBit::Z80::executeOther(const int x, const int y, const int z, const in case 0: switch (p) { case 0: // LD (BC),A - (MEMPTR() = BUS().ADDRESS() = BC())++; - MEMPTR().high = BUS().DATA() = A(); - memoryWrite(); + storeAccumulatorIndirect([this]() { return BC(); }); break; case 1: // LD (DE),A - (MEMPTR() = BUS().ADDRESS() = DE())++; - MEMPTR().high = BUS().DATA() = A(); - memoryWrite(); + storeAccumulatorIndirect([this]() { return DE(); }); break; case 2: // LD (nn),HL BUS().ADDRESS() = fetchWord(); setWord(HL2()); break; case 3: // LD (nn),A - (MEMPTR() = BUS().ADDRESS() = fetchWord())++; - MEMPTR().high = BUS().DATA() = A(); - memoryWrite(); + storeAccumulatorIndirect([this]() { return fetchWord(); }); break; default: UNREACHABLE; @@ -1107,20 +1174,17 @@ void EightBit::Z80::executeOther(const int x, const int y, const int z, const in case 1: switch (p) { case 0: // LD A,(BC) - (MEMPTR() = BUS().ADDRESS() = BC())++; - A() = memoryRead(); + loadAccumulatorIndirect([this]() { return BC(); }); break; case 1: // LD A,(DE) - (MEMPTR() = BUS().ADDRESS() = DE())++; - A() = memoryRead(); + loadAccumulatorIndirect([this]() { return DE(); }); break; case 2: // LD HL,(nn) BUS().ADDRESS() = fetchWord(); HL2() = getWord(); break; case 3: // LD A,(nn) - (MEMPTR() = BUS().ADDRESS() = fetchWord())++; - A() = memoryRead(); + loadAccumulatorIndirect([this]() { return fetchWord(); }); break; default: UNREACHABLE; @@ -1339,7 +1403,7 @@ void EightBit::Z80::executeOther(const int x, const int y, const int z, const in fetchDisplacement(); IntelProcessor::execute(fetchByte()); } else { - IntelProcessor::execute(fetchInitialOpCode()); + IntelProcessor::execute(fetchOpCode()); } break; case 2: // OUT (n),A @@ -1379,15 +1443,15 @@ void EightBit::Z80::executeOther(const int x, const int y, const int z, const in break; case 1: // DD prefix m_displaced = m_prefixDD = true; - IntelProcessor::execute(fetchInitialOpCode()); + IntelProcessor::execute(fetchOpCode()); break; case 2: // ED prefix m_prefixED = true; - IntelProcessor::execute(fetchInitialOpCode()); + IntelProcessor::execute(fetchOpCode()); break; case 3: // FD prefix m_displaced = m_prefixFD = true; - IntelProcessor::execute(fetchInitialOpCode()); + IntelProcessor::execute(fetchOpCode()); break; default: UNREACHABLE; diff --git a/inc/Device.h b/inc/Device.h index 5154056..6109ff7 100644 --- a/inc/Device.h +++ b/inc/Device.h @@ -44,17 +44,43 @@ #define DECLARE_PIN_MEMBER(name) \ PinLevel m_## name ## _Line = PinLevel::Low; +#define DEFINE_PIN_ACTIVATOR(name, on, off) \ + template class _Activate ## name final { \ + T& m_parent; \ + public: \ + _Activate ## name(T& parent) \ + : m_parent(parent) { \ + m_parent. on ## name(); \ + } \ + ~_Activate ## name() { \ + m_parent. off ## name(); \ + } \ + }; + +#define DEFINE_PIN_ACTIVATOR_LOW(name) \ + DEFINE_PIN_ACTIVATOR(name, lower, raise) + +#define DEFINE_PIN_ACTIVATOR_HIGH(name) \ + DEFINE_PIN_ACTIVATOR(name, raise, lower) + #define DECLARE_PIN(name, visibility) \ - public: DECLARE_PIN_SIGNALS(name) \ - [[nodiscard]] PinLevel& name () noexcept { return m_## name ## _Line; } \ - visibility : DECLARE_PIN_LEVEL_CHANGERS(name) \ - private: DECLARE_PIN_MEMBER(name) + public: \ + DECLARE_PIN_SIGNALS(name) \ + [[nodiscard]] PinLevel& name () noexcept { \ + return m_## name ## _Line; \ + } \ + visibility : \ + DECLARE_PIN_LEVEL_CHANGERS(name) \ + private: \ + DECLARE_PIN_MEMBER(name) -// Input pins have a degree of external control -#define DECLARE_PIN_INPUT(name) DECLARE_PIN(name, public) +// Input pins may be external controlled +#define DECLARE_PIN_INPUT(name) \ + DECLARE_PIN(name, public) -// Output pins may only be internally controlled -#define DECLARE_PIN_OUTPUT(name) DECLARE_PIN(name, protected) +// Output pins can only be internally controlled +#define DECLARE_PIN_OUTPUT(name) \ + DECLARE_PIN(name, protected) namespace EightBit { class Device {