Add an initial stab at LR35902 (aka GameBoy) support. Untested and no disassembler though...

Signed-off-by: Adrian Conlon <Adrian.conlon@gmail.com>
This commit is contained in:
Adrian Conlon 2019-07-14 17:46:57 +01:00
parent d0c620e709
commit 3eb3975e37
16 changed files with 2466 additions and 0 deletions

View File

@ -0,0 +1,18 @@
// <copyright file="AbstractColourPalette.cs" company="Adrian Conlon">
// Copyright (c) Adrian Conlon. All rights reserved.
// </copyright>
namespace EightBit
{
namespace GameBoy
{
public class AbstractColourPalette
{
private readonly uint[] colours = new uint[4];
protected AbstractColourPalette()
{ }
public uint Colour(int index) => this.colours[index];
}
}
}

16
LR35902/CartridgeType.cs Normal file
View File

@ -0,0 +1,16 @@
// <copyright file="CartridgeType.cs" company="Adrian Conlon">
// Copyright (c) Adrian Conlon. All rights reserved.
// </copyright>
namespace EightBit
{
namespace GameBoy
{
public enum CartridgeType
{
ROM = 0,
ROM_MBC1 = 1,
ROM_MBC1_RAM = 2,
ROM_MBC1_RAM_BATTERY = 3,
}
}
}

View File

@ -0,0 +1,42 @@
// <copyright file="CharacterDefinition.cs" company="Adrian Conlon">
// Copyright (c) Adrian Conlon. All rights reserved.
// </copyright>
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;
}
}
}
}

16
LR35902/ColourShades.cs Normal file
View File

@ -0,0 +1,16 @@
// <copyright file="ColourShades.cs" company="Adrian Conlon">
// Copyright (c) Adrian Conlon. All rights reserved.
// </copyright>
namespace EightBit
{
namespace GameBoy
{
public enum ColourShades
{
Off,
Light,
Medium,
Dark
}
}
}

178
LR35902/Display.cs Normal file
View File

@ -0,0 +1,178 @@
// <copyright file="Display.cs" company="Adrian Conlon">
// Copyright (c) Adrian Conlon. All rights reserved.
// </copyright>
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]);
}
}
}
}
}
}

434
LR35902/GameboyBus.cs Normal file
View File

@ -0,0 +1,434 @@
// <copyright file="GameboyBus.cs" company="Adrian Conlon">
// Copyright (c) Adrian Conlon. All rights reserved.
// </copyright>
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<Rom> gameRomBanks; // 0x0000 - 0x3fff, 0x4000 - 0x7fff (switchable)
private readonly List<Ram> 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;
}
}
}
}

20
LR35902/Interrupts.cs Normal file
View File

@ -0,0 +1,20 @@
// <copyright file="Interrupts.cs" company="Adrian Conlon">
// Copyright (c) Adrian Conlon. All rights reserved.
// </copyright>
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
}
}
}

396
LR35902/IoRegisters.cs Normal file
View File

@ -0,0 +1,396 @@
// <copyright file="IoRegisters.cs" company="Adrian Conlon">
// Copyright (c) Adrian Conlon. All rights reserved.
// </copyright>
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<LcdStatusModeEventArgs> 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;
}
}
}
}
}
}

1130
LR35902/LR35902.cs Normal file

File diff suppressed because it is too large Load Diff

67
LR35902/LR35902.csproj Normal file
View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{01F61A1D-CB4A-4EA3-96EF-222F831DF483}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>EightBit</RootNamespace>
<AssemblyName>LR35902</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="AbstractColourPalette.cs" />
<Compile Include="CartridgeType.cs" />
<Compile Include="CharacterDefinition.cs" />
<Compile Include="ColourShades.cs" />
<Compile Include="Display.cs" />
<Compile Include="GameboyBus.cs" />
<Compile Include="Interrupts.cs" />
<Compile Include="IoRegisters.cs" />
<Compile Include="LcdcControl.cs" />
<Compile Include="LcdStatusMode.cs" />
<Compile Include="LcdStatusModeEventArgs.cs" />
<Compile Include="LR35902.cs" />
<Compile Include="ObjectAttribute.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="StatusBits.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EightBit\EightBit.csproj">
<Project>{6ebf8857-62a3-4ef4-af21-c1844031d7e4}</Project>
<Name>EightBit</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

16
LR35902/LcdStatusMode.cs Normal file
View File

@ -0,0 +1,16 @@
// <copyright file="LcdcControl.cs" company="Adrian Conlon">
// Copyright (c) Adrian Conlon. All rights reserved.
// </copyright>
namespace EightBit
{
namespace GameBoy
{
public enum LcdStatusMode
{
HBlank = 0b00,
VBlank = 0b01,
SearchingOamRam = 0b10,
TransferringDataToLcd = 0b11
}
}
}

View File

@ -0,0 +1,17 @@
// <copyright file="LcdcControl.cs" company="Adrian Conlon">
// Copyright (c) Adrian Conlon. All rights reserved.
// </copyright>
namespace EightBit
{
namespace GameBoy
{
using System;
public class LcdStatusModeEventArgs : EventArgs
{
public LcdStatusModeEventArgs(LcdStatusMode value) => this.LcdStatusMode = value;
public LcdStatusMode LcdStatusMode { get; }
}
}
}

22
LR35902/LcdcControl.cs Normal file
View File

@ -0,0 +1,22 @@
// <copyright file="LcdcControl.cs" company="Adrian Conlon">
// Copyright (c) Adrian Conlon. All rights reserved.
// </copyright>
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
}
}
}

View File

@ -0,0 +1,40 @@
// <copyright file="ObjectAttribute.cs" company="Adrian Conlon">
// Copyright (c) Adrian Conlon. All rights reserved.
// </copyright>
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!
}
}
}

View File

@ -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")]

18
LR35902/StatusBits.cs Normal file
View File

@ -0,0 +1,18 @@
// <copyright file="StatusBits.cs" company="Adrian Conlon">
// Copyright (c) Adrian Conlon. All rights reserved.
// </copyright>
namespace EightBit
{
namespace GameBoy
{
[System.Flags]
public enum StatusBits
{
None = 0,
CF = Bits.Bit4,
HC = Bits.Bit5,
NF = Bits.Bit6,
ZF = Bits.Bit7,
}
}
}