Implemented a load of Z80 OpCodes. Added SimpleIOBus.

This commit is contained in:
2026-04-09 14:35:38 +01:00
parent f14d2c4ccc
commit 340583d663
8 changed files with 292 additions and 38 deletions

View File

@@ -8,6 +8,10 @@ namespace Core.Cpu
//T-State counter //T-State counter
public long TotalTStates { get; set; } public long TotalTStates { get; set; }
// Interrupt Flip-Flops
public bool IFF1;
public bool IFF2;
// Main Register Set // Main Register Set
public RegisterPair AF; public RegisterPair AF;
public RegisterPair BC; public RegisterPair BC;
@@ -32,10 +36,12 @@ namespace Core.Cpu
// The Memory Bus // The Memory Bus
private readonly IMemory _memory; private readonly IMemory _memory;
private readonly IIoBus _ioBus;
public Z80(IMemory memory) public Z80(IMemory memory, IIoBus ioBus)
{ {
_memory = memory; _memory = memory;
_ioBus = ioBus;
Reset(); Reset();
} }
@@ -62,6 +68,19 @@ namespace Core.Cpu
return tStates; 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() public string GetFlagsString()
{ {
byte f = AF.Low; byte f = AF.Low;
@@ -75,18 +94,110 @@ namespace Core.Cpu
$"C:{f & 1}"; $"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) private int ExecuteOpcode(byte opcode)
{ {
switch (opcode) switch (opcode)
{ {
case 0x00: // NOP (No Operation) case 0x00: // NOP
return 4; // Takes 4 T-states 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: default:
throw new NotImplementedException($"Opcode 0x{opcode:X2} at PC 0x{(PC - 1):X4} is not implemented."); 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.");
}
}
} }
} }

View File

@@ -0,0 +1,8 @@
namespace Core.Interfaces
{
public interface IIoBus
{
byte Read(ushort port);
void Write(ushort port, byte value);
}
}

20
Core/Io/SimpleIoBus.cs Normal file
View File

@@ -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}");
}
}
}

View File

@@ -24,15 +24,14 @@ namespace Core.Memory
if (address < 0x4000) if (address < 0x4000)
{ {
// Attempted to write to the ROM area. // Cannot write to ROM - Do nothing - maybe throw an exception
// We simply ignore the write command, just like real hardware.
return; return;
} }
_memory[address] = value; _memory[address] = value;
} }
// Helper method to load the original Sinclair ROM file // Load the ROM file
public void LoadRom(byte[] romData) public void LoadRom(byte[] romData)
{ {
if (romData.Length > 0x4000) if (romData.Length > 0x4000)
@@ -40,7 +39,7 @@ namespace Core.Memory
throw new ArgumentException("ROM file exceeds the 16KB capacity of Bank 0."); 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); Array.Copy(romData, 0, _memory, 0, romData.Length);
} }
} }

View File

