1 module gbaid.gba.rtc; 2 3 import core.thread : Thread; 4 import core.time : Duration, hnsecs, minutes; 5 6 import std.datetime : DateTime, Clock; 7 import std.format : format; 8 9 import gbaid.util; 10 11 import gbaid.gba.gpio; 12 import gbaid.gba.interrupt; 13 14 private enum Register { 15 NONE, 16 CONTROL, 17 DATETIME, 18 TIME, 19 FORCE_RESET, 20 FORCE_IRQ 21 } 22 23 private enum Register[] REGISTER_INDICES = [ 24 Register.FORCE_RESET, Register.NONE, Register.DATETIME, Register.FORCE_IRQ, 25 Register.CONTROL, Register.NONE, Register.TIME, Register.NONE 26 ]; 27 28 private enum State { 29 WRITE_COMMAND, WRITE_PARAMETERS, READ_PARAMETERS 30 } 31 32 public enum uint RTC_SIZE = RtcData.sizeof; 33 34 public struct Rtc { 35 private InterruptHandler _interruptHandler = null; 36 private MinuteInterrupter minuteInterrupter = null; 37 private bool selected = false; 38 private bool clock = false; 39 private bool io = false; 40 private ubyte bitBuffer; 41 private uint bufferIndex; 42 private State state; 43 private Register command; 44 private uint parameterIndex; 45 private RtcData data; 46 47 @disable public this(); 48 49 public ~this() { 50 minuteInterrupter.stop(); 51 } 52 53 public this(void[] data) { 54 if (data.length == 0) { 55 // Simulate power-on for the first time 56 this.data.powerOff(); 57 } else if (data.length == RTC_SIZE) { 58 (cast(void*) &this.data)[0 .. RtcData.sizeof] = data[]; 59 } else { 60 throw new Exception("Expected 0 or 24 bytes"); 61 } 62 } 63 64 @property public void interruptHandler(InterruptHandler interruptHandler) { 65 _interruptHandler = interruptHandler; 66 minuteInterrupter = new MinuteInterrupter(interruptHandler); 67 minuteInterrupter.enabled = cast(bool) (data.controlRegister & 0b1000); 68 } 69 70 @property private InterruptHandler interruptHandler() { 71 if (_interruptHandler is null) { 72 throw new Exception("The RTC was to request an interrupt, but no handler was set"); 73 } 74 return _interruptHandler; 75 } 76 77 @property public GpioChip chip() { 78 GpioChip chip; 79 chip.readPin0 = &readClock; 80 chip.writePin0 = &writeClock; 81 chip.readPin1 = &readIo; 82 chip.writePin1 = &writeIo; 83 chip.readPin2 = &readSelect; 84 chip.writePin2 = &writeSelect; 85 chip.readPin3 = &readFloating; 86 chip.writePin3 = &writeFloating; 87 return chip; 88 } 89 90 @property public ubyte[] dataArray() { 91 return (cast(ubyte*) &data)[0 .. RtcData.sizeof]; 92 } 93 94 private bool readClock() { 95 return clock; 96 } 97 98 private void writeClock(bool value) { 99 if (!selected) { 100 return; 101 } 102 // We read or write the IO pin on the rising edge 103 if (!clock && value) { 104 final switch (state) with (State) { 105 case WRITE_COMMAND: 106 case WRITE_PARAMETERS: { 107 // Read the IO pin into the bit buffer 108 bitBuffer |= io << bufferIndex; 109 bufferIndex += 1; 110 // We received a byte, so process it 111 if (bufferIndex == 8) { 112 processInputByte(bitBuffer); 113 // Clear the buffer for the next byte 114 bitBuffer = 0; 115 bufferIndex = 0; 116 } 117 break; 118 } 119 case READ_PARAMETERS: { 120 // Get the next byte to output if the buffer is empty 121 if (bufferIndex == 0) { 122 bitBuffer = nextOutputByte(); 123 bufferIndex = 8; 124 } 125 // Write the next buffered bit to the IO pin 126 io = (bitBuffer >>> (8 - bufferIndex)) & 0b1; 127 bufferIndex -= 1; 128 break; 129 } 130 } 131 } 132 clock = value; 133 } 134 135 private void processInputByte(ubyte input) { 136 final switch (state) with (State) { 137 case WRITE_COMMAND: { 138 // The first nibble must be 6 139 if ((input & 0b1111) != 6) { 140 break; 141 } 142 // Update the state accordingly 143 state = input & 0x80 ? READ_PARAMETERS : WRITE_PARAMETERS; 144 command = REGISTER_INDICES[input >>> 4 & 0b111]; 145 // Process commands that don't have any parameters 146 switch (command) with (Register) { 147 case FORCE_RESET: 148 data.forceReset(); 149 break; 150 case FORCE_IRQ: 151 interruptHandler.requestInterrupt(InterruptSource.GAMEPAK); 152 break; 153 default: 154 // Get ready for receiving parameters for the other commands 155 parameterIndex = 0; 156 } 157 break; 158 } 159 case WRITE_PARAMETERS: { 160 switch (command) with (Register) { 161 case CONTROL: 162 // Input one byte 163 if (parameterIndex < 1) { 164 // Clear the unused bits 165 data.controlRegister = input & 0b01101010; 166 // Enable minute interrupter if required 167 minuteInterrupter.enabled = cast(bool) (data.controlRegister & 0b1000); 168 } 169 break; 170 case DATETIME: 171 // Input seven bytes 172 if (parameterIndex < 7) { 173 data.datetimeRegisters[parameterIndex] = input; 174 // Update the last set datetime after the last parameter 175 if (parameterIndex == 6) { 176 data.updateLastSetDatetime(); 177 } 178 } 179 break; 180 case TIME: 181 // Input three bytes 182 if (parameterIndex < 3) { 183 data.datetimeRegisters[parameterIndex + 4] = input; 184 // Update the last set datetime after the last parameter 185 if (parameterIndex == 2) { 186 data.updateLastSetDatetime(); 187 } 188 } 189 break; 190 default: 191 // No parameters or doesn't correspond to any register, ignore the write 192 } 193 // Increment for the next parameter 194 parameterIndex += 1; 195 break; 196 } 197 case READ_PARAMETERS: 198 throw new Exception(format("Unexpected state when processing an input byte: %s", state)); 199 } 200 } 201 202 private ubyte nextOutputByte() { 203 if (state != State.READ_PARAMETERS) { 204 throw new Exception(format("Unexpected state when processing an output byte: %s", state)); 205 } 206 ubyte output = 0xFF; 207 switch (command) with (Register) { 208 case CONTROL: 209 // Output one byte 210 if (parameterIndex < 1) { 211 output = data.controlRegister; 212 } 213 break; 214 case DATETIME: 215 // Output seven bytes 216 if (parameterIndex < 7) { 217 // Update the current datetime before the first parameter 218 if (parameterIndex == 0) { 219 data.updateDateTimeRegisters(); 220 } 221 output = data.datetimeRegisters[parameterIndex]; 222 } 223 break; 224 case TIME: 225 // Output three bytes 226 if (parameterIndex < 3) { 227 // Update the current datetime before the first parameter 228 if (parameterIndex == 0) { 229 data.updateDateTimeRegisters(); 230 } 231 output = data.datetimeRegisters[parameterIndex + 4]; 232 } 233 break; 234 default: 235 // No parameters or doesn't correspond to any register 236 } 237 // Increment for the next parameter 238 parameterIndex += 1; 239 return output; 240 } 241 242 private bool readIo() { 243 return io; 244 } 245 246 private void writeIo(bool value) { 247 io = value; 248 } 249 250 private bool readSelect() { 251 return selected; 252 } 253 254 private void writeSelect(bool value) { 255 if (!selected && value) { 256 // Clear the bit buffer and update the state when selected 257 bitBuffer = 0; 258 bufferIndex = 0; 259 state = State.WRITE_COMMAND; 260 } 261 selected = value; 262 } 263 264 private bool readFloating() { 265 return false; 266 } 267 268 private void writeFloating(bool value) { 269 } 270 } 271 272 private class MinuteInterrupter { 273 private enum Duration ONE_MINUTE = minutes(1); 274 private InterruptHandler interruptHandler; 275 private Thread thread = null; 276 private bool running = false; 277 private bool _enabled = false; 278 279 private this(InterruptHandler interruptHandler) { 280 this.interruptHandler = interruptHandler; 281 } 282 283 private void enabled(bool enabled) { 284 _enabled = enabled; 285 if (_enabled) { 286 start(); 287 } 288 } 289 290 private void start() { 291 if (running) { 292 return; 293 } 294 thread = new Thread(&run); 295 thread.isDaemon = true; 296 running = true; 297 thread.start(); 298 } 299 300 private void stop() { 301 if (!running) { 302 return; 303 } 304 running = false; 305 thread = null; 306 } 307 308 private void run() { 309 auto timer = new Timer(); 310 size_t nextMinute = 1; 311 timer.start(); 312 while (running) { 313 timer.waitUntil(ONE_MINUTE * nextMinute); 314 nextMinute += 1; 315 if (running && _enabled) { 316 interruptHandler.requestInterrupt(InterruptSource.GAMEPAK); 317 } 318 } 319 } 320 } 321 322 private struct RtcData { 323 // Register order: year, month, day, day of the week, hour, minutes and seconds 324 private enum ubyte[] DATETIME_CLEARED_VALUES = [0, 1, 1, 0, 0, 0, 0]; 325 private long lastSetDatetimeTime; 326 private ubyte controlRegister; 327 private ubyte[7] datetimeRegisters; 328 private ubyte[7] lastSetDatetimeRegisters; 329 330 static assert (lastSetDatetimeTime.offsetof == 0); 331 static assert (controlRegister.offsetof == 8); 332 static assert (datetimeRegisters.offsetof == 9); 333 static assert (lastSetDatetimeRegisters.offsetof == 16); 334 static assert (RtcData.sizeof == 24); 335 336 private void powerOff() { 337 controlRegister = 0x80; 338 datetimeRegisters = DATETIME_CLEARED_VALUES; 339 updateLastSetDatetime(); 340 } 341 342 private void forceReset() { 343 controlRegister = 0; 344 datetimeRegisters = DATETIME_CLEARED_VALUES; 345 updateLastSetDatetime(); 346 } 347 348 private void updateLastSetDatetime() { 349 // Clear the unused bits 350 datetimeRegisters[1] &= 0b00011111; 351 datetimeRegisters[2] &= 0b00111111; 352 datetimeRegisters[3] &= 0b00000111; 353 datetimeRegisters[4] &= 0b10111111; 354 datetimeRegisters[5] &= 0b01111111; 355 datetimeRegisters[6] &= 0b01111111; 356 // We'll use this data to update the datetime registers when they are read 357 lastSetDatetimeRegisters = datetimeRegisters; 358 lastSetDatetimeTime = Clock.currStdTime(); 359 } 360 361 private void updateDateTimeRegisters() { 362 // Convert the original datetime registers to a DateTime object 363 auto oldYear = lastSetDatetimeRegisters[0].bcdToDecimal() + 2000; 364 auto oldHour = (lastSetDatetimeRegisters[4] & 0x3F).bcdToDecimal(); 365 // If the RTC is in 12h mode, we have to adjust the hour when it's PM 366 if (!(controlRegister & 0x40) && (lastSetDatetimeRegisters[4] & 0x80)) { 367 oldHour += 12; 368 } 369 auto oldDateTime = DateTime( 370 oldYear, lastSetDatetimeRegisters[1].bcdToDecimal(), lastSetDatetimeRegisters[2].bcdToDecimal(), 371 oldHour, lastSetDatetimeRegisters[5].bcdToDecimal(), lastSetDatetimeRegisters[6].bcdToDecimal() 372 ); 373 // Add to it the time elapsed since it was set. This is the RTC's current datetime 374 auto dateTime = oldDateTime + hnsecs(Clock.currStdTime() - lastSetDatetimeTime); 375 // Check that the year is valid for the RTC's range 376 if (dateTime.year < 2000 || dateTime.year > 2099) { 377 throw new Exception(format("I'm sorry, but the RTC wasn't designed to work in the year %d", dateTime.year)); 378 } 379 auto year = (dateTime.year - 2000).decimalToBcd(); 380 // Calculate the day of the week (the number assignment in the RTC is decided by the user) 381 auto dayOfTheWeekOffset = cast(int) lastSetDatetimeRegisters[3] - oldDateTime.dayOfWeek; 382 auto dayOfTheWeek = (dateTime.dayOfWeek + dayOfTheWeekOffset + 7) % 7; 383 // Calculate the hour register: start with the AM/PM flag 384 auto hour = (dateTime.hour >= 12) << 7; 385 // If the RTC is in 12h mode, we have to adjust the hour 386 if (controlRegister & 0x40) { 387 hour |= dateTime.hour.decimalToBcd(); 388 } else { 389 hour |= (dateTime.hour % 12).decimalToBcd(); 390 } 391 // Update the datetime registers to the current datetime 392 datetimeRegisters = [ 393 year, dateTime.month.decimalToBcd(), dateTime.day.decimalToBcd(), cast(ubyte) dayOfTheWeek, 394 cast(ubyte) hour, dateTime.minute.decimalToBcd(), dateTime.second.decimalToBcd() 395 ]; 396 } 397 } 398 399 private ubyte decimalToBcd(int decimal) { 400 return cast(ubyte) (((decimal / 10) << 4) + decimal % 10); 401 } 402 403 private int bcdToDecimal(ubyte bcd) { 404 return ((bcd & 0xF0) >>> 4) * 10 + (bcd & 0xF); 405 }