diff --git a/Core/NES/Mappers/FDS/Fds.cpp b/Core/NES/Mappers/FDS/Fds.cpp index 5f9c7317cb..e578f0d775 100644 --- a/Core/NES/Mappers/FDS/Fds.cpp +++ b/Core/NES/Mappers/FDS/Fds.cpp @@ -137,21 +137,58 @@ void Fds::WriteFdsDisk(uint8_t value) void Fds::ClockIrq() { + //Timer IRQ if(_irqEnabled) { if(_irqCounter == 0) { - _cpu->SetIrqSource(IRQSource::External); + _timerIrqPending = true; _irqCounter = _irqReloadValue; if(!_irqRepeatEnabled) { _irqEnabled = false; } + UpdateIrqState(); } else { _irqCounter--; } } + + //DRAM refresh watchdog: https://forums.nesdev.org/viewtopic.php?t=26359 + if(_diskRegEnabled) { + //"IRQ is asserted when the [12-bit] cycle timer underflows while $4023.D7 = 1." + if(_watchdogTimer == 0) { + _watchdogTimer = 4095; + _watchdogIrqPending = _watchdogIrqEnabled; + UpdateIrqState(); + } else { + _watchdogTimer--; + } + + //7-bit refresh counter: "...increment on every CPU cycle spent accessing memory outside of PRG-RAM, + // pause on CPU cycles accessing PRG-RAM" + if(!_dramAccessed) { + if(_dramRow == 0x7F) { + _dramRow = 0; + _watchdogTimer = 3245; //"...reload with 3245 when refresh counter overflows" + } else { + _dramRow++; + } + } + } + _dramAccessed = false; +} + +void Fds::UpdateIrqState() +{ + if(!(_timerIrqPending || _watchdogIrqPending)) { + _cpu->ClearIrqSource(IRQSource::External); + } else { + _cpu->SetIrqSource(IRQSource::External); + } } uint8_t Fds::ReadRam(uint16_t addr) { + _dramAccessed = (addr >= 0x6000 && addr < 0xE000); + if(addr == 0xE18C && !_gameStarted && (_memoryManager->DebugRead(0x100) & 0xC0)) { //$E18B is the NMI entry point (using $E18C due to dummy reads) //When NMI occurs while $100 & $C0 != 0, it typically means that the game is starting. @@ -215,6 +252,12 @@ uint8_t Fds::ReadRam(uint16_t addr) return BaseMapper::InternalRead(addr); } +void Fds::WriteRam(uint16_t addr, uint8_t value) +{ + _dramAccessed = (addr >= 0x6000 && addr < 0xE000); + BaseMapper::WriteRam(addr, value); +} + void Fds::ProcessAutoDiskInsert() { if(IsAutoInsertDiskEnabled()) { @@ -270,7 +313,6 @@ void Fds::EndFrame() - Should allow for accurate handling of level 2->3 load bug in Ai Senshi Nicol - Proper CRC handling using $4025 bits 4 and 6 - Verify End of Head handling (may affect Kosodate Gokko and FMC Disk Card Checker) - - DRAM refresh watchdog implementation (must track PRG-RAM vs external access cycles...) (Ongoing research, please consult TakuikaNinja for further details) **/ void Fds::ProcessCpuClock() @@ -278,6 +320,7 @@ void Fds::ProcessCpuClock() BaseProcessCpuClock(); ClockIrq(); + _audio->Clock(); if(_diskNumber == Fds::NoDiskInserted || !_motorOn) { @@ -438,7 +481,8 @@ void Fds::WriteRegister(uint16_t addr, uint8_t value) if(_irqEnabled) { _irqCounter = _irqReloadValue; } else { - _cpu->ClearIrqSource(IRQSource::External); + _timerIrqPending = false; + UpdateIrqState(); } break; @@ -446,6 +490,11 @@ void Fds::WriteRegister(uint16_t addr, uint8_t value) _diskRegEnabled = value & 0x01; //TODO This is actually an audio reset, rename this variable in Fds.h _soundRegEnabled = value & 0x02; + _watchdogIrqEnabled = value & 0x80; + + if(!_watchdogIrqEnabled) { + _watchdogIrqPending = false; + } if(!_diskRegEnabled) { _irqEnabled = false; @@ -464,10 +513,17 @@ void Fds::WriteRegister(uint16_t addr, uint8_t value) //Disabling disk registers forces $4026 (external connector) = 0x7F _extConWriteReg = 0x7F; - _cpu->ClearIrqSource(IRQSource::External); + //Reset DRAM refresh watchdog + _dramRow = 0; + _watchdogTimer = 4095; + + _timerIrqPending = false; + _watchdogIrqPending = false; _cpu->ClearIrqSource(IRQSource::FdsDisk); } + UpdateIrqState(); + /**TODO Determine/implement audio reset behaviour, should probably go in FdsAudio: - Proper method of resetting modulation state ($4085 write below doesn't always work) - Reset wave accumulator to 0 @@ -530,17 +586,18 @@ uint8_t Fds::ReadRegister(uint16_t addr) case 0x4030: //These 2 pins are open bus value &= 0x24; - /**TODO: - - DRAM refresh watchdog IRQ status is returned in bit 1, needs implementation - - End of Head is returned in bit 6, needs verification - **/ - value |= _cpu->HasIrqSource(IRQSource::External) ? 0x01 : 0x00; - //value |= _transferComplete ? 0x02 : 0x00; + /**TODO End of Head is returned in bit 6, needs verification + **/ + value |= _timerIrqPending ? 0x01 : 0x00; + value |= _watchdogIrqPending ? 0x02 : 0x00; value |= GetMirroringType() == MirroringType::Horizontal ? 0x08 : 0x00; value |= _useQdFormat && _badCrc ? 0x10 : 0x00; //value |= _endOfHead ? 0x40 : 0x00; value |= _transferComplete ? 0x80 : 0x00; + //"Reading this register acknowledges all IRQs from the FDS except for the byte transfer flag" + _timerIrqPending = false; + _watchdogIrqPending = false; _cpu->ClearIrqSource(IRQSource::External); return value; @@ -631,11 +688,37 @@ void Fds::Serialize(Serializer& s) SV(_crcAccumulator); if(s.IsSaving()) { + SV(_timerIrqPending); + SV(_watchdogIrqPending); + SV(_watchdogIrqEnabled); + SV(_dramAccessed); + SV(_dramRow); + SV(_watchdogTimer); + for(int i = 0; i < (int)_fdsDiskSides.size(); i++) { vector ipsData = IpsPatcher::CreatePatch(_orgDiskSides[i], _fdsDiskSides[i]); SVVectorI(ipsData); } } else { + if(!s.ContainsKey("timerIrqPending")) { + //Backwards compatibility with savetates that predate the watchdog's discovery + _timerIrqPending = _cpu->HasIrqSource(IRQSource::External); //External source was previously reserved for the timer IRQ + _watchdogIrqPending = false; + //$4023.D7 is missing, so assume based on $4023.D0 state + //(most commercial games don't touch the BIOS init value of $83) + _watchdogIrqEnabled = _diskRegEnabled; + _dramAccessed = false; + _dramRow = 0; + _watchdogTimer = 4095; + } else { + SV(_timerIrqPending); + SV(_watchdogIrqPending); + SV(_watchdogIrqEnabled); + SV(_dramAccessed); + SV(_dramRow); + SV(_watchdogTimer); + } + for(int i = 0; i < (int)_fdsDiskSides.size(); i++) { vector ipsData; SVVectorI(ipsData); @@ -698,9 +781,14 @@ vector Fds::GetMapperStateEntries() entries.push_back(MapperStateEntry("$4022.1", "IRQ Enabled", _irqEnabled, MapperStateValueType::Bool)); entries.push_back(MapperStateEntry("", "IRQ Counter", _irqCounter, MapperStateValueType::Number16)); + entries.push_back(MapperStateEntry("", "DRAM Refresh Watchdog")); + entries.push_back(MapperStateEntry("", "DRAM Row", _dramRow, MapperStateValueType::Number8)); + entries.push_back(MapperStateEntry("", "Watchdog Timer", _watchdogTimer, MapperStateValueType::Number16)); + entries.push_back(MapperStateEntry("", "I/O Control")); entries.push_back(MapperStateEntry("$4023.0", "Disk Registers Enabled", _diskRegEnabled, MapperStateValueType::Bool)); entries.push_back(MapperStateEntry("$4023.1", "Audio Enabled", _soundRegEnabled, MapperStateValueType::Bool)); + entries.push_back(MapperStateEntry("$4023.7", "DRAM Refresh Watchdog IRQ Enabled", _watchdogIrqEnabled, MapperStateValueType::Bool)); entries.push_back(MapperStateEntry("", "Disk Drive")); entries.push_back(MapperStateEntry("$4025", "Drive Control")); @@ -713,8 +801,8 @@ vector Fds::GetMapperStateEntries() entries.push_back(MapperStateEntry("$4025.7", "Disk IRQ Enabled", _diskIrqEnabled, MapperStateValueType::Bool)); entries.push_back(MapperStateEntry("$4030", "Drive/IRQ Status")); - entries.push_back(MapperStateEntry("$4030.0", "Timer IRQ", _cpu->HasIrqSource(IRQSource::External), MapperStateValueType::Bool)); - //entries.push_back(MapperStateEntry("$4030.1", "DRAM Refresh Watchdog IRQ", false, MapperStateValueType::Bool)); + entries.push_back(MapperStateEntry("$4030.0", "Timer IRQ", _timerIrqPending, MapperStateValueType::Bool)); + entries.push_back(MapperStateEntry("$4030.1", "DRAM Refresh Watchdog IRQ", _watchdogIrqPending, MapperStateValueType::Bool)); entries.push_back(MapperStateEntry("$4030.3", "Mirroring", GetMirroringType() == MirroringType::Horizontal ? "Horizontal" : "Vertical", GetMirroringType() == MirroringType::Horizontal ? 1 : 0)); entries.push_back(MapperStateEntry("$4030.5", "CRC Error", (_useQdFormat && _badCrc), MapperStateValueType::Bool)); //entries.push_back(MapperStateEntry("$4030.6", "End of Head", _endOfHead, MapperStateValueType::Bool)); diff --git a/Core/NES/Mappers/FDS/Fds.h b/Core/NES/Mappers/FDS/Fds.h index 256b5972fe..5ce2865580 100644 --- a/Core/NES/Mappers/FDS/Fds.h +++ b/Core/NES/Mappers/FDS/Fds.h @@ -26,6 +26,7 @@ class Fds : public BaseMapper uint16_t _irqCounter = 0; bool _irqEnabled = false; bool _irqRepeatEnabled = false; + bool _timerIrqPending = false; bool _diskRegEnabled = true; bool _soundRegEnabled = true; @@ -81,6 +82,12 @@ class Fds : public BaseMapper bool _gameStarted = false; bool _needSave = false; + bool _watchdogIrqEnabled = false; + bool _watchdogIrqPending = false; + bool _dramAccessed = false; + uint8_t _dramRow = 0; // 7 bits + uint16_t _watchdogTimer = 4095; // 12 bits + protected: uint16_t GetPrgPageSize() override { return 0x2000; } uint16_t GetChrPageSize() override { return 0x2000; } @@ -104,6 +111,7 @@ class Fds : public BaseMapper void ProcessAutoDiskInsert(); void ClockIrq(); + void UpdateIrqState(); void ProcessCpuClock() override; void UpdateCrc(uint8_t value); @@ -112,6 +120,7 @@ class Fds : public BaseMapper uint8_t ReadRegister(uint16_t addr) override; uint8_t ReadRam(uint16_t addr) override; + void WriteRam(uint16_t addr, uint8_t value) override; void Serialize(Serializer& s) override; vector GetMapperStateEntries() override; diff --git a/UI/Debugger/Labels/DefaultLabelHelper.cs b/UI/Debugger/Labels/DefaultLabelHelper.cs index 94f5123188..c835f0b4d7 100644 --- a/UI/Debugger/Labels/DefaultLabelHelper.cs +++ b/UI/Debugger/Labels/DefaultLabelHelper.cs @@ -295,7 +295,7 @@ private static void SetDefaultNesLabels() LabelManager.SetLabel(0x04A0, MemoryType.NesPrgRom, "FileMatchTest", "Input: $02-$03 = Pointer to File ID list " + Environment.NewLine + "Output: " + Environment.NewLine + "Desc: Uses a byte string pointed at by Ptr($02) to tell the disk system which files to load. The file ID's number is searched for in the string. If an exact match is found, [$09] is 0'd, and [$0e] is incremented. If no matches are found after 20 bytes, or a -1 entry is encountered, [$09] is set to -1. If the first byte in the string is -1, the BootID number is used for matching files (any FileID that is not greater than the BootID qualifies as a match)."); LabelManager.SetLabel(0x04DA, MemoryType.NesPrgRom, "SkipFiles", "Input: $06 = # of files to skip " + Environment.NewLine + "Output: " + Environment.NewLine + "Desc: Skips over a specified number of files."); - LabelManager.SetLabel(0x0149, MemoryType.NesPrgRom, "Delay131", "Input: " + Environment.NewLine + "Output: " + Environment.NewLine + "Affects: " + Environment.NewLine + "Desc: 131 clock cycle delay (including the JSR instruction to call it) used by the \"BIOS acknowledge and delay\" IRQ handler."); + LabelManager.SetLabel(0x0149, MemoryType.NesPrgRom, "Delay131", "Input: " + Environment.NewLine + "Output: " + Environment.NewLine + "Affects: " + Environment.NewLine + "Desc: 131 clock cycle delay (including the JSR instruction to call it) used by the \"BIOS acknowledge and delay\" IRQ handler. Intended for refreshing DRAM rows when the DRAM refresh watchdog has expired."); LabelManager.SetLabel(0x0153, MemoryType.NesPrgRom, "Delayms", "Input: Y = Delay in ms (approximate) " + Environment.NewLine + "Output: " + Environment.NewLine + "Affects: X, Y " + Environment.NewLine + "Desc: Delay routine, where the time in CPU cycles is 1790*Y+5. Y = 0 is treated as 256."); LabelManager.SetLabel(0x0161, MemoryType.NesPrgRom, "DisPFObj", "Input: $fe = PPUMASK value " + Environment.NewLine + "Output: " + Environment.NewLine + "Affects: A, $fe " + Environment.NewLine + "Desc: Disable rendering for both sprites and background."); LabelManager.SetLabel(0x016B, MemoryType.NesPrgRom, "EnPFObj", "Input: $fe = PPUMASK value " + Environment.NewLine + "Output: " + Environment.NewLine + "Affects: A, $fe " + Environment.NewLine + "Desc: Enable rendering for both sprites and background.");