@@ -129,7 +129,7 @@
// txtMemoryStart // txtMemoryStart
// //
txtMemoryStart.Location = new Point(238, 10); txtMemoryStart.Location = new Point(238, 10);
txtMemoryStart.Margin = new Padding(2, 2, 2, 2); txtMemoryStart.Margin = new Padding(2);
txtMemoryStart.Name = "txtMemoryStart"; txtMemoryStart.Name = "txtMemoryStart";
txtMemoryStart.Size = new Size(121, 27); txtMemoryStart.Size = new Size(121, 27);
txtMemoryStart.TabIndex = 9; txtMemoryStart.TabIndex = 9;
@@ -140,7 +140,7 @@
// btnStep // btnStep
// //
btnStep.Location = new Point(9, 315); btnStep.Location = new Point(9, 315);
btnStep.Margin = new Padding(2, 2, 2, 2); btnStep.Margin = new Padding(2);
btnStep.Name = "btnStep"; btnStep.Name = "btnStep";
btnStep.Size = new Size(90, 27); btnStep.Size = new Size(90, 27);
btnStep.TabIndex = 12; btnStep.TabIndex = 12;
@@ -151,17 +151,18 @@
// btnRun // btnRun
// //
btnRun.Location = new Point(122, 315); btnRun.Location = new Point(122, 315);
btnRun.Margin = new Padding(2, 2, 2, 2); btnRun.Margin = new Padding(2);
btnRun.Name = "btnRun"; btnRun.Name = "btnRun";
btnRun.Size = new Size(90, 27); btnRun.Size = new Size(90, 27);
btnRun.TabIndex = 13; btnRun.TabIndex = 13;
btnRun.Text = "Run"; btnRun.Text = "Run";
btnRun.UseVisualStyleBackColor = true; btnRun.UseVisualStyleBackColor = true;
btnRun.Click += btnRun_Click;
// //
// btnRefreshMemory // btnRefreshMemory
// //
btnRefreshMemory.Location = new Point(376, 7); btnRefreshMemory.Location = new Point(376, 7);
btnRefreshMemory.Margin = new Padding(2, 2, 2, 2); btnRefreshMemory.Margin = new Padding(2);
btnRefreshMemory.Name = "btnRefreshMemory"; btnRefreshMemory.Name = "btnRefreshMemory";
btnRefreshMemory.Size = new Size(90, 27); btnRefreshMemory.Size = new Size(90, 27);
btnRefreshMemory.TabIndex = 14; btnRefreshMemory.TabIndex = 14;
@@ -172,7 +173,7 @@
// txtMemoryView // txtMemoryView
// //
txtMemoryView.Location = new Point(88, 80); txtMemoryView.Location = new Point(88, 80);
txtMemoryView.Margin = new Padding(2, 2, 2, 2); txtMemoryView.Margin = new Padding(2);
txtMemoryView.Name = "txtMemoryView"; txtMemoryView.Name = "txtMemoryView";
txtMemoryView.Size = new Size(416, 195); txtMemoryView.Size = new Size(416, 195);
txtMemoryView.TabIndex = 15; txtMemoryView.TabIndex = 15;
@@ -182,7 +183,7 @@
// //
lstDisassembly.FormattingEnabled = true; lstDisassembly.FormattingEnabled = true;
lstDisassembly.Location = new Point(533, 11); lstDisassembly.Location = new Point(533, 11);
lstDisassembly.Margin = new Padding(2, 2, 2, 2); lstDisassembly.Margin = new Padding(2);
lstDisassembly.Name = "lstDisassembly"; lstDisassembly.Name = "lstDisassembly";
lstDisassembly.Size = new Size(186, 264); lstDisassembly.Size = new Size(186, 264);
lstDisassembly.TabIndex = 16; lstDisassembly.TabIndex = 16;
@@ -191,7 +192,7 @@
// //
lstStack.FormattingEnabled = true; lstStack.FormattingEnabled = true;
lstStack.Location = new Point(723, 11); lstStack.Location = new Point(723, 11);
lstStack.Margin = new Padding(2, 2, 2, 2); lstStack.Margin = new Padding(2);
lstStack.Name = "lstStack"; lstStack.Name = "lstStack";
lstStack.Size = new Size(186, 264); lstStack.Size = new Size(186, 264);
lstStack.TabIndex = 17; lstStack.TabIndex = 17;
@@ -199,7 +200,7 @@
// btnExit // btnExit
// //
btnExit.Location = new Point(818, 313); btnExit.Location = new Point(818, 313);
btnExit.Margin = new Padding(2, 2, 2, 2); btnExit.Margin = new Padding(2);
btnExit.Name = "btnExit"; btnExit.Name = "btnExit";
btnExit.Size = new Size(90, 27); btnExit.Size = new Size(90, 27);
btnExit.TabIndex = 18; btnExit.TabIndex = 18;
@@ -228,9 +229,9 @@
Controls.Add(lblDE); Controls.Add(lblDE);
Controls.Add(lblBC); Controls.Add(lblBC);
Controls.Add(lblAF); Controls.Add(lblAF);
Margin = new Padding(2, 2, 2, 2); Margin = new Padding(2);
Name = "DebuggerForm"; Name = "DebuggerForm";
Text = "DebuggerForm"; Text = "Debugger";
ResumeLayout(false); ResumeLayout(false);
PerformLayout(); PerformLayout();
} }
@@ -250,9 +251,9 @@
private Button btnRun; private Button btnRun;
private Button btnRefreshMemory; private Button btnRefreshMemory;
private RichTextBox txtMemoryView; private RichTextBox txtMemoryView;
private ListBox lstDisassembly;
private ListBox lstStack; private ListBox lstStack;
private Button btnExit; private Button btnExit;
public ListBox lstDisassembly;
//private TextBox textBox4; //private TextBox textBox4;
} }
} }

View File

@@ -10,6 +10,7 @@ namespace Desktop
{ {
private readonly Z80 _cpu; private readonly Z80 _cpu;
private readonly MemoryBus _memoryBus; private readonly MemoryBus _memoryBus;
private bool _isRunning = false;
public DebuggerForm(Z80 cpu, MemoryBus memoryBus) public DebuggerForm(Z80 cpu, MemoryBus memoryBus)
{ {
@@ -46,6 +47,53 @@ namespace Desktop
UpdateDisplay(); 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) private void btnExit_Click(object sender, EventArgs e)
{ {
Environment.Exit(0); Environment.Exit(0);
@@ -68,6 +116,8 @@ namespace Desktop
// 3. Update Memory Viewer // 3. Update Memory Viewer
UpdateMemoryView(); UpdateMemoryView();
UpdateStackView();
UpdateDisassemblyView();
} }
private void UpdateMemoryView() private void UpdateMemoryView()
@@ -108,9 +158,6 @@ namespace Desktop
private void UpdateStackView() private void UpdateStackView()
{ {
lstStack.Items.Clear(); 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; int itemsToShow = 5;
ushort currentSp = _cpu.SP; ushort currentSp = _cpu.SP;
@@ -139,36 +186,103 @@ namespace Desktop
{ {
lstDisassembly.Items.Clear(); 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; ushort currentPc = _cpu.PC;
int instructionsToShow = 8; int instructionsToShow = 8;
for (int i = 0; i < instructionsToShow; i++) for (int i = 0; i < instructionsToShow; i++)
{ {
byte opcode = _memoryBus.Read(currentPc); byte opcode = _memoryBus.Read(currentPc);
string mnemonic; 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) switch (opcode)
{ {
case 0x00: case 0x00: mnemonic = "NOP"; break;
mnemonic = "NOP"; 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; break;
case 0x3E: case 0x3E:
// LD A, n (Loads the next byte into register A) mnemonic = $"LD A, 0x{_memoryBus.Read((ushort)(currentPc + 1)):X2}";
byte nextByte = _memoryBus.Read((ushort)(currentPc + 1)); instructionLength = 2;
mnemonic = $"LD A, 0x{nextByte:X2}"; break;
instructionLength = 2; // This instruction takes up 2 bytes 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; break;
default: default:
mnemonic = $"UNKNOWN (0x{opcode:X2})"; mnemonic = $"UNKNOWN (0x{opcode:X2})";
break; break;
} }
// Add to the list box
lstDisassembly.Items.Add($"{currentPc:X4}: {mnemonic}"); 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; currentPc += (ushort)instructionLength;
} }
} }

View File

@@ -36,8 +36,7 @@
AutoScaleMode = AutoScaleMode.Font; AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(800, 450); ClientSize = new Size(800, 450);
Name = "Form1"; Name = "Form1";
Text = "Form1"; Text = "Parsons Sinclair ZX Spectrum 48K - 2026";
//this.Load += new System.EventHandler(this.Form1_Load);
ResumeLayout(false); ResumeLayout(false);
} }

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Windows.Forms; using System.Windows.Forms;
using Core.Cpu; using Core.Cpu;
using Core.Io;
using Core.Memory; using Core.Memory;
namespace Desktop namespace Desktop
@@ -9,6 +10,7 @@ namespace Desktop
{ {
private Z80 _cpu = null!; private Z80 _cpu = null!;
private MemoryBus _memoryBus = null!; private MemoryBus _memoryBus = null!;
private SimpleIoBus _simpleIoBus = null!;
public Form1() public Form1()
{ {
@@ -22,16 +24,16 @@ namespace Desktop
{ {
// 1. Initialize the memory bus // 1. Initialize the memory bus
_memoryBus = new MemoryBus(); _memoryBus = new MemoryBus();
_simpleIoBus = new SimpleIoBus();
// 2. Load the ROM from disk // 2. Load the ROM
// Make sure "48.rom" matches the name of the file you added to your project
byte[] romData = RomLoader.Load("48.rom"); byte[] romData = RomLoader.Load("48.rom");
// 3. Inject the ROM into the memory bus // 3. Inject the ROM into the memory bus
_memoryBus.LoadRom(romData); _memoryBus.LoadRom(romData);
// 4. Initialize the CPU with the populated memory // 4. Initialize the CPU with the populated memory
_cpu = new Z80(_memoryBus); _cpu = new Z80(_memoryBus, _simpleIoBus);
DebuggerForm debugger = new DebuggerForm(_cpu, _memoryBus); DebuggerForm debugger = new DebuggerForm(_cpu, _memoryBus);
debugger.Show(); debugger.Show();