mirror of
https://github.com/melonDS-emu/melonDS.git
synced 2025-06-28 09:59:41 -06:00
700 lines
14 KiB
C++
700 lines
14 KiB
C++
/*
|
|
Copyright 2016-2022 melonDS team
|
|
|
|
This file is part of melonDS.
|
|
|
|
melonDS is free software: you can redistribute it and/or modify it under
|
|
the terms of the GNU General Public License as published by the Free
|
|
Software Foundation, either version 3 of the License, or (at your option)
|
|
any later version.
|
|
|
|
melonDS is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along
|
|
with melonDS. If not, see http://www.gnu.org/licenses/.
|
|
*/
|
|
|
|
// Required by MinGW to enable localtime_r in time.h
|
|
#define _POSIX_THREAD_SAFE_FUNCTIONS
|
|
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include "NDS.h"
|
|
#include "RTC.h"
|
|
#include "Platform.h"
|
|
|
|
using Platform::Log;
|
|
using Platform::LogLevel;
|
|
|
|
namespace RTC
|
|
{
|
|
|
|
/// This value represents the Nintendo DS IO register,
|
|
/// \em not the value of the system's clock.
|
|
/// The actual system time is taken directly from the host.
|
|
u16 IO;
|
|
|
|
u8 Input;
|
|
u32 InputBit;
|
|
u32 InputPos;
|
|
|
|
u8 Output[8];
|
|
u32 OutputBit;
|
|
u32 OutputPos;
|
|
|
|
u8 CurCmd;
|
|
|
|
u8 StatusReg1;
|
|
u8 StatusReg2;
|
|
u8 DateTime[7];
|
|
u8 Alarm1[3];
|
|
u8 Alarm2[3];
|
|
u8 ClockAdjust;
|
|
u8 FreeReg;
|
|
|
|
// DSi registers
|
|
u32 MinuteCount;
|
|
u8 FOUT1;
|
|
u8 FOUT2;
|
|
u8 AlarmDate1[3];
|
|
u8 AlarmDate2[3];
|
|
|
|
s32 TimerError;
|
|
u32 ClockCount;
|
|
|
|
|
|
bool Init()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void DeInit()
|
|
{
|
|
}
|
|
|
|
void Reset()
|
|
{
|
|
Input = 0;
|
|
InputBit = 0;
|
|
InputPos = 0;
|
|
|
|
memset(Output, 0, sizeof(Output));
|
|
OutputPos = 0;
|
|
|
|
CurCmd = 0;
|
|
|
|
MinuteCount = 0;
|
|
ResetRegisters();
|
|
|
|
ClockCount = 0;
|
|
ScheduleTimer(true);
|
|
}
|
|
|
|
void DoSavestate(Savestate* file)
|
|
{
|
|
file->Section("RTC.");
|
|
|
|
file->Var16(&IO);
|
|
|
|
file->Var8(&Input);
|
|
file->Var32(&InputBit);
|
|
file->Var32(&InputPos);
|
|
|
|
file->VarArray(Output, sizeof(Output));
|
|
file->Var32(&OutputBit);
|
|
file->Var32(&OutputPos);
|
|
|
|
file->Var8(&CurCmd);
|
|
|
|
file->Var8(&StatusReg1);
|
|
file->Var8(&StatusReg2);
|
|
file->VarArray(DateTime, sizeof(DateTime));
|
|
file->VarArray(Alarm1, sizeof(Alarm1));
|
|
file->VarArray(Alarm2, sizeof(Alarm2));
|
|
file->Var8(&ClockAdjust);
|
|
file->Var8(&FreeReg);
|
|
|
|
file->Var32(&MinuteCount);
|
|
file->Var8(&FOUT1);
|
|
file->Var8(&FOUT2);
|
|
file->VarArray(AlarmDate1, sizeof(AlarmDate1));
|
|
file->VarArray(AlarmDate2, sizeof(AlarmDate2));
|
|
|
|
file->Var32((u32*)&TimerError);
|
|
file->Var32(&ClockCount);
|
|
}
|
|
|
|
|
|
void ResetRegisters()
|
|
{
|
|
StatusReg1 = 0;
|
|
StatusReg2 = 0;
|
|
DateTime[0] = 0; DateTime[1] = 1; DateTime[2] = 1; DateTime[3] = 0;
|
|
DateTime[4] = 0; DateTime[5] = 0; DateTime[6] = 0;
|
|
memset(Alarm1, 0, sizeof(Alarm1));
|
|
memset(Alarm2, 0, sizeof(Alarm2));
|
|
ClockAdjust = 0;
|
|
FreeReg = 0;
|
|
|
|
FOUT1 = 0;
|
|
FOUT2 = 0;
|
|
memset(AlarmDate1, 0, sizeof(AlarmDate1));
|
|
memset(AlarmDate2, 0, sizeof(AlarmDate2));
|
|
}
|
|
|
|
|
|
u8 BCD(u8 val)
|
|
{
|
|
return (val % 10) | ((val / 10) << 4);
|
|
}
|
|
|
|
u8 BCDIncrement(u8 val)
|
|
{
|
|
val++;
|
|
if ((val & 0x0F) >= 0x0A)
|
|
val += 0x06;
|
|
if ((val & 0xF0) >= 0xA0)
|
|
val += 0x60;
|
|
return val;
|
|
}
|
|
|
|
u8 BCDSanitize(u8 val, u8 vmin, u8 vmax)
|
|
{
|
|
if (val < vmin || val > vmax)
|
|
val = vmin;
|
|
else if ((val & 0x0F) >= 0x0A)
|
|
val = vmin;
|
|
else if ((val & 0xF0) >= 0xA0)
|
|
val = vmin;
|
|
|
|
return val;
|
|
}
|
|
|
|
u8 DaysInMonth()
|
|
{
|
|
u8 numdays;
|
|
|
|
switch (DateTime[1])
|
|
{
|
|
case 0x01: // Jan
|
|
case 0x03: // Mar
|
|
case 0x05: // May
|
|
case 0x07: // Jul
|
|
case 0x08: // Aug
|
|
case 0x10: // Oct
|
|
case 0x12: // Dec
|
|
numdays = 0x31;
|
|
break;
|
|
|
|
case 0x04: // Apr
|
|
case 0x06: // Jun
|
|
case 0x09: // Sep
|
|
case 0x11: // Nov
|
|
numdays = 0x30;
|
|
break;
|
|
|
|
case 0x02: // Feb
|
|
{
|
|
numdays = 0x28;
|
|
|
|
// leap year: if year divisible by 4 and not divisible by 100 unless divisible by 400
|
|
// the limited year range (2000-2099) simplifies this
|
|
int year = DateTime[0];
|
|
year = (year & 0xF) + ((year >> 4) * 10);
|
|
if (!(year & 3))
|
|
numdays = 0x29;
|
|
}
|
|
break;
|
|
|
|
default: // ???
|
|
return 0;
|
|
}
|
|
|
|
return numdays;
|
|
}
|
|
|
|
void CountYear()
|
|
{
|
|
DateTime[0] = BCDIncrement(DateTime[0]);
|
|
}
|
|
|
|
void CountMonth()
|
|
{
|
|
DateTime[1] = BCDIncrement(DateTime[1]);
|
|
if (DateTime[1] > 0x12)
|
|
{
|
|
DateTime[1] = 1;
|
|
CountYear();
|
|
}
|
|
}
|
|
|
|
void CheckEndOfMonth()
|
|
{
|
|
if (DateTime[2] > DaysInMonth())
|
|
{
|
|
DateTime[2] = 1;
|
|
CountMonth();
|
|
}
|
|
}
|
|
|
|
void CountDay()
|
|
{
|
|
// day-of-week counter
|
|
DateTime[3]++;
|
|
if (DateTime[3] >= 7) DateTime[3] = 0;
|
|
|
|
// day counter
|
|
DateTime[2] = BCDIncrement(DateTime[2]);
|
|
CheckEndOfMonth();
|
|
}
|
|
|
|
void CountHour()
|
|
{
|
|
u8 hour = BCDIncrement(DateTime[4] & 0x3F);
|
|
u8 pm = DateTime[4] & 0x40;
|
|
|
|
if (StatusReg1 & (1<<1))
|
|
{
|
|
// 24-hour mode
|
|
|
|
if (hour >= 0x24)
|
|
{
|
|
hour = 0;
|
|
CountDay();
|
|
}
|
|
|
|
pm = (hour >= 0x12) ? 0x40 : 0;
|
|
}
|
|
else
|
|
{
|
|
// 12-hour mode
|
|
|
|
if (hour >= 0x12)
|
|
{
|
|
hour = 0;
|
|
if (pm) CountDay();
|
|
pm ^= 0x40;
|
|
}
|
|
}
|
|
|
|
DateTime[4] = hour | pm;
|
|
}
|
|
|
|
void CountMinute()
|
|
{
|
|
MinuteCount++;
|
|
DateTime[5] = BCDIncrement(DateTime[5]);
|
|
if (DateTime[5] >= 0x60)
|
|
{
|
|
DateTime[5] = 0;
|
|
CountHour();
|
|
}
|
|
}
|
|
|
|
void CountSecond()
|
|
{
|
|
DateTime[6] = BCDIncrement(DateTime[6]);
|
|
if (DateTime[6] >= 0x60)
|
|
{
|
|
DateTime[6] = 0;
|
|
CountMinute();
|
|
}
|
|
}
|
|
|
|
|
|
void ScheduleTimer(bool first)
|
|
{
|
|
if (first) TimerError = 0;
|
|
|
|
// the RTC clock runs at 32768Hz
|
|
// cycles = 33513982 / 32768
|
|
s32 sysclock = 33513982 + TimerError;
|
|
s32 delay = sysclock >> 15;
|
|
TimerError = sysclock & 0x7FFF;
|
|
|
|
NDS::ScheduleEvent(NDS::Event_RTC, !first, delay, ClockTimer, 0);
|
|
}
|
|
|
|
void ClockTimer(u32 param)
|
|
{
|
|
ClockCount++;
|
|
|
|
if (!(ClockCount & 0x7FFF))
|
|
{
|
|
// count up one second
|
|
CountSecond();
|
|
}
|
|
|
|
ScheduleTimer(false);
|
|
}
|
|
|
|
|
|
void WriteDateTime(int num, u8 val)
|
|
{
|
|
switch (num)
|
|
{
|
|
case 1: // year
|
|
DateTime[0] = BCDSanitize(val, 0x00, 0x99);
|
|
break;
|
|
|
|
case 2: // month
|
|
DateTime[1] = BCDSanitize(val & 0x1F, 0x01, 0x12);
|
|
break;
|
|
|
|
case 3: // day
|
|
DateTime[2] = BCDSanitize(val & 0x3F, 0x01, 0x31);
|
|
CheckEndOfMonth();
|
|
break;
|
|
|
|
case 4: // day of week
|
|
DateTime[3] = BCDSanitize(val & 0x07, 0x00, 0x06);
|
|
break;
|
|
|
|
case 5: // hour
|
|
{
|
|
u8 hour = val & 0x3F;
|
|
u8 pm = val & 0x40;
|
|
|
|
if (StatusReg1 & (1<<1))
|
|
{
|
|
// 24-hour mode
|
|
|
|
hour = BCDSanitize(hour, 0x00, 0x23);
|
|
pm = (hour >= 0x12) ? 0x40 : 0;
|
|
}
|
|
else
|
|
{
|
|
// 12-hour mode
|
|
|
|
hour = BCDSanitize(hour, 0x00, 0x11);
|
|
}
|
|
|
|
DateTime[4] = hour | pm;
|
|
}
|
|
break;
|
|
|
|
case 6: // minute
|
|
DateTime[5] = BCDSanitize(val & 0x7F, 0x00, 0x59);
|
|
break;
|
|
|
|
case 7: // second
|
|
DateTime[6] = BCDSanitize(val & 0x7F, 0x00, 0x59);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void CmdRead()
|
|
{
|
|
if ((CurCmd & 0x0F) == 0x06)
|
|
{
|
|
switch (CurCmd & 0x70)
|
|
{
|
|
case 0x00: Output[0] = StatusReg1; break;
|
|
case 0x40: Output[0] = StatusReg2; break;
|
|
|
|
case 0x20:
|
|
Output[0] = DateTime[0];
|
|
Output[1] = DateTime[1];
|
|
Output[2] = DateTime[2];
|
|
Output[3] = DateTime[3];
|
|
Output[4] = DateTime[4];
|
|
Output[5] = DateTime[5];
|
|
Output[6] = DateTime[6];
|
|
break;
|
|
|
|
case 0x60:
|
|
Output[0] = DateTime[4];
|
|
Output[1] = DateTime[5];
|
|
Output[2] = DateTime[6];
|
|
break;
|
|
|
|
case 0x10:
|
|
if (StatusReg2 & 0x04)
|
|
{
|
|
Output[0] = Alarm1[0];
|
|
Output[1] = Alarm1[1];
|
|
Output[2] = Alarm1[2];
|
|
}
|
|
else
|
|
Output[0] = Alarm1[2];
|
|
break;
|
|
|
|
case 0x50:
|
|
Output[0] = Alarm2[0];
|
|
Output[1] = Alarm2[1];
|
|
Output[2] = Alarm2[2];
|
|
break;
|
|
|
|
case 0x30: Output[0] = ClockAdjust; break;
|
|
case 0x70: Output[0] = FreeReg; break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
else if ((CurCmd & 0x0F) == 0x0E)
|
|
{
|
|
if (NDS::ConsoleType != 1)
|
|
{
|
|
Log(LogLevel::Debug, "RTC: unknown read command %02X\n", CurCmd);
|
|
return;
|
|
}
|
|
|
|
switch (CurCmd & 0x70)
|
|
{
|
|
case 0x00:
|
|
Output[0] = (MinuteCount >> 16) & 0xFF;
|
|
Output[1] = (MinuteCount >> 8) & 0xFF;
|
|
Output[2] = MinuteCount & 0xFF;
|
|
break;
|
|
|
|
case 0x40: Output[0] = FOUT1; break;
|
|
case 0x20: Output[0] = FOUT2; break;
|
|
|
|
case 0x10:
|
|
Output[0] = AlarmDate1[0];
|
|
Output[1] = AlarmDate1[1];
|
|
Output[2] = AlarmDate1[2];
|
|
break;
|
|
|
|
case 0x50:
|
|
Output[0] = AlarmDate2[0];
|
|
Output[1] = AlarmDate2[1];
|
|
Output[2] = AlarmDate2[2];
|
|
break;
|
|
|
|
default:
|
|
Log(LogLevel::Debug, "RTC: unknown read command %02X\n", CurCmd);
|
|
break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
Log(LogLevel::Debug, "RTC: unknown read command %02X\n", CurCmd);
|
|
}
|
|
|
|
void CmdWrite(u8 val)
|
|
{
|
|
if ((CurCmd & 0x0F) == 0x06)
|
|
{
|
|
switch (CurCmd & 0x70)
|
|
{
|
|
case 0x00:
|
|
if (InputPos == 1)
|
|
{
|
|
u8 oldval = StatusReg1;
|
|
|
|
if (val & (1<<0)) // reset
|
|
ResetRegisters();
|
|
|
|
StatusReg1 = (StatusReg1 & 0xF0) | (val & 0x0E);
|
|
|
|
if ((StatusReg1 ^ oldval) & (1<<1)) // AM/PM changed
|
|
WriteDateTime(5, DateTime[4]);
|
|
}
|
|
break;
|
|
|
|
case 0x40:
|
|
if (InputPos == 1)
|
|
{
|
|
StatusReg2 = val;
|
|
if (StatusReg2 & 0x4F) Log(LogLevel::Debug, "RTC INTERRUPT ON: %02X\n", StatusReg2);
|
|
}
|
|
break;
|
|
|
|
case 0x20:
|
|
if (InputPos <= 7)
|
|
WriteDateTime(InputPos, val);
|
|
break;
|
|
|
|
case 0x60:
|
|
if (InputPos <= 3)
|
|
WriteDateTime(InputPos+4, val);
|
|
break;
|
|
|
|
case 0x10:
|
|
if (StatusReg2 & 0x04)
|
|
{
|
|
if (InputPos <= 3)
|
|
Alarm1[InputPos-1] = val;
|
|
}
|
|
else
|
|
{
|
|
if (InputPos == 1)
|
|
Alarm1[2] = val;
|
|
}
|
|
break;
|
|
|
|
case 0x50:
|
|
if (InputPos <= 3)
|
|
Alarm2[InputPos-1] = val;
|
|
break;
|
|
|
|
case 0x30:
|
|
if (InputPos == 1)
|
|
{
|
|
ClockAdjust = val;
|
|
Log(LogLevel::Debug, "RTC: CLOCK ADJUST = %02X\n", val);
|
|
}
|
|
break;
|
|
|
|
case 0x70:
|
|
if (InputPos == 1)
|
|
FreeReg = val;
|
|
break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
else if ((CurCmd & 0x0F) == 0x0E)
|
|
{
|
|
if (NDS::ConsoleType != 1)
|
|
{
|
|
Log(LogLevel::Debug, "RTC: unknown write command %02X\n", CurCmd);
|
|
return;
|
|
}
|
|
|
|
switch (CurCmd & 0x70)
|
|
{
|
|
case 0x00:
|
|
Log(LogLevel::Debug, "RTC: trying to write read-only minute counter\n");
|
|
break;
|
|
|
|
case 0x40:
|
|
if (InputPos == 1)
|
|
FOUT1 = val;
|
|
break;
|
|
|
|
case 0x20:
|
|
if (InputPos == 1)
|
|
FOUT2 = val;
|
|
break;
|
|
|
|
case 0x10:
|
|
if (InputPos <= 3)
|
|
AlarmDate1[InputPos-1] = val;
|
|
break;
|
|
|
|
case 0x50:
|
|
if (InputPos <= 3)
|
|
AlarmDate2[InputPos-1] = val;
|
|
break;
|
|
|
|
default:
|
|
Log(LogLevel::Debug, "RTC: unknown write command %02X\n", CurCmd);
|
|
break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
Log(LogLevel::Debug, "RTC: unknown write command %02X\n", CurCmd);
|
|
}
|
|
|
|
void ByteIn(u8 val)
|
|
{
|
|
if (InputPos == 0)
|
|
{
|
|
if ((val & 0xF0) == 0x60)
|
|
{
|
|
u8 rev[16] = {0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, 0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6};
|
|
CurCmd = rev[val & 0xF];
|
|
}
|
|
else
|
|
CurCmd = val;
|
|
|
|
if (NDS::ConsoleType == 1)
|
|
{
|
|
// for DSi: handle extra commands
|
|
|
|
if (((CurCmd & 0xF0) == 0x70) && ((CurCmd & 0xFE) != 0x76))
|
|
{
|
|
u8 rev[16] = {0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE};
|
|
CurCmd = rev[CurCmd & 0xF];
|
|
}
|
|
}
|
|
|
|
if (CurCmd & 0x80)
|
|
{
|
|
CmdRead();
|
|
}
|
|
return;
|
|
}
|
|
|
|
CmdWrite(val);
|
|
}
|
|
|
|
|
|
u16 Read()
|
|
{
|
|
//printf("RTC READ %04X\n", IO);
|
|
return IO;
|
|
}
|
|
|
|
void Write(u16 val, bool byte)
|
|
{
|
|
if (byte) val |= (IO & 0xFF00);
|
|
|
|
//printf("RTC WRITE %04X\n", val);
|
|
if (val & 0x0004)
|
|
{
|
|
if (!(IO & 0x0004))
|
|
{
|
|
// start transfer
|
|
Input = 0;
|
|
InputBit = 0;
|
|
InputPos = 0;
|
|
|
|
memset(Output, 0, sizeof(Output));
|
|
OutputBit = 0;
|
|
OutputPos = 0;
|
|
}
|
|
else
|
|
{
|
|
if (!(val & 0x0002)) // clock low
|
|
{
|
|
if (val & 0x0010)
|
|
{
|
|
// write
|
|
if (val & 0x0001)
|
|
Input |= (1<<InputBit);
|
|
|
|
InputBit++;
|
|
if (InputBit >= 8)
|
|
{
|
|
InputBit = 0;
|
|
ByteIn(Input);
|
|
Input = 0;
|
|
InputPos++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// read
|
|
if (Output[OutputPos] & (1<<OutputBit))
|
|
IO |= 0x0001;
|
|
else
|
|
IO &= 0xFFFE;
|
|
|
|
OutputBit++;
|
|
if (OutputBit >= 8)
|
|
{
|
|
OutputBit = 0;
|
|
if (OutputPos < 7)
|
|
OutputPos++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (val & 0x0010)
|
|
IO = val;
|
|
else
|
|
IO = (IO & 0x0001) | (val & 0xFFFE);
|
|
}
|
|
|
|
}
|