diff --git a/Core/Cpu/Z80.cs b/Core/Cpu/Z80.cs index 0d2326b..60dc8ac 100644 --- a/Core/Cpu/Z80.cs +++ b/Core/Cpu/Z80.cs @@ -40,11 +40,13 @@ namespace Core.Cpu // The Memory Bus private readonly IMemory _memory; private readonly IO_Bus _simpleIoBus; + public TapManager _tapManager; - public Z80(IMemory memory, IO_Bus ioBus) + public Z80(IMemory memory, IO_Bus ioBus, TapManager tapManager) { _memory = memory; _simpleIoBus = ioBus; + _tapManager = tapManager; Reset(); } @@ -129,6 +131,12 @@ namespace Core.Cpu public int Step() { + if (PC == 0x0556 && _tapManager.HasBlocks) + { + HandleInstantTapeLoad(); + return 100; // Return a dummy number of T-States for the hijacking + } + // Fetch the next opcode and increment the Program Counter byte opcode = _memory.Read(PC++); int tStates = ExecuteOpcode(opcode); @@ -138,6 +146,52 @@ namespace Core.Cpu return tStates; } + private void HandleInstantTapeLoad() + { + // 1. Grab the next block from the virtual cassette + byte[] block = _tapManager.GetNextBlock(); + if (block == null) return; // Tape ended unexpectedly + + // 2. Verify the block type. + // The ROM passes the expected flag (0x00 for Header, 0xFF for Data) in the A register. + byte expectedFlag = AF.High; + if (block[0] != expectedFlag) + { + // Tape loading error! Simulate a failure by clearing the Carry flag and returning. + AF.Low &= unchecked((byte)~0x01); + ExecuteRet(); + return; + } + + // 3. Copy the data block straight into the Spectrum's RAM! + // DE holds the number of bytes the ROM *wants*. We copy that much, skipping the Flag byte. + int bytesToCopy = DE.Word; + for (int i = 0; i < bytesToCopy; i++) + { + _memory.Write((ushort)(IX.Word + i), block[i + 1]); + } + + // 4. Update the registers exactly how the ROM would after a successful load + IX.Word = (ushort)(IX.Word + bytesToCopy); + DE.Word = 0; + + // 5. Set the Carry Flag to 1 (Success) + AF.Low |= 0x01; + + // 6. Simulate a standard 'RET' instruction to exit the LD-BYTES routine safely + ExecuteRet(); + } + + // A quick helper to simulate a RET instruction manually + private void ExecuteRet() + { + byte pcLow = _memory.Read(SP); + SP++; + byte pcHigh = _memory.Read(SP); + SP++; + PC = (ushort)((pcHigh << 8) | pcLow); + } + // Reads a 16-bit word from the current PC (Little-Endian) and advances PC by 2 private ushort FetchWord() { @@ -1624,6 +1678,17 @@ namespace Core.Cpu _memory.Write(address36, n36); return 19; + case 0x5E: // LD E, (IX+d) + // 1. Fetch the displacement byte and cast it to a signed sbyte + sbyte offset5E = (sbyte)FetchByte(); + + // 2. Calculate the exact memory address (IX + offset) + ushort address5E = (ushort)(IX.Word + offset5E); + + // 3. Read the byte from memory and drop it into the E register + DE.Low = _memory.Read(address5E); + + return 19; // 19 T-States case 0x74: // LD (IX+d), H // 1. Fetch the displacement byte and cast it to a signed sbyte sbyte offset74 = (sbyte)FetchByte(); @@ -1646,6 +1711,55 @@ namespace Core.Cpu _memory.Write(address75, HL.Low); return 19; + case 0x7E: // LD A, (IX+d) + // 1. Fetch the displacement byte and cast it to a signed sbyte + sbyte offset7E = (sbyte)FetchByte(); + + // 2. Calculate the exact memory address (IX + offset) + ushort address7E = (ushort)(IX.Word + offset7E); + + // 3. Read the byte from memory and drop it straight into the Accumulator (A) + AF.High = _memory.Read(address7E); + + return 19; + case 0xBE: // CP (IX+d) + // 1. Fetch the displacement byte and calculate the address + sbyte offsetBE = (sbyte)FetchByte(); + ushort addressBE = (ushort)(IX.Word + offsetBE); + + // 2. Read the value from memory + byte cpVal = _memory.Read(addressBE); + + // 3. Perform the phantom subtraction + int aVal = AF.High; + result = aVal - cpVal; + + // --- 8-Bit Compare Flag Calculation (Identical to SUB) --- + newFlags = 0; + + // S Flag (Bit 7): Set if the phantom result is negative + if ((result & 0x80) != 0) newFlags |= 0x80; + + // Z Flag (Bit 6): Set if A perfectly matches the memory value (A - value == 0) + if ((result & 0xFF) == 0) newFlags |= 0x40; + + // H Flag (Bit 4): Set if there was a borrow from Bit 3 + if (((aVal & 0x0F) - (cpVal & 0x0F)) < 0) newFlags |= 0x10; + + // P/V Flag (Bit 2): Set on Overflow + if ((((aVal ^ cpVal) & (aVal ^ result)) & 0x80) != 0) newFlags |= 0x04; + + // N Flag (Bit 1): Always set to 1 for Subtractions/Compares + newFlags |= 0x02; + + // C Flag (Bit 0): Set if A was smaller than the memory value + if (aVal < cpVal) newFlags |= 0x01; + + AF.Low = newFlags; + + // CRITICAL: Notice we do NOT update AF.High! The Accumulator is preserved. + + return 19; // 19 T-States case 0xE1: // POP IX // 1. Read the low byte from the top of the stack byte popLow = _memory.Read(SP); @@ -1875,6 +1989,42 @@ namespace Core.Cpu AF.High = (byte)result; return 19; + case 0xBE: // CP (IY+d) + // 1. Fetch the displacement byte and calculate the address using IY + sbyte offsetBE = (sbyte)FetchByte(); + ushort addressBE = (ushort)(IY.Word + offsetBE); + + // 2. Read the value from memory + byte cpVal = _memory.Read(addressBE); + + // 3. Perform the phantom subtraction + aVal = AF.High; + result = aVal - cpVal; + + // --- 8-Bit Compare Flag Calculation (Identical to SUB) --- + newFlags = 0; + + // S Flag (Bit 7): Set if the phantom result is negative + if ((result & 0x80) != 0) newFlags |= 0x80; + + // Z Flag (Bit 6): Set if A perfectly matches the memory value + if ((result & 0xFF) == 0) newFlags |= 0x40; + + // H Flag (Bit 4): Set if there was a borrow from Bit 3 + if (((aVal & 0x0F) - (cpVal & 0x0F)) < 0) newFlags |= 0x10; + + // P/V Flag (Bit 2): Set on Overflow + if ((((aVal ^ cpVal) & (aVal ^ result)) & 0x80) != 0) newFlags |= 0x04; + + // N Flag (Bit 1): Always set to 1 for Subtractions/Compares + newFlags |= 0x02; + + // C Flag (Bit 0): Set if A was smaller than the memory value + if (aVal < cpVal) newFlags |= 0x01; + + AF.Low = newFlags; + + return 19; // 19 T-States case 0xCB: // The FD CB nested prefix { sbyte displacement = (sbyte)FetchByte(); diff --git a/Core/Io/TapManager.cs b/Core/Io/TapManager.cs new file mode 100644 index 0000000..0c14462 --- /dev/null +++ b/Core/Io/TapManager.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace Core.Io +{ + public class TapManager + { + private Queue _blocks = new Queue(); + + + public void LoadTapData(byte[] fileData) + { + _blocks.Clear(); + int position = 0; + + while (position < fileData.Length) + { + // 1. Read the 16-bit block length (Little Endian) + int blockLength = fileData[position] | (fileData[position + 1] << 8); + position += 2; + + // 2. Extract the block payload + byte[] blockData = new byte[blockLength]; + Array.Copy(fileData, position, blockData, 0, blockLength); + position += blockLength; + + // 3. Queue it up + _blocks.Enqueue(blockData); + } + } + + public byte[] GetNextBlock() + { + return _blocks.Count > 0 ? _blocks.Dequeue() : null; + } + + public bool HasBlocks => _blocks.Count > 0; + } +} diff --git a/Desktop/DebuggerForm.cs b/Desktop/DebuggerForm.cs index 7b6dcb8..8d9b855 100644 --- a/Desktop/DebuggerForm.cs +++ b/Desktop/DebuggerForm.cs @@ -850,6 +850,13 @@ namespace Desktop mnemonic = $"LD (IX{sign}{d}), 0x{n:X2}"; instructionLength = 4; } + else if (ddOpcode == 0x5E) // LD E, (IX+d) + { + sbyte d = (sbyte)_memoryBus.Read((ushort)(currentPc + 2)); + string sign = d >= 0 ? "+" : ""; + mnemonic = $"LD E, (IX{sign}{d})"; + instructionLength = 3; + } else if (ddOpcode == 0x74) // LD (IX+d), H { sbyte d = (sbyte)_memoryBus.Read((ushort)(currentPc + 2)); @@ -864,6 +871,20 @@ namespace Desktop mnemonic = $"LD (IX{sign}{d}), L"; instructionLength = 3; } + else if (ddOpcode == 0x7E) // LD A, (IX+d) + { + sbyte d = (sbyte)_memoryBus.Read((ushort)(currentPc + 2)); + string sign = d >= 0 ? "+" : ""; + mnemonic = $"LD A, (IX{sign}{d})"; + instructionLength = 3; + } + else if (ddOpcode == 0xBE) // CP (IX+d) + { + sbyte d = (sbyte)_memoryBus.Read((ushort)(currentPc + 2)); + string sign = d >= 0 ? "+" : ""; + mnemonic = $"CP (IX{sign}{d})"; + instructionLength = 3; + } else if (ddOpcode == 0xE1) // POP IX { mnemonic = "POP IX"; diff --git a/Desktop/Form1.Designer.cs b/Desktop/Form1.Designer.cs index 6d1916f..4c01ab9 100644 --- a/Desktop/Form1.Designer.cs +++ b/Desktop/Form1.Designer.cs @@ -29,37 +29,72 @@ private void InitializeComponent() { picScreen = new PictureBox(); + menuStrip1 = new MenuStrip(); + fileToolStripMenuItem = new ToolStripMenuItem(); + openToolStripMenuItem = new ToolStripMenuItem(); ((System.ComponentModel.ISupportInitialize)picScreen).BeginInit(); + menuStrip1.SuspendLayout(); SuspendLayout(); // // picScreen // - picScreen.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; picScreen.BackColor = Color.Black; - picScreen.Location = new Point(13, 13); + picScreen.Location = new Point(13, 33); picScreen.Margin = new Padding(4); picScreen.Name = "picScreen"; - picScreen.Size = new Size(960, 768); + picScreen.Size = new Size(900, 720); picScreen.SizeMode = PictureBoxSizeMode.Zoom; picScreen.TabIndex = 0; picScreen.TabStop = false; // + // menuStrip1 + // + menuStrip1.ImageScalingSize = new Size(24, 24); + menuStrip1.Items.AddRange(new ToolStripItem[] { fileToolStripMenuItem }); + menuStrip1.Location = new Point(0, 0); + menuStrip1.Name = "menuStrip1"; + menuStrip1.Size = new Size(922, 33); + menuStrip1.TabIndex = 1; + menuStrip1.Text = "menuStrip1"; + // + // fileToolStripMenuItem + // + fileToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { openToolStripMenuItem }); + fileToolStripMenuItem.Name = "fileToolStripMenuItem"; + fileToolStripMenuItem.Size = new Size(54, 29); + fileToolStripMenuItem.Text = "File"; + // + // openToolStripMenuItem + // + openToolStripMenuItem.Name = "openToolStripMenuItem"; + openToolStripMenuItem.Size = new Size(158, 34); + openToolStripMenuItem.Text = "Open"; + openToolStripMenuItem.Click += loadTAPToolStripMenuItem_Click; + // // Form1 // AutoScaleDimensions = new SizeF(10F, 25F); AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(987, 791); + ClientSize = new Size(922, 760); Controls.Add(picScreen); + Controls.Add(menuStrip1); KeyPreview = true; + MainMenuStrip = menuStrip1; Margin = new Padding(4); Name = "Form1"; Text = "Parsons Sinclair ZX Spectrum 48K - 2026"; ((System.ComponentModel.ISupportInitialize)picScreen).EndInit(); + menuStrip1.ResumeLayout(false); + menuStrip1.PerformLayout(); ResumeLayout(false); + PerformLayout(); } #endregion private PictureBox picScreen; + private MenuStrip menuStrip1; + private ToolStripMenuItem fileToolStripMenuItem; + private ToolStripMenuItem openToolStripMenuItem; } } diff --git a/Desktop/Form1.cs b/Desktop/Form1.cs index 8ef1a55..935e053 100644 --- a/Desktop/Form1.cs +++ b/Desktop/Form1.cs @@ -14,6 +14,7 @@ namespace Desktop private Z80 _cpu = null!; private MemoryBus _memoryBus = null!; private IO_Bus _simpleIoBus = null!; + private TapManager _tapManager = null!; private int _ulaFrameCount = 0; // The 16 physical colors of the ZX Spectrum (ARGB format) @@ -52,12 +53,12 @@ namespace Desktop { _memoryBus = new MemoryBus(); _simpleIoBus = new IO_Bus(); - + _tapManager = new TapManager(); _memoryBus.CrapRAMData(); byte[] romData = RomLoader.Load("48.rom"); _memoryBus.LoadRom(romData); - _cpu = new Z80(_memoryBus, _simpleIoBus); + _cpu = new Z80(_memoryBus, _simpleIoBus, _tapManager); // Pass 'this' so the DebuggerForm can talk back to this main window DebuggerForm debugger = new DebuggerForm(_cpu, _memoryBus, this); @@ -69,6 +70,25 @@ namespace Desktop } } + // Inside Desktop/Form1.cs + private void loadTAPToolStripMenuItem_Click(object sender, EventArgs e) + { + using (OpenFileDialog ofd = new OpenFileDialog()) + { + ofd.Filter = "Spectrum TAP Files|*.tap"; + if (ofd.ShowDialog() == DialogResult.OK) + { + // The Desktop UI reads the file from the hard drive + byte[] tapBytes = System.IO.File.ReadAllBytes(ofd.FileName); + + // The pure Core logic processes the bytes + _cpu._tapManager.LoadTapData(tapBytes); + + MessageBox.Show("Tape inserted! Type LOAD \"\" and press Enter.", "Tape Deck"); + } + } + } + // Public so the Debugger's background thread can call it 50 times a second public void RenderScreen() { diff --git a/Desktop/Form1.resx b/Desktop/Form1.resx index 8b2ff64..b48baf1 100644 --- a/Desktop/Form1.resx +++ b/Desktop/Form1.resx @@ -117,4 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + 17, 17 + \ No newline at end of file