From 806fecaffa2f28300dab8534ebf631595aeef5d4 Mon Sep 17 00:00:00 2001 From: parsons Date: Sat, 6 Jun 2026 01:49:33 +0100 Subject: [PATCH] First OpCode Decoding attempt --- ParsonsMegadrive.Core/Class1.cs | 7 - .../Interfaces/IMemoryBus.cs | 21 ++ ParsonsMegadrive.Core/M68000.cs | 183 ++++++++++++++++++ ParsonsMegadrive.Core/Memory/MdMemoryBus.cs | 49 +++++ .../ParsonsMegadriveEmulator2026.slnx | 2 + ParsonsMegadrive.Tests/CpuTests.cs | 34 ++++ ParsonsMegadrive.Tests/UnitTest1.cs | 11 -- 7 files changed, 289 insertions(+), 18 deletions(-) delete mode 100644 ParsonsMegadrive.Core/Class1.cs create mode 100644 ParsonsMegadrive.Core/Interfaces/IMemoryBus.cs create mode 100644 ParsonsMegadrive.Core/M68000.cs create mode 100644 ParsonsMegadrive.Core/Memory/MdMemoryBus.cs create mode 100644 ParsonsMegadrive.Tests/CpuTests.cs delete mode 100644 ParsonsMegadrive.Tests/UnitTest1.cs diff --git a/ParsonsMegadrive.Core/Class1.cs b/ParsonsMegadrive.Core/Class1.cs deleted file mode 100644 index e790fa4..0000000 --- a/ParsonsMegadrive.Core/Class1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ParsonsMegadrive.Core -{ - public class Class1 - { - - } -} diff --git a/ParsonsMegadrive.Core/Interfaces/IMemoryBus.cs b/ParsonsMegadrive.Core/Interfaces/IMemoryBus.cs new file mode 100644 index 0000000..036a60d --- /dev/null +++ b/ParsonsMegadrive.Core/Interfaces/IMemoryBus.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ParsonsMegadrive.Core.Interfaces +{ + public interface IMemoryBus + { + // 8-bit (Byte) + byte Read8(uint address); + void Write8(uint address, byte value); + + // 16-bit (Word) + ushort Read16(uint address); + void Write16(uint address, ushort value); + + // 32-bit (Long) + uint Read32(uint address); + void Write32(uint address, uint value); + } +} diff --git a/ParsonsMegadrive.Core/M68000.cs b/ParsonsMegadrive.Core/M68000.cs new file mode 100644 index 0000000..cbe164d --- /dev/null +++ b/ParsonsMegadrive.Core/M68000.cs @@ -0,0 +1,183 @@ +using System; +using ParsonsMegadrive.Core.Interfaces; + +namespace ParsonsMegaDrive.Core +{ + public partial class M68000 + { + + public delegate int InstructionHandler(ushort opcode); + + // 2. The master lookup table + private readonly InstructionHandler[] _instructionTable = new InstructionHandler[65536]; + + // --- PUBLIC STATE FOR DEBUGGER --- + public uint[] D { get; private set; } = new uint[8]; + public uint[] A { get; private set; } = new uint[8]; + public uint PC { get; private set; } + public ushort SR { get; private set; } + public long TotalCycles { get; private set; } + + // --- STATUS REGISTER (SR) FLAGS --- + public bool FlagC + { + get => (SR & 0x0001) != 0; + internal set => SR = (ushort)(value ? (SR | 0x0001) : (SR & ~0x0001)); + } + public bool FlagV + { + get => (SR & 0x0002) != 0; + internal set => SR = (ushort)(value ? (SR | 0x0002) : (SR & ~0x0002)); + } + public bool FlagZ + { + get => (SR & 0x0004) != 0; + internal set => SR = (ushort)(value ? (SR | 0x0004) : (SR & ~0x0004)); + } + public bool FlagN + { + get => (SR & 0x0008) != 0; + internal set => SR = (ushort)(value ? (SR | 0x0008) : (SR & ~0x0008)); + } + public bool FlagX + { + get => (SR & 0x0010) != 0; + internal set => SR = (ushort)(value ? (SR | 0x0010) : (SR & ~0x0010)); + } + + private readonly IMemoryBus _memory; + + public M68000(IMemoryBus memory) + { + _memory = memory; + BuildOpcodeTable(); + Reset(); + } + + private void BuildOpcodeTable() + { + for (int i = 0; i < 65536; i++) + { + ushort opcode = (ushort)i; + int line = (opcode >> 12) & 0x0F; // Extract Bits 12-15 + + if (line == 0x07) + { + // Line 7 is MOVEQ! + // However, bit 8 MUST be 0. If it's 1, it's a completely different instruction. + if ((opcode & 0x0100) == 0) + { + _instructionTable[opcode] = ExecuteMoveQ; + continue; + } + } + + // If it matches nothing, point it to our fallback method + _instructionTable[opcode] = ExecuteUnknown; + } + } + + public void Reset() + { + Array.Clear(D, 0, D.Length); + Array.Clear(A, 0, A.Length); + + PC = 0; + SR = 0x2700; // Supervisor mode on, Interrupts masked to level 7 + TotalCycles = 0; + BuildOpcodeTable(); + } + + // --- INSTRUCTION FETCHING --- + + // Fetches a 16-bit word from the current Program Counter and advances it. + public ushort FetchWord() + { + ushort word = _memory.Read16(PC); + PC += 2; + return word; + } + + // Sometimes an instruction needs a 32-bit immediate value! + public uint FetchLong() + { + uint highWord = FetchWord(); + uint lowWord = FetchWord(); + return (highWord << 16) | lowWord; + } + + // --- EXECUTION LOOP --- + + public int Step() + { + ushort opcode = FetchWord(); + + // Absolute O(1) execution. No switch statements! + int cyclesTaken = _instructionTable[opcode](opcode); + + TotalCycles += cyclesTaken; + return cyclesTaken; + } + + // Extracts bits 0-2 (The target register: 0-7) + private static int GetEARegister(ushort opcode) + { + return opcode & 0x07; + } + + // Extracts bits 3-5 (The addressing mode: e.g., Address Indirect, Immediate, etc.) + private static int GetEAMode(ushort opcode) + { + return (opcode >> 3) & 0x07; + } + + // Extracts bits 6-7 (Often used to define if an operation is Byte, Word, or Long) + private static int GetSize(ushort opcode) + { + return (opcode >> 6) & 0x03; + } + + // Extracts bits 9-11 (Often used to specify a destination Data/Address register) + private static int GetDestinationRegister(ushort opcode) + { + return (opcode >> 9) & 0x07; + } + + private int ExecuteMoveQ(ushort opcode) + { + // Bit 8 must be 0 for a valid MOVEQ. If it's 1, it's a different instruction. + if ((opcode & 0x0100) != 0) return ExecuteUnknown(opcode); + + // 1. Where is it going? (Bits 9-11) + int registerIndex = GetDestinationRegister(opcode); + + // 2. What is the data? (Bits 0-7). MOVEQ sign-extends the 8-bit data to 32-bits! + sbyte data = (sbyte)(opcode & 0xFF); + + // 3. Execute + D[registerIndex] = (uint)data; + + // 4. Update Flags (MOVEQ clears V and C, and updates N and Z based on the data) + FlagV = false; + FlagC = false; + FlagN = data < 0; + FlagZ = data == 0; + + // MOVEQ always takes 4 cycles + return 4; + } + + private int ExecuteUnknown(ushort opcode) + { + throw new NotImplementedException($"Opcode 0x{opcode:X4} at PC 0x{(PC - 2):X8} is not implemented."); + } + + private int ExecuteOpcode(ushort opcode) + { + // This is where the magic happens! + // We will use bit-masking to figure out what this opcode is. + + throw new NotImplementedException($"Opcode 0x{opcode:X4} at PC 0x{(PC - 2):X8} is not implemented."); + } + } +} \ No newline at end of file diff --git a/ParsonsMegadrive.Core/Memory/MdMemoryBus.cs b/ParsonsMegadrive.Core/Memory/MdMemoryBus.cs new file mode 100644 index 0000000..c52520f --- /dev/null +++ b/ParsonsMegadrive.Core/Memory/MdMemoryBus.cs @@ -0,0 +1,49 @@ +using System; +using ParsonsMegadrive.Core.Interfaces; + +namespace ParsonsMegaDrive.Core.Memory +{ + public class MdMemoryBus : IMemoryBus + { + // For testing, we'll just give it a flat 16MB array to represent the whole address space. + // Later, we will map this properly to ROM (0x000000), Work RAM (0xFF0000), etc. + private readonly byte[] _memory = new byte[16 * 1024 * 1024]; + + public byte Read8(uint address) + { + return _memory[address & 0xFFFFFF]; // 24-bit address mask + } + + public void Write8(uint address, byte value) + { + _memory[address & 0xFFFFFF] = value; + } + + public ushort Read16(uint address) + { + // BIG-ENDIAN: High byte comes first! + byte high = Read8(address); + byte low = Read8(address + 1); + return (ushort)((high << 8) | low); + } + + public void Write16(uint address, ushort value) + { + Write8(address, (byte)(value >> 8)); // High byte + Write8(address + 1, (byte)(value & 0xFF)); // Low byte + } + + public uint Read32(uint address) + { + ushort highWord = Read16(address); + ushort lowWord = Read16(address + 2); + return (uint)((highWord << 16) | lowWord); + } + + public void Write32(uint address, uint value) + { + Write16(address, (ushort)(value >> 16)); + Write16(address + 2, (ushort)(value & 0xFFFF)); + } + } +} \ No newline at end of file diff --git a/ParsonsMegadrive.Core/ParsonsMegadriveEmulator2026.slnx b/ParsonsMegadrive.Core/ParsonsMegadriveEmulator2026.slnx index 9eb244c..88f300e 100644 --- a/ParsonsMegadrive.Core/ParsonsMegadriveEmulator2026.slnx +++ b/ParsonsMegadrive.Core/ParsonsMegadriveEmulator2026.slnx @@ -1,3 +1,5 @@ + + diff --git a/ParsonsMegadrive.Tests/CpuTests.cs b/ParsonsMegadrive.Tests/CpuTests.cs new file mode 100644 index 0000000..b25a4ac --- /dev/null +++ b/ParsonsMegadrive.Tests/CpuTests.cs @@ -0,0 +1,34 @@ +using ParsonsMegaDrive.Core; +using ParsonsMegaDrive.Core.Memory; +using Xunit; + +namespace ParsonsMegaDrive.Tests +{ + public class CpuTests + { + [Fact] + public void MoveQ_ShouldLoadDataAndSetFlags() + { + // 1. Arrange: Boot the machine + var bus = new MdMemoryBus(); + var cpu = new M68000(bus); + + // 2. Load the opcode for "MOVEQ #5, D1" (0x7205) into memory at PC 0 + bus.Write16(0x000000, 0x7205); + + // 3. Act: Step the CPU exactly once + int cycles = cpu.Step(); + + // 4. Assert: Did it work? + Assert.Equal(4, cycles); // Should take exactly 4 cycles + Assert.Equal(2u, cpu.PC); // PC should advance by 2 bytes + Assert.Equal(5u, cpu.D[1]); // D1 should now contain the number 5 + + // Assert Flags (5 is greater than 0, so Z and N should be false) + Assert.False(cpu.FlagZ); + Assert.False(cpu.FlagN); + Assert.False(cpu.FlagV); + Assert.False(cpu.FlagC); + } + } +} \ No newline at end of file diff --git a/ParsonsMegadrive.Tests/UnitTest1.cs b/ParsonsMegadrive.Tests/UnitTest1.cs deleted file mode 100644 index 175703e..0000000 --- a/ParsonsMegadrive.Tests/UnitTest1.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace ParsonsMegadrive.Tests -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } -}