From 340583d663d097a1d49b67edb38ea3378c1cb4bb Mon Sep 17 00:00:00 2001 From: Marc Parsons Date: Thu, 9 Apr 2026 14:35:38 +0100 Subject: [PATCH] Implemented a load of Z80 OpCodes. Added SimpleIOBus. --- Core/Cpu/Z80.cs | 121 ++++++++++++++++++++++++-- Core/Interfaces/IIoBus.cs | 8 ++ Core/Io/SimpleIoBus.cs | 20 +++++ Core/Memory/MemoryBus.cs | 7 +- Desktop/DebuggerForm.Designer.cs | 23 ++--- Desktop/DebuggerForm.cs | 140 ++++++++++++++++++++++++++++--- Desktop/Form1.Designer.cs | 3 +- Desktop/Form1.cs | 8 +- 8 files changed, 292 insertions(+), 38 deletions(-) create mode 100644 Core/Interfaces/IIoBus.cs create mode 100644 Core/Io/SimpleIoBus.cs diff --git a/Core/Cpu/Z80.cs b/Core/Cpu/Z80.cs index 0969367..a52b541 100644 --- a/Core/Cpu/Z80.cs +++ b/Core/Cpu/Z80.cs @@ -8,6 +8,10 @@ namespace Core.Cpu //T-State counter public long TotalTStates { get; set; } + // Interrupt Flip-Flops + public bool IFF1; + public bool IFF2; + // Main Register Set public RegisterPair AF; public RegisterPair BC; @@ -32,11 +36,13 @@ namespace Core.Cpu // The Memory Bus private readonly IMemory _memory; + private readonly IIoBus _ioBus; - public Z80(IMemory memory) + public Z80(IMemory memory, IIoBus ioBus) { _memory = memory; - Reset(); + _ioBus = ioBus; + Reset(); } public void Reset() @@ -62,6 +68,19 @@ namespace Core.Cpu return tStates; } + // Reads a 16-bit word from the current PC (Little-Endian) and advances PC by 2 + private ushort FetchWord() + { + byte low = _memory.Read(PC++); + byte high = _memory.Read(PC++); + return (ushort)((high << 8) | low); + } + + private byte FetchByte() + { + return _memory.Read(PC++); + } + public string GetFlagsString() { byte f = AF.Low; @@ -75,18 +94,110 @@ namespace Core.Cpu $"C:{f & 1}"; } + private void Sbc(byte value) + { + byte a = AF.High; + byte carry = (byte)(AF.Low & 0x01); // Get the current Carry flag (Bit 0) + + // Calculate the raw integer result to check for borrows/underflows + int result = a - value - carry; + + // Update the Accumulator + AF.High = (byte)result; + + // --- Update Flags (F Register) --- + AF.Low = 0; // Clear all flags + + // Sign Flag (Bit 7) + if ((result & 0x80) != 0) AF.Low |= 0x80; + + // Zero Flag (Bit 6) + if ((byte)result == 0) AF.Low |= 0x40; + + // Half-Carry Flag (Bit 4) - Set if borrow from bit 4 + if (((a & 0x0F) - (value & 0x0F) - carry) < 0) AF.Low |= 0x10; + + // Overflow Flag (Bit 2) - Set if operands have different signs and result sign changes + if ((((a ^ value) & 0x80) != 0) && (((a ^ result) & 0x80) != 0)) AF.Low |= 0x04; + + // Subtract Flag (Bit 1) - ALWAYS set for subtraction + AF.Low |= 0x02; + + // Carry Flag (Bit 0) - Set if the overall result dropped below 0 + if (result < 0) AF.Low |= 0x01; + } + private int ExecuteOpcode(byte opcode) { switch (opcode) { - case 0x00: // NOP (No Operation) - return 4; // Takes 4 T-states + case 0x00: // NOP + return 4; + case 0x11: //LD DE, nn + DE.Word = FetchWord(); + return 10; + case 0x2B: // DEC HL + HL.Word--; + return 6; + case 0x36: // LD (HL), n + byte nValue = FetchByte(); + _memory.Write(HL.Word, nValue); + return 10; + case 0x3E: //LD A, n + AF.High = FetchByte(); + return 7; + case 0x47: // LD B, A + BC.High = AF.High; + return 4; + case 0x62: // LD H, D + HL.High = DE.High; + return 4; + case 0x6B: // LD L, E + HL.Low = DE.Low; + return 4; + case 0xC3: + PC = FetchWord(); + return 10; + case 0xD3: // OUT (n), A + byte portOffset = FetchByte(); - // We will expand this massive list soon! + // The Z80 puts 'A' on the top 8 bits, and 'n' on the bottom 8 bits of the port address + ushort portAddress = (ushort)((AF.High << 8) | portOffset); + + _ioBus.Write(portAddress, AF.High); + + return 11; // Takes 11 T-States + case 0xDE: // SBC A, n + Sbc(FetchByte()); + return 7; + case 0xED: + return ExecuteExtendedPrefix(); + case 0xF3: // DI (Disable Interrupts) + IFF1 = false; + IFF2 = false; + return 4; + case 0xAF: // XOR A + AF.High = 0; + AF.Low = 0x44; + return 4; default: throw new NotImplementedException($"Opcode 0x{opcode:X2} at PC 0x{(PC - 1):X4} is not implemented."); } } + private int ExecuteExtendedPrefix() + { + // Fetch the actual extended instruction + byte extendedOpcode = _memory.Read(PC++); + + switch (extendedOpcode) + { + case 0x47: // LD I, A + I = AF.High; + return 9; + default: + throw new NotImplementedException($"Extended ED Opcode 0x{extendedOpcode:X2} at PC 0x{(PC - 1):X4} is not implemented."); + } + } } } \ No newline at end of file diff --git a/Core/Interfaces/IIoBus.cs b/Core/Interfaces/IIoBus.cs new file mode 100644 index 0000000..c28821e --- /dev/null +++ b/Core/Interfaces/IIoBus.cs @@ -0,0 +1,8 @@ +namespace Core.Interfaces +{ + public interface IIoBus + { + byte Read(ushort port); + void Write(ushort port, byte value); + } +} diff --git a/Core/Io/SimpleIoBus.cs b/Core/Io/SimpleIoBus.cs new file mode 100644 index 0000000..dbb90d9 --- /dev/null +++ b/Core/Io/SimpleIoBus.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; +using Core.Interfaces; + +namespace Core.Io +{ + public class SimpleIoBus : IIoBus + { + public byte Read(ushort port) + { + // If the CPU reads an unconnected port, the Z80 usually sees 0xFF + return 0xFF; + } + + public void Write(ushort port, byte value) + { + // For now, let's just log it to the Visual Studio Output window + Debug.WriteLine($"Hardware I/O Write -> Port: 0x{port:X4}, Value: 0x{value:X2}"); + } + } +} \ No newline at end of file diff --git a/Core/Memory/MemoryBus.cs b/Core/Memory/MemoryBus.cs index c135787..34280fb 100644 --- a/Core/Memory/MemoryBus.cs +++ b/Core/Memory/MemoryBus.cs @@ -24,15 +24,14 @@ namespace Core.Memory if (address < 0x4000) { - // Attempted to write to the ROM area. - // We simply ignore the write command, just like real hardware. + // Cannot write to ROM - Do nothing - maybe throw an exception return; } _memory[address] = value; } - // Helper method to load the original Sinclair ROM file + // Load the ROM file public void LoadRom(byte[] romData) { if (romData.Length > 0x4000) @@ -40,7 +39,7 @@ namespace Core.Memory throw new ArgumentException("ROM file exceeds the 16KB capacity of Bank 0."); } - // Copy the ROM data into the very beginning of the memory array + // Copy the ROM Array.Copy(romData, 0, _memory, 0, romData.Length); } } diff --git a/Desktop/DebuggerForm.Designer.cs b/Desktop/DebuggerForm.Designer.cs index e83dece..ab2cf75 100644 --- a/Desktop/DebuggerForm.Designer.cs +++ b/Desktop/DebuggerForm.Designer.cs @@ -129,7 +129,7 @@ // txtMemoryStart // txtMemoryStart.Location = new Point(238, 10); - txtMemoryStart.Margin = new Padding(2, 2, 2, 2); + txtMemoryStart.Margin = new Padding(2); txtMemoryStart.Name = "txtMemoryStart"; txtMemoryStart.Size = new Size(121, 27); txtMemoryStart.TabIndex = 9; @@ -140,7 +140,7 @@ // btnStep // btnStep.Location = new Point(9, 315); - btnStep.Margin = new Padding(2, 2, 2, 2); + btnStep.Margin = new Padding(2); btnStep.Name = "btnStep"; btnStep.Size = new Size(90, 27); btnStep.TabIndex = 12; @@ -151,17 +151,18 @@ // btnRun // btnRun.Location = new Point(122, 315); - btnRun.Margin = new Padding(2, 2, 2, 2); + btnRun.Margin = new Padding(2); btnRun.Name = "btnRun"; btnRun.Size = new Size(90, 27); btnRun.TabIndex = 13; btnRun.Text = "Run"; btnRun.UseVisualStyleBackColor = true; + btnRun.Click += btnRun_Click; // // btnRefreshMemory // btnRefreshMemory.Location = new Point(376, 7); - btnRefreshMemory.Margin = new Padding(2, 2, 2, 2); + btnRefreshMemory.Margin = new Padding(2); btnRefreshMemory.Name = "btnRefreshMemory"; btnRefreshMemory.Size = new Size(90, 27); btnRefreshMemory.TabIndex = 14; @@ -172,7 +173,7 @@ // txtMemoryView // txtMemoryView.Location = new Point(88, 80); - txtMemoryView.Margin = new Padding(2, 2, 2, 2); + txtMemoryView.Margin = new Padding(2); txtMemoryView.Name = "txtMemoryView"; txtMemoryView.Size = new Size(416, 195); txtMemoryView.TabIndex = 15; @@ -182,7 +183,7 @@ // lstDisassembly.FormattingEnabled = true; lstDisassembly.Location = new Point(533, 11); - lstDisassembly.Margin = new Padding(2, 2, 2, 2); + lstDisassembly.Margin = new Padding(2); lstDisassembly.Name = "lstDisassembly"; lstDisassembly.Size = new Size(186, 264); lstDisassembly.TabIndex = 16; @@ -191,7 +192,7 @@ // lstStack.FormattingEnabled = true; lstStack.Location = new Point(723, 11); - lstStack.Margin = new Padding(2, 2, 2, 2); + lstStack.Margin = new Padding(2); lstStack.Name = "lstStack"; lstStack.Size = new Size(186, 264); lstStack.TabIndex = 17; @@ -199,7 +200,7 @@ // btnExit // btnExit.Location = new Point(818, 313); - btnExit.Margin = new Padding(2, 2, 2, 2); + btnExit.Margin = new Padding(2); btnExit.Name = "btnExit"; btnExit.Size = new Size(90, 27); btnExit.TabIndex = 18; @@ -228,9 +229,9 @@ Controls.Add(lblDE); Controls.Add(lblBC); Controls.Add(lblAF); - Margin = new Padding(2, 2, 2, 2); + Margin = new Padding(2); Name = "DebuggerForm"; - Text = "DebuggerForm"; + Text = "Debugger"; ResumeLayout(false); PerformLayout(); } @@ -250,9 +251,9 @@ private Button btnRun; private Button btnRefreshMemory; private RichTextBox txtMemoryView; - private ListBox lstDisassembly; private ListBox lstStack; private Button btnExit; + public ListBox lstDisassembly; //private TextBox textBox4; } } \ No newline at end of file diff --git a/Desktop/DebuggerForm.cs b/Desktop/DebuggerForm.cs index 2efccc2..29e40fb 100644 --- a/Desktop/DebuggerForm.cs +++ b/Desktop/DebuggerForm.cs @@ -10,6 +10,7 @@ namespace Desktop { private readonly Z80 _cpu; private readonly MemoryBus _memoryBus; + private bool _isRunning = false; public DebuggerForm(Z80 cpu, MemoryBus memoryBus) { @@ -46,6 +47,53 @@ namespace Desktop UpdateDisplay(); } + private async void btnRun_Click(object sender, EventArgs e) + { + // If it is already running, this button acts as a STOP button + if (_isRunning) + { + _isRunning = false; + btnRun.Text = "Run"; + return; + } + + // Start the run state + _isRunning = true; + btnRun.Text = "Stop"; + + // Fire up a background thread so the Windows UI doesn't freeze + await Task.Run(() => + { + try + { + // Free-run the CPU until the flag is flipped or it crashes! + while (_isRunning) + { + _cpu.Step(); + } + } + catch (Exception ex) + { + _isRunning = false; + + // We are on a background thread. We MUST use Invoke to tell the + // main UI thread to update the labels and show the message box! + this.Invoke((MethodInvoker)delegate + { + btnRun.Text = "Run"; + UpdateDisplay(); + MessageBox.Show(ex.Message, "CPU Break", MessageBoxButtons.OK, MessageBoxIcon.Information); + }); + } + }); + + // If the user clicked Stop manually (no exception thrown), we still want to update the UI + if (!_isRunning) + { + UpdateDisplay(); + } + } + private void btnExit_Click(object sender, EventArgs e) { Environment.Exit(0); @@ -68,6 +116,8 @@ namespace Desktop // 3. Update Memory Viewer UpdateMemoryView(); + UpdateStackView(); + UpdateDisassemblyView(); } private void UpdateMemoryView() @@ -108,9 +158,6 @@ namespace Desktop private void UpdateStackView() { lstStack.Items.Clear(); - - // The Z80 stack starts at 0xFFFF and grows downwards. - // If SP is at the very top (e.g., 0xFFFF), we don't want to read past the end of memory and crash! int itemsToShow = 5; ushort currentSp = _cpu.SP; @@ -139,36 +186,103 @@ namespace Desktop { lstDisassembly.Items.Clear(); + // THIS is the critical link! It forces the top of the list + // to always be exactly where the CPU currently is. ushort currentPc = _cpu.PC; + int instructionsToShow = 8; for (int i = 0; i < instructionsToShow; i++) { byte opcode = _memoryBus.Read(currentPc); string mnemonic; - int instructionLength = 1; // Default to 1 byte long + int instructionLength = 1; // Default to 1 - // This switch statement will grow as you add more opcodes to the CPU! switch (opcode) { - case 0x00: - mnemonic = "NOP"; + case 0x00: mnemonic = "NOP"; break; + case 0x11: + // LD DE, nn + byte deLow = _memoryBus.Read((ushort)(currentPc + 1)); + byte deHigh = _memoryBus.Read((ushort)(currentPc + 2)); + mnemonic = $"LD DE, 0x{deHigh:X2}{deLow:X2}"; + instructionLength = 3; + break; + case 0x2B: + mnemonic = "DEC HL"; + break; + case 0x36: + byte memValue = _memoryBus.Read((ushort)(currentPc + 1)); + mnemonic = $"LD (HL), 0x{memValue:X2}"; + instructionLength = 2; break; case 0x3E: - // LD A, n (Loads the next byte into register A) - byte nextByte = _memoryBus.Read((ushort)(currentPc + 1)); - mnemonic = $"LD A, 0x{nextByte:X2}"; - instructionLength = 2; // This instruction takes up 2 bytes + mnemonic = $"LD A, 0x{_memoryBus.Read((ushort)(currentPc + 1)):X2}"; + instructionLength = 2; + break; + case 0x47: + mnemonic = "LD B, A"; + break; + case 0x62: + mnemonic = "LD H, D"; + break; + case 0x6B: + mnemonic = "LD L, E"; + break; + case 0xAF: + mnemonic = "XOR A"; + break; + case 0xC3: + // JP nn + byte jpLow = _memoryBus.Read((ushort)(currentPc + 1)); + byte jpHigh = _memoryBus.Read((ushort)(currentPc + 2)); + mnemonic = $"JP 0x{jpHigh:X2}{jpLow:X2}"; + instructionLength = 3; + break; + case 0xD3: + byte outPort = _memoryBus.Read((ushort)(currentPc + 1)); + mnemonic = $"OUT (0x{outPort:X2}), A"; + instructionLength = 2; + break; + case 0xDE: + byte sbcValue = _memoryBus.Read((ushort)(currentPc + 1)); + mnemonic = $"SBC A, 0x{sbcValue:X2}"; + instructionLength = 2; + break; + case 0xED: + byte extendedOp = _memoryBus.Read((ushort)(currentPc + 1)); + + switch (extendedOp) + { + // Example: ED 47 is LD I, A + case 0x47: + mnemonic = "LD I, A"; + instructionLength = 2; // 0xED + 0x47 + break; + + // Example: ED B0 is LDIR (a massive block copy instruction) + case 0xB0: + mnemonic = "LDIR"; + instructionLength = 2; + break; + + default: + mnemonic = $"EXT UNKNOWN (ED {extendedOp:X2})"; + instructionLength = 2; // Most ED instructions are 2 bytes, but some have operands! + break; + } + break; + case 0xF3: + mnemonic = "DI"; break; default: mnemonic = $"UNKNOWN (0x{opcode:X2})"; break; } - // Add to the list box lstDisassembly.Items.Add($"{currentPc:X4}: {mnemonic}"); - // Advance to the start of the next instruction + // Advance the fake PC just for drawing the next line in the UI currentPc += (ushort)instructionLength; } } diff --git a/Desktop/Form1.Designer.cs b/Desktop/Form1.Designer.cs index ca92e65..4abfaa3 100644 --- a/Desktop/Form1.Designer.cs +++ b/Desktop/Form1.Designer.cs @@ -36,8 +36,7 @@ AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(800, 450); Name = "Form1"; - Text = "Form1"; - //this.Load += new System.EventHandler(this.Form1_Load); + Text = "Parsons Sinclair ZX Spectrum 48K - 2026"; ResumeLayout(false); } diff --git a/Desktop/Form1.cs b/Desktop/Form1.cs index 978d9e3..f86aefe 100644 --- a/Desktop/Form1.cs +++ b/Desktop/Form1.cs @@ -1,6 +1,7 @@ using System; using System.Windows.Forms; using Core.Cpu; +using Core.Io; using Core.Memory; namespace Desktop @@ -9,6 +10,7 @@ namespace Desktop { private Z80 _cpu = null!; private MemoryBus _memoryBus = null!; + private SimpleIoBus _simpleIoBus = null!; public Form1() { @@ -22,16 +24,16 @@ namespace Desktop { // 1. Initialize the memory bus _memoryBus = new MemoryBus(); + _simpleIoBus = new SimpleIoBus(); - // 2. Load the ROM from disk - // Make sure "48.rom" matches the name of the file you added to your project + // 2. Load the ROM byte[] romData = RomLoader.Load("48.rom"); // 3. Inject the ROM into the memory bus _memoryBus.LoadRom(romData); // 4. Initialize the CPU with the populated memory - _cpu = new Z80(_memoryBus); + _cpu = new Z80(_memoryBus, _simpleIoBus); DebuggerForm debugger = new DebuggerForm(_cpu, _memoryBus); debugger.Show();