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 }