diff --git a/LR35902/AbstractColourPalette.cs b/LR35902/AbstractColourPalette.cs new file mode 100644 index 0000000..1d27adc --- /dev/null +++ b/LR35902/AbstractColourPalette.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace EightBit +{ + namespace GameBoy + { + public class AbstractColourPalette + { + private readonly uint[] colours = new uint[4]; + + protected AbstractColourPalette() + { } + + public uint Colour(int index) => this.colours[index]; + } + } +} diff --git a/LR35902/CartridgeType.cs b/LR35902/CartridgeType.cs new file mode 100644 index 0000000..e5e5b44 --- /dev/null +++ b/LR35902/CartridgeType.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace EightBit +{ + namespace GameBoy + { + public enum CartridgeType + { + ROM = 0, + ROM_MBC1 = 1, + ROM_MBC1_RAM = 2, + ROM_MBC1_RAM_BATTERY = 3, + } + } +} diff --git a/LR35902/CharacterDefinition.cs b/LR35902/CharacterDefinition.cs new file mode 100644 index 0000000..d146ea7 --- /dev/null +++ b/LR35902/CharacterDefinition.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace EightBit +{ + namespace GameBoy + { + public sealed class CharacterDefinition + { + private readonly Ram vram; + private readonly ushort address; + + public CharacterDefinition(Ram vram, ushort address) + { + this.vram = vram; + this.address = address; + } + + public int[] Get(int row) + { + var returned = new int[8]; + + var planeAddress = (ushort)(this.address + (row * 2)); + + var planeLow = this.vram.Peek(planeAddress); + var planeHigh = this.vram.Peek(++planeAddress); + + for (var bit = 0; bit < 8; ++bit) + { + var mask = 1 << bit; + + var bitLow = (planeLow & mask) != 0 ? 1 : 0; + var bitHigh = (planeHigh & mask) != 0 ? 0b10 : 0; + + returned[7 - bit] = bitHigh | bitLow; + } + + return returned; + } + } + } +} diff --git a/LR35902/ColourShades.cs b/LR35902/ColourShades.cs new file mode 100644 index 0000000..b500d9f --- /dev/null +++ b/LR35902/ColourShades.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace EightBit +{ + namespace GameBoy + { + public enum ColourShades + { + Off, + Light, + Medium, + Dark + } + } +} diff --git a/LR35902/Display.cs b/LR35902/Display.cs new file mode 100644 index 0000000..3e9be27 --- /dev/null +++ b/LR35902/Display.cs @@ -0,0 +1,178 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace EightBit +{ + namespace GameBoy + { + public sealed class Display + { + public static readonly int BufferWidth = 256; + public static readonly int BufferHeight = 256; + public static readonly int BufferCharacterWidth = BufferWidth / 8; + public static readonly int BufferCharacterHeight = BufferHeight / 8; + public static readonly int RasterWidth = 160; + public static readonly int RasterHeight = 144; + public static readonly int PixelCount = RasterWidth * RasterHeight; + + private readonly Bus bus; + private readonly Ram oam; + private readonly Ram vram; + private readonly AbstractColourPalette colours; + private readonly ObjectAttribute[] objectAttributes = new ObjectAttribute[40]; + private byte control; + private byte scanLine = 0; + + public Display(AbstractColourPalette colours, Bus bus, Ram oam, Ram vram) + { + this.colours = colours; + this.bus = bus; + this.oam = oam; + this.vram = vram; + } + + public uint[] Pixels { get; } = new uint[PixelCount]; + + public void Render() + { + this.scanLine = this.bus.IO.Peek(IoRegisters.LY); + if (this.scanLine < RasterHeight) + { + this.control = this.bus.IO.Peek(IoRegisters.LCDC); + if ((this.control & (byte)LcdcControl.LcdEnable) != 0) + { + if ((this.control & (byte)LcdcControl.DisplayBackground) != 0) + { + this.RenderBackground(); + } + + if ((this.control & (byte)LcdcControl.ObjectEnable) != 0) + { + this.RenderObjects(); + } + } + } + } + + public void LoadObjectAttributes() + { + for (var i = 0; i < 40; ++i) + { + this.objectAttributes[i] = new ObjectAttribute(this.oam, (ushort)(4 * i)); + } + } + + private int[] CreatePalette(ushort address) + { + var raw = this.bus.IO.Peek(address); + return new int[4] + { + raw & 0b11, + (raw & 0b1100) >> 2, + (raw & 0b110000) >> 4, + (raw & 0b11000000) >> 6, + }; + } + + private void RenderBackground() + { + var palette = this.CreatePalette(IoRegisters.BGP); + + var window = (this.control & (byte)LcdcControl.WindowEnable) != 0; + var bgArea = (this.control & (byte)LcdcControl.BackgroundCodeAreaSelection) != 0 ? 0x1c00 : 0x1800; + var bgCharacters = (this.control & (byte)LcdcControl.BackgroundCharacterDataSelection) != 0 ? 0 : 0x800; + + var wx = this.bus.IO.Peek(IoRegisters.WX); + var wy = this.bus.IO.Peek(IoRegisters.WY); + + var offsetX = window ? wx - 7 : 0; + var offsetY = window ? wy : 0; + + var scrollX = this.bus.IO.Peek(IoRegisters.SCX); + var scrollY = this.bus.IO.Peek(IoRegisters.SCY); + + this.RenderBackground(bgArea, bgCharacters, offsetX - scrollX, offsetY - scrollY, palette); + } + + private void RenderBackground(int bgArea, int bgCharacters, int offsetX, int offsetY, int[] palette) + { + var row = (this.scanLine - offsetY) / 8; + var address = bgArea + (row * BufferCharacterWidth); + + for (var column = 0; column < BufferCharacterWidth; ++column) + { + var character = this.vram.Peek((ushort)address++); + var definition = new CharacterDefinition(this.vram, (ushort)(bgCharacters + (16 * character))); + this.RenderTile(8, (column * 8) + offsetX, (row * 8) + offsetY, false, false, false, palette, definition); + } + } + + private void RenderObjects() + { + var objBlockHeight = (this.control & (byte)LcdcControl.ObjectBlockCompositionSelection) != 0 ? 16 : 8; + + var palettes = new int[2][]; + palettes[0] = this.CreatePalette(IoRegisters.OBP0); + palettes[1] = this.CreatePalette(IoRegisters.OBP1); + + var characterAddressMultiplier = objBlockHeight == 8 ? 16 : 8; + + for (var i = 0; i < 40; ++i) + { + var current = this.objectAttributes[i]; + + var spriteY = current.PositionY; + var drawY = spriteY - 16; + + if ((this.scanLine >= drawY) && (this.scanLine < (drawY + objBlockHeight))) + { + var spriteX = current.PositionX; + var drawX = spriteX - 8; + + var sprite = current.Pattern; + var definition = new CharacterDefinition(this.vram, (ushort)(characterAddressMultiplier * sprite)); + var palette = palettes[current.Palette]; + var flipX = current.FlipX; + var flipY = current.FlipY; + + this.RenderTile(objBlockHeight, drawX, drawY, flipX, flipY, true, palette, definition); + } + } + } + + private void RenderTile(int height, int drawX, int drawY, bool flipX, bool flipY, bool allowTransparencies, int[] palette, CharacterDefinition definition) + { + const int width = 8; + const int flipMaskX = width - 1; + var flipMaskY = height - 1; + + var y = this.scanLine; + + var cy = y - drawY; + if (flipY) + { + cy = ~cy & flipMaskY; + } + + var rowDefinition = definition.Get(cy); + + var lineAddress = y * RasterWidth; + for (var cx = 0; cx < width; ++cx) + { + var x = drawX + (flipX ? ~cx & flipMaskX : cx); + if (x >= RasterWidth) + { + break; + } + + var colour = rowDefinition[cx]; + if (!allowTransparencies || (allowTransparencies && (colour > 0))) + { + var outputPixel = lineAddress + x; + this.Pixels[outputPixel] = this.colours.Colour(palette[colour]); + } + } + } + } + } +} diff --git a/LR35902/GameboyBus.cs b/LR35902/GameboyBus.cs new file mode 100644 index 0000000..550b7e6 --- /dev/null +++ b/LR35902/GameboyBus.cs @@ -0,0 +1,434 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +using System; +using System.Collections.Generic; + +namespace EightBit +{ + namespace GameBoy + { + public abstract class Bus : EightBit.Bus + { + public const int CyclesPerSecond = 4 * 1024 * 1024; + public const int FramesPerSecond = 60; + public const int CyclesPerFrame = CyclesPerSecond / FramesPerSecond; + public const int TotalLineCount = 154; + public const int CyclesPerLine = CyclesPerFrame / TotalLineCount; + public const int RomPageSize = 0x4000; + + private readonly Rom bootRom = new Rom(0x100); // 0x0000 - 0x00ff + private readonly List gameRomBanks; // 0x0000 - 0x3fff, 0x4000 - 0x7fff (switchable) + private readonly List ramBanks; // 0xa000 - 0xbfff (switchable) + private readonly UnusedMemory unmapped2000 = new UnusedMemory(0x2000, 0xff); // 0xa000 - 0xbfff + private readonly Ram lowInternalRam = new Ram(0x2000); // 0xc000 - 0xdfff (mirrored at 0xe000) + private readonly UnusedMemory unmapped60 = new UnusedMemory(0x60, 0xff); // 0xfea0 - 0xfeff + private readonly Ram highInternalRam = new Ram(0x80); // 0xff80 - 0xffff + + private bool enabledLCD = false; + + private bool disabledGameRom = false; + + private bool rom = false; + private bool banked = false; + private bool ram = false; + private bool battery = false; + + private bool higherRomBank = true; + private bool ramBankSwitching = false; + + private int romBank = 1; + private int ramBank = 0; + + protected Bus() + { + this.IO = new IoRegisters(this); + this.CPU = new LR35902(this); + } + + public LR35902 CPU { get; } + + public Ram VRAM { get; } = new Ram(0x2000); + + public Ram OAMRAM { get; } = new Ram(0xa0); + + public IoRegisters IO { get; } + + public bool GameRomDisabled => this.disabledGameRom; + + public bool GameRomEnabled => !this.GameRomDisabled; + + public override void RaisePOWER() + { + base.RaisePOWER(); + this.CPU.RaisePOWER(); + this.CPU.RaiseINT(); + this.Reset(); + } + + public override void LowerPOWER() + { + this.CPU.LowerPOWER(); + base.LowerPOWER(); + } + + public void Reset() + { + this.IO.Reset(); + this.CPU.LowerRESET(); + } + + public void DisableGameRom() => this.disabledGameRom = true; + + public void EnableGameRom() => this.disabledGameRom = false; + + public void LoadBootRom(string path) => this.bootRom.Load(path); + + public void LoadGameRom(string path) + { + const int bankSize = 0x4000; + this.gameRomBanks.Clear(); + this.gameRomBanks.Add(new Rom()); + var size = this.gameRomBanks[0].Load(path, 0, 0, bankSize); + var banks = size / bankSize; + for (var bank = 1; bank < banks; ++bank) + { + this.gameRomBanks.Add(new Rom()); + this.gameRomBanks[bank].Load(path, 0, bankSize * bank, bankSize); + } + + this.ValidateCartridgeType(); + } + + public int RunRasterLines() + { + this.enabledLCD = (this.IO.Peek(IoRegisters.LCDC) & (byte)LcdcControl.LcdEnable) != 0; + this.IO.ResetLY(); + return this.RunRasterLines(Display.RasterHeight); + } + + public int RunVerticalBlankLines() + { + var lines = TotalLineCount - Display.RasterHeight; + return this.RunVerticalBlankLines(lines); + } + + public override MemoryMapping Mapping(ushort address) + { + if ((address < 0x100) && this.IO.BootRomEnabled) + { + return new MemoryMapping(this.bootRom, 0x0000, Mask.Mask16, AccessLevel.ReadOnly); + } + + if ((address < 0x4000) && this.GameRomEnabled) + { + return new MemoryMapping(this.gameRomBanks[0], 0x0000, 0xffff, AccessLevel.ReadOnly); + } + + if ((address < 0x8000) && this.GameRomEnabled) + { + return new MemoryMapping(this.gameRomBanks[this.romBank], 0x4000, 0xffff, AccessLevel.ReadOnly); + } + + if (address < 0xa000) + { + return new MemoryMapping(this.VRAM, 0x8000, 0xffff, AccessLevel.ReadWrite); + } + + if (address < 0xc000) + { + if (this.ramBanks.Count == 0) + { + return new MemoryMapping(this.unmapped2000, 0xa000, 0xffff, AccessLevel.ReadOnly); + } + else + { + return new MemoryMapping(this.ramBanks[this.ramBank], 0xa000, 0xffff, AccessLevel.ReadWrite); + } + } + + if (address < 0xe000) + { + return new MemoryMapping(this.lowInternalRam, 0xc000, 0xffff, AccessLevel.ReadWrite); + } + + if (address < 0xfe00) + { + return new MemoryMapping(this.lowInternalRam, 0xe000, 0xffff, AccessLevel.ReadWrite); // Low internal RAM mirror + } + + if (address < 0xfea0) + { + return new MemoryMapping(this.OAMRAM, 0xfe00, 0xffff, AccessLevel.ReadWrite); + } + + if (address < IoRegisters.BASE) + { + return new MemoryMapping(this.unmapped60, 0xfea0, 0xffff, AccessLevel.ReadOnly); + } + + if (address < 0xff80) + { + return new MemoryMapping(this.IO, IoRegisters.BASE, 0xffff, AccessLevel.ReadWrite); + } + + return new MemoryMapping(this.highInternalRam, 0xff80, 0xffff, AccessLevel.ReadWrite); + } + + private void ValidateCartridgeType() + { + this.rom = this.banked = this.ram = this.battery = false; + + // ROM type + switch (this.gameRomBanks[0].Peek(0x147)) + { + case (byte)CartridgeType.ROM: + this.rom = true; + break; + case (byte)CartridgeType.ROM_MBC1: + this.rom = this.banked = true; + break; + case (byte)CartridgeType.ROM_MBC1_RAM: + this.rom = this.banked = this.ram = true; + break; + case (byte)CartridgeType.ROM_MBC1_RAM_BATTERY: + this.rom = this.banked = this.ram = this.battery = true; + break; + default: + throw new InvalidOperationException("Unhandled cartridge ROM type"); + } + + // ROM size + { + var gameRomBanks = 0; + var romSizeSpecification = this.Peek(0x148); + switch (romSizeSpecification) + { + case 0x52: + gameRomBanks = 72; + break; + case 0x53: + gameRomBanks = 80; + break; + case 0x54: + gameRomBanks = 96; + break; + default: + if (romSizeSpecification > 6) + { + throw new InvalidOperationException("Invalid ROM size specification"); + } + + gameRomBanks = 1 << (romSizeSpecification + 1); + if (gameRomBanks != this.gameRomBanks.Count) + { + throw new InvalidOperationException("ROM size specification mismatch"); + } + + break; + } + + // RAM size + { + var ramSizeSpecification = this.gameRomBanks[0].Peek(0x149); + switch (ramSizeSpecification) + { + case 0: + break; + case 1: + this.ramBanks.Clear(); + this.ramBanks.Add(new Ram(2 * 1024)); + break; + case 2: + this.ramBanks.Clear(); + this.ramBanks.Add(new Ram(8 * 1024)); + break; + case 3: + this.ramBanks.Clear(); + for (var i = 0; i < 4; ++i) + { + this.ramBanks.Add(new Ram(8 * 1024)); + } + + break; + case 4: + this.ramBanks.Clear(); + for (var i = 0; i < 16; ++i) + { + this.ramBanks.Add(new Ram(8 * 1024)); + } + + break; + default: + throw new InvalidOperationException("Invalid RAM size specification"); + } + } + } + } + + protected override void OnWrittenByte() + { + base.OnWrittenByte(); + + var address = this.Address.Word; + var value = this.Data; + + switch (address & 0xe000) + { + case 0x0000: + // Register 0: RAMCS gate data + if (this.ram) + { + throw new InvalidOperationException("Register 0: RAMCS gate data: Not handled!"); + } + break; + case 0x2000: + // Register 1: ROM bank code + if (this.banked && this.higherRomBank) + { + //assert((address >= 0x2000) && (address < 0x4000)); + //assert((value > 0) && (value < 0x20)); + this.romBank = value & (byte)Mask.Mask5; + } + break; + case 0x4000: + // Register 2: ROM bank selection + if (this.banked) + { + throw new InvalidOperationException("Register 2: ROM bank selection: Not handled!"); + } + break; + case 0x6000: + // Register 3: ROM/RAM change + if (this.banked) + { + switch (value & (byte)Mask.Mask1) + { + case 0: + this.higherRomBank = true; + this.ramBankSwitching = false; + break; + case 1: + this.higherRomBank = false; + this.ramBankSwitching = true; + break; + default: + throw new InvalidOperationException("Unreachable"); + } + } + break; + } + } + + private int RunRasterLines(int lines) + { + var count = 0; + var allowed = CyclesPerLine; + for (var line = 0; line < lines; ++line) + { + var executed = this.RunRasterLine(allowed); + count += executed; + allowed = CyclesPerLine - (executed - CyclesPerLine); + } + + return count; + } + + private int RunVerticalBlankLines(int lines) + { + /* + Vertical Blank interrupt is triggered when the LCD + controller enters the VBL screen mode (mode 1, LY=144). + This happens once per frame, so this interrupt is + triggered 59.7 times per second. During this period the + VRAM and OAM can be accessed freely, so it's the best + time to update graphics (for example, use the OAM DMA to + update sprites for next frame, or update tiles to make + animations). + This period lasts 4560 clocks in normal speed mode and + 9120 clocks in double speed mode. That's exactly the + time needed to draw 10 scanlines. + The VBL interrupt isn't triggered when the LCD is + powered off or on, even when it was on VBL mode. + It's only triggered when the VBL period starts. + */ + if (this.enabledLCD) + { + this.IO.UpdateLcdStatusMode(LcdStatusMode.VBlank); + if ((this.IO.Peek(IoRegisters.STAT) & (byte)Bits.Bit4) != 0) + { + this.IO.TriggerInterrupt(Interrupts.DisplayControlStatus); + } + + this.IO.TriggerInterrupt(Interrupts.VerticalBlank); + } + return this.RunRasterLines(lines); + } + + private int RunRasterLine(int limit) + { + /* + A scanline normally takes 456 clocks (912 clocks in double speed + mode) to complete. A scanline starts in mode 2, then goes to + mode 3 and, when the LCD controller has finished drawing the + line (the timings depend on lots of things) it goes to mode 0. + During lines 144-153 the LCD controller is in mode 1. + Line 153 takes only a few clocks to complete (the exact + timings are below). The rest of the clocks of line 153 are + spent in line 0 in mode 1! + + During mode 0 and mode 1 the CPU can access both VRAM and OAM. + During mode 2 the CPU can only access VRAM, not OAM. + During mode 3 OAM and VRAM can't be accessed. + In GBC mode the CPU can't access Palette RAM(FF69h and FF6Bh) + during mode 3. + A scanline normally takes 456 clocks(912 clocks in double speed mode) to complete. + A scanline starts in mode 2, then goes to mode 3 and , when the LCD controller has + finished drawing the line(the timings depend on lots of things) it goes to mode 0. + During lines 144 - 153 the LCD controller is in mode 1. + Line 153 takes only a few clocks to complete(the exact timings are below). + The rest of the clocks of line 153 are spent in line 0 in mode 1! + */ + + var count = 0; + if (this.enabledLCD) + { + if (((this.IO.Peek(IoRegisters.STAT) & (byte)Bits.Bit6) != 0) && (this.IO.Peek(IoRegisters.LYC) == this.IO.Peek(IoRegisters.LY))) + { + this.IO.TriggerInterrupt(Interrupts.DisplayControlStatus); + } + + // Mode 2, OAM unavailable + this.IO.UpdateLcdStatusMode(LcdStatusMode.SearchingOamRam); + if ((this.IO.Peek(IoRegisters.STAT) & (byte)Bits.Bit5) != 0) + { + this.IO.TriggerInterrupt(Interrupts.DisplayControlStatus); + } + + count += this.CPU.Run(80); // ~19us + + // Mode 3, OAM/VRAM unavailable + this.IO.UpdateLcdStatusMode(LcdStatusMode.TransferringDataToLcd); + count += this.CPU.Run(170); // ~41us + + // Mode 0 + this.IO.UpdateLcdStatusMode(LcdStatusMode.HBlank); + if ((this.IO.Peek(IoRegisters.STAT) & (byte)Bits.Bit3) != 0) + { + this.IO.TriggerInterrupt(Interrupts.DisplayControlStatus); + } + + count += this.CPU.Run(limit - count); // ~48.6us + + this.IO.IncrementLY(); + } + else + { + count += this.CPU.Run(CyclesPerLine); + } + + return count; + } + } + } +} diff --git a/LR35902/Interrupts.cs b/LR35902/Interrupts.cs new file mode 100644 index 0000000..ed56783 --- /dev/null +++ b/LR35902/Interrupts.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace EightBit +{ + namespace GameBoy + { + // IF and IE flags + [System.Flags] + public enum Interrupts + { + None = 0, + VerticalBlank = Bits.Bit0, // VBLANK + DisplayControlStatus = Bits.Bit1, // LCDC Status + TimerOverflow = Bits.Bit2, // Timer Overflow + SerialTransfer = Bits.Bit3, // Serial Transfer + KeypadPressed = Bits.Bit4 // Hi-Lo transition of P10-P13 + } + } +} diff --git a/LR35902/IoRegisters.cs b/LR35902/IoRegisters.cs new file mode 100644 index 0000000..1828177 --- /dev/null +++ b/LR35902/IoRegisters.cs @@ -0,0 +1,396 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace EightBit +{ + namespace GameBoy + { + using System; + + public sealed class IoRegisters : EightBit.Ram + { + public const int BASE = 0xFF00; + + // Port/Mode Registers + public const int P1 = 0x0; // R/W Mask5 + public const int SB = 0x1; // R/W Mask8 + public const int SC = 0x2; // R/W Bit7 | Bit0 + + // Timer control + public const int DIV = 0x4; // R/W Mask8 + public const int TIMA = 0x5; // R/W Mask8 + public const int TMA = 0x6; // R/W Mask8 + public const int TAC = 0x7; // R/W Mask3 + + // Interrupt Flags + public const int IF = 0xF; // R/W Mask5 + public const int IE = 0xFF; // R/W Mask5 + + // Sound Registers + public const int NR10 = 0x10; // R/W Mask7 + public const int NR11 = 0x11; // R/W Bit7 | Bit6 + public const int NR12 = 0x12; // R/W Mask8 + public const int NR13 = 0x13; // W 0 + public const int NR14 = 0x14; // R/W Bit6 + public const int NR21 = 0x16; // R/W Bit7 | Bit6 + public const int NR22 = 0x17; // R/W Mask8 + public const int NR23 = 0x18; // W 0 + public const int NR24 = 0x19; // R/W Bit6 + public const int NR30 = 0x1A; // R/W Bit7 + public const int NR31 = 0x1B; // R/W Mask8 + public const int NR32 = 0x1C; // R/W Bit6 | Bit5 + public const int NR33 = 0x1D; // W 0 + public const int NR34 = 0x1E; // R/W Bit6 + public const int NR41 = 0x20; // R/W Mask6 + public const int NR42 = 0x21; // R/W Mask8 + public const int NR43 = 0x22; // R/W Mask8 + public const int NR44 = 0x23; // R/W Bit6 + public const int NR50 = 0x24; // R/W Mask8 + public const int NR51 = 0x25; // R/W Mask8 + public const int NR52 = 0x26; // R/W Mask8 Mask8 + + public const int WAVE_PATTERN_RAM_START = 0x30; + public const int WAVE_PATTERN_RAM_END = 0x3F; + + // LCD Display Registers + public const int LCDC = 0x40; // R/W Mask8 + public const int STAT = 0x41; // R/W Mask7 + public const int SCY = 0x42; // R/W Mask8 + public const int SCX = 0x43; // R/W Mask8 + public const int LY = 0x44; // R Mask8 zeroed + public const int LYC = 0x45; // R/W Mask8 + public const int DMA = 0x46; // W 0 + public const int BGP = 0x47; // R/W Mask8 + public const int OBP0 = 0x48; // R/W Mask8 + public const int OBP1 = 0x49; // R/W Mask8 + public const int WY = 0x4A; // R/W Mask8 + public const int WX = 0x4B; // R/W Mask8 + + // Boot rom control + public const int BOOT_DISABLE = 0x50; + + private readonly Bus bus; + private readonly Register16 divCounter = new Register16(0xab, 0xcc); + private int timerCounter = 0; + private int timerRate = 0; + + private readonly Register16 dmaAddress = new Register16(); + private bool dmaTransferActive = false; + + private bool scanP15 = false; + private bool scanP14 = false; + + private bool p15 = true; // misc keys + private bool p14 = true; // direction keys + private bool p13 = true; // down/start + private bool p12 = true; // up/select + private bool p11 = true; // left/b + private bool p10 = true; // right/a + + public IoRegisters(Bus bus) + : base(0x80) + { + this.bus = bus; + this.bus.ReadingByte += this.Bus_ReadingByte; + this.bus.WrittenByte += this.Bus_WrittenByte; + } + + public event EventHandler DisplayStatusModeUpdated; + + public bool BootRomDisabled { get; private set; } = false; + + public bool BootRomEnabled => !this.BootRomDisabled; + + public int TimerClock => this.Peek((ushort)TAC) & (byte)Mask.Mask2; + + public bool TimerEnabled => !this.TimerDisabled; + + public bool TimerDisabled => (this.Peek((ushort)TAC) & (byte)Bits.Bit2) == 0; + + public void Reset() + { + this.Poke((ushort)NR52, 0xf1); + this.Poke((ushort)LCDC, (byte)(LcdcControl.DisplayBackground | LcdcControl.BackgroundCharacterDataSelection | LcdcControl.LcdEnable)); + this.divCounter.Word = 0xabcc; + this.timerCounter = 0; + } + + public void TransferDma() + { + if (this.dmaTransferActive) + { + this.bus.OAMRAM.Poke(this.dmaAddress.Low, this.bus.Peek(this.dmaAddress)); + this.dmaTransferActive = ++this.dmaAddress.Low < 0xa0; + } + } + + public void TriggerInterrupt(Interrupts cause) => this.Poke((ushort)IF, (byte)(this.Peek((ushort)IF) | (byte)cause)); + + public void CheckTimers(int cycles) + { + this.IncrementDIV(cycles); + this.CheckTimer(cycles); + } + + public int TimerClockTicks + { + get + { + switch (this.TimerClock) + { + case 0b00: + return 1024; // 4.096 Khz + case 0b01: + return 16; // 262.144 Khz + case 0b10: + return 64; // 65.536 Khz + case 0b11: + return 256; // 16.384 Khz + } + throw new InvalidOperationException("Invalid timer clock specification"); + } + } + + public void IncrementDIV(int cycles) + { + this.divCounter.Word += (ushort)cycles; + this.Poke((ushort)DIV, this.divCounter.High); + } + + public void IncrementTIMA() + { + var updated = this.Peek((ushort)TIMA) + 1; + if ((updated & (int)Bits.Bit8) != 0) + { + this.TriggerInterrupt(Interrupts.TimerOverflow); + updated = this.Peek((ushort)TMA); + } + + this.Poke((ushort)TIMA, Chip.LowByte(updated)); + } + + public void IncrementLY() => this.Poke((ushort)LY, (byte)((this.Peek((ushort)LY) + 1) % GameBoy.Bus.TotalLineCount)); + + public void ResetLY() => this.Poke((ushort)LY, 0); + + public void UpdateLcdStatusMode(LcdStatusMode mode) + { + var current = this.Peek((ushort)STAT) & unchecked((byte)~Mask.Mask2); + this.Poke((ushort)STAT, (byte)(current | (int)mode)); + this.OnDisplayStatusModeUpdated(mode); + } + + public void DisableBootRom() => this.BootRomDisabled = true; + + public void EnableBootRom() => this.BootRomDisabled = false; + + public void PressRight() + { + this.p14 = this.p10 = false; + this.TriggerKeypadInterrupt(); + } + + public void ReleaseRight() => this.p14 = this.p10 = true; + + public void PressLeft() + { + this.p14 = this.p11 = false; + this.TriggerKeypadInterrupt(); + } + + public void ReleaseLeft() => this.p14 = this.p11 = true; + + public void PressUp() + { + this.p14 = this.p12 = false; + this.TriggerKeypadInterrupt(); + } + + public void ReleaseUp() => this.p14 = this.p12 = true; + + public void PressDown() + { + this.p14 = this.p13 = false; + this.TriggerKeypadInterrupt(); + } + + public void ReleaseDown() => this.p14 = this.p13 = true; + + public void PressA() + { + this.p15 = this.p10 = false; + this.TriggerKeypadInterrupt(); + } + + public void ReleaseA() => this.p15 = this.p10 = true; + + public void PressB() + { + this.p15 = this.p11 = false; + this.TriggerKeypadInterrupt(); + } + + public void ReleaseB() => this.p15 = this.p11 = true; + + public void PressSelect() + { + this.p15 = this.p12 = false; + this.TriggerKeypadInterrupt(); + } + + public void ReleaseSelect() => this.p15 = this.p12 = true; + + public void PressStart() + { + this.p15 = this.p13 = false; + this.TriggerKeypadInterrupt(); + } + + public void ReleaseStart() => this.p15 = this.p13 = true; + + private void OnDisplayStatusModeUpdated(LcdStatusMode mode) => this.DisplayStatusModeUpdated?.Invoke(this, new LcdStatusModeEventArgs(mode)); + + private void CheckTimer(int cycles) + { + if (this.TimerEnabled) + { + this.timerCounter -= cycles; + if (this.timerCounter <= 0) + { + this.timerCounter += this.timerRate; + this.IncrementTIMA(); + } + } + } + + private void ApplyMask(ushort address, byte masking) => this.Poke(address, (byte)(this.Peek(address) | ~masking)); + + private void TriggerKeypadInterrupt() => this.TriggerInterrupt(Interrupts.KeypadPressed); + + private void Bus_WrittenByte(object sender, System.EventArgs e) + { + var address = this.bus.Address.Word; + var value = this.bus.Data; + var port = (ushort)(address - BASE); + + switch (port) + { + case P1: + this.scanP14 = (value & (byte)Bits.Bit4) == 0; + this.scanP15 = (value & (byte)Bits.Bit5) == 0; + break; + + case SB: // R/W + case SC: // R/W + break; + + case DIV: // R/W + this.Poke(port, 0); + this.timerCounter = this.divCounter.Word = 0; + break; + case TIMA: // R/W + case TMA: // R/W + break; + case TAC: // R/W + this.timerRate = this.TimerClockTicks; + break; + + case IF: // R/W + break; + + case LCDC: + case STAT: + case SCY: + case SCX: + break; + case DMA: + this.dmaAddress.Word = Chip.PromoteByte(value); + this.dmaTransferActive = true; + break; + case LY: // R/O + this.Poke(port, 0); + break; + case BGP: + case OBP0: + case OBP1: + case WY: + case WX: + break; + + case BOOT_DISABLE: + this.BootRomDisabled = value != 0; + break; + } + } + + private void Bus_ReadingByte(object sender, System.EventArgs e) + { + var address = this.bus.Address.Word; + var io = (address >= BASE) && (address < 0xff80); + if (io) + { + var port = (ushort)(address - BASE); + switch (port) + { + // Port/Mode Registers + case P1: + { + var p14 = this.scanP14 && !this.p14; + var p15 = this.scanP15 && !this.p15; + var live = p14 || p15; + var p10 = live && this.p10 ? 1 : 0; + var p11 = live && this.p11 ? 1 : 0; + var p12 = live && this.p12 ? 1 : 0; + var p13 = live && this.p13 ? 1 : 0; + this.Poke(port, + (byte)(p10 | (p11 << 1) | (p12 << 2) | (p13 << 3) + | (int)(Bits.Bit4 | Bits.Bit5 | Bits.Bit6 | Bits.Bit7))); + } + break; + case SB: + break; + case SC: + this.ApplyMask(port, (byte)(Bits.Bit7 | Bits.Bit0)); + break; + + // Timer control + case DIV: + case TIMA: + case TMA: + break; + case TAC: + this.ApplyMask(port, (byte)Mask.Mask3); + break; + + // Interrupt Flags + case IF: + this.ApplyMask(port, (byte)Mask.Mask5); + break; + + // LCD Display Registers + case LCDC: + break; + case STAT: + this.ApplyMask(port, (byte)Mask.Mask7); + break; + case SCY: + case SCX: + case LY: + case LYC: + case DMA: + case BGP: + case OBP0: + case OBP1: + case WY: + case WX: + break; + + default: + this.ApplyMask(port, 0); + break; + } + } + } + } + } +} diff --git a/LR35902/LR35902.cs b/LR35902/LR35902.cs new file mode 100644 index 0000000..6e11cd3 --- /dev/null +++ b/LR35902/LR35902.cs @@ -0,0 +1,1130 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// + +namespace EightBit +{ + namespace GameBoy + { + using System; + + public class LR35902 : IntelProcessor + { + private readonly Bus bus; + private bool prefixCB = false; + + public LR35902(Bus bus) + : base(bus) => this.bus = bus; + + public event EventHandler ExecutingInstruction; + + public event EventHandler ExecutedInstruction; + + public int ClockCycles => this.Cycles * 4; + + public override Register16 AF { get; } = new Register16((int)Mask.Mask16); + + public override Register16 BC { get; } = new Register16((int)Mask.Mask16); + + public override Register16 DE { get; } = new Register16((int)Mask.Mask16); + + public override Register16 HL { get; } = new Register16((int)Mask.Mask16); + + private bool IME { get; set; } = false; + + private bool Stopped { get; set; } = false; + + public override int Execute() + { + var decoded = this.GetDecodedOpCode(this.OpCode); + + var x = decoded.X; + var y = decoded.Y; + var z = decoded.Z; + + var p = decoded.P; + var q = decoded.Q; + + if (this.prefixCB) + { + this.ExecuteCB(x, y, z); + } + else + { + this.ExecuteOther(x, y, z, p, q); + } + + return this.ClockCycles; + } + + public override int Step() + { + this.OnExecutingInstruction(); + this.prefixCB = false; + this.ResetCycles(); + if (this.Powered) + { + var interruptEnable = this.Bus.Peek(IoRegisters.BASE + IoRegisters.IE); + var interruptFlags = this.bus.IO.Peek(IoRegisters.IF); + + var masked = interruptEnable & interruptFlags; + if (masked != 0) + { + if (this.IME) + { + this.bus.IO.Poke(IoRegisters.IF, 0); + this.LowerINT(); + var index = Chip.FindFirstSet(masked); + this.Bus.Data = (byte)(0x38 + (index << 3)); + } + else + { + if (this.Halted) + { + this.Proceed(); + } + } + } + + if (this.RESET.Lowered()) + { + this.HandleRESET(); + } + else if (this.INT.Lowered()) + { + this.HandleINT(); + } + else if (this.Halted) + { + this.Execute(0); // NOP + } + else + { + this.Execute(this.FetchByte()); + } + + this.bus.IO.CheckTimers(this.ClockCycles); + this.bus.IO.TransferDma(); + } + + this.OnExecutedInstruction(); + return this.ClockCycles; + } + + protected virtual void OnExecutingInstruction() => this.ExecutingInstruction?.Invoke(this, EventArgs.Empty); + + protected virtual void OnExecutedInstruction() => this.ExecutedInstruction?.Invoke(this, EventArgs.Empty); + + protected override void HandleRESET() + { + base.HandleRESET(); + this.DI(); + this.SP.Word = (ushort)(Mask.Mask16 - 1); + this.Tick(4); + } + + protected override void HandleINT() + { + base.HandleINT(); + this.RaiseHALT(); + this.DI(); + this.Restart(this.Bus.Data); + this.Tick(4); + } + + private static byte SetBit(byte f, StatusBits flag) => SetBit(f, (byte)flag); + + private static byte SetBit(byte f, StatusBits flag, int condition) => SetBit(f, (byte)flag, condition); + + private static byte SetBit(byte f, StatusBits flag, bool condition) => SetBit(f, (byte)flag, condition); + + private static byte ClearBit(byte f, StatusBits flag) => ClearBit(f, (byte)flag); + + private static byte ClearBit(byte f, StatusBits flag, int condition) => ClearBit(f, (byte)flag, condition); + + private static byte AdjustZero(byte input, byte value) => ClearBit(input, StatusBits.ZF, value); + + private static byte AdjustHalfCarryAdd(byte input, byte before, byte value, int calculation) => SetBit(input, StatusBits.HC, CalculateHalfCarryAdd(before, value, calculation)); + + private static byte AdjustHalfCarrySub(byte input, byte before, byte value, int calculation) => SetBit(input, StatusBits.HC, CalculateHalfCarrySub(before, value, calculation)); + + private void DI() => this.IME = false; + + private void EI() => this.IME = true; + + private void Stop() => this.Stopped = true; + + private void Start() => this.Stopped = false; + + private byte R(int r) + { + switch (r) + { + case 0: + return this.B; + case 1: + return this.C; + case 2: + return this.D; + case 3: + return this.E; + case 4: + return this.H; + case 5: + return this.L; + case 6: + return this.BusRead(this.HL.Word); + case 7: + return this.A; + default: + throw new ArgumentOutOfRangeException(nameof(r)); + } + } + + private void R(int r, byte value) + { + switch (r) + { + case 0: + this.B = value; + break; + case 1: + this.C = value; + break; + case 2: + this.D = value; + break; + case 3: + this.E = value; + break; + case 4: + this.H = value; + break; + case 5: + this.L = value; + break; + case 6: + this.BusWrite(this.HL.Word, value); + break; + case 7: + this.A = value; + break; + default: + throw new ArgumentOutOfRangeException(nameof(r)); + } + } + + private Register16 RP(int rp) + { + switch (rp) + { + case 0: + return this.BC; + case 1: + return this.DE; + case 2: + return this.HL; + case 3: + return this.SP; + default: + throw new ArgumentOutOfRangeException(nameof(rp)); + } + } + + private Register16 RP2(int rp) + { + switch (rp) + { + case 0: + return this.BC; + case 1: + return this.DE; + case 2: + return this.HL; + case 3: + return this.AF; + default: + throw new ArgumentOutOfRangeException(nameof(rp)); + } + } + + private void ExecuteCB(int x, int y, int z) + { + switch (x) + { + case 0: // rot[y] r[z] + { + var operand = this.R(z); + switch (y) + { + case 0: + operand = this.RLC(operand); + break; + case 1: + operand = this.RRC(operand); + break; + case 2: + operand = this.RL(operand); + break; + case 3: + operand = this.RR(operand); + break; + case 4: + operand = this.SLA(operand); + break; + case 5: + operand = this.SRA(operand); + break; + case 6: // GB: SWAP r + operand = this.Swap(operand); + break; + case 7: + operand = this.SRL(operand); + break; + default: + throw new InvalidOperationException("Unreachable code block reached"); + } + + this.Tick(2); + this.R(z, operand); + this.F = AdjustZero(this.F, operand); + if (z == 6) + { + this.Tick(2); + } + + break; + } + + case 1: // BIT y, r[z] + this.Bit(y, this.R(z)); + this.Tick(2); + if (z == 6) + { + this.Tick(2); + } + + break; + + case 2: // RES y, r[z] + this.R(z, Res(y, this.R(z))); + this.Tick(2); + if (z == 6) + { + this.Tick(2); + } + + break; + + case 3: // SET y, r[z] + this.R(z, Set(y, this.R(z))); + this.Tick(2); + if (z == 6) + { + this.Tick(2); + } + + break; + + default: + throw new InvalidOperationException("Unreachable code block reached"); + } + } + + private void ExecuteOther(int x, int y, int z, int p, int q) + { + switch (x) + { + case 0: + switch (z) + { + case 0: // Relative jumps and assorted ops + switch (y) + { + case 0: // NOP + this.Tick(4); + break; + case 1: // GB: LD (nn),SP + this.Bus.Address.Word = this.FetchWord().Word; + this.SetWord(this.SP); + this.Tick(5); + break; + case 2: // GB: STOP + this.Stop(); + this.Tick(); + break; + case 3: // JR d + this.JumpRelative((sbyte)this.FetchByte()); + this.Tick(4); + break; + case 4: // JR cc,d + case 5: + case 6: + case 7: + if (this.JumpRelativeConditionalFlag(y - 4)) + { + this.Tick(); + } + + this.Tick(2); + break; + default: + throw new InvalidOperationException("Unreachable code block reached"); + } + + break; + + case 1: // 16-bit load immediate/add + switch (q) + { + case 0: // LD rp,nn + this.RP(p).Word = this.FetchWord().Word; + this.Tick(3); + break; + + case 1: // ADD HL,rp + this.Add(this.HL, this.RP(p)); + this.Tick(2); + break; + + default: + throw new NotSupportedException("Invalid operation mode"); + } + + break; + + case 2: // Indirect loading + switch (q) + { + case 0: + switch (p) + { + case 0: // LD (BC),A + this.BusWrite(this.BC, this.A); + this.Tick(2); + break; + + case 1: // LD (DE),A + this.BusWrite(this.DE, this.A); + this.Tick(2); + break; + + case 2: // GB: LDI (HL),A + this.BusWrite(this.HL.Word++, this.A); + this.Tick(2); + break; + + case 3: // GB: LDD (HL),A + this.BusWrite(this.HL.Word--, this.A); + this.Tick(2); + break; + + default: + throw new NotSupportedException("Invalid operation mode"); + } + + break; + + case 1: + switch (p) + { + case 0: // LD A,(BC) + this.A = this.BusRead(this.BC); + this.Tick(2); + break; + + case 1: // LD A,(DE) + this.A = this.BusRead(this.DE); + this.Tick(2); + break; + + case 2: // GB: LDI A,(HL) + this.A = this.BusRead(this.HL.Word++); + this.Tick(2); + break; + + case 3: // GB: LDD A,(HL) + this.A = this.BusRead(this.HL.Word--); + this.Tick(2); + break; + + default: + throw new NotSupportedException("Invalid operation mode"); + } + + break; + + default: + throw new NotSupportedException("Invalid operation mode"); + } + + break; + + case 3: // 16-bit INC/DEC + switch (q) + { + case 0: // INC rp + ++this.RP(p).Word; + break; + + case 1: // DEC rp + --this.RP(p).Word; + break; + + default: + throw new NotSupportedException("Invalid operation mode"); + } + + this.Tick(2); + break; + + case 4: // 8-bit INC + this.R(y, this.Increment(this.R(y))); + this.Tick(); + if (y == 6) + { + this.Tick(2); + } + + break; + + case 5: // 8-bit DEC + this.R(y, this.Decrement(this.R(y))); + this.Tick(); + if (y == 6) + { + this.Tick(2); + } + + break; + + case 6: // 8-bit load immediate + this.R(y, this.FetchByte()); + this.Tick(2); + break; + + case 7: // Assorted operations on accumulator/flags + switch (y) + { + case 0: + this.A = this.RLC(this.A); + break; + case 1: + this.A = this.RRC(this.A); + break; + case 2: + this.A = this.RL(this.A); + break; + case 3: + this.A = this.RR(this.A); + break; + case 4: + this.DAA(); + break; + case 5: + this.Cpl(); + break; + case 6: + this.SCF(); + break; + case 7: + this.CCF(); + break; + default: + throw new NotSupportedException("Invalid operation mode"); + } + + this.Tick(4); + break; + + default: + throw new NotSupportedException("Invalid operation mode"); + } + + break; + + case 1: // 8-bit loading + if (z == 6 && y == 6) + { + this.Halt(); // Exception (replaces LD (HL), (HL)) + } + else + { + this.R(y, this.R(z)); + if ((y == 6) || (z == 6)) + { + this.Tick(); // M operations + } + } + + this.Tick(); + break; + + case 2: // Operate on accumulator and register/memory location + switch (y) + { + case 0: // ADD A,r + this.A = this.Add(this.A, this.R(z)); + break; + case 1: // ADC A,r + this.A = this.ADC(this.A, this.R(z)); + break; + case 2: // SUB r + this.A = this.Subtract(this.A, this.R(z)); + break; + case 3: // SBC A,r + this.A = this.SBC(this.A, this.R(z)); + break; + case 4: // AND r + this.AndR(this.R(z)); + break; + case 5: // XOR r + this.XorR(this.R(z)); + break; + case 6: // OR r + this.OrR(this.R(z)); + break; + case 7: // CP r + this.Compare(this.R(z)); + break; + default: + throw new NotSupportedException("Invalid operation mode"); + } + + this.Tick(); + if (z == 6) + { + this.Tick(); + } + + break; + case 3: + switch (z) + { + case 0: // Conditional return + switch (y) + { + case 0: + case 1: + case 2: + case 3: + if (this.ReturnConditionalFlag(y)) + { + this.Tick(3); + } + + this.Tick(2); + break; + + case 4: // GB: LD (FF00 + n),A + this.BusWrite((ushort)(IoRegisters.BASE + this.FetchByte()), this.A); + this.Tick(3); + break; + + case 5: + { // GB: ADD SP,dd + var before = this.SP.Word; + var value = (sbyte)this.FetchByte(); + var result = before + value; + this.SP.Word = (ushort)result; + var carried = before ^ value ^ (result & (int)Mask.Mask16); + this.F = ClearBit(this.F, StatusBits.ZF | StatusBits.NF); + this.F = SetBit(this.F, StatusBits.CF, carried & (int)Bits.Bit8); + this.F = SetBit(this.F, StatusBits.HC, carried & (int)Bits.Bit4); + } + + this.Tick(4); + break; + + case 6: // GB: LD A,(FF00 + n) + this.A = this.BusRead((ushort)(IoRegisters.BASE + this.FetchByte())); + this.Tick(3); + break; + + case 7: + { // GB: LD HL,SP + dd + var before = this.SP.Word; + var value = (sbyte)this.FetchByte(); + var result = before + value; + this.HL.Word = (ushort)result; + var carried = before ^ value ^ (result & (int)Mask.Mask16); + this.F = ClearBit(this.F, StatusBits.ZF | StatusBits.NF); + this.F = SetBit(this.F, StatusBits.CF, carried & (int)Bits.Bit8); + this.F = SetBit(this.F, StatusBits.HC, carried & (int)Bits.Bit4); + } + + this.Tick(3); + break; + + default: + throw new NotSupportedException("Invalid operation mode"); + } + + break; + case 1: // POP & various ops + switch (q) + { + case 0: // POP rp2[p] + this.RP2(p).Word = this.PopWord().Word; + this.Tick(3); + break; + case 1: + switch (p) + { + case 0: // RET + this.Return(); + this.Tick(4); + break; + case 1: // GB: RETI + this.RetI(); + this.Tick(4); + break; + case 2: // JP HL + this.Jump(this.HL.Word); + this.Tick(); + break; + case 3: // LD SP,HL + this.SP.Word = this.HL.Word; + this.Tick(2); + break; + default: + throw new NotSupportedException("Invalid operation mode"); + } + + break; + + default: + throw new NotSupportedException("Invalid operation mode"); + } + + break; + case 2: // Conditional jump + switch (y) + { + case 0: + case 1: + case 2: + case 3: + this.JumpConditionalFlag(y); + this.Tick(3); + break; + case 4: // GB: LD (FF00 + C),A + this.BusWrite((ushort)(IoRegisters.BASE + this.C), this.A); + this.Tick(2); + break; + case 5: // GB: LD (nn),A + this.Bus.Address.Word = this.MEMPTR.Word = this.FetchWord().Word; + this.BusWrite(this.A); + this.Tick(4); + break; + case 6: // GB: LD A,(FF00 + C) + this.A = this.BusRead((ushort)(IoRegisters.BASE + this.C)); + this.Tick(2); + break; + case 7: // GB: LD A,(nn) + this.Bus.Address.Word = this.MEMPTR.Word = this.FetchWord().Word; + this.A = this.BusRead(); + this.Tick(4); + break; + default: + throw new NotSupportedException("Invalid operation mode"); + } + break; + case 3: // Assorted operations + switch (y) + { + case 0: // JP nn + this.Jump(this.FetchWord().Word); + this.Tick(4); + break; + case 1: // CB prefix + this.prefixCB = true; + this.Execute(this.FetchByte()); + break; + case 6: // DI + this.DI(); + this.Tick(); + break; + case 7: // EI + this.EI(); + this.Tick(); + break; + } + + break; + + case 4: // Conditional call: CALL cc[y], nn + if (this.CallConditionalFlag(y)) + { + this.Tick(3); + } + + this.Tick(3); + break; + + case 5: // PUSH & various ops + switch (q) + { + case 0: // PUSH rp2[p] + this.PushWord(this.RP2(p)); + this.Tick(4); + break; + + case 1: + switch (p) + { + case 0: // CALL nn + this.Call(this.FetchWord().Word); + this.Tick(6); + break; + } + + break; + + default: + throw new NotSupportedException("Invalid operation mode"); + } + + break; + + case 6: // Operate on accumulator and immediate operand: alu[y] n + switch (y) + { + case 0: // ADD A,n + this.A = this.Add(this.A, this.FetchByte()); + break; + case 1: // ADC A,n + this.A = this.ADC(this.A, this.FetchByte()); + break; + case 2: // SUB n + this.A = this.Subtract(this.A, this.FetchByte()); + break; + case 3: // SBC A,n + this.A = this.SBC(this.A, this.FetchByte()); + break; + case 4: // AND n + this.AndR(this.FetchByte()); + break; + case 5: // XOR n + this.XorR(this.FetchByte()); + break; + case 6: // OR n + this.OrR(this.FetchByte()); + break; + case 7: // CP n + this.Compare(this.FetchByte()); + break; + default: + throw new NotSupportedException("Invalid operation mode"); + } + + this.Tick(2); + break; + + case 7: // Restart: RST y * 8 + this.Restart((byte)(y << 3)); + this.Tick(4); + break; + + default: + throw new NotSupportedException("Invalid operation mode"); + } + + break; + } + } + + private byte Increment(byte operand) + { + this.F = ClearBit(this.F, StatusBits.NF); + this.F = AdjustZero(this.F, ++operand); + this.F = ClearBit(this.F, StatusBits.HC, LowNibble(operand)); + return operand; + } + + private byte Decrement(byte operand) + { + this.F = SetBit(this.F, StatusBits.NF); + this.F = ClearBit(this.F, StatusBits.HC, LowNibble(operand)); + this.F = AdjustZero(this.F, --operand); + return operand; + } + + private bool JumpConditionalFlag(int flag) + { + switch (flag) + { + case 0: // NZ + return this.JumpConditional((this.F & (byte)StatusBits.ZF) == 0); + case 1: // Z + return this.JumpConditional((this.F & (byte)StatusBits.ZF) != 0); + case 2: // NC + return this.JumpConditional((this.F & (byte)StatusBits.CF) == 0); + case 3: // C + return this.JumpConditional((this.F & (byte)StatusBits.CF) != 0); + default: + throw new ArgumentOutOfRangeException(nameof(flag)); + } + } + + private bool JumpRelativeConditionalFlag(int flag) + { + switch (flag) + { + case 0: // NZ + return this.JumpRelativeConditional((this.F & (byte)StatusBits.ZF) == 0); + case 1: // Z + return this.JumpRelativeConditional((this.F & (byte)StatusBits.ZF) != 0); + case 2: // NC + return this.JumpRelativeConditional((this.F & (byte)StatusBits.CF) == 0); + case 3: // C + return this.JumpRelativeConditional((this.F & (byte)StatusBits.CF) != 0); + default: + throw new ArgumentOutOfRangeException(nameof(flag)); + } + } + + private bool ReturnConditionalFlag(int flag) + { + switch (flag) + { + case 0: // NZ + return this.ReturnConditional((this.F & (byte)StatusBits.ZF) == 0); + case 1: // Z + return this.ReturnConditional((this.F & (byte)StatusBits.ZF) != 0); + case 2: // NC + return this.ReturnConditional((this.F & (byte)StatusBits.CF) == 0); + case 3: // C + return this.ReturnConditional((this.F & (byte)StatusBits.CF) != 0); + default: + throw new ArgumentOutOfRangeException(nameof(flag)); + } + } + + private bool CallConditionalFlag(int flag) + { + switch (flag) + { + case 0: // NZ + return this.CallConditional((this.F & (byte)StatusBits.ZF) == 0); + case 1: // Z + return this.CallConditional((this.F & (byte)StatusBits.ZF) != 0); + case 2: // NC + return this.CallConditional((this.F & (byte)StatusBits.CF) == 0); + case 3: // C + return this.CallConditional((this.F & (byte)StatusBits.CF) != 0); + default: + throw new ArgumentOutOfRangeException(nameof(flag)); + } + } + + + private void Add(Register16 operand, Register16 value) + { + this.MEMPTR.Word = operand.Word; + + var result = this.MEMPTR.Word + value.Word; + + operand.Word = (ushort)result; + + this.F = ClearBit(this.F, StatusBits.NF); + this.F = SetBit(this.F, StatusBits.CF, result & (int)Bits.Bit16); + this.F = AdjustHalfCarryAdd(this.F, this.MEMPTR.High, value.High, operand.High); + } + + private byte Add(byte operand, byte value, int carry = 0) + { + this.MEMPTR.Word = (ushort)(operand + value + carry); + + this.F = AdjustHalfCarryAdd(this.F, operand, value, this.MEMPTR.Low); + + operand = this.MEMPTR.Low; + + this.F = ClearBit(this.F, StatusBits.NF); + this.F = SetBit(this.F, StatusBits.CF, this.MEMPTR.Word & (ushort)Bits.Bit8); + this.F = AdjustZero(this.F, operand); + + return operand; + } + + private byte ADC(byte operand, byte value) => this.Add(operand, value, (this.F & (byte)StatusBits.CF) >> 4); + + private byte Subtract(byte operand, byte value, int carry = 0) + { + this.MEMPTR.Word = (ushort)(operand - value - carry); + + this.F = AdjustHalfCarrySub(this.F, operand, value, this.MEMPTR.Low); + + var result = operand = this.MEMPTR.Low; + + this.F = SetBit(this.F, StatusBits.NF); + this.F = SetBit(this.F, StatusBits.CF, this.MEMPTR.High & (byte)Bits.Bit0); + this.F = AdjustZero(this.F, operand); + + return result; + } + + private byte SBC(byte operand, byte value) => this.Subtract(operand, value, (this.F & (byte)StatusBits.CF) >> 4); + + private byte AndR(byte operand, byte value) + { + this.F = SetBit(this.F, StatusBits.HC); + this.F = ClearBit(this.F, StatusBits.CF | StatusBits.NF); + this.F = AdjustZero(this.F, operand &= value); + return operand; + } + + private void AndR(byte value) => this.A = this.AndR(this.A, value); + + private byte XorR(byte operand, byte value) + { + this.F = ClearBit(this.F, StatusBits.HC | StatusBits.CF | StatusBits.NF); + this.F = AdjustZero(this.F, operand ^= value); + return operand; + } + + private void XorR(byte value) => this.A = this.XorR(this.A, value); + + private byte OrR(byte operand, byte value) + { + this.F = ClearBit(this.F, StatusBits.HC | StatusBits.CF | StatusBits.NF); + this.F = AdjustZero(this.F, operand |= value); + return operand; + } + + private void OrR(byte value) => this.A = this.OrR(this.A, value); + + private void Compare(byte value) => this.Subtract(this.A, value); + + private byte RLC(byte operand) + { + this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.ZF); + var carry = operand & (byte)Bits.Bit7; + this.F = SetBit(this.F, StatusBits.CF, carry); + return (byte)((operand << 1) | (carry >> 7)); + } + + private byte RRC(byte operand) + { + this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.ZF); + var carry = operand & (byte)Bits.Bit0; + this.F = SetBit(this.F, StatusBits.CF, carry); + return (byte)((operand >> 1) | (carry << 7)); + } + + private byte RL(byte operand) + { + this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.ZF); + var carry = this.F & (byte)StatusBits.CF; + this.F = SetBit(this.F, StatusBits.CF, operand & (byte)Bits.Bit7); + return (byte)((operand << 1) | (carry >> 4)); // CF at Bit4 + } + + private byte RR(byte operand) + { + this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.ZF); + var carry = this.F & (byte)StatusBits.CF; + this.F = SetBit(this.F, StatusBits.CF, operand & (byte)Bits.Bit0); + return (byte)((operand >> 1) | (carry << 3)); // CF at Bit4 + } + + private byte SLA(byte operand) + { + this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.ZF); + this.F = SetBit(this.F, StatusBits.CF, operand & (byte)Bits.Bit7); + return (byte)(operand << 1); + } + + private byte SRA(byte operand) + { + this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.ZF); + this.F = SetBit(this.F, StatusBits.CF, operand & (byte)Bits.Bit0); + return (byte)((operand >> 1) | (operand & (byte)Bits.Bit7)); + } + + private byte Swap(byte operand) + { + this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.CF); + return (byte)(PromoteNibble(operand) | DemoteNibble(operand)); + } + + private byte SRL(byte operand) + { + this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC | StatusBits.ZF); + this.F = SetBit(this.F, StatusBits.CF, operand & (byte)Bits.Bit0); + return (byte)((operand >> 1) & ~(byte)Bits.Bit7); + } + + private void Bit(int n, byte operand) + { + var carry = this.F & (byte)StatusBits.CF; + this.AndR(operand, (byte)(1 << n)); + this.F = SetBit(this.F, StatusBits.CF, carry); + } + + private static byte Res(int n, byte operand) => (byte)(operand & ~(1 << n)); + + private static byte Set(int n, byte operand) => (byte)(operand | (1 << n)); + + private void DAA() + { + int updated = this.A; + + if ((this.F & (byte)StatusBits.NF) != 0) + { + if ((this.F & (byte)StatusBits.HC) != 0) + { + updated = LowByte(updated - 6); + } + + if ((this.F & (byte)StatusBits.CF) != 0) + { + updated -= 0x60; + } + } + else + { + if (((this.F & (byte)StatusBits.HC) != 0) || LowNibble((byte)updated) > 9) + { + updated += 6; + } + + if (((this.F & (byte)StatusBits.CF) != 0) || updated > 0x9F) + { + updated += 0x60; + } + } + + this.F = ClearBit(this.F, (byte)StatusBits.HC | (byte)StatusBits.ZF); + this.F = SetBit(this.F, StatusBits.CF, ((this.F & (byte)StatusBits.CF) != 0) || ((updated & (int)Bits.Bit8) != 0)); + this.A = LowByte(updated); + + this.F = AdjustZero(this.F, this.A); + } + + private void Cpl() + { + this.F = SetBit(this.F, StatusBits.HC | StatusBits.NF); + this.A = (byte)~this.A; + } + + private void SCF() + { + this.F = SetBit(this.F, StatusBits.CF); + this.F = ClearBit(this.F, StatusBits.HC | StatusBits.NF); + } + + private void CCF() + { + this.F = ClearBit(this.F, StatusBits.NF | StatusBits.HC); + this.F = ClearBit(this.F, StatusBits.CF, this.F & (byte)StatusBits.CF); + } + + private void RetI() + { + this.Return(); + this.EI(); + } + } + } +} diff --git a/LR35902/LR35902.csproj b/LR35902/LR35902.csproj new file mode 100644 index 0000000..76d44e2 --- /dev/null +++ b/LR35902/LR35902.csproj @@ -0,0 +1,67 @@ + + + + + Debug + AnyCPU + {01F61A1D-CB4A-4EA3-96EF-222F831DF483} + Library + Properties + EightBit + LR35902 + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {6ebf8857-62a3-4ef4-af21-c1844031d7e4} + EightBit + + + + \ No newline at end of file diff --git a/LR35902/LcdStatusMode.cs b/LR35902/LcdStatusMode.cs new file mode 100644 index 0000000..9e31ba3 --- /dev/null +++ b/LR35902/LcdStatusMode.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace EightBit +{ + namespace GameBoy + { + public enum LcdStatusMode + { + HBlank = 0b00, + VBlank = 0b01, + SearchingOamRam = 0b10, + TransferringDataToLcd = 0b11 + } + } +} diff --git a/LR35902/LcdStatusModeEventArgs.cs b/LR35902/LcdStatusModeEventArgs.cs new file mode 100644 index 0000000..94da61b --- /dev/null +++ b/LR35902/LcdStatusModeEventArgs.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace EightBit +{ + namespace GameBoy + { + using System; + + public class LcdStatusModeEventArgs : EventArgs + { + public LcdStatusModeEventArgs(LcdStatusMode value) => this.LcdStatusMode = value; + + public LcdStatusMode LcdStatusMode { get; } + } + } +} \ No newline at end of file diff --git a/LR35902/LcdcControl.cs b/LR35902/LcdcControl.cs new file mode 100644 index 0000000..0021089 --- /dev/null +++ b/LR35902/LcdcControl.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace EightBit +{ + namespace GameBoy + { + [System.Flags] + public enum LcdcControl + { + None = 0, + DisplayBackground = Bits.Bit0, + ObjectEnable = Bits.Bit1, + ObjectBlockCompositionSelection = Bits.Bit2, + BackgroundCodeAreaSelection = Bits.Bit3, + BackgroundCharacterDataSelection = Bits.Bit4, + WindowEnable = Bits.Bit5, + WindowCodeAreaSelection = Bits.Bit6, + LcdEnable = Bits.Bit7 + } + } +} diff --git a/LR35902/ObjectAttribute.cs b/LR35902/ObjectAttribute.cs new file mode 100644 index 0000000..e38766b --- /dev/null +++ b/LR35902/ObjectAttribute.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace EightBit +{ + namespace GameBoy + { + public class ObjectAttribute + { + public ObjectAttribute() + { + } + + public ObjectAttribute(Ram ram, ushort address) + { + this.PositionY = ram.Peek(address); + this.PositionX = ram.Peek(++address); + this.Pattern = ram.Peek(++address); + this.Flags = ram.Peek(++address); + } + + public byte PositionY { get; } + public byte PositionX { get; } + public byte Pattern { get; } + public byte Flags { get; } + + public int Priority => this.Flags & (byte)Bits.Bit7; + + public bool HighPriority => this.Priority != 0; + + public bool LowPriority => this.Priority == 0; + + public bool FlipY => (this.Flags & (byte)Bits.Bit6) != 0; + + public bool FlipX => (this.Flags & (byte)Bits.Bit5) != 0; + + public int Palette => (this.Flags & (byte)Bits.Bit4) >> 3; // TODO: Check this! + } + } +} diff --git a/LR35902/Properties/AssemblyInfo.cs b/LR35902/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..348642f --- /dev/null +++ b/LR35902/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("LR35902")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("LR35902")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("01f61a1d-cb4a-4ea3-96ef-222f831df483")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/LR35902/StatusBits.cs b/LR35902/StatusBits.cs new file mode 100644 index 0000000..3240fc2 --- /dev/null +++ b/LR35902/StatusBits.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace EightBit +{ + namespace GameBoy + { + [System.Flags] + public enum StatusBits + { + None = 0, + CF = Bits.Bit4, + HC = Bits.Bit5, + NF = Bits.Bit6, + ZF = Bits.Bit7, + } + } +}