Interrupts added at 50fps. Dummy keyboard. Ready for graphics!
This commit is contained in:
117
Core/Cpu/Z80.cs
117
Core/Cpu/Z80.cs
@@ -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.");
|
||||||
|
|||||||
@@ -83,23 +83,65 @@ 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}";
|
||||||
|
|||||||
Reference in New Issue
Block a user