Interrupts added at 50fps. Dummy keyboard. Ready for graphics!

This commit is contained in:
2026-04-15 15:44:24 +01:00
parent d9966099f8
commit 960f2b85cc
2 changed files with 205 additions and 23 deletions

View File

@@ -79,6 +79,60 @@ namespace Core.Cpu
TotalTStates = 0; // Reset the system clock! 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() public int Step()
{ {
// Fetch the next opcode and increment the Program Counter // Fetch the next opcode and increment the Program Counter
@@ -494,6 +548,9 @@ namespace Core.Cpu
return 7; return 7;
case 0x24: HL.High = Inc8(HL.High); return 4; // INC H case 0x24: HL.High = Inc8(HL.High); return 4; // INC H
case 0x2C: HL.Low = Inc8(HL.Low); return 4; // INC L case 0x2C: HL.Low = Inc8(HL.Low); return 4; // INC L
case 0x2E: // LD L, n
HL.Low = FetchByte();
return 7;
case 0x34: case 0x34:
_memory.Write(HL.Word, Inc8(_memory.Read(HL.Word))); _memory.Write(HL.Word, Inc8(_memory.Read(HL.Word)));
return 11; // INC (HL) takes 11 T-States 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 0x1D: DE.Low = Dec8(DE.Low); return 4; // DEC E
case 0x25: HL.High = Dec8(HL.High); return 4; // DEC H case 0x25: HL.High = Dec8(HL.High); return 4; // DEC H
case 0x2D: HL.Low = Dec8(HL.Low); return 4; // DEC L 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: case 0x35:
_memory.Write(HL.Word, Dec8(_memory.Read(HL.Word))); _memory.Write(HL.Word, Dec8(_memory.Read(HL.Word)));
return 11; // DEC (HL) takes 11 T-States return 11; // DEC (HL) takes 11 T-States
@@ -1158,6 +1224,25 @@ namespace Core.Cpu
_memory.Write(dest73, (byte)SP); _memory.Write(dest73, (byte)SP);
_memory.Write((ushort)(dest73 + 1), (byte)(SP >> 8)); _memory.Write((ushort)(dest73 + 1), (byte)(SP >> 8));
return 20; 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 case 0xB0: // LDIR
// 1. Read byte from (HL) // 1. Read byte from (HL)
val = _memory.Read(HL.Word); val = _memory.Read(HL.Word);
@@ -1271,7 +1356,37 @@ namespace Core.Cpu
break; // Proceed to write-back break; // Proceed to write-back
case 0: // ALL Shift/Rotate Instructions 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: default:
throw new Exception("Invalid CB operation."); throw new Exception("Invalid CB operation.");

View File

@@ -83,24 +83,66 @@ namespace Desktop
// Fire up a background thread
// Fire up a background thread // Fire up a background thread
await Task.Run(() => await Task.Run(() =>
{ {
try 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) while (_isRunning)
{ {
// --- NEW: Breakpoint Check --- // --- Breakpoint Check ---
// We check BEFORE stepping so it stops exactly on the instruction
if (_breakpoint.HasValue && _cpu.PC == _breakpoint.Value) if (_breakpoint.HasValue && _cpu.PC == _breakpoint.Value)
{ {
_isRunning = false; _isRunning = false;
break; // Cleanly exit the while loop break;
} }
// -----------------------------
// --- Execute Instruction ---
_cpu.Step(); _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) catch (Exception ex)
{ {
@@ -250,6 +292,11 @@ namespace Desktop
string mnemonic; string mnemonic;
int instructionLength = 1; // Default to 1 int instructionLength = 1; // Default to 1
byte cbOp = 0; 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) switch (opcode)
{ {
@@ -388,6 +435,14 @@ namespace Desktop
case 0x2B: case 0x2B:
mnemonic = "DEC HL"; mnemonic = "DEC HL";
break; 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: case 0x30:
sbyte jrNcOffset = (sbyte)_memoryBus.Read((ushort)(currentPc + 1)); sbyte jrNcOffset = (sbyte)_memoryBus.Read((ushort)(currentPc + 1));
ushort dest = (ushort)(currentPc + 2 + jrNcOffset); ushort dest = (ushort)(currentPc + 2 + jrNcOffset);
@@ -675,27 +730,37 @@ namespace Desktop
break; break;
case 0xCB: case 0xCB:
cbOp = _memoryBus.Read((ushort)(currentPc + 1)); 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 else
{ {
mnemonic = $"CB UNKNOWN (0x{cbOp:X2})"; mnemonic = $"EXT UNKNOWN (ED {cbOp:X2})";
} }
instructionLength = 2; instructionLength = 2;
break; break;
case 0xCD: case 0xCD:
@@ -791,6 +856,10 @@ namespace Desktop
mnemonic = $"LD (0x{addr73:X4}), SP"; mnemonic = $"LD (0x{addr73:X4}), SP";
instructionLength = 4; instructionLength = 4;
break; break;
case 0x78:
mnemonic = "IN A, (C)";
instructionLength = 2;
break;
case 0xB0: case 0xB0:
mnemonic = "LDIR"; mnemonic = "LDIR";
instructionLength = 2; instructionLength = 2;
@@ -877,13 +946,11 @@ namespace Desktop
{ {
cbOp = _memoryBus.Read((ushort)(currentPc + 1)); cbOp = _memoryBus.Read((ushort)(currentPc + 1));
int opGroup = cbOp >> 6; opGroup = cbOp >> 6;
int targetBit = (cbOp >> 3) & 0x07; targetBit = (cbOp >> 3) & 0x07;
int regIdx = cbOp & 0x07; regIdx = cbOp & 0x07;
// Map the 0-7 index directly to the Z80 register names targetReg = regNames[regIdx];
string[] regNames = { "B", "C", "D", "E", "H", "L", "(HL)", "A" };
string targetReg = regNames[regIdx];
if (opGroup == 1) mnemonic = $"BIT {targetBit}, {targetReg}"; if (opGroup == 1) mnemonic = $"BIT {targetBit}, {targetReg}";
else if (opGroup == 2) mnemonic = $"RES {targetBit}, {targetReg}"; else if (opGroup == 2) mnemonic = $"RES {targetBit}, {targetReg}";