391 lines
9.8 KiB
C#
391 lines
9.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.IO;
|
|
|
|
namespace BizHawk.Emulation.Consoles.Nintendo
|
|
{
|
|
/*
|
|
* http://sourceforge.net/p/fceultra/code/2696/tree/fceu/src/fds.cpp - only used for timer info
|
|
* http://nesdev.com/FDS%20technical%20reference.txt - implementation is mostly a combination of
|
|
* http://wiki.nesdev.com/w/index.php/Family_Computer_Disk_System - these two documents
|
|
* http://nesdev.com/diskspec.txt - not useless
|
|
*/
|
|
[NES.INESBoardImplCancel]
|
|
public class FDS : NES.NESBoardBase
|
|
{
|
|
#region configuration
|
|
/// <summary>FDS bios image; should be 8192 bytes</summary>
|
|
public byte[] biosrom;
|
|
/// <summary>.FDS disk image</summary>
|
|
byte[] diskimage;
|
|
#endregion
|
|
|
|
#region state
|
|
RamAdapter diskdrive;
|
|
FDSAudio audio;
|
|
/// <summary>currently loaded side of the .FDS image, 0 based</summary>
|
|
int? currentside = null;
|
|
/// <summary>collection of diffs (as provided by the RamAdapter) for each side in the .FDS image</summary>
|
|
byte[][] diskdiffs;
|
|
|
|
bool _diskirq;
|
|
bool _timerirq;
|
|
|
|
/// <summary>disk io ports enabled; see 4023.0</summary>
|
|
bool diskenable = false;
|
|
/// <summary>sound io ports enabled; see 4023.1</summary>
|
|
bool soundenable = false;
|
|
/// <summary>read on 4033, write on 4026</summary>
|
|
byte reg4026;
|
|
|
|
/// <summary>timer reload</summary>
|
|
int timerlatch;
|
|
/// <summary>timer current value</summary>
|
|
int timervalue;
|
|
/// <summary>4022.0,1</summary>
|
|
byte timerreg;
|
|
#endregion
|
|
|
|
public override void SyncState(Serializer ser)
|
|
{
|
|
base.SyncState(ser);
|
|
ser.BeginSection("FDS");
|
|
ser.BeginSection("RamAdapter");
|
|
diskdrive.SyncState(ser);
|
|
ser.EndSection();
|
|
ser.BeginSection("audio");
|
|
audio.SyncState(ser);
|
|
ser.EndSection();
|
|
{
|
|
// silly little hack
|
|
int tmp = currentside != null ? (int)currentside : 1234567;
|
|
ser.Sync("currentside", ref tmp);
|
|
currentside = tmp == 1234567 ? null : (int?)tmp;
|
|
}
|
|
for (int i = 0; i < NumSides; i++)
|
|
ser.Sync("diskdiffs" + i, ref diskdiffs[i], true);
|
|
ser.Sync("_timerirq", ref _timerirq);
|
|
ser.Sync("_diskirq", ref _diskirq);
|
|
ser.Sync("diskenable", ref diskenable);
|
|
ser.Sync("soundenable", ref soundenable);
|
|
ser.Sync("reg4026", ref reg4026);
|
|
ser.Sync("timerlatch", ref timerlatch);
|
|
ser.Sync("timervalue", ref timervalue);
|
|
ser.Sync("timerreg", ref timerreg);
|
|
ser.EndSection();
|
|
|
|
SetIRQ();
|
|
}
|
|
|
|
public void SetDriveLightCallback(Action<bool> callback)
|
|
{
|
|
diskdrive.DriveLightCallback = callback;
|
|
}
|
|
|
|
/// <summary>
|
|
/// should only be called once, before emulation begins
|
|
/// </summary>
|
|
/// <param name="diskimage"></param>
|
|
public void SetDiskImage(byte[] diskimage)
|
|
{
|
|
this.diskimage = diskimage;
|
|
diskdiffs = new byte[NumSides][];
|
|
}
|
|
|
|
/// <summary>
|
|
/// returns the currently set disk image. no effect on emulation (provided the image is not modified).
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public byte[] GetDiskImage()
|
|
{
|
|
return diskimage;
|
|
}
|
|
|
|
// as we have [INESBoardImplCancel], this will only be called with an fds disk image
|
|
public override bool Configure(NES.EDetectionOrigin origin)
|
|
{
|
|
if (biosrom == null || biosrom.Length != 8192)
|
|
throw new Exception("FDS bios image needed!");
|
|
|
|
Cart.vram_size = 8;
|
|
Cart.wram_size = 32;
|
|
Cart.wram_battery = false;
|
|
Cart.system = "Famicom";
|
|
Cart.board_type = "FAMICOM_DISK_SYSTEM";
|
|
|
|
diskdrive = new RamAdapter();
|
|
audio = new FDSAudio();
|
|
|
|
InsertSide(0);
|
|
// set mirroring??
|
|
return true;
|
|
}
|
|
|
|
// with a bit of change, these methods could work with a better disk format
|
|
|
|
public int NumSides { get { return diskimage[4]; } }
|
|
|
|
public void Eject()
|
|
{
|
|
if (currentside != null)
|
|
{
|
|
diskdiffs[(int)currentside] = diskdrive.MakeDiff();
|
|
diskdrive.Eject();
|
|
currentside = null;
|
|
}
|
|
}
|
|
|
|
public void InsertSide(int side)
|
|
{
|
|
if (side >= NumSides)
|
|
throw new ArgumentOutOfRangeException();
|
|
byte[] buf = new byte[65500];
|
|
Buffer.BlockCopy(diskimage, 16 + side * 65500, buf, 0, 65500);
|
|
diskdrive.InsertBrokenImage(buf, false /*true*/);
|
|
if (diskdiffs[side] != null && diskdiffs[side].Length > 0)
|
|
diskdrive.ApplyDiff(diskdiffs[side]);
|
|
currentside = side;
|
|
}
|
|
|
|
public byte[] ReadSaveRam()
|
|
{
|
|
// update diff for currently loaded disk first!
|
|
if (currentside != null)
|
|
diskdiffs[(int)currentside] = diskdrive.MakeDiff();
|
|
MemoryStream ms = new MemoryStream();
|
|
BinaryWriter bw = new BinaryWriter(ms);
|
|
bw.Write(Encoding.ASCII.GetBytes("FDSS"));
|
|
bw.Write(NumSides);
|
|
for (int i = 0; i < NumSides; i++)
|
|
{
|
|
if (diskdiffs[i] != null)
|
|
{
|
|
bw.Write(diskdiffs[i].Length);
|
|
bw.Write(diskdiffs[i]);
|
|
}
|
|
else
|
|
{
|
|
bw.Write((int)0);
|
|
}
|
|
}
|
|
bw.Close();
|
|
return ms.ToArray();
|
|
}
|
|
|
|
public void StoreSaveRam(byte[] data)
|
|
{
|
|
// it's strange to modify a disk that's in the process of being read.
|
|
// but in fact, StoreSaveRam() is only called once right at startup, so this is no big deal
|
|
//if (currentside != null)
|
|
// throw new Exception("FDS Saveram: Can't load when a disk is active!");
|
|
MemoryStream ms = new MemoryStream(data, false);
|
|
BinaryReader br = new BinaryReader(ms);
|
|
byte[] cmp = Encoding.ASCII.GetBytes("FDSS");
|
|
byte[] tmp = br.ReadBytes(cmp.Length);
|
|
if (!cmp.SequenceEqual(tmp))
|
|
throw new Exception("FDS Saveram: bad header");
|
|
int n = br.ReadInt32();
|
|
if (n != NumSides)
|
|
throw new Exception("FDS Saveram: wrong number of sides");
|
|
for (int i = 0; i < NumSides; i++)
|
|
{
|
|
int l = br.ReadInt32();
|
|
if (l > 0)
|
|
diskdiffs[i] = br.ReadBytes(l);
|
|
else
|
|
diskdiffs[i] = null;
|
|
}
|
|
if (currentside != null && diskdiffs[(int)currentside] != null)
|
|
diskdrive.ApplyDiff(diskdiffs[(int)currentside]);
|
|
}
|
|
|
|
public void ClearSaveRam()
|
|
{
|
|
if (currentside != null)
|
|
throw new Exception("FDS Saveram: Can't clear when a disk is active!");
|
|
for (int i = 0; i < diskdiffs.Length; i++)
|
|
diskdiffs[i] = null;
|
|
}
|
|
|
|
public override byte[] SaveRam
|
|
{ get { throw new Exception("FDS Saveram: Must access with method api!"); } }
|
|
|
|
|
|
public MemoryDomain GetDiskPeeker()
|
|
{
|
|
return new MemoryDomain("FDS SIDE", diskdrive.NumBytes, Endian.Little, diskdrive.PeekData, null);
|
|
}
|
|
|
|
void SetIRQ()
|
|
{
|
|
IRQSignal = _diskirq || _timerirq;
|
|
}
|
|
bool diskirq { get { return _diskirq; } set { _diskirq = value; SetIRQ(); } }
|
|
bool timerirq { get { return _timerirq; } set { _timerirq = value; SetIRQ(); } }
|
|
|
|
|
|
public override void WriteEXP(int addr, byte value)
|
|
{
|
|
//if (addr == 0x0025)
|
|
// Console.WriteLine("W{0:x4}:{1:x2} {2:x4}", addr + 0x4000, value, NES.cpu.PC);
|
|
|
|
if (addr >= 0x0040)
|
|
{
|
|
audio.WriteReg(addr + 0x4000, value);
|
|
return;
|
|
}
|
|
|
|
switch (addr)
|
|
{
|
|
case 0x0020:
|
|
timerlatch &= 0xff00;
|
|
timerlatch |= value;
|
|
timerirq = false;
|
|
break;
|
|
case 0x0021:
|
|
timerlatch &= 0x00ff;
|
|
timerlatch |= value << 8;
|
|
timerirq = false;
|
|
break;
|
|
case 0x0022:
|
|
timerreg = (byte)(value & 3);
|
|
timervalue = timerlatch;
|
|
break;
|
|
case 0x0023:
|
|
diskenable = (value & 1) != 0;
|
|
soundenable = (value & 2) != 0;
|
|
break;
|
|
case 0x0024:
|
|
if (diskenable)
|
|
diskdrive.Write4024(value);
|
|
break;
|
|
case 0x0025:
|
|
if (diskenable)
|
|
diskdrive.Write4025(value);
|
|
SetMirrorType((value & 8) == 0 ? EMirrorType.Vertical : EMirrorType.Horizontal);
|
|
break;
|
|
case 0x0026:
|
|
if (diskenable)
|
|
reg4026 = value;
|
|
break;
|
|
}
|
|
diskirq = diskdrive.irq;
|
|
}
|
|
|
|
public override byte ReadEXP(int addr)
|
|
{
|
|
byte ret = NES.DB;
|
|
|
|
if (addr >= 0x0040)
|
|
return audio.ReadReg(addr + 0x4000, ret);
|
|
|
|
switch (addr)
|
|
{
|
|
case 0x0030:
|
|
if (diskenable)
|
|
{
|
|
int tmp = diskdrive.Read4030() & 0xd2;
|
|
ret &= 0x2c;
|
|
if (timerirq)
|
|
ret |= 1;
|
|
ret |= (byte)tmp;
|
|
timerirq = false;
|
|
}
|
|
break;
|
|
case 0x0031:
|
|
if (diskenable)
|
|
ret = diskdrive.Read4031();
|
|
break;
|
|
case 0x0032:
|
|
if (diskenable)
|
|
{
|
|
int tmp = diskdrive.Read4032() & 0x47;
|
|
ret &= 0xb8;
|
|
ret |= (byte)tmp;
|
|
}
|
|
break;
|
|
case 0x0033:
|
|
if (diskenable)
|
|
{
|
|
ret = reg4026;
|
|
// uncomment to set low battery flag
|
|
// ret &= 0x7f;
|
|
}
|
|
break;
|
|
}
|
|
diskirq = diskdrive.irq;
|
|
//if (addr != 0x0032)
|
|
// Console.WriteLine("R{0:x4}:{1:x2} {2:x4}", addr + 0x4000, ret, NES.cpu.PC);
|
|
return ret;
|
|
}
|
|
|
|
public override byte PeekCart(int addr)
|
|
{
|
|
if (addr >= 0x6000)
|
|
return base.PeekCart(addr);
|
|
else
|
|
return 0; // lazy
|
|
}
|
|
|
|
public override void ClockCPU()
|
|
{
|
|
if ((timerreg & 2) != 0 && timervalue > 0)
|
|
{
|
|
timervalue--;
|
|
if (timervalue == 0)
|
|
{
|
|
if ((timerreg & 1) != 0)
|
|
{
|
|
timervalue = timerlatch;
|
|
}
|
|
else
|
|
{
|
|
timerreg &= unchecked((byte)~2);
|
|
timervalue = 0;
|
|
timerlatch = 0;
|
|
}
|
|
timerirq = true;
|
|
}
|
|
}
|
|
audio.Clock();
|
|
}
|
|
|
|
public override void ClockPPU()
|
|
{
|
|
diskdrive.Clock();
|
|
diskirq = diskdrive.irq;
|
|
}
|
|
|
|
public override byte ReadWRAM(int addr)
|
|
{
|
|
return WRAM[addr & 0x1fff];
|
|
}
|
|
|
|
public override void WriteWRAM(int addr, byte value)
|
|
{
|
|
WRAM[addr & 0x1fff] = value;
|
|
}
|
|
|
|
public override byte ReadPRG(int addr)
|
|
{
|
|
if (addr >= 0x6000)
|
|
return biosrom[addr & 0x1fff];
|
|
else
|
|
return WRAM[addr + 0x2000];
|
|
}
|
|
|
|
public override void WritePRG(int addr, byte value)
|
|
{
|
|
if (addr < 0x6000)
|
|
WRAM[addr + 0x2000] = value;
|
|
}
|
|
|
|
public override void ApplyCustomAudio(short[] samples)
|
|
{
|
|
audio.ApplyCustomAudio(samples);
|
|
}
|
|
}
|
|
}
|