From 719a8b3d7468b76444c6549c767b2ac3a1a72347 Mon Sep 17 00:00:00 2001 From: Adrian Siekierka Date: Sat, 9 May 2026 14:47:42 +0200 Subject: [PATCH 01/11] GBA: Separate S3511A RTC emulation code from GBA interface emulation code. --- Core/Core.vcxproj | 1 + Core/GBA/Cart/GbaRtc.cpp | 341 +-------------------------- Core/GBA/Cart/GbaRtc.h | 62 +---- Core/Shared/Utilities/S3511ARtc.cpp | 353 ++++++++++++++++++++++++++++ Core/Shared/Utilities/S3511ARtc.h | 77 ++++++ 5 files changed, 441 insertions(+), 393 deletions(-) create mode 100644 Core/Shared/Utilities/S3511ARtc.cpp create mode 100644 Core/Shared/Utilities/S3511ARtc.h diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index 29fd33122..7c7147600 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -931,6 +931,7 @@ + diff --git a/Core/GBA/Cart/GbaRtc.cpp b/Core/GBA/Cart/GbaRtc.cpp index 1b2d19ca0..839abed59 100644 --- a/Core/GBA/Cart/GbaRtc.cpp +++ b/Core/GBA/Cart/GbaRtc.cpp @@ -1,292 +1,11 @@ +#include "Shared/Utilities/S3511ARtc.h" #include "pch.h" #include "GBA/Cart/GbaRtc.h" #include "Utilities/Serializer.h" -#include "Utilities/TimeUtilities.h" #include "Shared/Emulator.h" -#include "Shared/BatteryManager.h" -GbaRtc::GbaRtc(Emulator* emu) +GbaRtc::GbaRtc(Emulator* emu) : S3511ARtc(emu) { - _emu = emu; - - _state.Month = 1; - _state.Day = 1; - _state.Status = 0x02; - _state.IntHour = 0x80; - - _lastUpdateTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); -} - -uint8_t GbaRtc::SanitizeData(uint8_t value, uint8_t maxValue, uint8_t fixedValue) -{ - if(value > maxValue || (value & 0x0F) >= 0x0A || (value & 0xF0) >= 0xA0) { - //Invalid values - return fixedValue; - } - - return value; -} - -void GbaRtc::FromDateTime(uint64_t data, bool includeYmd) -{ - UpdateTime(); - - //TODOGBA process invalid days (e.g feb 30) - if(includeYmd) { - _state.Year = SanitizeData(data & 0xFF, 0x99, 0); - _state.Month = SanitizeData((data >> 8) & 0x1F, 0x12, 1); - _state.Day = SanitizeData((data >> 16) & 0x3F, 0x31, 1); - _state.DoW = SanitizeData((data >> 24) & 0x07, 0x06, 0); - } - _state.Minute = SanitizeData((data >> 40) & 0x7F, 0x59, 0); - _state.Second = SanitizeData((data >> 48) & 0x7F, 0x59, 0); - - bool is24hMode = _state.Status & 0x40; - uint8_t hourData = (data >> 32) & 0xBF; - if(is24hMode) { - _state.Hour = SanitizeData(hourData & 0x3F, 0x23, 0); - if(_state.Hour >= 0x12) { - _state.Hour |= 0x80; //Set PM flag - } - } else { - _state.Hour = SanitizeData(hourData & 0x3F, 0x11, 0) | (hourData & 0x80); - } -} - -uint64_t GbaRtc::ToDateTime() -{ - UpdateTime(); - return _state.Year | (_state.Month << 8) | (_state.Day << 16) | (_state.DoW << 24) | ((uint64_t)_state.Hour << 32) | ((uint64_t)_state.Minute << 40) | ((uint64_t)_state.Second << 48); -} - -uint8_t GbaRtc::GetCommandLength(Command cmd) -{ - switch(cmd) { - default: - case Command::Reset: return 0; - case Command::Status: return 8; - case Command::DateTime: return 8 * 7; - case Command::Time: return 8 * 3; - case Command::Alarm1: return 8 * 2; - case Command::Alarm2: return 8; - case Command::TestStart: return 0; - case Command::TestEnd: return 0; - } -} - -void GbaRtc::ProcessDataIn(uint8_t bit) -{ - Command cmd = (Command)((_command & 0x0F) >> 1); - - uint8_t length = GetCommandLength(cmd); - _dataIn |= ((uint64_t)bit << (length - _dataInSize)); - _dataInSize--; - - if(_dataInSize == 0) { - switch(cmd) { - case Command::Status: - _state.Status &= 0x80; - _state.Status |= (_dataIn & 0x6A); - break; - - case Command::DateTime: FromDateTime(_dataIn, true); break; - case Command::Time: FromDateTime(_dataIn << 32, false); break; - - default: - //TODOGBA not implemented - break; - } - - for(int i = 0; i < 8; i++) { - //Re-write over the last byte if written beyond the expected length - _dataInSize++; - _dataIn &= ~((uint64_t)1 << (length - _dataInSize)); - } - } -} - -void GbaRtc::ProcessDataOut() -{ - _bitOut = _dataOut & 0x01; - _dataOut >>= 1; - _dataOutSize--; - if(_dataOutSize == 0) { - //Repeat command output if read beyond expected length - ProcessCommand(); - } -} - -void GbaRtc::ProcessCommand() -{ - Command cmd = (Command)((_command & 0x0F) >> 1); - uint8_t length = GetCommandLength(cmd); - bool read = _command & 0x01; - if(read) { - _dataOutSize = length; - - if(_emu->IsDebugging()) { - _emu->DebugLog("[RTC] Read command: " + string(magic_enum::enum_name(cmd))); - } - - switch(cmd) { - case Command::Reset: Reset(); break; - case Command::Status: _dataOut = _state.Status; break; - case Command::DateTime: _dataOut = ToDateTime(); break; - case Command::Time: _dataOut = ToDateTime() >> 32; break; - default: - //TODOGBA not implemented - _dataOut = 0; - break; - } - } else { - if(_emu->IsDebugging()) { - _emu->DebugLog("[RTC] Write command: " + string(magic_enum::enum_name(cmd))); - } - - if(cmd == Command::Reset) { - Reset(); - } - - _dataInSize = length; - _dataIn = 0; - } -} - -void GbaRtc::Reset() -{ - _state = {}; - _state.Month = 1; - _state.Day = 1; -} - -void GbaRtc::UpdateTime() -{ - uint64_t currentTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); - uint32_t elapsedSeconds = (uint32_t)(currentTime - _lastUpdateTime); - if(elapsedSeconds <= 0) { - return; - } - - std::tm tm = {}; - tm.tm_sec = (_state.Second & 0x0F) + ((_state.Second >> 4) * 10); - tm.tm_min = (_state.Minute & 0x0F) + ((_state.Minute >> 4) * 10); - int hour = _state.Hour & 0x3F; - tm.tm_hour = (hour & 0x0F) + ((hour >> 4) * 10); - if(tm.tm_hour < 12 && (_state.Hour & 0x80)) { - tm.tm_hour += 12; - } - tm.tm_mday = (_state.Day & 0x0F) + ((_state.Day >> 4) * 10); - - int month = _state.Month - 1; - tm.tm_mon = (month & 0x0F) + ((month >> 4) * 10); - tm.tm_year = 100 + (_state.Year & 0x0F) + ((_state.Year >> 4) * 10); - - std::time_t tt = TimeUtilities::TmToUtc(&tm); - if(tt == -1) { - //Invalid time - _lastUpdateTime = currentTime; - return; - } - - int8_t dowGap = 0; - if(tm.tm_wday != _state.DoW) { - //The DoW on the RTC can be set to any arbitrary value for a specific date - //Check the gap between the value set by the game & the real dow for that date - dowGap = (int8_t)tm.tm_wday - (int8_t)_state.DoW; - } - - std::chrono::system_clock::time_point timePoint = std::chrono::system_clock::from_time_t(tt); - timePoint += std::chrono::seconds((uint32_t)elapsedSeconds); - - std::time_t newTime = system_clock::to_time_t(timePoint); - std::tm newTm; -#if (defined(WIN32) || defined(_WIN32) || defined(__WIN32__)) - gmtime_s(&newTm, &newTime); -#else - gmtime_r(&newTime, &newTm); -#endif - - _state.Second = (newTm.tm_sec % 10) + ((newTm.tm_sec / 10) << 4); - _state.Minute = (newTm.tm_min % 10) + ((newTm.tm_min / 10) << 4); - - bool is24hMode = _state.Status & 0x40; - if(is24hMode) { - _state.Hour = (newTm.tm_hour % 10) + ((newTm.tm_hour / 10) << 4); - } else { - hour = newTm.tm_hour >= 12 ? newTm.tm_hour - 12 : newTm.tm_hour; - _state.Hour = (hour % 10) + ((hour / 10) << 4); - } - - if(newTm.tm_hour >= 12) { - //Set PM flag (in both 12h and 24h modes) - _state.Hour |= 0x80; - } - - _state.Day = (newTm.tm_mday % 10) + ((newTm.tm_mday / 10) << 4); - - month = newTm.tm_mon + 1; - _state.Month = (month % 10) + ((month / 10) << 4); - - int year = newTm.tm_year - 100; - _state.Year = (year % 10) + ((year / 10) << 4); - - int dow = newTm.tm_wday - dowGap; - _state.DoW = dow < 0 ? (dow + 7) : (dow % 7); - - _lastUpdateTime = currentTime; -} - -void GbaRtc::LoadBattery() -{ - vector rtcData = _emu->GetBatteryManager()->LoadBattery(".rtc"); - - if(rtcData.size() == sizeof(_state) + sizeof(uint64_t)) { - _state.Year = rtcData[0]; - _state.Month = rtcData[1]; - _state.Day = rtcData[2]; - _state.DoW = rtcData[3]; - _state.Hour = rtcData[4]; - _state.Minute = rtcData[5]; - _state.Second = rtcData[6]; - _state.Status = rtcData[7]; - _state.IntHour = rtcData[8]; - _state.IntMinute = rtcData[9]; - - uint64_t time = 0; - for(uint32_t i = 0; i < sizeof(uint64_t); i++) { - time <<= 8; - time |= rtcData[sizeof(_state) + i]; - } - _lastUpdateTime = time; - } else { - _lastUpdateTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); - } -} - -void GbaRtc::SaveBattery() -{ - vector rtcData; - - rtcData.push_back(_state.Year); - rtcData.push_back(_state.Month); - rtcData.push_back(_state.Day); - rtcData.push_back(_state.DoW); - rtcData.push_back(_state.Hour); - rtcData.push_back(_state.Minute); - rtcData.push_back(_state.Second); - rtcData.push_back(_state.Status); - rtcData.push_back(_state.IntHour); - rtcData.push_back(_state.IntMinute); - - rtcData.resize(sizeof(_state) + sizeof(uint64_t), 0); - - uint64_t time = _lastUpdateTime; - for(uint32_t i = 0; i < sizeof(uint64_t); i++) { - rtcData[sizeof(_state) + i] = (time >> 56) & 0xFF; - time <<= 8; - } - - _emu->GetBatteryManager()->SaveBattery(".rtc", rtcData.data(), (uint32_t)rtcData.size()); } uint8_t GbaRtc::Read() @@ -309,72 +28,28 @@ void GbaRtc::Write(uint8_t value) bool chipSelect = value & 0x04; if(!chipSelect || !_chipSelect) { - _command = 0; - _bitCounter = 0; + ResetBus(); _clk = 0; - _dataOut = 0; - _dataOutSize = 0; - _dataIn = 0; - _dataInSize = 0; _bitOut = 1; _chipSelect = chipSelect; return; } if(clk && !_clk) { - if(_dataOutSize) { - //Data output is updated on falling edge of clock - } else if(_dataInSize) { - ProcessDataIn(data); - } else { - _command <<= 1; - _command |= data; - _bitCounter++; - - if(_bitCounter == 8) { - if((_command & 0xF0) == 0x60) { - ProcessCommand(); - } else { - //invalid command - _command = 0; - } - - _bitCounter = 0; - } - } + WriteBits(data, 1); } else if(!clk && _clk) { - if(_dataOutSize) { - ProcessDataOut(); - } else { - _bitOut = 1; - } + _bitOut = ReadBits(1); } + _clk = clk; } void GbaRtc::Serialize(Serializer& s) { - SV(_bitCounter); - SV(_command); + S3511ARtc::Serialize(s); + SV(_clk); - SV(_dataOut); - SV(_dataOutSize); - SV(_dataIn); - SV(_dataInSize); SV(_bitOut); SV(_chipSelect); - SV(_lastUpdateTime); SV(_prevValue); - - SV(_state.Year); - SV(_state.Month); - SV(_state.Day); - SV(_state.DoW); - SV(_state.Hour); - SV(_state.Minute); - SV(_state.Second); - SV(_state.Status); - SV(_state.IntHour); - SV(_state.IntMinute); - SV(_state.TestMode); } diff --git a/Core/GBA/Cart/GbaRtc.h b/Core/GBA/Cart/GbaRtc.h index 0ec736768..0cdb9be57 100644 --- a/Core/GBA/Cart/GbaRtc.h +++ b/Core/GBA/Cart/GbaRtc.h @@ -1,79 +1,21 @@ #pragma once #include "pch.h" #include "Utilities/ISerializable.h" +#include "Shared/Utilities/S3511ARtc.h" class Emulator; -struct GbaRtcState -{ - uint8_t Year; - uint8_t Month; - uint8_t Day; - uint8_t DoW; - uint8_t Hour; - uint8_t Minute; - uint8_t Second; - - uint8_t Status; - uint8_t IntHour; //unimplemented - uint8_t IntMinute; //unimplemented - - bool TestMode; //unimplemented -}; - -class GbaRtc final : public ISerializable +class GbaRtc final : public S3511ARtc { private: - enum class Command : uint8_t - { - Reset, - Status, - DateTime, - Time, - Alarm1, - Alarm2, - TestStart, - TestEnd - }; - - Emulator* _emu = nullptr; - - GbaRtcState _state = {}; - uint64_t _lastUpdateTime = 0; - - uint8_t _bitCounter = 0; - uint8_t _command = 0; uint8_t _clk = 0; uint8_t _prevValue = 0; - - uint64_t _dataOut = 0; - uint8_t _dataOutSize = 0; - - uint64_t _dataIn = 0; - uint8_t _dataInSize = 0; - uint8_t _bitOut = 0; bool _chipSelect = false; - uint8_t SanitizeData(uint8_t value, uint8_t maxValue, uint8_t fixedValue); - void FromDateTime(uint64_t data, bool includeYmd); - uint64_t ToDateTime(); - - uint8_t GetCommandLength(Command cmd); - - void ProcessDataIn(uint8_t value); - void ProcessDataOut(); - void ProcessCommand(); - - void Reset(); - void UpdateTime(); - public: GbaRtc(Emulator* emu); - void LoadBattery(); - void SaveBattery(); - uint8_t Read(); void Write(uint8_t value); diff --git a/Core/Shared/Utilities/S3511ARtc.cpp b/Core/Shared/Utilities/S3511ARtc.cpp new file mode 100644 index 000000000..38fd321c1 --- /dev/null +++ b/Core/Shared/Utilities/S3511ARtc.cpp @@ -0,0 +1,353 @@ +#include "pch.h" +#include "S3511ARtc.h" +#include "Utilities/Serializer.h" +#include "Utilities/TimeUtilities.h" +#include "Shared/Emulator.h" +#include "Shared/BatteryManager.h" + +S3511ARtc::S3511ARtc(Emulator* emu) +{ + _emu = emu; + + _state.Month = 1; + _state.Day = 1; + _state.Status = 0x02; + _state.IntHour = 0x80; + + _lastUpdateTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); +} + +uint8_t S3511ARtc::SanitizeData(uint8_t value, uint8_t maxValue, uint8_t fixedValue) +{ + if(value > maxValue || (value & 0x0F) >= 0x0A || (value & 0xF0) >= 0xA0) { + //Invalid values + return fixedValue; + } + + return value; +} + +void S3511ARtc::FromDateTime(uint64_t data, bool includeYmd) +{ + UpdateTime(); + + //TODO process invalid days (e.g feb 30) + if(includeYmd) { + _state.Year = SanitizeData(data & 0xFF, 0x99, 0); + _state.Month = SanitizeData((data >> 8) & 0x1F, 0x12, 1); + _state.Day = SanitizeData((data >> 16) & 0x3F, 0x31, 1); + _state.DoW = SanitizeData((data >> 24) & 0x07, 0x06, 0); + } + _state.Minute = SanitizeData((data >> 40) & 0x7F, 0x59, 0); + _state.Second = SanitizeData((data >> 48) & 0x7F, 0x59, 0); + + bool is24hMode = _state.Status & 0x40; + uint8_t hourData = (data >> 32) & 0xBF; + if(is24hMode) { + _state.Hour = SanitizeData(hourData & 0x3F, 0x23, 0); + if(_state.Hour >= 0x12) { + _state.Hour |= 0x80; //Set PM flag + } + } else { + _state.Hour = SanitizeData(hourData & 0x3F, 0x11, 0) | (hourData & 0x80); + } +} + +uint64_t S3511ARtc::ToDateTime() +{ + UpdateTime(); + return _state.Year | (_state.Month << 8) | (_state.Day << 16) | (_state.DoW << 24) | ((uint64_t)_state.Hour << 32) | ((uint64_t)_state.Minute << 40) | ((uint64_t)_state.Second << 48); +} + +uint8_t S3511ARtc::GetCommandLength(Command cmd) +{ + switch(cmd) { + default: + case Command::Reset: return 0; + case Command::Status: return 1; + case Command::DateTime: return 7; + case Command::Time: return 3; + case Command::Alarm1: return 2; + case Command::Alarm2: return 1; + case Command::TestStart: return 0; + case Command::TestEnd: return 0; + } +} + +void S3511ARtc::ResetBus() +{ + _command = 0; + _bitCounter = 0; + _dataOut = 0; + _dataOutSize = 0; + _dataIn = 0; + _dataInSize = 0; +} + +void S3511ARtc::WriteBits(uint32_t value, int count) +{ + if(_dataOutSize) { + return; + } + if(_dataInSize == 0) { + _command <<= count; + _command |= value; + _bitCounter += count; + + assert(_bitCounter <= 8); + if(_bitCounter == 8) { + if((_command & 0xF0) == 0x60) { + ProcessCommand(); + } else { + //invalid command + _command = 0; + } + + _bitCounter = 0; + } + + return; + } + assert(_dataInSize >= count); + + Command cmd = (Command)((_command & 0x0F) >> 1); + + uint8_t length = GetCommandLength(cmd) * 8; + _dataIn |= ((uint64_t)value << (length - _dataInSize)); + _dataInSize -= count; + + if(_dataInSize == 0) { + switch(cmd) { + case Command::Status: + _state.Status &= 0x80; + _state.Status |= (_dataIn & 0x6A); + break; + + case Command::DateTime: FromDateTime(_dataIn, true); break; + case Command::Time: FromDateTime(_dataIn << 32, false); break; + + default: + //TODO not implemented + break; + } + + for(int i = 0; i < 8; i++) { + //Re-write over the last byte if written beyond the expected length + _dataInSize++; + _dataIn &= ~((uint64_t)1 << (length - _dataInSize)); + } + } +} + +uint32_t S3511ARtc::ReadBits(int count) +{ + if(_dataOutSize == 0) { + return (1 << count) - 1; + } + assert(_dataOutSize >= count); + + uint32_t output = _dataOut & ((1 << count) - 1); + _dataOut >>= count; + _dataOutSize -= count; + if(_dataOutSize == 0) { + //Repeat command output if read beyond expected length + ProcessCommand(); + } + return output; +} + +void S3511ARtc::ProcessCommand() +{ + Command cmd = (Command)((_command & 0x0F) >> 1); + uint8_t length = GetCommandLength(cmd) * 8; + bool read = _command & 0x01; + if(read) { + _dataOutSize = length; + + if(_emu->IsDebugging()) { + _emu->DebugLog("[RTC] Read command: " + string(magic_enum::enum_name(cmd))); + } + + switch(cmd) { + case Command::Reset: Reset(); break; + case Command::Status: _dataOut = _state.Status; break; + case Command::DateTime: _dataOut = ToDateTime(); break; + case Command::Time: _dataOut = ToDateTime() >> 32; break; + default: + //TODO not implemented + _dataOut = 0; + break; + } + } else { + if(_emu->IsDebugging()) { + _emu->DebugLog("[RTC] Write command: " + string(magic_enum::enum_name(cmd))); + } + + if(cmd == Command::Reset) { + Reset(); + } + + _dataInSize = length; + _dataIn = 0; + } +} + +void S3511ARtc::Reset() +{ + _state = {}; + _state.Month = 1; + _state.Day = 1; +} + +void S3511ARtc::UpdateTime() +{ + uint64_t currentTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + uint32_t elapsedSeconds = (uint32_t)(currentTime - _lastUpdateTime); + if(elapsedSeconds <= 0) { + return; + } + + std::tm tm = {}; + tm.tm_sec = (_state.Second & 0x0F) + ((_state.Second >> 4) * 10); + tm.tm_min = (_state.Minute & 0x0F) + ((_state.Minute >> 4) * 10); + int hour = _state.Hour & 0x3F; + tm.tm_hour = (hour & 0x0F) + ((hour >> 4) * 10); + if(tm.tm_hour < 12 && (_state.Hour & 0x80)) { + tm.tm_hour += 12; + } + tm.tm_mday = (_state.Day & 0x0F) + ((_state.Day >> 4) * 10); + + int month = _state.Month - 1; + tm.tm_mon = (month & 0x0F) + ((month >> 4) * 10); + tm.tm_year = 100 + (_state.Year & 0x0F) + ((_state.Year >> 4) * 10); + + std::time_t tt = TimeUtilities::TmToUtc(&tm); + if(tt == -1) { + //Invalid time + _lastUpdateTime = currentTime; + return; + } + + int8_t dowGap = 0; + if(tm.tm_wday != _state.DoW) { + //The DoW on the RTC can be set to any arbitrary value for a specific date + //Check the gap between the value set by the game & the real dow for that date + dowGap = (int8_t)tm.tm_wday - (int8_t)_state.DoW; + } + + std::chrono::system_clock::time_point timePoint = std::chrono::system_clock::from_time_t(tt); + timePoint += std::chrono::seconds((uint32_t)elapsedSeconds); + + std::time_t newTime = system_clock::to_time_t(timePoint); + std::tm newTm; +#if (defined(WIN32) || defined(_WIN32) || defined(__WIN32__)) + gmtime_s(&newTm, &newTime); +#else + gmtime_r(&newTime, &newTm); +#endif + + _state.Second = (newTm.tm_sec % 10) + ((newTm.tm_sec / 10) << 4); + _state.Minute = (newTm.tm_min % 10) + ((newTm.tm_min / 10) << 4); + + bool is24hMode = _state.Status & 0x40; + if(is24hMode) { + _state.Hour = (newTm.tm_hour % 10) + ((newTm.tm_hour / 10) << 4); + } else { + hour = newTm.tm_hour >= 12 ? newTm.tm_hour - 12 : newTm.tm_hour; + _state.Hour = (hour % 10) + ((hour / 10) << 4); + } + + if(newTm.tm_hour >= 12) { + //Set PM flag (in both 12h and 24h modes) + _state.Hour |= 0x80; + } + + _state.Day = (newTm.tm_mday % 10) + ((newTm.tm_mday / 10) << 4); + + month = newTm.tm_mon + 1; + _state.Month = (month % 10) + ((month / 10) << 4); + + int year = newTm.tm_year - 100; + _state.Year = (year % 10) + ((year / 10) << 4); + + int dow = newTm.tm_wday - dowGap; + _state.DoW = dow < 0 ? (dow + 7) : (dow % 7); + + _lastUpdateTime = currentTime; +} + +void S3511ARtc::LoadBattery() +{ + vector rtcData = _emu->GetBatteryManager()->LoadBattery(".rtc"); + + if(rtcData.size() == sizeof(_state) + sizeof(uint64_t)) { + _state.Year = rtcData[0]; + _state.Month = rtcData[1]; + _state.Day = rtcData[2]; + _state.DoW = rtcData[3]; + _state.Hour = rtcData[4]; + _state.Minute = rtcData[5]; + _state.Second = rtcData[6]; + _state.Status = rtcData[7]; + _state.IntHour = rtcData[8]; + _state.IntMinute = rtcData[9]; + + uint64_t time = 0; + for(uint32_t i = 0; i < sizeof(uint64_t); i++) { + time <<= 8; + time |= rtcData[sizeof(_state) + i]; + } + _lastUpdateTime = time; + } else { + _lastUpdateTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + } +} + +void S3511ARtc::SaveBattery() +{ + vector rtcData; + + rtcData.push_back(_state.Year); + rtcData.push_back(_state.Month); + rtcData.push_back(_state.Day); + rtcData.push_back(_state.DoW); + rtcData.push_back(_state.Hour); + rtcData.push_back(_state.Minute); + rtcData.push_back(_state.Second); + rtcData.push_back(_state.Status); + rtcData.push_back(_state.IntHour); + rtcData.push_back(_state.IntMinute); + + rtcData.resize(sizeof(_state) + sizeof(uint64_t), 0); + + uint64_t time = _lastUpdateTime; + for(uint32_t i = 0; i < sizeof(uint64_t); i++) { + rtcData[sizeof(_state) + i] = (time >> 56) & 0xFF; + time <<= 8; + } + + _emu->GetBatteryManager()->SaveBattery(".rtc", rtcData.data(), (uint32_t)rtcData.size()); +} + +void S3511ARtc::Serialize(Serializer& s) +{ + SV(_bitCounter); + SV(_command); + SV(_dataOut); + SV(_dataOutSize); + SV(_dataIn); + SV(_dataInSize); + SV(_lastUpdateTime); + + SV(_state.Year); + SV(_state.Month); + SV(_state.Day); + SV(_state.DoW); + SV(_state.Hour); + SV(_state.Minute); + SV(_state.Second); + SV(_state.Status); + SV(_state.IntHour); + SV(_state.IntMinute); + SV(_state.TestMode); +} diff --git a/Core/Shared/Utilities/S3511ARtc.h b/Core/Shared/Utilities/S3511ARtc.h new file mode 100644 index 000000000..5354b8c1c --- /dev/null +++ b/Core/Shared/Utilities/S3511ARtc.h @@ -0,0 +1,77 @@ +#pragma once +#include "pch.h" +#include "Utilities/ISerializable.h" + +class Emulator; + +struct S3511ARtcState +{ + uint8_t Year; + uint8_t Month; + uint8_t Day; + uint8_t DoW; + uint8_t Hour; + uint8_t Minute; + uint8_t Second; + + uint8_t Status; + uint8_t IntHour; //unimplemented + uint8_t IntMinute; //unimplemented + + bool TestMode; //unimplemented +}; + +class S3511ARtc : public ISerializable +{ +private: + enum class Command : uint8_t + { + Reset, + Status, + DateTime, + Time, + Alarm1, + Alarm2, + TestStart, + TestEnd + }; + + S3511ARtcState _state = {}; + uint64_t _lastUpdateTime = 0; + + uint8_t _bitCounter = 0; + uint8_t _command = 0; + + uint64_t _dataOut = 0; + uint8_t _dataOutSize = 0; + + uint64_t _dataIn = 0; + uint8_t _dataInSize = 0; + + uint8_t SanitizeData(uint8_t value, uint8_t maxValue, uint8_t fixedValue); + void FromDateTime(uint64_t data, bool includeYmd); + uint64_t ToDateTime(); + + uint8_t GetCommandLength(Command cmd); + + void ProcessCommand(); + + void Reset(); + void UpdateTime(); + +protected: + Emulator* _emu = nullptr; + + //GBA only uses count values of 1, WS only uses 8 + void WriteBits(uint32_t value, int count); + uint32_t ReadBits(int count); + void ResetBus(); + +public: + S3511ARtc(Emulator* emu); + + void LoadBattery(); + void SaveBattery(); + + void Serialize(Serializer& s) override; +}; From e89a123dcfc5f84f6f9343903c6804efae95cf47 Mon Sep 17 00:00:00 2001 From: Adrian Siekierka Date: Sat, 9 May 2026 16:25:17 +0200 Subject: [PATCH 02/11] WS: Initial cartridge RTC implementation. --- Core/Shared/Utilities/S3511ARtc.cpp | 5 ++ Core/Shared/Utilities/S3511ARtc.h | 29 ++++---- Core/WS/Carts/WsCart.cpp | 12 +++- Core/WS/Carts/WsCart.h | 7 +- Core/WS/Carts/WsRtc.cpp | 101 ++++++++++++++++++++++++++++ Core/WS/Carts/WsRtc.h | 32 +++++++++ Core/WS/WsConsole.cpp | 26 ++++++- Core/WS/WsConsole.h | 4 ++ 8 files changed, 196 insertions(+), 20 deletions(-) create mode 100644 Core/WS/Carts/WsRtc.cpp create mode 100644 Core/WS/Carts/WsRtc.h diff --git a/Core/Shared/Utilities/S3511ARtc.cpp b/Core/Shared/Utilities/S3511ARtc.cpp index 38fd321c1..d817d472b 100644 --- a/Core/Shared/Utilities/S3511ARtc.cpp +++ b/Core/Shared/Utilities/S3511ARtc.cpp @@ -74,6 +74,11 @@ uint8_t S3511ARtc::GetCommandLength(Command cmd) } } +uint8_t S3511ARtc::GetCommandLength(uint8_t cmd) +{ + return GetCommandLength((Command) ((cmd >> 1) & 0x07)); +} + void S3511ARtc::ResetBus() { _command = 0; diff --git a/Core/Shared/Utilities/S3511ARtc.h b/Core/Shared/Utilities/S3511ARtc.h index 5354b8c1c..b3412713e 100644 --- a/Core/Shared/Utilities/S3511ARtc.h +++ b/Core/Shared/Utilities/S3511ARtc.h @@ -24,18 +24,6 @@ struct S3511ARtcState class S3511ARtc : public ISerializable { private: - enum class Command : uint8_t - { - Reset, - Status, - DateTime, - Time, - Alarm1, - Alarm2, - TestStart, - TestEnd - }; - S3511ARtcState _state = {}; uint64_t _lastUpdateTime = 0; @@ -52,16 +40,29 @@ class S3511ARtc : public ISerializable void FromDateTime(uint64_t data, bool includeYmd); uint64_t ToDateTime(); - uint8_t GetCommandLength(Command cmd); - void ProcessCommand(); void Reset(); void UpdateTime(); protected: + enum class Command : uint8_t + { + Reset, + Status, + DateTime, + Time, + Alarm1, + Alarm2, + TestStart, + TestEnd + }; + Emulator* _emu = nullptr; + uint8_t GetCommandLength(Command cmd); + uint8_t GetCommandLength(uint8_t cmd); + //GBA only uses count values of 1, WS only uses 8 void WriteBits(uint32_t value, int count); uint32_t ReadBits(int count); diff --git a/Core/WS/Carts/WsCart.cpp b/Core/WS/Carts/WsCart.cpp index 15aef58bc..e09ca57f7 100644 --- a/Core/WS/Carts/WsCart.cpp +++ b/Core/WS/Carts/WsCart.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include "WS/Carts/WsCart.h" +#include "WS/Carts/WsRtc.h" #include "WS/WsMemoryManager.h" #include "WS/WsEeprom.h" #include "Utilities/Serializer.h" @@ -22,10 +23,11 @@ WsCart::WsCart() _state.SelectedBanks[3] = 0xFF; } -void WsCart::Init(WsMemoryManager* memoryManager, WsEeprom* cartEeprom) +void WsCart::Init(WsMemoryManager* memoryManager, WsEeprom* cartEeprom, WsRtc* cartRtc) { _memoryManager = memoryManager; _cartEeprom = cartEeprom; + _cartRtc = cartRtc; } void WsCart::RefreshMappings() @@ -40,8 +42,10 @@ uint8_t WsCart::ReadPort(uint16_t port) { if(port < 0xC4) { return _state.SelectedBanks[port - 0xC0]; - } else if(port < 0xC9 && _cartEeprom) { + } else if(port >= 0xC4 && port < 0xC9 && _cartEeprom) { return _cartEeprom->ReadPort(port - 0xC4); + } else if(port >= 0xCA && port < 0xCC && _cartRtc) { + return _cartRtc->ReadPort(port); } return _memoryManager->GetUnmappedPort(); @@ -52,8 +56,10 @@ void WsCart::WritePort(uint16_t port, uint8_t value) if(port < 0xC4) { _state.SelectedBanks[port - 0xC0] = value; _memoryManager->RefreshMappings(); - } else if(port < 0xC9 && _cartEeprom) { + } else if(port >= 0xC4 && port < 0xC9 && _cartEeprom) { _cartEeprom->WritePort(port - 0xC4, value); + } else if(port >= 0xCA && port < 0xCC && _cartRtc) { + _cartRtc->WritePort(port, value); } } diff --git a/Core/WS/Carts/WsCart.h b/Core/WS/Carts/WsCart.h index 87af797a9..1779c395d 100644 --- a/Core/WS/Carts/WsCart.h +++ b/Core/WS/Carts/WsCart.h @@ -4,10 +4,11 @@ #include "Utilities/ISerializable.h" #include "Shared/MemoryType.h" +class WsConsole; class WsMemoryManager; class WsEeprom; +class WsRtc; -//TODOWS RTC //TODOWS Flash class WsCart final : public ISerializable @@ -17,6 +18,7 @@ class WsCart final : public ISerializable WsMemoryManager* _memoryManager = nullptr; WsEeprom* _cartEeprom = nullptr; + WsRtc* _cartRtc = nullptr; void Map(uint32_t start, uint32_t end, MemoryType type, uint32_t offset, bool readonly); void Unmap(uint32_t start, uint32_t end); @@ -25,11 +27,12 @@ class WsCart final : public ISerializable WsCart(); virtual ~WsCart() {} - void Init(WsMemoryManager* memoryManager, WsEeprom* cartEeprom); + void Init(WsMemoryManager* memoryManager, WsEeprom* cartEeprom, WsRtc* cartRtc); void RefreshMappings(); WsCartState& GetState() { return _state; } WsEeprom* GetEeprom() { return _cartEeprom; } + WsRtc* GetRtc() { return _cartRtc; } virtual uint8_t ReadPort(uint16_t port); virtual void WritePort(uint16_t port, uint8_t value); diff --git a/Core/WS/Carts/WsRtc.cpp b/Core/WS/Carts/WsRtc.cpp new file mode 100644 index 000000000..0d47f64f1 --- /dev/null +++ b/Core/WS/Carts/WsRtc.cpp @@ -0,0 +1,101 @@ +#include "Shared/Utilities/S3511ARtc.h" +#include "WS/WsConsole.h" +#include "pch.h" +#include "WS/Carts/WsRtc.h" +#include "Utilities/Serializer.h" +#include "Shared/Emulator.h" + +// TODOWS clock-accurate behavior, needs tests +// TODOWS edge case: changing commands mid-transfer, needs tests + +WsRtc::WsRtc(Emulator* emu, WsConsole* console) : S3511ARtc(emu), _console(console) +{ + _ready = true; +} + +bool WsRtc::IsCommandRead() +{ + return (_command & 0x01) != 0; +} + +void WsRtc::StartByteTransfer() +{ + if(_busy) { + _ready = false; + _transferClock = _console->GetCartridgeClock() + 8; + } +} + +void WsRtc::Update() +{ + if(!_ready && _console->GetCartridgeClock() >= _transferClock) { + if(!_commandTransferred) { + WriteBits((_command & 0x0F) | 0x60, 8); + _commandTransferred = true; + _ready = true; + if(_bytesLeft) { + StartByteTransfer(); + } + } else if(_bytesLeft) { + if(IsCommandRead()) { + _data = ReadBits(8); + } else { + WriteBits(_data, 8); + } + _ready = true; + _bytesLeft--; + } + + if(_commandTransferred && !_bytesLeft) { + ResetBus(); + _busy = false; + _commandTransferred = false; + } + } +} + +uint8_t WsRtc::ReadPort(uint16_t port) +{ + Update(); + + if(port == 0xCA) { + return (_command & 0x0F) | (_busy ? 0x10 : 0) | (_ready ? 0x80 : 0); + } else if(port == 0xCB) { + if(_bytesLeft && IsCommandRead()) { + StartByteTransfer(); + } + return _data; + } + + return 0; +} + +void WsRtc::WritePort(uint16_t port, uint8_t value) +{ + Update(); + + if(port == 0xCA) { + _busy = (value & 0x10) == 0x10; + _command = (value & 0x0F); + _bytesLeft = GetCommandLength(_command); + StartByteTransfer(); + } else if(port == 0xCB) { + _data = value; + if(_bytesLeft && !IsCommandRead()) { + StartByteTransfer(); + } + } +} + +void WsRtc::Serialize(Serializer& s) +{ + S3511ARtc::Serialize(s); + + SV(_command); + SV(_data); + SV(_bytesLeft); + SV(_commandTransferred); + SV(_transferClock); + SV(_ready); + SV(_busy); +} diff --git a/Core/WS/Carts/WsRtc.h b/Core/WS/Carts/WsRtc.h new file mode 100644 index 000000000..f0561a6a1 --- /dev/null +++ b/Core/WS/Carts/WsRtc.h @@ -0,0 +1,32 @@ +#pragma once +#include "pch.h" +#include "Utilities/ISerializable.h" +#include "Shared/Utilities/S3511ARtc.h" + +class Emulator; + +class WsRtc final : public S3511ARtc +{ +private: + WsConsole* _console; + + uint8_t _command = 0; + uint8_t _data = 0; + uint8_t _bytesLeft = 0; + bool _commandTransferred = false; + uint32_t _transferClock = 0; + bool _ready = true; + bool _busy = false; + + bool IsCommandRead(); + void StartByteTransfer(); + void Update(); + +public: + WsRtc(Emulator* emu, WsConsole* console); + + uint8_t ReadPort(uint16_t port); + void WritePort(uint16_t port, uint8_t value); + + void Serialize(Serializer& s) override; +}; diff --git a/Core/WS/WsConsole.cpp b/Core/WS/WsConsole.cpp index b6445985b..cdd5079c3 100644 --- a/Core/WS/WsConsole.cpp +++ b/Core/WS/WsConsole.cpp @@ -4,6 +4,7 @@ #include "WS/WsPpu.h" #include "WS/WsTimer.h" #include "WS/Carts/WsCart.h" +#include "WS/Carts/WsRtc.h" #include "WS/WsControlManager.h" #include "WS/WsMemoryManager.h" #include "WS/WsDmaController.h" @@ -94,6 +95,10 @@ LoadRomResult WsConsole::LoadRom(VirtualFile& romFile) _cartEeprom.reset(new WsEeprom(_emu, this, (WsEepromSize)_cartEepromSize, _cartEepromData, false)); } + if(mapperType >= 0x01) { + _cartRtc.reset(new WsRtc(_emu, this)); + } + _model = _emu->GetSettings()->GetWsConfig().Model; if(_model == WsModel::Auto) { _model = hasColorSupport ? WsModel::Color : WsModel::Monochrome; @@ -129,7 +134,7 @@ LoadRomResult WsConsole::LoadRom(VirtualFile& romFile) _apu.reset(new WsApu(_emu, this, _memoryManager.get(), _dmaController.get())); _cart.reset(new WsCart()); - _cart->Init(_memoryManager.get(), _cartEeprom.get()); + _cart->Init(_memoryManager.get(), _cartEeprom.get(), _cartRtc.get()); _memoryManager->Init(_emu, this, _cpu.get(), _ppu.get(), _controlManager.get(), _cart.get(), _timer.get(), _dmaController.get(), _internalEeprom.get(), _apu.get(), _serial.get()); _timer->Init(_memoryManager.get()); _dmaController->Init(_memoryManager.get(), _apu.get()); @@ -311,6 +316,9 @@ void WsConsole::LoadBattery() if(_cartEeprom) { _cartEeprom->LoadBattery(); } + if(_cartRtc) { + _cartRtc->LoadBattery(); + } if(_saveRam) { _emu->GetBatteryManager()->LoadBattery(".sav", _saveRam, _saveRamSize); @@ -323,6 +331,9 @@ void WsConsole::SaveBattery() if(_cartEeprom) { _cartEeprom->SaveBattery(); } + if(_cartRtc) { + _cartRtc->SaveBattery(); + } if(_saveRam) { _emu->GetBatteryManager()->SaveBattery(".sav", _saveRam, _saveRamSize); @@ -349,6 +360,16 @@ vector WsConsole::GetCpuTypes() return { CpuType::Ws }; } +uint64_t WsConsole::GetCartridgeClock() +{ + return _cpu->GetCycleCount() / 8; +} + +uint32_t WsConsole::GetCartridgeClockRate() +{ + return 12288000 / 32; +} + uint64_t WsConsole::GetMasterClock() { return _cpu->GetCycleCount(); @@ -496,4 +517,7 @@ void WsConsole::Serialize(Serializer& s) if(_cartEeprom) { SV(_cartEeprom); } + if(_cartRtc) { + SV(_cartRtc); + } } diff --git a/Core/WS/WsConsole.h b/Core/WS/WsConsole.h index 628016f73..b48814a52 100644 --- a/Core/WS/WsConsole.h +++ b/Core/WS/WsConsole.h @@ -14,6 +14,7 @@ class WsMemoryManager; class WsControlManager; class WsDmaController; class WsEeprom; +class WsRtc; enum class WsModel : uint8_t; class WsConsole final : public IConsole @@ -31,6 +32,7 @@ class WsConsole final : public IConsole unique_ptr _dmaController; unique_ptr _internalEeprom; unique_ptr _cartEeprom; + unique_ptr _cartRtc; uint8_t* _workRam = nullptr; uint32_t _workRamSize = 0; @@ -81,6 +83,8 @@ class WsConsole final : public IConsole ConsoleRegion GetRegion() override; ConsoleType GetConsoleType() override; vector GetCpuTypes() override; + uint64_t GetCartridgeClock(); + uint32_t GetCartridgeClockRate(); uint64_t GetMasterClock() override; uint32_t GetMasterClockRate() override; double GetFps() override; From 55f507ad80ee8f96187c778bf19cb9e0957aa5ff Mon Sep 17 00:00:00 2001 From: Adrian Siekierka Date: Sat, 9 May 2026 16:44:53 +0200 Subject: [PATCH 03/11] WS: Expose cartridge RTC state to register viewer. --- Core/WS/Carts/WsCart.cpp | 1 + Core/WS/Carts/WsRtc.cpp | 43 ++++++++++--------- Core/WS/Carts/WsRtc.h | 8 ++-- Core/WS/WsConsole.cpp | 3 ++ Core/WS/WsTypes.h | 10 +++++ .../RegisterViewer/WsRegisterViewer.cs | 10 +++++ UI/Interop/ConsoleState/WsState.cs | 10 +++++ 7 files changed, 60 insertions(+), 25 deletions(-) diff --git a/Core/WS/Carts/WsCart.cpp b/Core/WS/Carts/WsCart.cpp index e09ca57f7..6895417a7 100644 --- a/Core/WS/Carts/WsCart.cpp +++ b/Core/WS/Carts/WsCart.cpp @@ -28,6 +28,7 @@ void WsCart::Init(WsMemoryManager* memoryManager, WsEeprom* cartEeprom, WsRtc* c _memoryManager = memoryManager; _cartEeprom = cartEeprom; _cartRtc = cartRtc; + _state.HasRtc = cartRtc != nullptr; } void WsCart::RefreshMappings() diff --git a/Core/WS/Carts/WsRtc.cpp b/Core/WS/Carts/WsRtc.cpp index 0d47f64f1..9212cbbe3 100644 --- a/Core/WS/Carts/WsRtc.cpp +++ b/Core/WS/Carts/WsRtc.cpp @@ -10,45 +10,45 @@ WsRtc::WsRtc(Emulator* emu, WsConsole* console) : S3511ARtc(emu), _console(console) { - _ready = true; + _state.Ready = true; } bool WsRtc::IsCommandRead() { - return (_command & 0x01) != 0; + return (_state.Command & 0x01) != 0; } void WsRtc::StartByteTransfer() { - if(_busy) { - _ready = false; + if(_state.Busy) { + _state.Ready = false; _transferClock = _console->GetCartridgeClock() + 8; } } void WsRtc::Update() { - if(!_ready && _console->GetCartridgeClock() >= _transferClock) { + if(!_state.Ready && _console->GetCartridgeClock() >= _transferClock) { if(!_commandTransferred) { - WriteBits((_command & 0x0F) | 0x60, 8); + WriteBits((_state.Command & 0x0F) | 0x60, 8); _commandTransferred = true; - _ready = true; + _state.Ready = true; if(_bytesLeft) { StartByteTransfer(); } } else if(_bytesLeft) { if(IsCommandRead()) { - _data = ReadBits(8); + _state.Data = ReadBits(8); } else { - WriteBits(_data, 8); + WriteBits(_state.Data, 8); } - _ready = true; + _state.Ready = true; _bytesLeft--; } if(_commandTransferred && !_bytesLeft) { ResetBus(); - _busy = false; + _state.Busy = false; _commandTransferred = false; } } @@ -59,12 +59,12 @@ uint8_t WsRtc::ReadPort(uint16_t port) Update(); if(port == 0xCA) { - return (_command & 0x0F) | (_busy ? 0x10 : 0) | (_ready ? 0x80 : 0); + return (_state.Command & 0x0F) | (_state.Busy ? 0x10 : 0) | (_state.Ready ? 0x80 : 0); } else if(port == 0xCB) { if(_bytesLeft && IsCommandRead()) { StartByteTransfer(); } - return _data; + return _state.Data; } return 0; @@ -75,12 +75,12 @@ void WsRtc::WritePort(uint16_t port, uint8_t value) Update(); if(port == 0xCA) { - _busy = (value & 0x10) == 0x10; - _command = (value & 0x0F); - _bytesLeft = GetCommandLength(_command); + _state.Busy = (value & 0x10) == 0x10; + _state.Command = (value & 0x0F); + _bytesLeft = GetCommandLength(_state.Command); StartByteTransfer(); } else if(port == 0xCB) { - _data = value; + _state.Data = value; if(_bytesLeft && !IsCommandRead()) { StartByteTransfer(); } @@ -91,11 +91,12 @@ void WsRtc::Serialize(Serializer& s) { S3511ARtc::Serialize(s); - SV(_command); - SV(_data); + SV(_state.Command); + SV(_state.Data); + SV(_state.Ready); + SV(_state.Busy); + SV(_bytesLeft); SV(_commandTransferred); SV(_transferClock); - SV(_ready); - SV(_busy); } diff --git a/Core/WS/Carts/WsRtc.h b/Core/WS/Carts/WsRtc.h index f0561a6a1..076a9cee6 100644 --- a/Core/WS/Carts/WsRtc.h +++ b/Core/WS/Carts/WsRtc.h @@ -1,5 +1,6 @@ #pragma once #include "pch.h" +#include "WS/WsTypes.h" #include "Utilities/ISerializable.h" #include "Shared/Utilities/S3511ARtc.h" @@ -9,14 +10,11 @@ class WsRtc final : public S3511ARtc { private: WsConsole* _console; + WsRtcState _state = {}; - uint8_t _command = 0; - uint8_t _data = 0; uint8_t _bytesLeft = 0; bool _commandTransferred = false; uint32_t _transferClock = 0; - bool _ready = true; - bool _busy = false; bool IsCommandRead(); void StartByteTransfer(); @@ -25,6 +23,8 @@ class WsRtc final : public S3511ARtc public: WsRtc(Emulator* emu, WsConsole* console); + WsRtcState& GetState() { return _state; } + uint8_t ReadPort(uint16_t port); void WritePort(uint16_t port, uint8_t value); diff --git a/Core/WS/WsConsole.cpp b/Core/WS/WsConsole.cpp index cdd5079c3..ed2f42693 100644 --- a/Core/WS/WsConsole.cpp +++ b/Core/WS/WsConsole.cpp @@ -468,6 +468,9 @@ WsState WsConsole::GetState() if(_cartEeprom) { state.CartEeprom = _cartEeprom->GetState(); } + if(_cartRtc) { + state.CartRtc = _cartRtc->GetState(); + } return state; } diff --git a/Core/WS/WsTypes.h b/Core/WS/WsTypes.h index f7c80fa90..367d22327 100644 --- a/Core/WS/WsTypes.h +++ b/Core/WS/WsTypes.h @@ -445,9 +445,18 @@ struct WsEepromState struct WsCartState { + bool HasRtc; uint8_t SelectedBanks[4]; }; +struct WsRtcState +{ + uint8_t Data; + uint8_t Command; + bool Ready; + bool Busy; +}; + struct WsState { WsCpuState Cpu; @@ -461,6 +470,7 @@ struct WsState WsEepromState InternalEeprom; WsCartState Cart; WsEepromState CartEeprom; + WsRtcState CartRtc; WsModel Model; }; diff --git a/UI/Debugger/RegisterViewer/WsRegisterViewer.cs b/UI/Debugger/RegisterViewer/WsRegisterViewer.cs index 40ac369a9..2e209c904 100644 --- a/UI/Debugger/RegisterViewer/WsRegisterViewer.cs +++ b/UI/Debugger/RegisterViewer/WsRegisterViewer.cs @@ -383,6 +383,16 @@ private static RegisterViewerTab GetCartTab(ref WsState ws) }); } + if(cart.HasRtc) { + entries.AddRange(new List() { + new RegEntry("", "Cart RTC"), + new RegEntry("$CA.0-3", "Command", ws.CartRtc.Command, Format.X8), + new RegEntry("$CA.4", "Busy", ws.CartRtc.Busy), + new RegEntry("$CA.7", "Ready", ws.CartRtc.Ready), + new RegEntry("$CB", "Data", ws.CartRtc.Data) + }); + } + return new RegisterViewerTab("Cart", entries, CpuType.Ws, MemoryType.WsPort); } diff --git a/UI/Interop/ConsoleState/WsState.cs b/UI/Interop/ConsoleState/WsState.cs index af8d317cc..7df108e42 100644 --- a/UI/Interop/ConsoleState/WsState.cs +++ b/UI/Interop/ConsoleState/WsState.cs @@ -442,10 +442,19 @@ public struct WsEepromState public struct WsCartState { + [MarshalAs(UnmanagedType.I1)] public bool HasRtc; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] SelectedBanks; } +public struct WsRtcState +{ + public byte Data; + public byte Command; + [MarshalAs(UnmanagedType.I1)] public bool Ready; + [MarshalAs(UnmanagedType.I1)] public bool Busy; +} + public struct WsState : BaseState { public WsCpuState Cpu; @@ -459,5 +468,6 @@ public struct WsState : BaseState public WsEepromState InternalEeprom; public WsCartState Cart; public WsEepromState CartEeprom; + public WsRtcState CartRtc; public WsModel Model; } From 61948e83d501f6bed1128bc9389b92c6448ceba1 Mon Sep 17 00:00:00 2001 From: Adrian Siekierka Date: Sat, 9 May 2026 16:49:25 +0200 Subject: [PATCH 04/11] Code: Fix formatting. --- Core/GBA/Cart/GbaRtc.cpp | 2 +- Core/Shared/Utilities/S3511ARtc.cpp | 2 +- Core/Shared/Utilities/S3511ARtc.h | 2 +- Core/WS/Carts/WsRtc.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Core/GBA/Cart/GbaRtc.cpp b/Core/GBA/Cart/GbaRtc.cpp index 839abed59..8a87c47ea 100644 --- a/Core/GBA/Cart/GbaRtc.cpp +++ b/Core/GBA/Cart/GbaRtc.cpp @@ -40,7 +40,7 @@ void GbaRtc::Write(uint8_t value) } else if(!clk && _clk) { _bitOut = ReadBits(1); } - + _clk = clk; } diff --git a/Core/Shared/Utilities/S3511ARtc.cpp b/Core/Shared/Utilities/S3511ARtc.cpp index d817d472b..50e4f81f1 100644 --- a/Core/Shared/Utilities/S3511ARtc.cpp +++ b/Core/Shared/Utilities/S3511ARtc.cpp @@ -76,7 +76,7 @@ uint8_t S3511ARtc::GetCommandLength(Command cmd) uint8_t S3511ARtc::GetCommandLength(uint8_t cmd) { - return GetCommandLength((Command) ((cmd >> 1) & 0x07)); + return GetCommandLength((Command)((cmd >> 1) & 0x07)); } void S3511ARtc::ResetBus() diff --git a/Core/Shared/Utilities/S3511ARtc.h b/Core/Shared/Utilities/S3511ARtc.h index b3412713e..474d5837d 100644 --- a/Core/Shared/Utilities/S3511ARtc.h +++ b/Core/Shared/Utilities/S3511ARtc.h @@ -63,7 +63,7 @@ class S3511ARtc : public ISerializable uint8_t GetCommandLength(Command cmd); uint8_t GetCommandLength(uint8_t cmd); - //GBA only uses count values of 1, WS only uses 8 + //GBA only uses count values of 1, WS only uses 8 void WriteBits(uint32_t value, int count); uint32_t ReadBits(int count); void ResetBus(); diff --git a/Core/WS/Carts/WsRtc.cpp b/Core/WS/Carts/WsRtc.cpp index 9212cbbe3..e3e5c2b1b 100644 --- a/Core/WS/Carts/WsRtc.cpp +++ b/Core/WS/Carts/WsRtc.cpp @@ -95,7 +95,7 @@ void WsRtc::Serialize(Serializer& s) SV(_state.Data); SV(_state.Ready); SV(_state.Busy); - + SV(_bytesLeft); SV(_commandTransferred); SV(_transferClock); From 0e93f2a706303ce05ca8903474bdaf438891bb27 Mon Sep 17 00:00:00 2001 From: Adrian Siekierka Date: Sat, 9 May 2026 17:35:59 +0200 Subject: [PATCH 05/11] WS: Update Core.vcxproj. --- Core/Core.vcxproj | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index 7c7147600..eab83ef2e 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -931,7 +931,7 @@ - + @@ -1094,6 +1094,7 @@ + @@ -1337,4 +1338,4 @@ - \ No newline at end of file + From 7186a304c6d95e1bed3d6ea6c2506aeeb573321e Mon Sep 17 00:00:00 2001 From: Adrian Siekierka Date: Sat, 9 May 2026 18:18:27 +0200 Subject: [PATCH 06/11] WS: Add support for 2003 mapper ports 0xCE and 0xD0-0xD5 --- Core/WS/Carts/WsCart.cpp | 40 +++++++++++++++---- Core/WS/WsTypes.h | 3 +- .../RegisterViewer/WsRegisterViewer.cs | 10 +++-- UI/Interop/ConsoleState/WsState.cs | 3 +- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/Core/WS/Carts/WsCart.cpp b/Core/WS/Carts/WsCart.cpp index 6895417a7..545666f93 100644 --- a/Core/WS/Carts/WsCart.cpp +++ b/Core/WS/Carts/WsCart.cpp @@ -3,6 +3,7 @@ #include "WS/Carts/WsRtc.h" #include "WS/WsMemoryManager.h" #include "WS/WsEeprom.h" +#include "Utilities/BitUtilities.h" #include "Utilities/Serializer.h" void WsCart::Map(uint32_t start, uint32_t end, MemoryType type, uint32_t offset, bool readonly) @@ -17,10 +18,10 @@ void WsCart::Unmap(uint32_t start, uint32_t end) WsCart::WsCart() { - _state.SelectedBanks[0] = 0xFF; - _state.SelectedBanks[1] = 0xFF; - _state.SelectedBanks[2] = 0xFF; - _state.SelectedBanks[3] = 0xFF; + _state.SelectedBanks[0] = 0x3F; + _state.SelectedBanks[1] = 0x3FF; + _state.SelectedBanks[2] = 0x3FF; + _state.SelectedBanks[3] = 0x3FF; } void WsCart::Init(WsMemoryManager* memoryManager, WsEeprom* cartEeprom, WsRtc* cartRtc) @@ -33,7 +34,11 @@ void WsCart::Init(WsMemoryManager* memoryManager, WsEeprom* cartEeprom, WsRtc* c void WsCart::RefreshMappings() { - Map(0x10000, 0x1FFFF, MemoryType::WsCartRam, _state.SelectedBanks[1] * 0x10000, false); + if(_state.RomInRamBank) { + Map(0x10000, 0x1FFFF, MemoryType::WsPrgRom, _state.SelectedBanks[1] * 0x10000, true); + } else { + Map(0x10000, 0x1FFFF, MemoryType::WsCartRam, _state.SelectedBanks[1] * 0x10000, false); + } Map(0x20000, 0x2FFFF, MemoryType::WsPrgRom, _state.SelectedBanks[2] * 0x10000, true); Map(0x30000, 0x3FFFF, MemoryType::WsPrgRom, _state.SelectedBanks[3] * 0x10000, true); Map(0x40000, 0xFFFFF, MemoryType::WsPrgRom, _state.SelectedBanks[0] * 0x100000 + 0x40000, true); @@ -41,12 +46,18 @@ void WsCart::RefreshMappings() uint8_t WsCart::ReadPort(uint16_t port) { - if(port < 0xC4) { + if(port == 0xC0 || port == 0xCF) { + return _state.SelectedBanks[0]; + } else if(port < 0xC4) { return _state.SelectedBanks[port - 0xC0]; } else if(port >= 0xC4 && port < 0xC9 && _cartEeprom) { return _cartEeprom->ReadPort(port - 0xC4); } else if(port >= 0xCA && port < 0xCC && _cartRtc) { return _cartRtc->ReadPort(port); + } else if(port == 0xCE) { + return _state.RomInRamBank ? 0x01 : 0; + } else if(port >= 0xD0 && port < 0xD6) { + return _state.SelectedBanks[1 + ((port - 0xD0) >> 1)] >> ((port & 1) ? 8 : 0); } return _memoryManager->GetUnmappedPort(); @@ -54,13 +65,26 @@ uint8_t WsCart::ReadPort(uint16_t port) void WsCart::WritePort(uint16_t port, uint8_t value) { - if(port < 0xC4) { - _state.SelectedBanks[port - 0xC0] = value; + if(port == 0xC0 || port == 0xCF) { + _state.SelectedBanks[0] = value & 0x3F; + _memoryManager->RefreshMappings(); + } else if(port < 0xC4) { + BitUtilities::SetBits<0>(_state.SelectedBanks[port - 0xC0], value); _memoryManager->RefreshMappings(); } else if(port >= 0xC4 && port < 0xC9 && _cartEeprom) { _cartEeprom->WritePort(port - 0xC4, value); } else if(port >= 0xCA && port < 0xCC && _cartRtc) { _cartRtc->WritePort(port, value); + } else if(port == 0xCE) { + _state.RomInRamBank = (value & 0x01) != 0; + _memoryManager->RefreshMappings(); + } else if(port >= 0xD0 && port < 0xD6) { + if(port & 1) { + BitUtilities::SetBits<8>(_state.SelectedBanks[1 + ((port - 0xD0) >> 1)], value & 0x03); + } else { + BitUtilities::SetBits<0>(_state.SelectedBanks[1 + ((port - 0xD0) >> 1)], value); + } + _memoryManager->RefreshMappings(); } } diff --git a/Core/WS/WsTypes.h b/Core/WS/WsTypes.h index 367d22327..dc9b28d27 100644 --- a/Core/WS/WsTypes.h +++ b/Core/WS/WsTypes.h @@ -446,7 +446,8 @@ struct WsEepromState struct WsCartState { bool HasRtc; - uint8_t SelectedBanks[4]; + bool RomInRamBank; + uint16_t SelectedBanks[4]; }; struct WsRtcState diff --git a/UI/Debugger/RegisterViewer/WsRegisterViewer.cs b/UI/Debugger/RegisterViewer/WsRegisterViewer.cs index 2e209c904..c28b97d0a 100644 --- a/UI/Debugger/RegisterViewer/WsRegisterViewer.cs +++ b/UI/Debugger/RegisterViewer/WsRegisterViewer.cs @@ -366,9 +366,13 @@ private static RegisterViewerTab GetCartTab(ref WsState ws) entries.AddRange(new List() { new RegEntry("$C0", "ROM Linear Bank", cart.SelectedBanks[0], Format.X8), - new RegEntry("$C1", "RAM Bank", cart.SelectedBanks[1], Format.X8), - new RegEntry("$C2", "ROM0 Bank", cart.SelectedBanks[2], Format.X8), - new RegEntry("$C3", "ROM1 Bank", cart.SelectedBanks[3], Format.X8), + new RegEntry("$C1", "RAM Bank", cart.SelectedBanks[1] & 0xFF, Format.X8), + new RegEntry("$C2", "ROM0 Bank", cart.SelectedBanks[2] & 0xFF, Format.X8), + new RegEntry("$C3", "ROM1 Bank", cart.SelectedBanks[3] & 0xFF, Format.X8), + new RegEntry("$CE.0", "ROM in RAM Bank", cart.RomInRamBank), + new RegEntry("$D0", "Expanded RAM Bank", cart.SelectedBanks[1], Format.X16), + new RegEntry("$D2", "Expanded ROM0 Bank", cart.SelectedBanks[2], Format.X16), + new RegEntry("$D4", "Expanded ROM1 Bank", cart.SelectedBanks[3], Format.X16), }); if(ws.CartEeprom.Size != WsEepromSize.Size0) { diff --git a/UI/Interop/ConsoleState/WsState.cs b/UI/Interop/ConsoleState/WsState.cs index 7df108e42..ead3ec765 100644 --- a/UI/Interop/ConsoleState/WsState.cs +++ b/UI/Interop/ConsoleState/WsState.cs @@ -443,8 +443,9 @@ public struct WsEepromState public struct WsCartState { [MarshalAs(UnmanagedType.I1)] public bool HasRtc; + [MarshalAs(UnmanagedType.I1)] public bool RomInRamBank; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] - public byte[] SelectedBanks; + public UInt16[] SelectedBanks; } public struct WsRtcState From 105719ad516f0013c6dc49dda79c0ab2fa149e8d Mon Sep 17 00:00:00 2001 From: Adrian Siekierka Date: Sat, 9 May 2026 18:31:06 +0200 Subject: [PATCH 07/11] WS: Distinguish WW from non-WW cartridges. --- Core/Core.vcxproj | 1 + Core/WS/Carts/WsCart.h | 4 +--- Core/WS/Carts/WsCartFlash.cpp | 8 ++++++++ Core/WS/Carts/WsCartFlash.h | 13 +++++++++++++ Core/WS/WsConsole.cpp | 21 +++++++++++++++++++-- Core/WS/WsConsole.h | 1 + 6 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 Core/WS/Carts/WsCartFlash.cpp create mode 100644 Core/WS/Carts/WsCartFlash.h diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index eab83ef2e..64ecf08c6 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -1094,6 +1094,7 @@ + diff --git a/Core/WS/Carts/WsCart.h b/Core/WS/Carts/WsCart.h index 1779c395d..1bb58f1d0 100644 --- a/Core/WS/Carts/WsCart.h +++ b/Core/WS/Carts/WsCart.h @@ -9,9 +9,7 @@ class WsMemoryManager; class WsEeprom; class WsRtc; -//TODOWS Flash - -class WsCart final : public ISerializable +class WsCart : public ISerializable { protected: WsCartState _state = {}; diff --git a/Core/WS/Carts/WsCartFlash.cpp b/Core/WS/Carts/WsCartFlash.cpp new file mode 100644 index 000000000..4256661e6 --- /dev/null +++ b/Core/WS/Carts/WsCartFlash.cpp @@ -0,0 +1,8 @@ +#include "pch.h" +#include "WS/Carts/WsCart.h" +#include "WS/Carts/WsCartFlash.h" + +WsCartFlash::WsCartFlash() : WsCart() +{ + +} diff --git a/Core/WS/Carts/WsCartFlash.h b/Core/WS/Carts/WsCartFlash.h new file mode 100644 index 000000000..4529c9c07 --- /dev/null +++ b/Core/WS/Carts/WsCartFlash.h @@ -0,0 +1,13 @@ +#pragma once +#include "pch.h" +#include "WS/WsTypes.h" +#include "WS/Carts/WsCart.h" +#include "Utilities/ISerializable.h" +#include "Shared/MemoryType.h" + +class WsCartFlash : public WsCart +{ +public: + WsCartFlash(); + virtual ~WsCartFlash() {} +}; diff --git a/Core/WS/WsConsole.cpp b/Core/WS/WsConsole.cpp index ed2f42693..032e8babc 100644 --- a/Core/WS/WsConsole.cpp +++ b/Core/WS/WsConsole.cpp @@ -1,9 +1,11 @@ +#include "WS/Carts/WsCartFlash.h" #include "pch.h" #include "WS/WsConsole.h" #include "WS/WsCpu.h" #include "WS/WsPpu.h" #include "WS/WsTimer.h" #include "WS/Carts/WsCart.h" +#include "WS/Carts/WsCartFlash.h" #include "WS/Carts/WsRtc.h" #include "WS/WsControlManager.h" #include "WS/WsMemoryManager.h" @@ -33,6 +35,21 @@ WsConsole::~WsConsole() delete[] _cartEepromData; } +bool WsConsole::IsWWCart() +{ + //TODOWS exclude static FreyaBIOS cartridges + return _prgRomSize == 0x80000 + && _prgRom[0x70000] == 'E' // FreyaBIOS header + && _prgRom[0x70001] == 'L' + && _prgRom[0x70002] == 'I' + && _prgRom[0x70003] == 'S' + && _prgRom[0x70004] == 'A' + && _prgRom[0x7fff6] == 0x00 // Publisher ID + && _prgRom[0x7fff8] == 0x00 // Game ID + && _prgRom[0x7fffb] == 0x04 // Save format + && _prgRom[0x7fffd] == 0x01; // Mapper +} + LoadRomResult WsConsole::LoadRom(VirtualFile& romFile) { vector romData; @@ -78,7 +95,7 @@ LoadRomResult WsConsole::LoadRom(VirtualFile& romFile) MessageManager::Log(string("Color supported: ") + (hasColorSupport ? "Yes" : "No")); MessageManager::Log("Save RAM size: " + std::to_string(_saveRamSize / 1024) + " KB"); MessageManager::Log("Cart EEPROM size: " + std::to_string(_cartEepromSize) + " bytes"); - MessageManager::Log(string("Mapper: ") + (mapperType == 0 ? "Bandai 2001 / KARNAK" : (mapperType == 1 ? "Bandai 2003" : ("Unknown: " + std::to_string(mapperType))))); + MessageManager::Log(string("Mapper: ") + (IsWWCart() ? "Bandai 2003 + NOR flash" : (mapperType == 0 ? "Bandai 2001 / KARNAK" : (mapperType == 1 ? "Bandai 2003" : ("Unknown: " + std::to_string(mapperType)))))); MessageManager::Log("------------------------------"); @@ -132,7 +149,7 @@ LoadRomResult WsConsole::LoadRom(VirtualFile& romFile) _dmaController.reset(new WsDmaController()); _ppu.reset(new WsPpu(_emu, this, _memoryManager.get(), _timer.get(), _workRam)); _apu.reset(new WsApu(_emu, this, _memoryManager.get(), _dmaController.get())); - _cart.reset(new WsCart()); + _cart.reset(IsWWCart() ? new WsCartFlash() : new WsCart()); _cart->Init(_memoryManager.get(), _cartEeprom.get(), _cartRtc.get()); _memoryManager->Init(_emu, this, _cpu.get(), _ppu.get(), _controlManager.get(), _cart.get(), _timer.get(), _dmaController.get(), _internalEeprom.get(), _apu.get(), _serial.get()); diff --git a/Core/WS/WsConsole.h b/Core/WS/WsConsole.h index b48814a52..b6efa7a64 100644 --- a/Core/WS/WsConsole.h +++ b/Core/WS/WsConsole.h @@ -55,6 +55,7 @@ class WsConsole final : public IConsole WsModel _model = {}; bool _verticalMode = false; + bool IsWWCart(); void InitPostBootRomState(); public: From 45ef3f04b6afe1487adc547c1becfbfc83e5a98b Mon Sep 17 00:00:00 2001 From: Adrian Siekierka Date: Sat, 9 May 2026 20:09:08 +0200 Subject: [PATCH 08/11] WS: Route WW cartridges through ReadMemory/WriteMemory methods. --- Core/WS/Carts/WsCart.cpp | 32 +++++++++++++++++++++++++++++++- Core/WS/Carts/WsCart.h | 14 +++++++++++++- Core/WS/Carts/WsCartFlash.cpp | 31 ++++++++++++++++++++++++++++++- Core/WS/Carts/WsCartFlash.h | 5 +++++ Core/WS/WsConsole.cpp | 2 +- Core/WS/WsMemoryManager.cpp | 12 ++++++++++++ Core/WS/WsMemoryManager.h | 24 +++++++++++++++++------- 7 files changed, 109 insertions(+), 11 deletions(-) diff --git a/Core/WS/Carts/WsCart.cpp b/Core/WS/Carts/WsCart.cpp index 545666f93..57fb6b1f8 100644 --- a/Core/WS/Carts/WsCart.cpp +++ b/Core/WS/Carts/WsCart.cpp @@ -24,12 +24,32 @@ WsCart::WsCart() _state.SelectedBanks[3] = 0x3FF; } -void WsCart::Init(WsMemoryManager* memoryManager, WsEeprom* cartEeprom, WsRtc* cartRtc) +void WsCart::Init(WsMemoryManager* memoryManager, WsEeprom* cartEeprom, WsRtc* cartRtc, uint8_t* prgRom, uint32_t prgRomSize, uint8_t* saveRam, uint32_t saveRamSize) { _memoryManager = memoryManager; _cartEeprom = cartEeprom; _cartRtc = cartRtc; _state.HasRtc = cartRtc != nullptr; + _prgRom = prgRom; + _prgRomSize = prgRomSize; + _saveRam = saveRam; + _saveRamSize = saveRamSize; + + this->PostInit(); +} + +void WsCart::PostInit() +{ + +} + +uint32_t WsCart::GetPhysicalAddress(uint32_t addr) +{ + if(addr >= 0x40000) { + return (_state.SelectedBanks[0] << 20) | (addr & 0xFFFFF); + } else { + return (_state.SelectedBanks[addr >> 16] << 16) | (addr & 0xFFFF); + } } void WsCart::RefreshMappings() @@ -88,6 +108,16 @@ void WsCart::WritePort(uint16_t port, uint8_t value) } } +uint8_t WsCart::ReadMemory(uint32_t addr) +{ + assert(false); +} + +void WsCart::WriteMemory(uint32_t addr, uint8_t value) +{ + assert(false); +} + void WsCart::Serialize(Serializer& s) { SVArray(_state.SelectedBanks, 4); diff --git a/Core/WS/Carts/WsCart.h b/Core/WS/Carts/WsCart.h index 1bb58f1d0..d48b68776 100644 --- a/Core/WS/Carts/WsCart.h +++ b/Core/WS/Carts/WsCart.h @@ -18,20 +18,32 @@ class WsCart : public ISerializable WsEeprom* _cartEeprom = nullptr; WsRtc* _cartRtc = nullptr; + uint8_t* _prgRom = nullptr; + uint32_t _prgRomSize = 0; + uint8_t* _saveRam = nullptr; + uint32_t _saveRamSize = 0; + void Map(uint32_t start, uint32_t end, MemoryType type, uint32_t offset, bool readonly); void Unmap(uint32_t start, uint32_t end); + virtual void PostInit(); + + uint32_t GetPhysicalAddress(uint32_t addr); + public: WsCart(); virtual ~WsCart() {} - void Init(WsMemoryManager* memoryManager, WsEeprom* cartEeprom, WsRtc* cartRtc); + virtual void Init(WsMemoryManager* memoryManager, WsEeprom* cartEeprom, WsRtc* cartRtc, uint8_t* prgRom, uint32_t prgRomSize, uint8_t* saveRam, uint32_t saveRamSize); void RefreshMappings(); WsCartState& GetState() { return _state; } WsEeprom* GetEeprom() { return _cartEeprom; } WsRtc* GetRtc() { return _cartRtc; } + virtual uint8_t ReadMemory(uint32_t addr); + virtual void WriteMemory(uint32_t addr, uint8_t value); + virtual uint8_t ReadPort(uint16_t port); virtual void WritePort(uint16_t port, uint8_t value); diff --git a/Core/WS/Carts/WsCartFlash.cpp b/Core/WS/Carts/WsCartFlash.cpp index 4256661e6..80336aab9 100644 --- a/Core/WS/Carts/WsCartFlash.cpp +++ b/Core/WS/Carts/WsCartFlash.cpp @@ -1,8 +1,37 @@ #include "pch.h" -#include "WS/Carts/WsCart.h" +#include "WS/WsMemoryManager.h" #include "WS/Carts/WsCartFlash.h" WsCartFlash::WsCartFlash() : WsCart() { } + +void WsCartFlash::PostInit() +{ + assert(_prgRomSize == 0x80000); + _memoryManager->SetCartFlash(true); +} + +uint8_t WsCartFlash::ReadMemory(uint32_t addr) +{ + bool sram = ((addr >> 16) == 1) && !_state.RomInRamBank; + uint32_t physAddr = GetPhysicalAddress(addr); + + if(sram) { + return _saveRam[physAddr & (_saveRamSize - 1)]; + } + + return _prgRom[physAddr & (_prgRomSize - 1)]; +} + +void WsCartFlash::WriteMemory(uint32_t addr, uint8_t value) +{ + bool sram = ((addr >> 16) == 1) && !_state.RomInRamBank; + uint32_t physAddr = GetPhysicalAddress(addr); + + if(sram) { + _saveRam[physAddr & (_saveRamSize - 1)] = value; + return; + } +} diff --git a/Core/WS/Carts/WsCartFlash.h b/Core/WS/Carts/WsCartFlash.h index 4529c9c07..2d77d5809 100644 --- a/Core/WS/Carts/WsCartFlash.h +++ b/Core/WS/Carts/WsCartFlash.h @@ -10,4 +10,9 @@ class WsCartFlash : public WsCart public: WsCartFlash(); virtual ~WsCartFlash() {} + + void PostInit() override; + + uint8_t ReadMemory(uint32_t addr) override; + void WriteMemory(uint32_t addr, uint8_t value) override; }; diff --git a/Core/WS/WsConsole.cpp b/Core/WS/WsConsole.cpp index 032e8babc..7f2745b13 100644 --- a/Core/WS/WsConsole.cpp +++ b/Core/WS/WsConsole.cpp @@ -151,7 +151,7 @@ LoadRomResult WsConsole::LoadRom(VirtualFile& romFile) _apu.reset(new WsApu(_emu, this, _memoryManager.get(), _dmaController.get())); _cart.reset(IsWWCart() ? new WsCartFlash() : new WsCart()); - _cart->Init(_memoryManager.get(), _cartEeprom.get(), _cartRtc.get()); + _cart->Init(_memoryManager.get(), _cartEeprom.get(), _cartRtc.get(), _prgRom, _prgRomSize, _saveRam, _saveRamSize); _memoryManager->Init(_emu, this, _cpu.get(), _ppu.get(), _controlManager.get(), _cart.get(), _timer.get(), _dmaController.get(), _internalEeprom.get(), _apu.get(), _serial.get()); _timer->Init(_memoryManager.get()); _dmaController->Init(_memoryManager.get(), _apu.get()); diff --git a/Core/WS/WsMemoryManager.cpp b/Core/WS/WsMemoryManager.cpp index db1a38643..b32fdd3c6 100644 --- a/Core/WS/WsMemoryManager.cpp +++ b/Core/WS/WsMemoryManager.cpp @@ -54,6 +54,11 @@ void WsMemoryManager::RefreshMappings() } } +void WsMemoryManager::SetCartFlash(bool cartFlash) +{ + _cartFlash = cartFlash; +} + void WsMemoryManager::Map(uint32_t start, uint32_t end, MemoryType type, uint32_t offset, bool readonly) { uint8_t* src = (uint8_t*)_emu->GetMemory(type).Memory; @@ -93,6 +98,9 @@ void WsMemoryManager::Unmap(uint32_t start, uint32_t end) uint8_t WsMemoryManager::DebugRead(uint32_t addr) { + if(_cartFlash && addr >= 0x10000) { + return _cart->ReadMemory(addr); + } uint8_t* handler = _reads[addr >> 12]; if(handler) { return handler[addr & 0xFFF]; @@ -102,6 +110,10 @@ uint8_t WsMemoryManager::DebugRead(uint32_t addr) void WsMemoryManager::DebugWrite(uint32_t addr, uint8_t value) { + if(_cartFlash && addr >= 0x10000) { + _cart->WriteMemory(addr, value); + return; + } uint8_t* handler = _writes[addr >> 12]; if(handler) { handler[addr & 0xFFF] = value; diff --git a/Core/WS/WsMemoryManager.h b/Core/WS/WsMemoryManager.h index 2ca926254..c41fe5959 100644 --- a/Core/WS/WsMemoryManager.h +++ b/Core/WS/WsMemoryManager.h @@ -2,6 +2,7 @@ #include "pch.h" #include "WS/WsCpu.h" #include "WS/APU/WsApu.h" +#include "WS/Carts/WsCart.h" #include "WS/WsPpu.h" #include "WS/WsTypes.h" #include "Utilities/ISerializable.h" @@ -9,7 +10,6 @@ class WsConsole; class WsTimer; class WsControlManager; -class WsCart; class WsSerial; class WsDmaController; class WsEeprom; @@ -40,6 +40,7 @@ class WsMemoryManager final : public ISerializable WsMemoryManagerState _state = {}; + bool _cartFlash; uint8_t* _reads[256] = {}; uint8_t* _writes[256] = {}; @@ -57,6 +58,7 @@ class WsMemoryManager final : public ISerializable uint8_t GetUnmappedPort(); + void SetCartFlash(bool cartFlash); void Map(uint32_t start, uint32_t end, MemoryType type, uint32_t offset, bool readonly); void Unmap(uint32_t start, uint32_t end); @@ -69,10 +71,14 @@ class WsMemoryManager final : public ISerializable __forceinline uint8_t InternalRead(uint32_t addr) { - uint8_t* handler = _reads[addr >> 12]; uint8_t value = 0x90; - if(handler) { - value = handler[addr & 0xFFF]; + if(_cartFlash && addr >= 0x10000) { + value = _cart->ReadMemory(addr); + } else { + uint8_t* handler = _reads[addr >> 12]; + if(handler) { + value = handler[addr & 0xFFF]; + } } //TODOWS open bus @@ -82,9 +88,13 @@ class WsMemoryManager final : public ISerializable __forceinline void InternalWrite(uint32_t addr, uint8_t value) { //TODOWS open bus - uint8_t* handler = _writes[addr >> 12]; - if(handler) { - handler[addr & 0xFFF] = value; + if(_cartFlash && addr >= 0x10000) { + _cart->WriteMemory(addr, value); + } else { + uint8_t* handler = _writes[addr >> 12]; + if(handler) { + handler[addr & 0xFFF] = value; + } } } From 6b34c4c6c4f911ad6138ff2f463918bdf7c517ee Mon Sep 17 00:00:00 2001 From: Adrian Siekierka Date: Sat, 9 May 2026 20:38:23 +0200 Subject: [PATCH 09/11] WS: Initial WW flash chip emulation. --- Core/WS/Carts/WsCart.cpp | 1 - Core/WS/Carts/WsCartFlash.cpp | 130 ++++++++++++++++++++++++++++++---- Core/WS/Carts/WsCartFlash.h | 18 ++++- Core/WS/WsConsole.cpp | 13 ++-- 4 files changed, 136 insertions(+), 26 deletions(-) diff --git a/Core/WS/Carts/WsCart.cpp b/Core/WS/Carts/WsCart.cpp index 57fb6b1f8..555c465b9 100644 --- a/Core/WS/Carts/WsCart.cpp +++ b/Core/WS/Carts/WsCart.cpp @@ -40,7 +40,6 @@ void WsCart::Init(WsMemoryManager* memoryManager, WsEeprom* cartEeprom, WsRtc* c void WsCart::PostInit() { - } uint32_t WsCart::GetPhysicalAddress(uint32_t addr) diff --git a/Core/WS/Carts/WsCartFlash.cpp b/Core/WS/Carts/WsCartFlash.cpp index 80336aab9..f31038931 100644 --- a/Core/WS/Carts/WsCartFlash.cpp +++ b/Core/WS/Carts/WsCartFlash.cpp @@ -2,36 +2,136 @@ #include "WS/WsMemoryManager.h" #include "WS/Carts/WsCartFlash.h" +//TODOWS sector protection +//TODOWS timing, more accurate status reads during programming +//TODOWS more verification with datasheet and hardware + WsCartFlash::WsCartFlash() : WsCart() { - } void WsCartFlash::PostInit() { - assert(_prgRomSize == 0x80000); - _memoryManager->SetCartFlash(true); + assert(_prgRomSize == 0x80000); + _memoryManager->SetCartFlash(true); } uint8_t WsCartFlash::ReadMemory(uint32_t addr) { - bool sram = ((addr >> 16) == 1) && !_state.RomInRamBank; - uint32_t physAddr = GetPhysicalAddress(addr); + bool sram = ((addr >> 16) == 1) && !_state.RomInRamBank; + uint32_t physAddr = GetPhysicalAddress(addr); + + if(sram) { + return _saveRam[physAddr & (_saveRamSize - 1)]; + } - if(sram) { - return _saveRam[physAddr & (_saveRamSize - 1)]; - } + physAddr &= _prgRomSize - 1; - return _prgRom[physAddr & (_prgRomSize - 1)]; + uint8_t sector = physAddr >= 0x60000 ? 1 : 0; + uint8_t value = _prgRom[physAddr]; + if(_mode[sector] == Mode::Fast) { + return (value & 0x80) | 0x04; + } + if(_mode[sector] == Mode::Autoselect) { + //TODOWS support word bus reads correctly + if((physAddr & 0x87) == 0x00) { + return 0x04; + } + if((physAddr & 0x87) == 0x02) { + return 0x0C; + } + if((physAddr & 0x87) == 0x04) { + return 0x00; + } + //TODOWS what is read at other addresses? + } + return value; } void WsCartFlash::WriteMemory(uint32_t addr, uint8_t value) { - bool sram = ((addr >> 16) == 1) && !_state.RomInRamBank; - uint32_t physAddr = GetPhysicalAddress(addr); + bool sram = ((addr >> 16) == 1) && !_state.RomInRamBank; + uint32_t physAddr = GetPhysicalAddress(addr); + + if(sram) { + _saveRam[physAddr & (_saveRamSize - 1)] = value; + return; + } + + physAddr &= _prgRomSize - 1; + + uint8_t sector = physAddr >= 0x60000 ? 1 : 0; + if(_mode[sector] == Mode::Program) { + _prgRom[physAddr] &= value; + _mode[sector] = Mode::Read; + } else if(_mode[sector] == Mode::FastProgram) { + _prgRom[physAddr] &= value; + _mode[sector] = Mode::Fast; + } else if(_mode[sector] == Mode::Fast) { + if(value == 0xA0) { + _mode[sector] = Mode::FastProgram; + } else if(value == 0x90) { + _mode[sector] = Mode::Read; + } + } else { + //TODOWS how does this operate in erase mode? + if(value == 0xF0) { + _mode[sector] = Mode::Read; + } + + if(_unlock == 0 && value == 0xAA && (physAddr & 0xFFF) == 0xAAA) { + _unlock = 1; + } else if(_unlock == 1) { + _unlock = (value == 0x55 && (physAddr & 0xFFF) == 0x555) ? 2 : 0; + } else if(_unlock == 2) { + if(_mode[sector] == Mode::Erase) { + if(value == 0x10) { + memset(_prgRom, 0xFF, _prgRomSize); + } else if(value == 0x30) { + uint32_t offset, size; + if(physAddr < 0x60000) { + offset = physAddr & 0x70000; + size = 0x10000; + } else if(physAddr < 0x64000) { + offset = 0x60000; + size = 0x4000; + } else if(physAddr < 0x6c000) { + offset = 0x64000; + size = 0x8000; + } else if(physAddr < 0x74000) { + offset = physAddr & 0x7e000; + size = 0x2000; + } else if(physAddr < 0x7c000) { + offset = 0x74000; + size = 0x8000; + } else { + offset = 0x7c000; + size = 0x4000; + } + memset(_prgRom + offset, 0xFF, size); + } + _mode[sector] = Mode::Read; + } else { + switch(value) { + case 0x90: _mode[sector] = Mode::Autoselect; break; + case 0xA0: _mode[sector] = Mode::Program; break; + case 0x80: _mode[sector] = Mode::Erase; break; + case 0x20: _mode[sector] = Mode::Fast; break; + } + } + + _unlock = 0; + if(_mode[sector] != Mode::Read) { + _mode[sector ^ 1] = Mode::Read; + } + } + } +} + +void WsCartFlash::Serialize(Serializer& s) +{ + WsCart::Serialize(s); - if(sram) { - _saveRam[physAddr & (_saveRamSize - 1)] = value; - return; - } + SV(_unlock); + SVArray(_mode, 2); } diff --git a/Core/WS/Carts/WsCartFlash.h b/Core/WS/Carts/WsCartFlash.h index 2d77d5809..91b2158c9 100644 --- a/Core/WS/Carts/WsCartFlash.h +++ b/Core/WS/Carts/WsCartFlash.h @@ -5,8 +5,22 @@ #include "Utilities/ISerializable.h" #include "Shared/MemoryType.h" -class WsCartFlash : public WsCart +class WsCartFlash final : public WsCart { +private: + enum class Mode : uint8_t + { + Read = 0, + Autoselect, + Fast, + Program, + FastProgram, + Erase + }; + + Mode _mode[2]; + uint8_t _unlock; + public: WsCartFlash(); virtual ~WsCartFlash() {} @@ -15,4 +29,6 @@ class WsCartFlash : public WsCart uint8_t ReadMemory(uint32_t addr) override; void WriteMemory(uint32_t addr, uint8_t value) override; + + void Serialize(Serializer& s) override; }; diff --git a/Core/WS/WsConsole.cpp b/Core/WS/WsConsole.cpp index 7f2745b13..855470a2b 100644 --- a/Core/WS/WsConsole.cpp +++ b/Core/WS/WsConsole.cpp @@ -38,15 +38,10 @@ WsConsole::~WsConsole() bool WsConsole::IsWWCart() { //TODOWS exclude static FreyaBIOS cartridges - return _prgRomSize == 0x80000 - && _prgRom[0x70000] == 'E' // FreyaBIOS header - && _prgRom[0x70001] == 'L' - && _prgRom[0x70002] == 'I' - && _prgRom[0x70003] == 'S' - && _prgRom[0x70004] == 'A' - && _prgRom[0x7fff6] == 0x00 // Publisher ID - && _prgRom[0x7fff8] == 0x00 // Game ID - && _prgRom[0x7fffb] == 0x04 // Save format + return _prgRomSize == 0x80000 && _prgRom[0x70000] == 'E' // FreyaBIOS header + && _prgRom[0x70001] == 'L' && _prgRom[0x70002] == 'I' && _prgRom[0x70003] == 'S' && _prgRom[0x70004] == 'A' && _prgRom[0x7fff6] == 0x00 // Publisher ID + && _prgRom[0x7fff8] == 0x00 // Game ID + && _prgRom[0x7fffb] == 0x04 // Save format && _prgRom[0x7fffd] == 0x01; // Mapper } From d2a95d6851105065fcb6c7e68cf07b91f4cd3006 Mon Sep 17 00:00:00 2001 From: Adrian Siekierka Date: Sun, 10 May 2026 08:17:14 +0200 Subject: [PATCH 10/11] WS: Add saving to WW flash cartridge. --- Core/WS/WsConsole.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Core/WS/WsConsole.cpp b/Core/WS/WsConsole.cpp index 855470a2b..396a7ff6b 100644 --- a/Core/WS/WsConsole.cpp +++ b/Core/WS/WsConsole.cpp @@ -332,6 +332,9 @@ void WsConsole::LoadBattery() _cartRtc->LoadBattery(); } + if(IsWWCart()) { + _emu->GetBatteryManager()->LoadBattery(".flash", _prgRom, _prgRomSize); + } if(_saveRam) { _emu->GetBatteryManager()->LoadBattery(".sav", _saveRam, _saveRamSize); } @@ -347,6 +350,9 @@ void WsConsole::SaveBattery() _cartRtc->SaveBattery(); } + if(IsWWCart()) { + _emu->GetBatteryManager()->SaveBattery(".flash", _prgRom, _prgRomSize); + } if(_saveRam) { _emu->GetBatteryManager()->SaveBattery(".sav", _saveRam, _saveRamSize); } From 0328af5b2658d46e580bede9051cbaae6b6531fe Mon Sep 17 00:00:00 2001 From: Adrian Siekierka Date: Sun, 10 May 2026 08:48:10 +0200 Subject: [PATCH 11/11] WS: Optimize WW flash cartridge mapping. --- Core/WS/Carts/WsCart.cpp | 6 ------ Core/WS/Carts/WsCart.h | 4 +--- Core/WS/Carts/WsCartFlash.cpp | 10 ++++++---- Core/WS/Carts/WsCartFlash.h | 2 +- Core/WS/WsMemoryManager.cpp | 6 +++--- Core/WS/WsMemoryManager.h | 8 ++++---- Core/WS/WsTypes.h | 8 ++++++++ 7 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Core/WS/Carts/WsCart.cpp b/Core/WS/Carts/WsCart.cpp index 555c465b9..1cd5678c8 100644 --- a/Core/WS/Carts/WsCart.cpp +++ b/Core/WS/Carts/WsCart.cpp @@ -34,12 +34,6 @@ void WsCart::Init(WsMemoryManager* memoryManager, WsEeprom* cartEeprom, WsRtc* c _prgRomSize = prgRomSize; _saveRam = saveRam; _saveRamSize = saveRamSize; - - this->PostInit(); -} - -void WsCart::PostInit() -{ } uint32_t WsCart::GetPhysicalAddress(uint32_t addr) diff --git a/Core/WS/Carts/WsCart.h b/Core/WS/Carts/WsCart.h index d48b68776..9a269b01c 100644 --- a/Core/WS/Carts/WsCart.h +++ b/Core/WS/Carts/WsCart.h @@ -26,8 +26,6 @@ class WsCart : public ISerializable void Map(uint32_t start, uint32_t end, MemoryType type, uint32_t offset, bool readonly); void Unmap(uint32_t start, uint32_t end); - virtual void PostInit(); - uint32_t GetPhysicalAddress(uint32_t addr); public: @@ -35,7 +33,7 @@ class WsCart : public ISerializable virtual ~WsCart() {} virtual void Init(WsMemoryManager* memoryManager, WsEeprom* cartEeprom, WsRtc* cartRtc, uint8_t* prgRom, uint32_t prgRomSize, uint8_t* saveRam, uint32_t saveRamSize); - void RefreshMappings(); + virtual void RefreshMappings(); WsCartState& GetState() { return _state; } WsEeprom* GetEeprom() { return _cartEeprom; } diff --git a/Core/WS/Carts/WsCartFlash.cpp b/Core/WS/Carts/WsCartFlash.cpp index f31038931..364e6e8b9 100644 --- a/Core/WS/Carts/WsCartFlash.cpp +++ b/Core/WS/Carts/WsCartFlash.cpp @@ -10,10 +10,10 @@ WsCartFlash::WsCartFlash() : WsCart() { } -void WsCartFlash::PostInit() +void WsCartFlash::RefreshMappings() { - assert(_prgRomSize == 0x80000); - _memoryManager->SetCartFlash(true); + WsCart::RefreshMappings(); + _memoryManager->SetCartFlash((_mode[0] != Mode::Read || _mode[1] != Mode::Read) ? WsRegisterAccess::ReadWrite : WsRegisterAccess::Write); } uint8_t WsCartFlash::ReadMemory(uint32_t addr) @@ -121,11 +121,13 @@ void WsCartFlash::WriteMemory(uint32_t addr, uint8_t value) } _unlock = 0; - if(_mode[sector] != Mode::Read) { + if(_mode[0] != Mode::Read && _mode[1] != Mode::Read) { _mode[sector ^ 1] = Mode::Read; } } } + + _memoryManager->SetCartFlash((_mode[0] != Mode::Read || _mode[1] != Mode::Read) ? WsRegisterAccess::ReadWrite : WsRegisterAccess::Write); } void WsCartFlash::Serialize(Serializer& s) diff --git a/Core/WS/Carts/WsCartFlash.h b/Core/WS/Carts/WsCartFlash.h index 91b2158c9..4799ea2fb 100644 --- a/Core/WS/Carts/WsCartFlash.h +++ b/Core/WS/Carts/WsCartFlash.h @@ -25,7 +25,7 @@ class WsCartFlash final : public WsCart WsCartFlash(); virtual ~WsCartFlash() {} - void PostInit() override; + void RefreshMappings() override; uint8_t ReadMemory(uint32_t addr) override; void WriteMemory(uint32_t addr, uint8_t value) override; diff --git a/Core/WS/WsMemoryManager.cpp b/Core/WS/WsMemoryManager.cpp index b32fdd3c6..221e322f4 100644 --- a/Core/WS/WsMemoryManager.cpp +++ b/Core/WS/WsMemoryManager.cpp @@ -54,7 +54,7 @@ void WsMemoryManager::RefreshMappings() } } -void WsMemoryManager::SetCartFlash(bool cartFlash) +void WsMemoryManager::SetCartFlash(WsRegisterAccess cartFlash) { _cartFlash = cartFlash; } @@ -98,7 +98,7 @@ void WsMemoryManager::Unmap(uint32_t start, uint32_t end) uint8_t WsMemoryManager::DebugRead(uint32_t addr) { - if(_cartFlash && addr >= 0x10000) { + if(((int)_cartFlash & (int)WsRegisterAccess::Read) && addr >= 0x10000) { return _cart->ReadMemory(addr); } uint8_t* handler = _reads[addr >> 12]; @@ -110,7 +110,7 @@ uint8_t WsMemoryManager::DebugRead(uint32_t addr) void WsMemoryManager::DebugWrite(uint32_t addr, uint8_t value) { - if(_cartFlash && addr >= 0x10000) { + if(((int)_cartFlash & (int)WsRegisterAccess::Write) && addr >= 0x10000) { _cart->WriteMemory(addr, value); return; } diff --git a/Core/WS/WsMemoryManager.h b/Core/WS/WsMemoryManager.h index c41fe5959..86e60591b 100644 --- a/Core/WS/WsMemoryManager.h +++ b/Core/WS/WsMemoryManager.h @@ -40,7 +40,7 @@ class WsMemoryManager final : public ISerializable WsMemoryManagerState _state = {}; - bool _cartFlash; + WsRegisterAccess _cartFlash; uint8_t* _reads[256] = {}; uint8_t* _writes[256] = {}; @@ -58,7 +58,7 @@ class WsMemoryManager final : public ISerializable uint8_t GetUnmappedPort(); - void SetCartFlash(bool cartFlash); + void SetCartFlash(WsRegisterAccess cartFlash); void Map(uint32_t start, uint32_t end, MemoryType type, uint32_t offset, bool readonly); void Unmap(uint32_t start, uint32_t end); @@ -72,7 +72,7 @@ class WsMemoryManager final : public ISerializable __forceinline uint8_t InternalRead(uint32_t addr) { uint8_t value = 0x90; - if(_cartFlash && addr >= 0x10000) { + if(((int)_cartFlash & (int)WsRegisterAccess::Read) && addr >= 0x10000) { value = _cart->ReadMemory(addr); } else { uint8_t* handler = _reads[addr >> 12]; @@ -88,7 +88,7 @@ class WsMemoryManager final : public ISerializable __forceinline void InternalWrite(uint32_t addr, uint8_t value) { //TODOWS open bus - if(_cartFlash && addr >= 0x10000) { + if(((int)_cartFlash & (int)WsRegisterAccess::Write) && addr >= 0x10000) { _cart->WriteMemory(addr, value); } else { uint8_t* handler = _writes[addr >> 12]; diff --git a/Core/WS/WsTypes.h b/Core/WS/WsTypes.h index dc9b28d27..030f570ca 100644 --- a/Core/WS/WsTypes.h +++ b/Core/WS/WsTypes.h @@ -221,6 +221,14 @@ enum class WsIrqSource : uint8_t HorizontalBlankTimer = 0x80 }; +enum class WsRegisterAccess +{ + None = 0, + Read = 1, + Write = 2, + ReadWrite = 3 +}; + struct WsMemoryManagerState { uint8_t ActiveIrqs;