diff --git a/BizHawk.Emulation.Common/Database/FirmwareDatabase.cs b/BizHawk.Emulation.Common/Database/FirmwareDatabase.cs
index 3d40ef5de1..9d9810b5e6 100644
--- a/BizHawk.Emulation.Common/Database/FirmwareDatabase.cs
+++ b/BizHawk.Emulation.Common/Database/FirmwareDatabase.cs
@@ -54,6 +54,7 @@ namespace BizHawk.Emulation.Common
FirmwareAndOption("5EA7C2B824672E914525D1D5C419D71B84A426A2", 16384, "ZXSpectrum", "48ROM", "48.ROM", "Spectrum 48K ROM");
FirmwareAndOption("16375D42EA109B47EDDED7A16028DE7FDB3013A1", 32768, "ZXSpectrum", "128ROM", "128.ROM", "Spectrum 128K ROM");
FirmwareAndOption("8CAFB292AF58617907B9E6B9093D3588A75849B8", 32768, "ZXSpectrum", "PLUS2ROM", "PLUS2.ROM", "Spectrum 128K +2 ROM");
+ FirmwareAndOption("929BF1A5E5687EBD8D7245F9B513A596C0EC21A4", 65563, "ZXSpectrum", "PLUS3ROM", "PLUS3.ROM", "Spectrum 128K +3 ROM");
// for saturn, we think any bios region can pretty much run any iso
// so, we're going to lay this out carefully so that we choose things in a sensible order, but prefer the correct region
diff --git a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj
index 2a18cd1e56..439b004828 100644
--- a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj
+++ b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj
@@ -277,6 +277,9 @@
+
+
+
diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Memory.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Memory.cs
new file mode 100644
index 0000000000..76dbb31891
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Memory.cs
@@ -0,0 +1,284 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
+{
+ public partial class ZX128Plus3 : SpectrumBase
+ {
+ /* 128k paging controlled by writes to port 0x7ffd
+ *
+ *
+
+ #7FFD (32765) - decoded as A15=0, A1=0 and /IORQ=0. Bits 0..5 are latched. Bits 0..2 select RAM bank in secton D. Bit 3 selects RAM bank to dispay screen (0 - RAM5, 1 - RAM7). Bit 4 selects ROM bank (0 - ROM0, 1 - ROM1). Bit 5, when set locks future writing to #7FFD port until reset. Reading #7FFD port is the same as writing #FF into it.
+ #BFFD (49149) - write data byte into AY-3-8912 chip.
+ #FFFD (65533) - select AY-3-8912 addres (D4..D7 ignored) and reading data byte.
+
+ * 0xffff +--------+--------+--------+--------+--------+--------+--------+--------+
+ | Bank 0 | Bank 1 | Bank 2 | Bank 3 | Bank 4 | Bank 5 | Bank 6 | Bank 7 |
+ | | |(also at| | |(also at| | |
+ | | | 0x8000)| | | 0x4000)| | |
+ | | | | | | screen | | screen |
+ 0xc000 +--------+--------+--------+--------+--------+--------+--------+--------+
+ | Bank 2 | Any one of these pages may be switched in.
+ | |
+ | |
+ | |
+ 0x8000 +--------+
+ | Bank 5 |
+ | |
+ | |
+ | screen |
+ 0x4000 +--------+--------+
+ | ROM 0 | ROM 1 | Either ROM may be switched in.
+ | | |
+ | | |
+ | | |
+ 0x0000 +--------+--------+
+ */
+
+ ///
+ /// Simulates reading from the bus (no contention)
+ /// Paging should be handled here
+ ///
+ ///
+ ///
+ public override byte ReadBus(ushort addr)
+ {
+ int divisor = addr / 0x4000;
+ byte result = 0xff;
+ switch (divisor)
+ {
+ // ROM 0x000
+ case 0:
+ if (!ROMPaged)
+ result = Memory[0][addr % 0x4000];
+ else
+ result = Memory[1][addr % 0x4000];
+ break;
+
+ // RAM 0x4000 (RAM5 - Bank5 or shadow bank RAM7)
+ case 1:
+ result = Memory[7][addr % 0x4000];
+ break;
+
+ // RAM 0x8000 (RAM2 - Bank2)
+ case 2:
+ result = Memory[4][addr % 0x4000];
+ break;
+
+ // RAM 0xc000 (any ram bank 0 - 7 may be paged in - default bank0)
+ case 3:
+ switch (RAMPaged)
+ {
+ case 0:
+ result = Memory[2][addr % 0x4000];
+ break;
+ case 1:
+ result = Memory[3][addr % 0x4000];
+ break;
+ case 2:
+ result = Memory[4][addr % 0x4000];
+ break;
+ case 3:
+ result = Memory[5][addr % 0x4000];
+ break;
+ case 4:
+ result = Memory[6][addr % 0x4000];
+ break;
+ case 5:
+ result = Memory[7][addr % 0x4000];
+ break;
+ case 6:
+ result = Memory[8][addr % 0x4000];
+ break;
+ case 7:
+ result = Memory[9][addr % 0x4000];
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Simulates writing to the bus (no contention)
+ /// Paging should be handled here
+ ///
+ ///
+ ///
+ public override void WriteBus(ushort addr, byte value)
+ {
+ int divisor = addr / 0x4000;
+ switch (divisor)
+ {
+ // ROM 0x000
+ case 0:
+ if (!ROMPaged)
+ Memory[0][addr % 0x4000] = value;
+ else
+ Memory[1][addr % 0x4000] = value;
+ break;
+
+ // RAM 0x4000 (RAM5 - Bank5 or shadow bank RAM7)
+ case 1:
+ Memory[7][addr % 0x4000] = value;
+ break;
+
+ // RAM 0x8000 (RAM2 - Bank2)
+ case 2:
+ Memory[4][addr % 0x4000] = value;
+ break;
+
+ // RAM 0xc000 (any ram bank 0 - 7 may be paged in - default bank0)
+ case 3:
+ switch (RAMPaged)
+ {
+ case 0:
+ Memory[2][addr % 0x4000] = value;
+ break;
+ case 1:
+ Memory[3][addr % 0x4000] = value;
+ break;
+ case 2:
+ Memory[4][addr % 0x4000] = value;
+ break;
+ case 3:
+ Memory[5][addr % 0x4000] = value;
+ break;
+ case 4:
+ Memory[6][addr % 0x4000] = value;
+ break;
+ case 5:
+ Memory[7][addr % 0x4000] = value;
+ break;
+ case 6:
+ Memory[8][addr % 0x4000] = value;
+ break;
+ case 7:
+ Memory[9][addr % 0x4000] = value;
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ ///
+ /// Reads a byte of data from a specified memory address
+ /// (with memory contention if appropriate)
+ ///
+ ///
+ ///
+ public override byte ReadMemory(ushort addr)
+ {
+ var data = ReadBus(addr);
+ if ((addr & 0xC000) == 0x4000)
+ {
+ // addr is in RAM not ROM - apply memory contention if neccessary
+ var delay = GetContentionValue(CurrentFrameCycle);
+ CPU.TotalExecutedCycles += delay;
+ }
+ return data;
+ }
+
+ ///
+ /// Writes a byte of data to a specified memory address
+ /// (with memory contention if appropriate)
+ ///
+ ///
+ ///
+ public override void WriteMemory(ushort addr, byte value)
+ {
+ if (addr < 0x4000)
+ {
+ // Do nothing - we cannot write to ROM
+ return;
+ }
+ else if (addr < 0xC000)
+ {
+ // possible contended RAM
+ var delay = GetContentionValue(CurrentFrameCycle);
+ CPU.TotalExecutedCycles += delay;
+ }
+
+ WriteBus(addr, value);
+ }
+
+ public override void ReInitMemory()
+ {
+ if (Memory.ContainsKey(0))
+ Memory[0] = ROM0;
+ else
+ Memory.Add(0, ROM0);
+
+ if (Memory.ContainsKey(1))
+ Memory[1] = ROM1;
+ else
+ Memory.Add(1, ROM1);
+
+ if (Memory.ContainsKey(2))
+ Memory[2] = RAM0;
+ else
+ Memory.Add(2, RAM0);
+
+ if (Memory.ContainsKey(3))
+ Memory[3] = RAM1;
+ else
+ Memory.Add(3, RAM1);
+
+ if (Memory.ContainsKey(4))
+ Memory[4] = RAM2;
+ else
+ Memory.Add(4, RAM2);
+
+ if (Memory.ContainsKey(5))
+ Memory[5] = RAM3;
+ else
+ Memory.Add(5, RAM3);
+
+ if (Memory.ContainsKey(6))
+ Memory[6] = RAM4;
+ else
+ Memory.Add(6, RAM4);
+
+ if (Memory.ContainsKey(7))
+ Memory[7] = RAM5;
+ else
+ Memory.Add(7, RAM5);
+
+ if (Memory.ContainsKey(8))
+ Memory[8] = RAM6;
+ else
+ Memory.Add(8, RAM6);
+
+ if (Memory.ContainsKey(9))
+ Memory[9] = RAM7;
+ else
+ Memory.Add(9, RAM7);
+ }
+
+ ///
+ /// Sets up the ROM
+ ///
+ ///
+ ///
+ public override void InitROM(RomData romData)
+ {
+ RomData = romData;
+ // 128k uses ROM0 and ROM1
+ // 128k loader is in ROM0, and fallback 48k rom is in ROM1
+ for (int i = 0; i < 0x4000; i++)
+ {
+ ROM0[i] = RomData.RomBytes[i];
+ ROM1[i] = RomData.RomBytes[i + 0x4000];
+ }
+ }
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Port.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Port.cs
new file mode 100644
index 0000000000..35fe0c3391
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Port.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
+{
+ public partial class ZX128Plus3 : SpectrumBase
+ {
+ ///
+ /// Reads a byte of data from a specified port address
+ ///
+ ///
+ ///
+ public override byte ReadPort(ushort port)
+ {
+ int result = 0xFF;
+
+ // Check whether the low bit is reset
+ // Technically the ULA should respond to every even I/O address
+ bool lowBitReset = (port & 0x0001) == 0;
+
+ ContendPort((ushort)port);
+
+ // Kempston Joystick
+ if ((port & 0xe0) == 0 || (port & 0x20) == 0)
+ {
+ return (byte)KempstonDevice.JoyLine;
+ }
+ else if (lowBitReset)
+ {
+ // Even I/O address so get input
+ // The high byte indicates which half-row of keys is being polled
+ /*
+ IN: Reads keys (bit 0 to bit 4 inclusive)
+ 0xfefe SHIFT, Z, X, C, V 0xeffe 0, 9, 8, 7, 6
+ 0xfdfe A, S, D, F, G 0xdffe P, O, I, U, Y
+ 0xfbfe Q, W, E, R, T 0xbffe ENTER, L, K, J, H
+ 0xf7fe 1, 2, 3, 4, 5 0x7ffe SPACE, SYM SHFT, M, N, B
+ */
+
+ if ((port & 0x8000) == 0)
+ result &= KeyboardDevice.KeyLine[7];
+
+ if ((port & 0x4000) == 0)
+ result &= KeyboardDevice.KeyLine[6];
+
+ if ((port & 0x2000) == 0)
+ result &= KeyboardDevice.KeyLine[5];
+
+ if ((port & 0x1000) == 0)
+ result &= KeyboardDevice.KeyLine[4];
+
+ if ((port & 0x800) == 0)
+ result &= KeyboardDevice.KeyLine[3];
+
+ if ((port & 0x400) == 0)
+ result &= KeyboardDevice.KeyLine[2];
+
+ if ((port & 0x200) == 0)
+ result &= KeyboardDevice.KeyLine[1];
+
+ if ((port & 0x100) == 0)
+ result &= KeyboardDevice.KeyLine[0];
+
+ result = result & 0x1f; //mask out lower 4 bits
+ result = result | 0xa0; //set bit 5 & 7 to 1
+
+
+ if (TapeDevice.CurrentMode == TapeOperationMode.Load)
+ {
+ if (!TapeDevice.GetEarBit(CPU.TotalExecutedCycles))
+ {
+ result &= ~(TAPE_BIT); // reset is EAR ON
+ }
+ else
+ {
+ result |= (TAPE_BIT); // set is EAR Off
+ }
+ }
+ else
+ {
+ if (KeyboardDevice.IsIssue2Keyboard)
+ {
+ if ((LastULAOutByte & (EAR_BIT + MIC_BIT)) == 0)
+ {
+ result &= ~(TAPE_BIT);
+ }
+ else
+ {
+ result |= TAPE_BIT;
+ }
+ }
+ else
+ {
+ if ((LastULAOutByte & EAR_BIT) == 0)
+ {
+ result &= ~(TAPE_BIT);
+ }
+ else
+ {
+ result |= TAPE_BIT;
+ }
+ }
+ }
+
+ }
+ else
+ {
+ // devices other than the ULA will respond here
+ // (e.g. the AY sound chip in a 128k spectrum
+
+ // AY register activate
+ // Kemptson Mouse
+
+
+ // if unused port the floating memory bus should be returned (still todo)
+ }
+
+ return (byte)result;
+ }
+
+ ///
+ /// Writes a byte of data to a specified port address
+ ///
+ ///
+ ///
+ public override void WritePort(ushort port, byte value)
+ {
+ // paging
+ if (port == 0x7ffd)
+ {
+ // Bits 0, 1, 2 select the RAM page
+ var rp = value & 0x07;
+ if (rp < 8)
+ RAMPaged = rp;
+
+ // ROM page
+ if ((value & 0x10) != 0)
+ {
+ // 48k ROM
+ ROMPaged = true;
+ }
+ else
+ {
+ ROMPaged = false;
+ }
+
+ // Bit 5 signifies that paging is disabled until next reboot
+ if ((value & 0x20) != 0)
+ PagingDisabled = true;
+
+
+ return;
+ }
+
+ // Check whether the low bit is reset
+ // Technically the ULA should respond to every even I/O address
+ bool lowBitReset = (port & 0x01) == 0;
+
+ ContendPort(port);
+
+ // Only even addresses address the ULA
+ if (lowBitReset)
+ {
+ // store the last OUT byte
+ LastULAOutByte = value;
+
+ /*
+ Bit 7 6 5 4 3 2 1 0
+ +-------------------------------+
+ | | | | E | M | Border |
+ +-------------------------------+
+ */
+
+ // Border - LSB 3 bits hold the border colour
+ BorderColour = value & BORDER_BIT;
+
+ // Buzzer
+ BuzzerDevice.ProcessPulseValue(false, (value & EAR_BIT) != 0);
+
+ // Tape
+ TapeDevice.ProcessMicBit((value & MIC_BIT) != 0);
+ }
+ }
+ }
+}
diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.cs
new file mode 100644
index 0000000000..2a75a600f7
--- /dev/null
+++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.cs
@@ -0,0 +1,56 @@
+using BizHawk.Emulation.Cores.Components.Z80A;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
+{
+ public partial class ZX128Plus3 : SpectrumBase
+ {
+ #region Construction
+
+ ///
+ /// Main constructor
+ ///
+ ///
+ ///
+ public ZX128Plus3(ZXSpectrum spectrum, Z80A cpu, byte[] file)
+ {
+ Spectrum = spectrum;
+ CPU = cpu;
+
+ ROMPaged = false;
+ SHADOWPaged = false;
+ RAMPaged = 0;
+ PagingDisabled = false;
+
+ // init addressable memory from ROM and RAM banks
+ ReInitMemory();
+
+ //RAM = new byte[0x4000 + 0xC000];
+
+ //DisplayLineTime = 132;
+ VsyncNumerator = 3546900;
+
+ InitScreenConfig();
+ InitScreen();
+
+ ResetULACycle();
+
+ BuzzerDevice = new Buzzer(this);
+ BuzzerDevice.Init(44100, UlaFrameCycleCount);
+
+ KeyboardDevice = new Keyboard48(this);
+ KempstonDevice = new KempstonJoystick(this);
+
+ TapeProvider = new DefaultTapeProvider(file);
+
+ TapeDevice = new Tape(TapeProvider);
+ TapeDevice.Init(this);
+ }
+
+ #endregion
+ }
+}