From 02680cb92d4654784dd7ac802767e5715f01c3cc Mon Sep 17 00:00:00 2001 From: Marc Parsons Date: Wed, 22 Apr 2026 22:38:53 +0100 Subject: [PATCH] More OpCodes - working towards ZEXALL perfection --- Core/Cpu/Z80.cs | 64 +++++++++++++- Core/Io/IO_Bus.cs | 58 +++++++----- Core/Io/TapManager.cs | 179 +++++++++++++++++++++++++++++++++++++- Desktop/DebuggerForm.cs | 10 +++ Desktop/Form1.Designer.cs | 31 ++++++- Desktop/Form1.cs | 60 +++++++++++-- 6 files changed, 364 insertions(+), 38 deletions(-) diff --git a/Core/Cpu/Z80.cs b/Core/Cpu/Z80.cs index bcba1fa..cc7551b 100644 --- a/Core/Cpu/Z80.cs +++ b/Core/Cpu/Z80.cs @@ -6,6 +6,7 @@ namespace Core.Cpu { public partial class Z80 { + public bool IsZexDocMode { get; set; } = false; //T-State counter public long TotalTStates { get; set; } @@ -47,6 +48,7 @@ namespace Core.Cpu public Func? WaitStateCallback { get; set; } //Misc Variables + public bool EnableFastLoad { get; set; } = true; byte newFlags = 0; int result = 0; @@ -194,10 +196,44 @@ namespace Core.Cpu public int Step() { - if (PC == 0x0556 && _tapManager.HasBlocks) + if (IsZexDocMode && PC == 0x0005) { - HandleInstantTapeLoad(); - return 100; // Return a dummy number of T-States for the hijacking + // CP/M System Call Hook + if (BC.Low == 2) // C = 2: Print a single character stored in register E + { + System.Diagnostics.Debug.Write((char)DE.Low); + } + else if (BC.Low == 9) // C = 9: Print a string starting at memory address DE, terminated by '$' + { + ushort addr = DE.Word; + while (true) + { + char c = (char)ReadMemory(addr++); + if (c == '$') break; + System.Diagnostics.Debug.Write(c); + } + } + + // Emulate a 'RET' instruction to return from the CALL 0x0005 + byte retLow = ReadMemory(SP); + SP++; + byte retHigh = ReadMemory(SP); + SP++; + PC = (ushort)((retHigh << 8) | retLow); + + return 10; // Skip normal execution for this cycle + } + if (PC == 0x0556) + { + if (EnableFastLoad) + { + HandleInstantTapeLoad(); + return 100; + } + else + { + _tapManager.Play(); + } } // Fetch the next opcode and increment the Program Counter @@ -2819,7 +2855,7 @@ namespace Core.Cpu AF.Low = newFlags; - return 19; // 19 T-States + return 19; case 0xCB: // The FD CB nested prefix { sbyte displacement = (sbyte)FetchByte(); @@ -2866,6 +2902,26 @@ namespace Core.Cpu throw new Exception("Invalid bitwise operation."); } } + case 0xE1: // POP IY + // 1. Read the Low byte from the current Stack Pointer, then increment SP + IY.Low = ReadMemory(SP); + SP++; + + // 2. Read the High byte from the new Stack Pointer, then increment SP + IY.High = ReadMemory(SP); + SP++; + + return 14; // 14 T-States + case 0xE5: // PUSH IY + // 1. Decrement SP and write the High byte + SP--; + WriteMemory(SP, IY.High); + + // 2. Decrement SP again and write the Low byte + SP--; + WriteMemory(SP, IY.Low); + + return 15; // 15 T-States default: throw new NotImplementedException($"FD prefix opcode {opcode:X2} at PC 0x{(PC - 2):X4} not implemented!"); } diff --git a/Core/Io/IO_Bus.cs b/Core/Io/IO_Bus.cs index 4ccd033..d0c88a2 100644 --- a/Core/Io/IO_Bus.cs +++ b/Core/Io/IO_Bus.cs @@ -5,37 +5,47 @@ using static System.Runtime.InteropServices.JavaScript.JSType; namespace Core.Io { public class IO_Bus -{ + { public byte BorderColorIndex { get; private set; } = 7; public bool BeeperState { get; private set; } = false; public byte[] KeyboardRows = new byte[8] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; + TapManager _tapManager = new TapManager(); - public byte ReadPort(ushort portAddress) - { - // The Spectrum ULA responds to any even port address (where the lowest bit is 0) - if ((portAddress & 0x01) == 0) + public IO_Bus(TapManager tapManager) { - byte highByte = (byte)(portAddress >> 8); // The B register! - byte result = 0xFF; // Start assuming no keys are pressed - - // The ROM pulls a specific bit low (0) in the high byte to request a row. - // Sometimes it pulls multiple bits low to scan multiple rows at once, so we AND the results. - if ((highByte & 0x01) == 0) result &= KeyboardRows[0]; // 0xFE: CAPS, Z, X, C, V - if ((highByte & 0x02) == 0) result &= KeyboardRows[1]; // 0xFD: A, S, D, F, G - if ((highByte & 0x04) == 0) result &= KeyboardRows[2]; // 0xFB: Q, W, E, R, T - if ((highByte & 0x08) == 0) result &= KeyboardRows[3]; // 0xF7: 1, 2, 3, 4, 5 - if ((highByte & 0x10) == 0) result &= KeyboardRows[4]; // 0xEF: 0, 9, 8, 7, 6 - if ((highByte & 0x20) == 0) result &= KeyboardRows[5]; // 0xDF: P, O, I, U, Y - if ((highByte & 0x40) == 0) result &= KeyboardRows[6]; // 0xBF: ENTER, L, K, J, H - if ((highByte & 0x80) == 0) result &= KeyboardRows[7]; // 0x7F: SPACE, SYM, M, N, B - - // The top 3 bits (5, 6, 7) are unused by the keyboard and usually return 1 on a real Spectrum - return (byte)(result | 0xE0); + _tapManager = tapManager; } - // Return 0xFF for unhandled ports - return 0xFF; - } + public byte ReadPort(ushort portAddress) + { + // The Spectrum ULA responds to any even port address (where the lowest bit is 0) + if ((portAddress & 0x01) == 0) + { + byte highByte = (byte)(portAddress >> 8); // The B register! + byte result = 0xFF; // Start assuming no keys are pressed + + // The ROM pulls a specific bit low (0) in the high byte to request a row. + // Sometimes it pulls multiple bits low to scan multiple rows at once, so we AND the results. + if ((highByte & 0x01) == 0) result &= KeyboardRows[0]; // 0xFE: CAPS, Z, X, C, V + if ((highByte & 0x02) == 0) result &= KeyboardRows[1]; // 0xFD: A, S, D, F, G + if ((highByte & 0x04) == 0) result &= KeyboardRows[2]; // 0xFB: Q, W, E, R, T + if ((highByte & 0x08) == 0) result &= KeyboardRows[3]; // 0xF7: 1, 2, 3, 4, 5 + if ((highByte & 0x10) == 0) result &= KeyboardRows[4]; // 0xEF: 0, 9, 8, 7, 6 + if ((highByte & 0x20) == 0) result &= KeyboardRows[5]; // 0xDF: P, O, I, U, Y + if ((highByte & 0x40) == 0) result &= KeyboardRows[6]; // 0xBF: ENTER, L, K, J, H + if ((highByte & 0x80) == 0) result &= KeyboardRows[7]; // 0x7F: SPACE, SYM, M, N, B + if (_tapManager.EarBit) result |= 0x40; // Set Bit 6 high + else result &= 0xBF; // Set Bit 6 low + + + //return result; + // The top 3 bits (5, 6, 7) are unused by the keyboard and usually return 1 on a real Spectrum + return (byte)(result | 0xE0); + } + + // Return 0xFF for unhandled ports + return 0xFF; + } public void WritePort(ushort portAddress, byte portValue) { diff --git a/Core/Io/TapManager.cs b/Core/Io/TapManager.cs index 0c14462..c59b6b7 100644 --- a/Core/Io/TapManager.cs +++ b/Core/Io/TapManager.cs @@ -6,8 +6,20 @@ namespace Core.Io public class TapManager { private Queue _blocks = new Queue(); + private byte[] _currentBlock; + + // State Machine Tracking + private enum TapeState { Idle, Pilot, Sync1, Sync2, Data, Pause } + private TapeState _state = TapeState.Idle; + + public bool EarBit { get; private set; } = false; // The physical audio signal sent to the CPU + + private int _pulseTimer = 0; + private int _pilotPulsesRemaining = 0; + private int _byteIndex = 0; + private int _bitIndex = 0; + private int _pulseCountForCurrentBit = 0; - public void LoadTapData(byte[] fileData) { _blocks.Clear(); @@ -15,20 +27,136 @@ namespace Core.Io 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 void Play() + { + if (_blocks.Count > 0 && _state == TapeState.Idle) + { + LoadNextBlock(); + } + } + + private void LoadNextBlock() + { + if (_blocks.Count == 0) + { + _state = TapeState.Idle; + return; + } + + _currentBlock = _blocks.Dequeue(); + _byteIndex = 0; + _bitIndex = 7; // MSB first + _pulseCountForCurrentBit = 0; + + // Header blocks (flag 0x00) have longer pilot tones than Data blocks + bool isHeader = _currentBlock.Length > 0 && _currentBlock[0] == 0; + _pilotPulsesRemaining = isHeader ? 8063 : 3223; + + _state = TapeState.Pilot; + _pulseTimer = 2168; // Length of a pilot pulse + EarBit = false; + } + + // --- THE ENGINE SYNC --- + // Call this every time the CPU steps, passing in how many T-States elapsed + public void Update(int tStatesElapsed) + { + if (_state == TapeState.Idle) return; + + _pulseTimer -= tStatesElapsed; + + if (_pulseTimer <= 0) + { + EarBit = !EarBit; // Flip the audio square wave + AdvanceState(); + } + } + + private void AdvanceState() + { + switch (_state) + { + case TapeState.Pilot: + _pilotPulsesRemaining--; + if (_pilotPulsesRemaining > 0) + { + _pulseTimer += 2168; + } + else + { + _state = TapeState.Sync1; + _pulseTimer += 667; + } + break; + + case TapeState.Sync1: + _state = TapeState.Sync2; + _pulseTimer += 735; + break; + + case TapeState.Sync2: + _state = TapeState.Data; + CalculateNextDataPulse(); + break; + + case TapeState.Data: + _pulseCountForCurrentBit++; + if (_pulseCountForCurrentBit < 2) + { + // A bit requires 2 pulses (one high, one low) + CalculateNextDataPulse(); + } + else + { + // Move to next bit + _pulseCountForCurrentBit = 0; + _bitIndex--; + + if (_bitIndex < 0) + { + // Move to next byte + _bitIndex = 7; + _byteIndex++; + + if (_byteIndex >= _currentBlock.Length) + { + // Block finished! 1 second pause before the next block + _state = TapeState.Pause; + _pulseTimer += 3500000; + return; + } + } + CalculateNextDataPulse(); + } + break; + + case TapeState.Pause: + LoadNextBlock(); + break; + } + } + + private void CalculateNextDataPulse() + { + // Extract the current bit from the current byte + bool isBitOne = (_currentBlock[_byteIndex] & (1 << _bitIndex)) != 0; + + // Bit 0 = 855 T-States, Bit 1 = 1710 T-States + _pulseTimer += isBitOne ? 1710 : 855; + } + + // --- FAST LOAD METHODS (For ROM Hijack) --- public byte[] GetNextBlock() { return _blocks.Count > 0 ? _blocks.Dequeue() : null; @@ -37,3 +165,46 @@ namespace Core.Io public bool HasBlocks => _blocks.Count > 0; } } + + + + +//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 a7c256a..6a541af 100644 --- a/Desktop/DebuggerForm.cs +++ b/Desktop/DebuggerForm.cs @@ -1285,6 +1285,16 @@ namespace Desktop mnemonic = $"LD (IY{sign}{d}), L"; instructionLength = 3; } + else if (fdOpcode == 0xE1) + { + mnemonic = "POP IY"; + instructionLength = 2; + } + else if(fdOpcode == 0xE5) + { + mnemonic = "PUSH IY"; + instructionLength = 2; + } else { mnemonic = $"FD PREFIX UNKNOWN (0x{fdOpcode:X2})"; diff --git a/Desktop/Form1.Designer.cs b/Desktop/Form1.Designer.cs index 6f6e4d2..74d67b8 100644 --- a/Desktop/Form1.Designer.cs +++ b/Desktop/Form1.Designer.cs @@ -42,6 +42,9 @@ resetToolStripMenuItem = new ToolStripMenuItem(); stepToolStripMenuItem = new ToolStripMenuItem(); resetToolStripMenuItem1 = new ToolStripMenuItem(); + optionsToolStripMenuItem = new ToolStripMenuItem(); + fastLoadingToolStripMenuItem = new ToolStripMenuItem(); + runZEXDOCToolStripMenuItem = new ToolStripMenuItem(); ((System.ComponentModel.ISupportInitialize)picScreen).BeginInit(); menuStrip1.SuspendLayout(); SuspendLayout(); @@ -59,7 +62,7 @@ // menuStrip1 // menuStrip1.ImageScalingSize = new Size(24, 24); - menuStrip1.Items.AddRange(new ToolStripItem[] { fileToolStripMenuItem, viewToolStripMenuItem, machineToolStripMenuItem }); + menuStrip1.Items.AddRange(new ToolStripItem[] { fileToolStripMenuItem, viewToolStripMenuItem, machineToolStripMenuItem, optionsToolStripMenuItem }); menuStrip1.Location = new Point(0, 0); menuStrip1.Name = "menuStrip1"; menuStrip1.Padding = new Padding(5, 2, 0, 2); @@ -151,6 +154,29 @@ resetToolStripMenuItem1.Text = "Reset"; resetToolStripMenuItem1.Click += btnReset_Click; // + // optionsToolStripMenuItem + // + optionsToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { fastLoadingToolStripMenuItem, runZEXDOCToolStripMenuItem }); + optionsToolStripMenuItem.Name = "optionsToolStripMenuItem"; + optionsToolStripMenuItem.Size = new Size(75, 24); + optionsToolStripMenuItem.Text = "Options"; + // + // fastLoadingToolStripMenuItem + // + fastLoadingToolStripMenuItem.Checked = true; + fastLoadingToolStripMenuItem.CheckState = CheckState.Checked; + fastLoadingToolStripMenuItem.Name = "fastLoadingToolStripMenuItem"; + fastLoadingToolStripMenuItem.Size = new Size(224, 26); + fastLoadingToolStripMenuItem.Text = "Fast TAP Loading"; + fastLoadingToolStripMenuItem.Click += fastLoadingToolStripMenuItem_Click; + // + // runZEXDOCToolStripMenuItem + // + runZEXDOCToolStripMenuItem.Name = "runZEXDOCToolStripMenuItem"; + runZEXDOCToolStripMenuItem.Size = new Size(224, 26); + runZEXDOCToolStripMenuItem.Text = "Run ZEXDOC"; + runZEXDOCToolStripMenuItem.Click += btnRunZexDoc_Click; + // // Form1 // AutoScaleDimensions = new SizeF(8F, 20F); @@ -185,5 +211,8 @@ private ToolStripMenuItem resetToolStripMenuItem; private ToolStripMenuItem stepToolStripMenuItem; private ToolStripMenuItem resetToolStripMenuItem1; + private ToolStripMenuItem optionsToolStripMenuItem; + private ToolStripMenuItem fastLoadingToolStripMenuItem; + private ToolStripMenuItem runZEXDOCToolStripMenuItem; } } diff --git a/Desktop/Form1.cs b/Desktop/Form1.cs index f8e84d9..7422b48 100644 --- a/Desktop/Form1.cs +++ b/Desktop/Form1.cs @@ -26,6 +26,7 @@ namespace Desktop public double FramesPerSecond = 0; public double TotalFrameTime = 0; public double FrameTime = 0; + public bool exceptionRaised = false; public Form1() @@ -40,10 +41,10 @@ namespace Desktop { _baseTitle = this.Text; _memoryBus = new MemoryBus(); - _simpleIoBus = new IO_Bus(); + _tapManager = new TapManager(); + _simpleIoBus = new IO_Bus(_tapManager); _ula = new ULA(_memoryBus, _simpleIoBus); _beeper = new BeeperDevice(); - _tapManager = new TapManager(); _memoryBus.CrapRAMData(); byte[] romData = RomLoader.Load("48.rom"); _memoryBus.LoadRom(romData); @@ -92,13 +93,19 @@ namespace Desktop continue; } + long tStatesBefore = _cpu.TotalTStates; + // --- Execute Instruction --- _cpu.Step(); + int elapsedTStates = (int)(_cpu.TotalTStates - tStatesBefore); + _tapManager.Update(elapsedTStates); + //Process audio at the correct time while (_cpu.TotalTStates >= (long)(audioSampleCount * 79.365)) { - _beeper.AddSample(_simpleIoBus.BeeperState); + bool finalAudioOutput = _simpleIoBus.BeeperState ^ _tapManager.EarBit; + _beeper.AddSample(finalAudioOutput); audioSampleCount++; } @@ -119,7 +126,7 @@ namespace Desktop this.Invoke((MethodInvoker)delegate { UpdateScreenBitmap(); - this.Text = $"{_baseTitle} - FPS: {FramesPerSecond:F1}"; + this.Text = $"{_baseTitle} - FPS: {FramesPerSecond:F1} - Fast Loading: {_cpu.EnableFastLoad.ToString()}"; }); TotalFrameCount++; @@ -129,7 +136,7 @@ namespace Desktop if (elapsedMs < targetTimeMs) { - Thread.Sleep((int)(targetTimeMs - elapsedMs)); + //Thread.Sleep((int)(targetTimeMs - elapsedMs)); } TotalFrameTime += fpsStopwatch.Elapsed.TotalMilliseconds; if (TotalFrameCount % 50 == 0) @@ -150,6 +157,7 @@ namespace Desktop this.Invoke((MethodInvoker)delegate { MessageBox.Show(ex.Message, "CPU Crash", MessageBoxButtons.OK, MessageBoxIcon.Error); + }); } }); @@ -188,6 +196,15 @@ namespace Desktop } _isPaused = false; } + + private void fastLoadingToolStripMenuItem_Click(object sender, EventArgs e) + { + // Toggle the checkmark UI + fastLoadingToolStripMenuItem.Checked = !fastLoadingToolStripMenuItem.Checked; + + // Tell the CPU to enable or disable the ROM hijack + _cpu.EnableFastLoad = fastLoadingToolStripMenuItem.Checked; + } private void openSNAToolStripMenuItem_Click(object sender, EventArgs e) { _isPaused = true; @@ -203,6 +220,39 @@ namespace Desktop _isPaused = false; } + private void btnRunZexDoc_Click(object sender, EventArgs e) + { + _isPaused = true; + + using (OpenFileDialog ofd = new OpenFileDialog()) + { + ofd.Filter = "CP/M Binaries (*.com, *.bin)|*.com;*.bin"; + if (ofd.ShowDialog() == DialogResult.OK) + { + byte[] zexdocBytes = System.IO.File.ReadAllBytes(ofd.FileName); + + // 1. Wipe the RAM completely clean + _memoryBus.CleanRAMData(); + + // 2. Load ZEXDOC exactly at CP/M start address 0x0100 + for (int i = 0; i < zexdocBytes.Length; i++) + { + _memoryBus.Write((ushort)(0x0100 + i), zexdocBytes[i]); + } + + // 3. Configure the CPU for CP/M + _cpu.IsZexDocMode = true; + _cpu.PC = 0x0100; // Execution starts here + _cpu.SP = 0xFFFF; // Put the stack at the very top of memory + + // 4. Fake a RET address at 0x0000 so the test terminates gracefully when finished + _memoryBus.Write(0x0000, 0xD3); // OUT (n), A (A fake halt sequence) + + // Unpause and watch the Visual Studio Output window! + _isPaused = false; + } + } + } private void btnRun_Click(object sender, EventArgs e) => _isPaused = false; private void btnPause_Click(object sender, EventArgs e) => _isPaused = true;