diff --git a/CHANGES b/CHANGES index 5077b7b4c..4c4bf3547 100644 --- a/CHANGES +++ b/CHANGES @@ -87,8 +87,9 @@ Misc: - GBA: Automatically skip BIOS if ROM has invalid logo - GBA: Refine multiboot detection (fixes mgba.io/i/2192) - GBA Cheats: Implement "never" type codes (closes mgba.io/i/915) - - GBA Memory: Implement adjustable EWRAM waitstates (closes mgba.io/i/1276) - GBA DMA: Enhanced logging (closes mgba.io/i/2454) + - GBA Memory: Implement adjustable EWRAM waitstates (closes mgba.io/i/1276) + - GBA Savedata: Store RTC data in savegames (closes mgba.io/i/240) - GBA Video: Implement layer placement for OpenGL renderer (fixes mgba.io/i/1962) - GBA Video: Fix highlighting for sprites with mid-frame palette changes - mGUI: Add margin to right-aligned menu text (fixes mgba.io/i/871) diff --git a/include/mgba/internal/gba/cart/gpio.h b/include/mgba/internal/gba/cart/gpio.h index fa90aed7c..86ddc941f 100644 --- a/include/mgba/internal/gba/cart/gpio.h +++ b/include/mgba/internal/gba/cart/gpio.h @@ -42,7 +42,7 @@ enum GPIODirection { GPIO_READ_WRITE = 1 }; -DECL_BITFIELD(RTCControl, uint32_t); +DECL_BITFIELD(RTCControl, uint8_t); DECL_BIT(RTCControl, MinIRQ, 3); DECL_BIT(RTCControl, Hour24, 6); DECL_BIT(RTCControl, Poweroff, 7); @@ -69,6 +69,8 @@ struct GBARTC { RTCCommandData command; RTCControl control; uint8_t time[7]; + time_t lastLatch; + time_t offset; }; DECL_BITFIELD(GPIOPin, uint16_t); diff --git a/include/mgba/internal/gba/savedata.h b/include/mgba/internal/gba/savedata.h index 7f0b84c83..102c23041 100644 --- a/include/mgba/internal/gba/savedata.h +++ b/include/mgba/internal/gba/savedata.h @@ -72,6 +72,7 @@ struct GBASavedata { uint8_t* data; enum SavedataCommand command; struct VFile* vf; + struct GBACartridgeHardware* gpio; int mapMode; bool maskWriteback; @@ -93,6 +94,12 @@ struct GBASavedata { enum FlashStateMachine flashState; }; +struct GBASavedataRTCBuffer { + uint8_t time[7]; + uint8_t control; + uint64_t lastLatch; +}; + void GBASavedataInit(struct GBASavedata* savedata, struct VFile* vf); void GBASavedataDeinit(struct GBASavedata* savedata); @@ -116,6 +123,9 @@ void GBASavedataWriteEEPROM(struct GBASavedata* savedata, uint16_t value, uint32 void GBASavedataClean(struct GBASavedata* savedata, uint32_t frameCount); +void GBASavedataRTCRead(struct GBASavedata* savedata); +void GBASavedataRTCWrite(struct GBASavedata* savedata); + struct GBASerializedState; void GBASavedataSerialize(const struct GBASavedata* savedata, struct GBASerializedState* state); void GBASavedataDeserialize(struct GBASavedata* savedata, const struct GBASerializedState* state); diff --git a/include/mgba/internal/gba/serialize.h b/include/mgba/internal/gba/serialize.h index cb1a3b384..ec44dc8fd 100644 --- a/include/mgba/internal/gba/serialize.h +++ b/include/mgba/internal/gba/serialize.h @@ -348,7 +348,8 @@ struct GBASerializedState { int32_t rtcBits; int32_t rtcCommandActive; RTCCommandData rtcCommand; - RTCControl rtcControl; + uint8_t rtcControl; + uint8_t reserved[3]; uint8_t time[7]; uint8_t devices; uint16_t gyroSample; diff --git a/src/gba/cart/gpio.c b/src/gba/cart/gpio.c index 6f7fe15e9..0bb7f3429 100644 --- a/src/gba/cart/gpio.c +++ b/src/gba/cart/gpio.c @@ -100,6 +100,9 @@ void GBAHardwareInitRTC(struct GBACartridgeHardware* hw) { hw->rtc.command = 0; hw->rtc.control = 0x40; memset(hw->rtc.time, 0, sizeof(hw->rtc.time)); + + hw->rtc.lastLatch = 0; + hw->rtc.offset = 0; } void _readPins(struct GBACartridgeHardware* hw) { @@ -278,6 +281,9 @@ void _rtcUpdateClock(struct GBACartridgeHardware* hw) { } else { t = time(0); } + hw->rtc.lastLatch = t; + t -= hw->rtc.offset; + struct tm date; localtime_r(&t, &date); hw->rtc.time[0] = _rtcBCD(date.tm_year - 100); diff --git a/src/gba/gba.c b/src/gba/gba.c index 70da8e345..bd0353d0c 100644 --- a/src/gba/gba.c +++ b/src/gba/gba.c @@ -77,6 +77,7 @@ static void GBAInit(void* cpu, struct mCPUComponent* component) { gba->memory.savedata.timing = &gba->timing; gba->memory.savedata.vf = NULL; gba->memory.savedata.realVf = NULL; + gba->memory.savedata.gpio = &gba->memory.hw; GBASavedataInit(&gba->memory.savedata, NULL); gba->video.p = gba; diff --git a/src/gba/overrides.c b/src/gba/overrides.c index a0593a90e..e15db50d8 100644 --- a/src/gba/overrides.c +++ b/src/gba/overrides.c @@ -341,6 +341,7 @@ void GBAOverrideApply(struct GBA* gba, const struct GBACartridgeOverride* overri if (override->hardware & HW_RTC) { GBAHardwareInitRTC(&gba->memory.hw); + GBASavedataRTCRead(&gba->memory.savedata); } if (override->hardware & HW_GYRO) { diff --git a/src/gba/savedata.c b/src/gba/savedata.c index 752d3874c..47d2b37ee 100644 --- a/src/gba/savedata.c +++ b/src/gba/savedata.c @@ -575,6 +575,7 @@ void GBASavedataClean(struct GBASavedata* savedata, uint32_t frameCount) { if (savedata->mapMode & MAP_WRITE) { size_t size = GBASavedataSize(savedata); if (savedata->data && savedata->vf->sync(savedata->vf, savedata->data, size)) { + GBASavedataRTCWrite(savedata); mLOG(GBA_SAVE, INFO, "Savedata synced"); } else { mLOG(GBA_SAVE, INFO, "Savedata failed to sync!"); @@ -583,6 +584,58 @@ void GBASavedataClean(struct GBASavedata* savedata, uint32_t frameCount) { } } +void GBASavedataRTCWrite(struct GBASavedata* savedata) { + if (!(savedata->gpio->devices & HW_RTC)) { + return; + } + + struct GBASavedataRTCBuffer buffer; + + memcpy(&buffer.time, savedata->gpio->rtc.time, 7); + buffer.control = savedata->gpio->rtc.control; + STORE_64LE(savedata->gpio->rtc.lastLatch, 0, &buffer.lastLatch); + + size_t size = GBASavedataSize(savedata) & ~0xFF; + savedata->vf->seek(savedata->vf, size, SEEK_SET); + savedata->vf->write(savedata->vf, &buffer, sizeof(buffer)); +} + +static uint8_t _unBCD(uint8_t byte) { + return (byte >> 4) * 10 + (byte & 0xF); +} + +void GBASavedataRTCRead(struct GBASavedata* savedata) { + struct GBASavedataRTCBuffer buffer; + + size_t size = GBASavedataSize(savedata) & ~0xFF; + savedata->vf->seek(savedata->vf, size, SEEK_SET); + size = savedata->vf->read(savedata->vf, &buffer, sizeof(buffer)); + if (size < sizeof(buffer)) { + return; + } + + memcpy(savedata->gpio->rtc.time, &buffer.time, 7); + + // Older FlashGBX sets this to 0x01 instead of the control flag. + // Since that bit is invalid on hardware, we can check for != 0x01 + // to see if it's a valid value instead of just a filler value. + if (buffer.control != 1) { + savedata->gpio->rtc.control = buffer.control; + } + LOAD_64LE(savedata->gpio->rtc.lastLatch, 0, &buffer.lastLatch); + + struct tm date; + date.tm_year = _unBCD(savedata->gpio->rtc.time[0]) + 100; + date.tm_mon = _unBCD(savedata->gpio->rtc.time[1]) - 1; + date.tm_mday = _unBCD(savedata->gpio->rtc.time[2]); + date.tm_hour = _unBCD(savedata->gpio->rtc.time[4]); + date.tm_min = _unBCD(savedata->gpio->rtc.time[5]); + date.tm_sec = _unBCD(savedata->gpio->rtc.time[6]); + date.tm_isdst = -1; + + savedata->gpio->rtc.offset = savedata->gpio->rtc.lastLatch - mktime(&date); +} + void GBASavedataSerialize(const struct GBASavedata* savedata, struct GBASerializedState* state) { state->savedata.type = savedata->type; state->savedata.command = savedata->command;