From 960f2b85cc93c25037bf005f22dc36070a045276 Mon Sep 17 00:00:00 2001 From: Marc Parsons Date: Wed, 15 Apr 2026 15:44:24 +0100 Subject: [PATCH] Interrupts added at 50fps. Dummy keyboard. Ready for graphics! --- Core/Cpu/Z80.cs | 117 +++++++++++++++++++++++++++++++++++++++- Desktop/DebuggerForm.cs | 111 ++++++++++++++++++++++++++++++-------- 2 files changed, 205 insertions(+), 23 deletions(-) diff --git a/Core/Cpu/Z80.cs b/Core/Cpu/Z80.cs index dcc5bc1..7ce3dec 100644 --- a/Core/Cpu/Z80.cs +++ b/Core/Cpu/Z80.cs @@ -79,6 +79,60 @@ namespace Core.Cpu TotalTStates = 0; // Reset the system clock! } + public int RequestInterrupt() + { + // 1. If the ROM has disabled interrupts (DI), ignore the request + if (!IFF1) return 0; + + // 2. Acknowledge the interrupt by immediately disabling further interrupts + // This prevents an endless loop of interrupts triggering each other! + IFF1 = false; + IFF2 = false; + + // 3. Push the current Program Counter to the stack so we can return + Push(PC); + + // 4. Jump to the Interrupt Service Routine + // The ZX Spectrum standard is Mode 1, which maps directly to 0x0038 + if (InterruptMode == 1) + { + PC = 0x0038; + } + else + { + // (Games will use Mode 2 later, but the ROM uses Mode 1) + throw new NotImplementedException($"Interrupt Mode {InterruptMode} not implemented!"); + } + + // 5. The CPU takes exactly 13 T-States to process this hardware CALL + return 13; + } + + // Helper method to calculate if a byte has an Even Parity of 1s + private bool CalculateParity(byte b) + { + int bits = 0; + for (int i = 0; i < 8; i++) + { + if ((b & (1 << i)) != 0) bits++; + } + return (bits % 2) == 0; + } + + // Placeholder for your hardware I/O + private byte ReadPort(ushort portAddress) + { + // If the port is 0xFE, the ROM is asking for keyboard/tape/ULA data! + // For now, we will return 0xFF (meaning "No keys are currently pressed") + if ((portAddress & 0xFF) == 0xFE) + { + return 0xFF; + } + + // Default floating bus return + return 0xFF; + } + public int Step() { // Fetch the next opcode and increment the Program Counter @@ -494,6 +548,9 @@ namespace Core.Cpu return 7; case 0x24: HL.High = Inc8(HL.High); return 4; // INC H case 0x2C: HL.Low = Inc8(HL.Low); return 4; // INC L + case 0x2E: // LD L, n + HL.Low = FetchByte(); + return 7; case 0x34: _memory.Write(HL.Word, Inc8(_memory.Read(HL.Word))); return 11; // INC (HL) takes 11 T-States @@ -506,6 +563,15 @@ namespace Core.Cpu case 0x1D: DE.Low = Dec8(DE.Low); return 4; // DEC E case 0x25: HL.High = Dec8(HL.High); return 4; // DEC H case 0x2D: HL.Low = Dec8(HL.Low); return 4; // DEC L + case 0x2F: // CPL + // Flip all bits in the Accumulator + AF.High = (byte)(~AF.High); + + // Set Half-Carry (Bit 4) and Subtract (Bit 1). + // Bitwise OR forces them to 1 while perfectly preserving S, Z, P/V, and C. + AF.Low |= 0x12; + + return 4; case 0x35: _memory.Write(HL.Word, Dec8(_memory.Read(HL.Word))); return 11; // DEC (HL) takes 11 T-States @@ -1158,6 +1224,25 @@ namespace Core.Cpu _memory.Write(dest73, (byte)SP); _memory.Write((ushort)(dest73 + 1), (byte)(SP >> 8)); return 20; + case 0x78: // IN A, (C) + // Read from the hardware port using the full BC register as the address + byte portVal78 = ReadPort(BC.Word); + AF.High = portVal78; + + // --- Update Flags --- + // S (Bit 7), Z (Bit 6), P/V (Bit 2) are set based on the input. + // H (Bit 4) and N (Bit 1) are RESET. + // C (Bit 0) is PRESERVED. + + byte newFlags = (byte)(AF.Low & 0x01); // Preserve Carry + + if ((portVal78 & 0x80) != 0) newFlags |= 0x80; // Sign Flag + if (portVal78 == 0) newFlags |= 0x40; // Zero Flag + if (CalculateParity(portVal78)) newFlags |= 0x04; // Parity/Overflow Flag + + AF.Low = newFlags; + + return 12; // Takes 12 T-States case 0xB0: // LDIR // 1. Read byte from (HL) val = _memory.Read(HL.Word); @@ -1271,7 +1356,37 @@ namespace Core.Cpu break; // Proceed to write-back case 0: // ALL Shift/Rotate Instructions - throw new NotImplementedException($"CB Shift/Rotate opcode {cbOpcode:X2} at PC 0x{(PC - 1):X4} not implemented!"); + // The specific shift type is in the same bits we previously used for 'bitIndex' + int shiftType = (cbOpcode >> 3) & 0x07; + bool carryOut = false; + + switch (shiftType) + { + case 0: // RLC + // Grab Bit 7 to see if it's going to fall off + carryOut = (val & 0x80) != 0; + // Shift left, and loop the falling bit back into Bit 0 + val = (byte)((val << 1) | (carryOut ? 1 : 0)); + break; + + // (We will add RRC, RL, RR, SLA, SRA, SRL here as the ROM asks for them!) + default: + throw new NotImplementedException($"CB Shift instruction type {shiftType} not implemented!"); + } + + // --- Update Flags --- + // All CB Shift instructions calculate flags the exact same way! + // They set S, Z, P/V, and C. They forcefully clear H and N. + byte newFlags = 0; + + if (carryOut) newFlags |= 0x01; // C Flag + if ((val & 0x80) != 0) newFlags |= 0x80; // S Flag + if (val == 0) newFlags |= 0x40; // Z Flag + if (CalculateParity(val)) newFlags |= 0x04; // P/V Flag + + AF.Low = newFlags; // Apply the new flags + + break; // Proceed to the write-back phase default: throw new Exception("Invalid CB operation."); diff --git a/Desktop/DebuggerForm.cs b/Desktop/DebuggerForm.cs index e8726b9..46f512f 100644 --- a/Desktop/DebuggerForm.cs +++ b/Desktop/DebuggerForm.cs @@ -81,26 +81,68 @@ namespace Desktop _isRunning = true; btnRun.Text = "Stop"; - + + // Fire up a background thread // Fire up a background thread await Task.Run(() => { try { + // 1. Setup Frame Timing Variables + const int TStatesPerFrame = 69888; + long nextFrameTargetTStates = _cpu.TotalTStates + TStatesPerFrame; + + // 2. Setup Real-World Throttling + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + long frameCount = 0; + while (_isRunning) { - // --- NEW: Breakpoint Check --- - // We check BEFORE stepping so it stops exactly on the instruction + // --- Breakpoint Check --- if (_breakpoint.HasValue && _cpu.PC == _breakpoint.Value) { _isRunning = false; - break; // Cleanly exit the while loop + break; } - // ----------------------------- + // --- Execute Instruction --- _cpu.Step(); - } + + // --- Check for End of Frame --- + if (_cpu.TotalTStates >= nextFrameTargetTStates) + { + // 1. Fire the 50Hz Interrupt! + _cpu.RequestInterrupt(); + + // 2. Advance the target to the next frame + nextFrameTargetTStates += TStatesPerFrame; + frameCount++; + + // 3. Throttle to real-time (50 frames per second = 20ms per frame) + long targetTimeMs = frameCount * 20; + long elapsedMs = stopwatch.ElapsedMilliseconds; + + if (elapsedMs < targetTimeMs) + { + // The CPU ran too fast! Put the thread to sleep to let reality catch up. + System.Threading.Thread.Sleep((int)(targetTimeMs - elapsedMs)); + } + + // Optional: Update the UI every 10 frames so you can watch it run safely + // without overwhelming the WinForms rendering engine. + if (frameCount % 10 == 0) + { + this.Invoke((MethodInvoker)delegate + { + UpdateDisplay(); + }); + } + } + //this.Invoke((MethodInvoker)delegate { + // UpdateDisplay(); + // }); + } } catch (Exception ex) { @@ -250,6 +292,11 @@ namespace Desktop string mnemonic; int instructionLength = 1; // Default to 1 byte cbOp = 0; + int opGroup = 0; + int targetBit = 0; + int regIdx = 0; + string[] regNames = { "B", "C", "D", "E", "H", "L", "(HL)", "A" }; + string targetReg = ""; switch (opcode) { @@ -388,6 +435,14 @@ namespace Desktop case 0x2B: mnemonic = "DEC HL"; break; + case 0x2E: + byte lImm = _memoryBus.Read((ushort)(currentPc + 1)); + mnemonic = $"LD L, 0x{lImm:X2}"; + instructionLength = 2; + break; + case 0x2F: + mnemonic = "CPL"; + break; case 0x30: sbyte jrNcOffset = (sbyte)_memoryBus.Read((ushort)(currentPc + 1)); ushort dest = (ushort)(currentPc + 2 + jrNcOffset); @@ -675,27 +730,37 @@ namespace Desktop break; case 0xCB: cbOp = _memoryBus.Read((ushort)(currentPc + 1)); - if (cbOp == 0x7E) + + // --- THE MISSING MATH EXTRACTION --- + opGroup = cbOp >> 6; // 00 = Shift, 01 = BIT, 10 = RES, 11 = SET + targetBit = (cbOp >> 3) & 0x07; // Extracts a number 0-7 + regIdx = cbOp & 0x07; // Extracts register index 0-7 + + // Map the 0-7 index directly to the Z80 register names + targetReg = regNames[regIdx]; + + if (opGroup == 0) // Shift/Rotate Group (0x00 to 0x3F) { - mnemonic = "BIT 7, (HL)"; + string[] shiftNames = { "RLC", "RRC", "RL", "RR", "SLA", "SRA", "SLL", "SRL" }; + string shiftOp = shiftNames[(cbOp >> 3) & 0x07]; + mnemonic = $"{shiftOp} {targetReg}"; } - else if (cbOp == 0x86) + else if (opGroup == 1) // BIT Group (0x40 to 0x7F) { - mnemonic = "RES 0, (HL)"; + mnemonic = $"BIT {targetBit}, {targetReg}"; } - else if (cbOp == 0xAE) + else if (opGroup == 2) // RES Group (0x80 to 0xBF) { - mnemonic = "RES 5, (HL)"; + mnemonic = $"RES {targetBit}, {targetReg}"; } - else if (cbOp == 0xC6) + else if (opGroup == 3) // SET Group (0xC0 to 0xFF) { - mnemonic = "SET 0, (HL)"; + mnemonic = $"SET {targetBit}, {targetReg}"; } else { - mnemonic = $"CB UNKNOWN (0x{cbOp:X2})"; + mnemonic = $"EXT UNKNOWN (ED {cbOp:X2})"; } - instructionLength = 2; break; case 0xCD: @@ -791,6 +856,10 @@ namespace Desktop mnemonic = $"LD (0x{addr73:X4}), SP"; instructionLength = 4; break; + case 0x78: + mnemonic = "IN A, (C)"; + instructionLength = 2; + break; case 0xB0: mnemonic = "LDIR"; instructionLength = 2; @@ -877,13 +946,11 @@ namespace Desktop { cbOp = _memoryBus.Read((ushort)(currentPc + 1)); - int opGroup = cbOp >> 6; - int targetBit = (cbOp >> 3) & 0x07; - int regIdx = cbOp & 0x07; + opGroup = cbOp >> 6; + targetBit = (cbOp >> 3) & 0x07; + regIdx = cbOp & 0x07; - // Map the 0-7 index directly to the Z80 register names - string[] regNames = { "B", "C", "D", "E", "H", "L", "(HL)", "A" }; - string targetReg = regNames[regIdx]; + targetReg = regNames[regIdx]; if (opGroup == 1) mnemonic = $"BIT {targetBit}, {targetReg}"; else if (opGroup == 2) mnemonic = $"RES {targetBit}, {targetReg}";