stella/src/emucore/CartCTY.cxx

516 lines
14 KiB
C++

//============================================================================
//
// SSSS tt lll lll
// SS SS tt ll ll
// SS tttttt eeee ll ll aaaa
// SSSS tt ee ee ll ll aa
// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
// SS SS tt ee ll ll aa aa
// SSSS ttt eeeee llll llll aaaaa
//
// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony
// and the Stella Team
//
// See the file "License.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//============================================================================
#include "OSystem.hxx"
#include "Serializer.hxx"
#include "System.hxx"
#include "CartCTYTunes.hxx"
#include "CartCTY.hxx"
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CartridgeCTY::CartridgeCTY(const BytePtr& image, uInt32 size,
const OSystem& osystem)
: Cartridge(osystem.settings()),
myOSystem(osystem),
myOperationType(0),
myCounter(0),
myLDAimmediate(false),
myRandomNumber(0x2B435044),
myRamAccessTimeout(0),
myAudioCycles(0),
myFractionalClocks(0.0),
myBankOffset(0)
{
// Copy the ROM image into my buffer
memcpy(myImage, image.get(), std::min(32768u, size));
createCodeAccessBase(32768);
// Point to the first tune
myFrequencyImage = CartCTYTunes;
// Remember startup bank (not bank 0, since that's ARM code)
myStartBank = 1;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void CartridgeCTY::reset()
{
initializeRAM(myRAM, 64);
myRAM[0] = myRAM[1] = myRAM[2] = myRAM[3] = 0xFF;
myAudioCycles = 0;
myFractionalClocks = 0.0;
// Upon reset we switch to the startup bank
bank(myStartBank);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void CartridgeCTY::install(System& system)
{
mySystem = &system;
// Map all RAM accesses to call peek and poke
System::PageAccess access(this, System::PA_READ);
for(uInt16 addr = 0x1000; addr < 0x1080; addr += System::PAGE_SIZE)
mySystem->setPageAccess(addr, access);
// Install pages for the startup bank
bank(myStartBank);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt8 CartridgeCTY::peek(uInt16 address)
{
uInt16 peekAddress = address;
address &= 0x0FFF;
uInt8 peekValue = myImage[myBankOffset + address];
// In debugger/bank-locked mode, we ignore all hotspots and in general
// anything that can change the internal state of the cart
if(bankLocked())
return peekValue;
// Check for aliasing to 'LDA #$F2'
if(myLDAimmediate && peekValue == 0xF2)
{
myLDAimmediate = false;
// Update the music data fetchers (counter & flag)
updateMusicModeDataFetchers();
#if 0
// using myDisplayImage[] instead of myProgramImage[] because waveforms
// can be modified during runtime.
uInt32 i = myDisplayImage[(myMusicWaveforms[0] << 5) + (myMusicCounters[0] >> 27)] +
myDisplayImage[(myMusicWaveforms[1] << 5) + (myMusicCounters[1] >> 27)] +
myDisplayImage[(myMusicWaveforms[2] << 5) + (myMusicCounters[2] >> 27)];
return = (uInt8)i;
#endif
return 0xF2; // FIXME - return frequency value here
}
else
myLDAimmediate = false;
if(address < 0x0040) // Write port is at $1000 - $103F (64 bytes)
{
// Reading from the write port triggers an unwanted write
uInt8 value = mySystem->getDataBusState(0xFF);
if(bankLocked())
return value;
else
{
triggerReadFromWritePort(peekAddress);
return myRAM[address] = value;
}
}
else if(address < 0x0080) // Read port is at $1040 - $107F (64 bytes)
{
address -= 0x40;
switch(address)
{
case 0x00: // Error code after operation
return myRAM[0];
case 0x01: // Get next Random Number (8-bit LFSR)
myRandomNumber = ((myRandomNumber & (1<<10)) ? 0x10adab1e: 0x00) ^
((myRandomNumber >> 11) | (myRandomNumber << 21));
return myRandomNumber & 0xFF;
case 0x02: // Get Tune position (low byte)
return myCounter & 0xFF;
case 0x03: // Get Tune position (high byte)
return (myCounter >> 8) & 0xFF;
default:
return myRAM[address];
}
}
else // Check hotspots
{
switch(address)
{
case 0x0FF4:
// Bank 0 is ARM code and not actually accessed
return ramReadWrite();
case 0x0FF5:
case 0x0FF6:
case 0x0FF7:
case 0x0FF8:
case 0x0FF9:
case 0x0FFA:
case 0x0FFB:
// Banks 1 through 7
bank(address - 0x0FF4);
break;
default:
break;
}
// Is this instruction an immediate mode LDA?
myLDAimmediate = (peekValue == 0xA9);
return peekValue;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool CartridgeCTY::poke(uInt16 address, uInt8 value)
{
address &= 0x0FFF;
//cerr << "POKE: address=" << HEX4 << address << ", value=" << HEX2 << value << endl;
if(address < 0x0040) // Write port is at $1000 - $103F (64 bytes)
{
switch(address) // FIXME for functionality
{
case 0x00: // Operation type for $1FF4
myOperationType = value;
break;
case 0x01: // Set Random seed value (reset)
myRandomNumber = 0x2B435044;
break;
case 0x02: // Reset fetcher to beginning of tune
myCounter = 0;
break;
case 0x03: // Advance fetcher to next tune position
myCounter = (myCounter + 3) & 0x0fff;
break;
default:
myRAM[address] = value;
break;
}
}
else // Check hotspots
{
switch(address)
{
case 0x0FF4:
// Bank 0 is ARM code and not actually accessed
ramReadWrite();
break;
case 0x0FF5:
case 0x0FF6:
case 0x0FF7:
case 0x0FF8:
case 0x0FF9:
case 0x0FFA:
case 0x0FFB:
// Banks 1 through 7
bank(address - 0x0FF4);
break;
default:
break;
}
}
return false;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool CartridgeCTY::bank(uInt16 bank)
{
if(bankLocked()) return false;
// Remember what bank we're in
myBankOffset = bank << 12;
// Setup the page access methods for the current bank
System::PageAccess access(this, System::PA_READ);
for(uInt16 addr = 0x1080; addr < 0x2000; addr += System::PAGE_SIZE)
{
access.codeAccessBase = &myCodeAccessBase[myBankOffset + (addr & 0x0FFF)];
mySystem->setPageAccess(addr, access);
}
return myBankChanged = true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt16 CartridgeCTY::getBank() const
{
return myBankOffset >> 12;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt16 CartridgeCTY::bankCount() const
{
return 8;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool CartridgeCTY::patch(uInt16 address, uInt8 value)
{
address &= 0x0FFF;
if(address < 0x0080)
{
// Normally, a write to the read port won't do anything
// However, the patch command is special in that ignores such
// cart restrictions
myRAM[address & 0x003F] = value;
}
else
myImage[myBankOffset + address] = value;
return myBankChanged = true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const uInt8* CartridgeCTY::getImage(uInt32& size) const
{
size = 32768;
return myImage;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool CartridgeCTY::save(Serializer& out) const
{
try
{
out.putShort(getBank());
out.putByteArray(myRAM, 64);
out.putByte(myOperationType);
out.putShort(myCounter);
out.putBool(myLDAimmediate);
out.putInt(myRandomNumber);
out.putLong(myAudioCycles);
out.putDouble(myFractionalClocks);
}
catch(...)
{
cerr << "ERROR: CartridgeCTY::save" << endl;
return false;
}
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool CartridgeCTY::load(Serializer& in)
{
try
{
// Remember what bank we were in
bank(in.getShort());
in.getByteArray(myRAM, 64);
myOperationType = in.getByte();
myCounter = in.getShort();
myLDAimmediate = in.getBool();
myRandomNumber = in.getInt();
myAudioCycles = in.getLong();
myFractionalClocks = in.getDouble();
}
catch(...)
{
cerr << "ERROR: CartridgeCTY::load" << endl;
return false;
}
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void CartridgeCTY::setRomName(const string& name)
{
myEEPROMFile = myOSystem.nvramDir() + name + "_eeprom.dat";
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt8 CartridgeCTY::ramReadWrite()
{
/* The following algorithm implements accessing Harmony cart EEPROM
1. Wait for an access to hotspot location $1FF4 (return 1 in bit 6
while busy).
2. Determine operation from myOperationType.
3. Save or load relevant EEPROM memory to/from a file.
4. Set byte 0 of RAM+ memory to zero to indicate success (will
always happen in emulation).
5. Return 0 (in bit 6) on the next access to $1FF4, if enough time has
passed to complete the operation on a real system (0.5 s for read,
1 s for write).
*/
if(bankLocked()) return 0xff;
// First access sets the timer
if(myRamAccessTimeout == 0)
{
// Opcode and value in form of XXXXYYYY (from myOperationType), where:
// XXXX = index and YYYY = operation
uInt8 index = myOperationType >> 4;
switch(myOperationType & 0xf)
{
case 1: // Load tune (index = tune)
if(index < 7)
{
// Add 0.5 s delay for read
myRamAccessTimeout = myOSystem.getTicks() + 500000;
loadTune(index);
}
break;
case 2: // Load score table (index = table)
if(index < 4)
{
// Add 0.5 s delay for read
myRamAccessTimeout = myOSystem.getTicks() + 500000;
loadScore(index);
}
break;
case 3: // Save score table (index = table)
if(index < 4)
{
// Add 1 s delay for write
myRamAccessTimeout = myOSystem.getTicks() + 1000000;
saveScore(index);
}
break;
case 4: // Wipe all score tables
// Add 1 s delay for write
myRamAccessTimeout = myOSystem.getTicks() + 1000000;
wipeAllScores();
break;
}
// Bit 6 is 1, busy
return myImage[myBankOffset + 0xFF4] | 0x40;
}
else
{
// Have we reached the timeout value yet?
if(myOSystem.getTicks() >= myRamAccessTimeout)
{
myRamAccessTimeout = 0; // Turn off timer
myRAM[0] = 0; // Successful operation
// Bit 6 is 0, ready/success
return myImage[myBankOffset + 0xFF4] & ~0x40;
}
else
// Bit 6 is 1, busy
return myImage[myBankOffset + 0xFF4] | 0x40;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void CartridgeCTY::loadTune(uInt8 index)
{
// Each tune is offset by 4096 bytes
// Instead of copying non-modifiable data around (as would happen on the
// Harmony), we simply point to the appropriate tune
myFrequencyImage = CartCTYTunes + (index << 12);
// Reset to beginning of tune
myCounter = 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void CartridgeCTY::loadScore(uInt8 index)
{
Serializer serializer(myEEPROMFile, true);
if(serializer)
{
uInt8 scoreRAM[256];
try
{
serializer.getByteArray(scoreRAM, 256);
}
catch(...)
{
memset(scoreRAM, 0, 256);
}
// Grab 60B slice @ given index (first 4 bytes are ignored)
memcpy(myRAM+4, scoreRAM + (index << 6) + 4, 60);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void CartridgeCTY::saveScore(uInt8 index)
{
Serializer serializer(myEEPROMFile);
if(serializer)
{
// Load score RAM
uInt8 scoreRAM[256];
try
{
serializer.getByteArray(scoreRAM, 256);
}
catch(...)
{
memset(scoreRAM, 0, 256);
}
// Add 60B RAM to score table @ given index (first 4 bytes are ignored)
memcpy(scoreRAM + (index << 6) + 4, myRAM+4, 60);
// Save score RAM
serializer.rewind();
try
{
serializer.putByteArray(scoreRAM, 256);
}
catch(...)
{
// Maybe add logging here that save failed?
cerr << name() << ": ERROR saving score table " << int(index) << endl;
}
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void CartridgeCTY::wipeAllScores()
{
Serializer serializer(myEEPROMFile);
if(serializer)
{
// Erase score RAM
uInt8 scoreRAM[256];
memset(scoreRAM, 0, 256);
try
{
serializer.putByteArray(scoreRAM, 256);
}
catch(...)
{
// Maybe add logging here that save failed?
cerr << name() << ": ERROR wiping score tables" << endl;
}
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
inline void CartridgeCTY::updateMusicModeDataFetchers()
{
// Calculate the number of cycles since the last update
uInt32 cycles = uInt32(mySystem->cycles() - myAudioCycles);
myAudioCycles = mySystem->cycles();
// Calculate the number of CTY OSC clocks since the last update
double clocks = ((20000.0 * cycles) / 1193191.66666667) + myFractionalClocks;
uInt32 wholeClocks = uInt32(clocks);
myFractionalClocks = clocks - double(wholeClocks);
// Let's update counters and flags of the music mode data fetchers
if(wholeClocks > 0)
for(int x = 0; x <= 2; ++x)
; //myMusicCounters[x] += myMusicFrequencies[x] * wholeClocks;
}