1 //
2 // Copyright (c) 2010-2023 Antmicro
3 //
4 // This file is licensed under the MIT License.
5 // Full license text is available in 'licenses/MIT.txt'.
6 //
7 using Antmicro.Renode.Core;
8 using Antmicro.Renode.Core.Structure.Registers;
9 using Antmicro.Renode.Exceptions;
10 using Antmicro.Renode.Logging;
11 using Antmicro.Renode.Peripherals.Bus;
12 using Antmicro.Renode.Time;
13 using Antmicro.Renode.Utilities;
14 using System;
15 
16 namespace Antmicro.Renode.Peripherals.Timers
17 {
18     [AllowedTranslations(AllowedTranslation.ByteToDoubleWord | AllowedTranslation.WordToDoubleWord)]
19     public class AmbiqApollo4_RTC : BasicDoubleWordPeripheral, IKnownSize
20     {
AmbiqApollo4_RTC(IMachine machine)21         public AmbiqApollo4_RTC(IMachine machine) : base(machine)
22         {
23             IRQ = new GPIO();
24             machine.RealTimeClockModeChanged += _ => SetDateTimeFromMachine();
25 
26             var baseDateTime = Misc.UnixEpoch;
27             internalTimer = new RTCTimer(machine, this, baseDateTime, alarmAction: () => InterruptStatus = true);
28 
29             DefineRegisters();
30             Reset();
31         }
32 
ReadDoubleWord(long offset)33         public override uint ReadDoubleWord(long offset)
34         {
35             if(offset == (long)Registers.CountersLower || offset == (long)Registers.CountersUpper)
36             {
37                 // Cannot be done in read callback because field values are established first.
38                 UpdateCounterFields();
39             }
40 
41             return base.ReadDoubleWord(offset);
42         }
43 
Reset()44         public override void Reset()
45         {
46             interruptStatus = false;
47             lastUpdateTimerValue = ulong.MaxValue;
48             writeBusy = false;
49             valueReadWithCountersLower = 0;
50 
51             InitializeBCDValueFields();
52             IRQ.Unset();
53             internalTimer.Reset();
54 
55             base.Reset();
56 
57             SetDateTimeFromMachine();
58         }
59 
WriteDoubleWord(long offset, uint value)60         public override void WriteDoubleWord(long offset, uint value)
61         {
62             if(!counterWritesEnabled.Value &&
63                 (offset == (long)Registers.CountersLower || offset == (long)Registers.CountersUpper))
64             {
65                 this.Log(LogLevel.Warning, "The {0} register ({1}) cannot be written to; WRTC isn't set!", (Registers)offset, offset);
66                 return;
67             }
68 
69             base.WriteDoubleWord(offset, value);
70         }
71 
PrintNextAlarmDateTime()72         public string PrintNextAlarmDateTime()
73         {
74             return internalTimer.IsAlarmSet() ? internalTimer.GetNextAlarmDateTime().ToString("o") : "Alarm not set.";
75         }
76 
PrintPreciseCurrentDateTime()77         public string PrintPreciseCurrentDateTime()
78         {
79             return CurrentDateTime.ToString("o");
80         }
81 
SetDateTime(int? year = null, int? month = null, int? day = null, int? hours = null, int? minutes = null, int? seconds = null, int? secondHundredths = null)82         public void SetDateTime(int? year = null, int? month = null, int? day = null, int? hours = null, int? minutes = null, int? seconds = null, int? secondHundredths = null)
83         {
84             UpdateCounterFields();
85 
86             if(year == null)
87             {
88                 year = CalculateYear(centuryBit.Value, yearsOfCentury);
89             }
90             else
91             {
92                 // The 200 years range simply makes it possible to tell a specific year from the century bit and a two-digit year.
93                 if(year < 1970 || year > 2169)
94                 {
95                     throw new RecoverableException("Year has to be in range: 1970 .. 2169.");
96                 }
97             }
98 
99             try
100             {
101                 SetDateTimeInternal(new DateTime(
102                     year.Value,
103                     month ?? this.month,
104                     day ?? this.day,
105                     hours ?? this.hours,
106                     minutes ?? this.minutes,
107                     seconds ?? this.seconds,
108                     (secondHundredths ?? this.secondHundredths) * 10));
109             }
110             catch(ArgumentOutOfRangeException)
111             {
112                 throw new RecoverableException("Provided date or time is invalid.");
113             }
114         }
115 
SetDateTimeFromMachine()116         public void SetDateTimeFromMachine()
117         {
118             // Normally the warning is logged if the millisecond value isn't a multiple of 10 since that's an RTC
119             // precision. It doesn't make sense to log such a warning for the value taken from the machine.
120             SetDateTimeInternal(machine.RealTimeClockDateTime, hushPrecisionWarning: true);
121         }
122 
123         public DateTime CurrentDateTime => internalTimer.GetCurrentDateTime();
124 
125         public GPIO IRQ { get; }
126 
127         public long Size => 0x210;
128 
CalculateYear(bool centuryBit, int yearsOfCentury)129         static private int CalculateYear(bool centuryBit, int yearsOfCentury)
130         {
131             // The century bit set indicates "1900s/2100s" according to the documentation.
132             // To make it specific, the range supported has to be 200 years. The arbitrarily chosen range is 1970-2169
133             // because of the default Machine's RTC Mode, 'Epoch', which starts in 1970. Hence the century bit set
134             // translates to 1900 only if a two-digit year number ('yearsOfCentury') is >= 70 and to 2100 otherwise.
135             if(centuryBit)
136             {
137                 return (yearsOfCentury < 70 ? 2100 : 1900) + yearsOfCentury;
138             }
139             else
140             {
141                 return 2000 + yearsOfCentury;
142             }
143         }
144 
DefineRegisters()145         private void DefineRegisters()
146         {
147             Registers.AlarmsLower.Define(this)
148                 .WithValueField(0, 8, name: "ALM100", writeCallback: (_, newValue) => alarmSecondHundredths.BCDSet((byte)newValue), valueProviderCallback: _ => alarmSecondHundredths.BCDGet())
149                 .WithValueField(8, 7, name: "ALMSEC", writeCallback: (_, newValue) => alarmSeconds.BCDSet((byte)newValue), valueProviderCallback: _ => alarmSeconds.BCDGet())
150                 .WithReservedBits(15, 1)
151                 .WithValueField(16, 7, name: "ALMMIN", writeCallback: (_, newValue) => alarmMinutes.BCDSet((byte)newValue), valueProviderCallback: _ => alarmMinutes.BCDGet())
152                 .WithReservedBits(23, 1)
153                 .WithValueField(24, 6, name: "ALMHR", writeCallback: (_, newValue) => alarmHours.BCDSet((byte)newValue), valueProviderCallback: _ => alarmHours.BCDGet())
154                 .WithReservedBits(30, 2)
155                 .WithChangeCallback((_, __) => UpdateAlarm())
156                 ;
157 
158             Registers.AlarmsUpper.Define(this)
159                 .WithValueField(0, 6, name: "ALMDATE", writeCallback: (_, newValue) => alarmDay.BCDSet((byte)newValue), valueProviderCallback: _ => alarmDay.BCDGet())
160                 .WithReservedBits(6, 2)
161                 .WithValueField(8, 5, name: "ALMMO", writeCallback: (_, newValue) => alarmMonth.BCDSet((byte)newValue), valueProviderCallback: _ => alarmMonth.BCDGet())
162                 .WithReservedBits(13, 3)
163                 .WithValueField(16, 3, name: "ALMWKDY", writeCallback: (_, newValue) => alarmWeekday.BCDSet((byte)newValue), valueProviderCallback: _ => alarmWeekday.BCDGet())
164                 .WithReservedBits(19, 13)
165                 .WithChangeCallback((_, __) => UpdateAlarm())
166                 ;
167 
168             Registers.Control.Define(this)
169                 .WithFlag(0, out counterWritesEnabled, name: "WRTC")
170                 .WithEnumField(1, 3, out alarmRepeatInterval, name: "RPT", changeCallback: (_, __) => UpdateAlarm())
171                 .WithFlag(4, name: "RSTOP", writeCallback: (_, newValue) => { internalTimer.Enabled = !newValue; }, valueProviderCallback: _ => !internalTimer.Enabled)
172                 .WithReservedBits(5, 27)
173                 ;
174 
175             Registers.CountersLower.Define(this)
176                 .WithValueField(0, 8, name: "CTR100", writeCallback: (_, newValue) => secondHundredths.BCDSet((byte)newValue), valueProviderCallback: _ => secondHundredths.BCDGet())
177                 .WithValueField(8, 7, name: "CTRSEC", writeCallback: (_, newValue) => seconds.BCDSet((byte)newValue), valueProviderCallback: _ => seconds.BCDGet())
178                 .WithReservedBits(15, 1)
179                 .WithValueField(16, 7, name: "CTRMIN", writeCallback: (_, newValue) => minutes.BCDSet((byte)newValue), valueProviderCallback: _ => minutes.BCDGet())
180                 .WithReservedBits(23, 1)
181                 .WithValueField(24, 6, name: "CTRHR", writeCallback: (_, newValue) => hours.BCDSet((byte)newValue), valueProviderCallback: _ => hours.BCDGet())
182                 .WithReservedBits(30, 2)
183                 .WithReadCallback((_, __) => { valueReadWithCountersLower = internalTimer.Value; readError.Value = false; })
184                 .WithWriteCallback((_, __) => writeBusy = true)
185                 ;
186 
187             Registers.CountersUpper.Define(this)
188                 .WithValueField(0, 6, name: "CTRDATE", writeCallback: (_, newValue) => day.BCDSet((byte)newValue), valueProviderCallback: _ => day.BCDGet())
189                 .WithReservedBits(6, 2)
190                 .WithValueField(8, 5, name: "CTRMO", writeCallback: (_, newValue) => month.BCDSet((byte)newValue), valueProviderCallback: _ => month.BCDGet())
191                 .WithReservedBits(13, 3)
192                 .WithValueField(16, 8, name: "CTRYR", writeCallback: (_, newValue) => yearsOfCentury.BCDSet((byte)newValue), valueProviderCallback: _ => yearsOfCentury.BCDGet())
193                 .WithValueField(24, 3, name: "CTRWKDY", writeCallback: (_, newValue) => weekday.BCDSet((byte)newValue), valueProviderCallback: _ => weekday.BCDGet())
194                 .WithReservedBits(27, 1)
195                 // Documentation on Century Bit set: "Century is 1900s/2100s". In this model the century bit is set for years 1970-1999 and 2100-2169.
196                 .WithFlag(28, out centuryBit, name: "CB")
197                 .WithFlag(29, out centuryChangeEnabled, name: "CEB")
198                 .WithReservedBits(30, 1)
199                 .WithFlag(31, out readError, name: "CTERR")
200                 .WithWriteCallback((_, __) =>
201                 {
202                     readError.Value = valueReadWithCountersLower == internalTimer.Value;
203                     if(!writeBusy)
204                     {
205                         this.Log(LogLevel.Warning, "The Counters Upper register written without prior write to the Counters Lower register!", Registers.CountersLower);
206                     }
207                     writeBusy = false;
208 
209                     var year = CalculateYear(centuryBit.Value, yearsOfCentury);
210                     var newDateTime = new DateTime(year, month, day, hours, minutes, seconds, secondHundredths * 10);
211 
212                     // Check if weekday matches (Sunday is 0 for both).
213                     if(weekday != (int)newDateTime.DayOfWeek)
214                     {
215                         this.Log(LogLevel.Warning, "Weekday given doesn't match the given date! New date's day of week: {0} ({1})", newDateTime.DayOfWeek, (int)newDateTime.DayOfWeek);
216                     }
217 
218                     SetDateTimeInternal(newDateTime);
219                 })
220                 ;
221 
222             Registers.InterruptClear.Define(this)
223                 .WithFlag(0, FieldMode.Write, name: "ALM", writeCallback: (_, newValue) => { if(newValue) InterruptStatus = false; })
224                 .WithReservedBits(1, 31)
225                 ;
226 
227             Registers.InterruptEnable.Define(this)
228                 .WithFlag(0, out interruptEnable, name: "ALM", changeCallback: (_, __) => UpdateInterrupt())
229                 .WithReservedBits(1, 31)
230                 ;
231 
232             Registers.InterruptSet.Define(this)
233                 .WithFlag(0, FieldMode.Write, name: "ALM", writeCallback: (_, newValue) => { if(newValue) InterruptStatus = true; })
234                 .WithReservedBits(1, 31)
235                 ;
236 
237             Registers.InterruptStatus.Define(this)
238                 .WithFlag(0, FieldMode.Read, name: "ALM", valueProviderCallback: _ => InterruptStatus)
239                 .WithReservedBits(1, 31)
240                 ;
241 
242             Registers.Status.Define(this)
243                 .WithFlag(0, FieldMode.Read, name: "WRITEBUSY", valueProviderCallback: _ => writeBusy)
244                 .WithReservedBits(1, 31)
245                 ;
246         }
247 
InitializeBCDValueFields()248         private void InitializeBCDValueFields()
249         {
250             alarmDay = new BCDValueField(this, "days", 0x31, zeroAllowed: false);
251             alarmHours = new BCDValueField(this, "hours", 0x23);
252             alarmMinutes = new BCDValueField(this, "minutes", 0x59);
253             alarmMonth = new BCDValueField(this, "months", 0x12, zeroAllowed: false);
254             alarmSeconds = new BCDValueField(this, "seconds", 0x59);
255             alarmSecondHundredths = new BCDValueField(this, "hundredths of a second");
256             alarmWeekday = new BCDValueField(this, "weekdays", 0x6);
257             day = new BCDValueField(this, "days", 0x31, zeroAllowed: false);
258             hours = new BCDValueField(this, "hours", 0x23);
259             minutes = new BCDValueField(this, "minutes", 0x59);
260             month = new BCDValueField(this, "months", 0x12, zeroAllowed: false);
261             secondHundredths = new BCDValueField(this, "hundredths of a second");
262             seconds = new BCDValueField(this, "seconds", 0x59);
263             weekday = new BCDValueField(this, "weekdays", 0x6);
264             yearsOfCentury = new BCDValueField(this, "years of a century");
265         }
266 
SetDateTimeInternal(DateTime dateTime, bool hushPrecisionWarning = false)267         private void SetDateTimeInternal(DateTime dateTime, bool hushPrecisionWarning = false)
268         {
269             internalTimer.SetDateTime(dateTime, hushPrecisionWarning);
270 
271             // All the other registers will be updated before reading any of the Counters registers
272             // but the century bit might not get updated if the centuryChangeEnabled is false.
273             UpdateCenturyBit(dateTime.Year);
274 
275             UpdateAlarm();
276         }
277 
UpdateAlarm()278         private void UpdateAlarm()
279         {
280             internalTimer.UpdateAlarm(alarmRepeatInterval.Value, alarmMonth, alarmWeekday, alarmDay, alarmHours, alarmMinutes, alarmSeconds, alarmSecondHundredths * 10);
281         }
282 
UpdateCenturyBit(int year)283         private void UpdateCenturyBit(int year)
284         {
285             centuryBit.Value = year < 2000 || year >= 2100;
286         }
287 
UpdateCounterFields()288         private void UpdateCounterFields()
289         {
290             if(lastUpdateTimerValue == internalTimer.Value)
291             {
292                 return;
293             }
294 
295             var dateTime = internalTimer.GetCurrentDateTime();
296 
297             secondHundredths.SetFromInteger((int)Math.Round(dateTime.Millisecond / 10.0, 0));
298             seconds.SetFromInteger(dateTime.Second);
299             minutes.SetFromInteger(dateTime.Minute);
300             hours.SetFromInteger(dateTime.Hour);
301             day.SetFromInteger(dateTime.Day);
302             month.SetFromInteger(dateTime.Month);
303             yearsOfCentury.SetFromInteger(dateTime.Year % 100);
304             weekday.SetFromInteger((int)dateTime.DayOfWeek);
305             if(centuryChangeEnabled.Value)
306             {
307                 UpdateCenturyBit(dateTime.Year);
308             }
309 
310             lastUpdateTimerValue = internalTimer.Value;
311         }
312 
UpdateInterrupt()313         private void UpdateInterrupt()
314         {
315             var newIrqState = interruptEnable.Value && interruptStatus;
316             if(newIrqState != IRQ.IsSet)
317             {
318                 this.Log(LogLevel.Debug, "IRQ {0}", newIrqState ? "set" : "reset");
319                 IRQ.Set(newIrqState);
320             }
321         }
322 
323         private bool InterruptStatus
324         {
325             get => interruptStatus;
326             set
327             {
328                 interruptStatus = value;
329                 UpdateInterrupt();
330             }
331         }
332 
333         private readonly RTCTimer internalTimer;
334 
335         private bool interruptStatus;
336         private ulong lastUpdateTimerValue;
337         private bool writeBusy;
338         private ulong valueReadWithCountersLower;
339 
340         private BCDValueField alarmDay;
341         private BCDValueField alarmHours;
342         private BCDValueField alarmMinutes;
343         private BCDValueField alarmMonth;
344         private BCDValueField alarmSeconds;
345         private BCDValueField alarmSecondHundredths;
346         private BCDValueField alarmWeekday;
347         private BCDValueField day;
348         private BCDValueField hours;
349         private BCDValueField minutes;
350         private BCDValueField month;
351         private BCDValueField secondHundredths;  // 0.01s
352         private BCDValueField seconds;
353         private BCDValueField weekday;
354         private BCDValueField yearsOfCentury;
355 
356         private IEnumRegisterField<AlarmRepeatIntervals> alarmRepeatInterval;
357         private IFlagRegisterField centuryBit;
358         private IFlagRegisterField centuryChangeEnabled;
359         private IFlagRegisterField counterWritesEnabled;
360         private IFlagRegisterField interruptEnable;
361         private IFlagRegisterField readError;
362 
363         private class BCDValueField
364         {
operator int(BCDValueField field)365             static public implicit operator int(BCDValueField field)
366             {
367                 return field.GetInteger();
368             }
369 
BCDValueField(IPeripheral owner, string fieldTypeName, byte maxValueBCD = 0x99, bool zeroAllowed = true)370             public BCDValueField(IPeripheral owner, string fieldTypeName, byte maxValueBCD = 0x99, bool zeroAllowed = true)
371             {
372                 this.fieldTypeName = fieldTypeName;
373                 this.maxValueBCD = maxValueBCD;
374                 this.owner = owner;
375                 this.zeroAllowed = zeroAllowed;
376             }
377 
BCDGet()378             public byte BCDGet()
379             {
380                 return value;
381             }
382 
BCDSet(byte bcdValue)383             public void BCDSet(byte bcdValue)
384             {
385                 if(bcdValue > maxValueBCD || (!zeroAllowed && bcdValue == 0x0))
386                 {
387                     owner.Log(LogLevel.Warning, "Invalid value for {0}: {1:X}", fieldTypeName, bcdValue);
388                     return;
389                 }
390 
391                 value = bcdValue;
392             }
393 
GetInteger()394             public int GetInteger()
395             {
396                 return BCDHelper.DecodeFromBCD(value);
397             }
398 
SetFromInteger(int value)399             public void SetFromInteger(int value)
400             {
401                 BCDSet(BCDHelper.EncodeToBCD((byte)value));
402             }
403 
ToString()404             public override string ToString()
405             {
406                 // BCD value printed in hex is always equal to its decimal value.
407                 return $"{value:X}";
408             }
409 
410             private readonly string fieldTypeName;
411             private readonly byte maxValueBCD;
412             private readonly IPeripheral owner;
413             private readonly bool zeroAllowed;
414 
415             private byte value;
416         }
417 
418         private class RTCTimer : LimitTimer
419         {
RTCTimer(IMachine machine, IBusPeripheral owner, DateTime baseDateTime, Action alarmAction)420             public RTCTimer(IMachine machine, IBusPeripheral owner, DateTime baseDateTime, Action alarmAction) : base(machine.ClockSource, Frequency, owner, "RTC",
421                 limit: ulong.MaxValue, direction: Direction.Ascending, enabled: true, workMode: WorkMode.Periodic, eventEnabled: true)
422             {
423                 this.alarmAction = alarmAction;
424                 baseDateTimeTicks = baseDateTime.Ticks;
425                 this.owner = owner;
426                 systemBus = machine.GetSystemBus(owner);
427 
428                 // It can be reached only after setting up the alarm.
429                 LimitReached += AlarmHandler;
430             }
431 
GetCurrentDateTime()432             public DateTime GetCurrentDateTime()
433             {
434                 return ValueToDateTime(Value);
435             }
436 
GetNextAlarmDateTime()437             public DateTime GetNextAlarmDateTime()
438             {
439                 return ValueToDateTime(nextAlarmValue);
440             }
441 
IsAlarmSet()442             public bool IsAlarmSet()
443             {
444                 return Limit != ulong.MaxValue;
445             }
446 
Reset()447             public override void Reset()
448             {
449                 ResetAlarm();
450                 base.Reset();
451             }
452 
SetDateTime(DateTime newDateTime, bool hushPrecisionWarning = false)453             public void SetDateTime(DateTime newDateTime, bool hushPrecisionWarning = false)
454             {
455                 ResetAlarm();
456                 Value = ValueFromDateTime(newDateTime, hushPrecisionWarning);
457 
458                 // The format is the same as 'o' but with only first two millisecond digits.
459                 // Further digits, if nonzero, were ignored setting RTC's value so let's not print them.
460                 owner.Log(LogLevel.Info, "New date time set: {0:yyyy-MM-ddTHH:mm:ss.ffK}", newDateTime);
461             }
462 
UpdateAlarm(AlarmRepeatIntervals interval, int month, int weekday, int day, int hour, int minute, int second, int millisecond)463             public void UpdateAlarm(AlarmRepeatIntervals interval, int month, int weekday, int day, int hour, int minute, int second, int millisecond)
464             {
465                 if(interval == AlarmRepeatIntervals.Month || interval == AlarmRepeatIntervals.Year)
466                 {
467                     if(day == 0)
468                     {
469                         owner.Log(LogLevel.Warning, "Day cannot be zero for the {0} alarm repeat interval! Using 1st as an alarm day.", interval);
470                         day = 1;
471                     }
472                 }
473 
474                 if(interval == AlarmRepeatIntervals.Year)
475                 {
476                     if(month == 0)
477                     {
478                         owner.Log(LogLevel.Warning, "Month cannot be zero for the {0} alarm repeat interval! Using January as an alarm month.", interval);
479                         month = 1;
480                     }
481                 }
482 
483                 var currentDateTime = GetCurrentDateTime();
484                 DateTime firstAlarm, intervalDateTime;
485                 switch(interval)
486                 {
487                     case AlarmRepeatIntervals.Second:
488                         firstAlarm = new DateTime(currentDateTime.Year, currentDateTime.Month, currentDateTime.Day,
489                             currentDateTime.Hour, currentDateTime.Minute, currentDateTime.Second, millisecond);
490                         intervalDateTime = new DateTime().AddSeconds(1);
491                         break;
492                     case AlarmRepeatIntervals.Minute:
493                         firstAlarm = new DateTime(currentDateTime.Year, currentDateTime.Month, currentDateTime.Day,
494                             currentDateTime.Hour, currentDateTime.Minute, second, millisecond);
495                         intervalDateTime = new DateTime().AddMinutes(1);
496                         break;
497                     case AlarmRepeatIntervals.Hour:
498                         firstAlarm = new DateTime(currentDateTime.Year, currentDateTime.Month, currentDateTime.Day,
499                             currentDateTime.Hour, minute, second, millisecond);
500                         intervalDateTime = new DateTime().AddHours(1);
501                         break;
502                     case AlarmRepeatIntervals.Day:
503                         firstAlarm = new DateTime(currentDateTime.Year, currentDateTime.Month, currentDateTime.Day, hour,
504                             minute, second, millisecond);
505                         intervalDateTime = new DateTime().AddDays(1);
506                         break;
507                     case AlarmRepeatIntervals.Week:
508                         firstAlarm = new DateTime(currentDateTime.Year, currentDateTime.Month, currentDateTime.Day, hour,
509                             minute, second, millisecond);
510                         // This can take us "back in time" but we're always adjusting such a 'firstAlarm' nevertheless.
511                         var daysToTheNearestAlarmWeekday = weekday - (int)firstAlarm.DayOfWeek;
512                         firstAlarm = firstAlarm.AddDays(daysToTheNearestAlarmWeekday);
513                         intervalDateTime = new DateTime().AddDays(7);
514                         break;
515                     case AlarmRepeatIntervals.Month:
516                         firstAlarm = new DateTime(currentDateTime.Year, currentDateTime.Month, day, hour, minute, second,
517                             millisecond);
518                         intervalDateTime = new DateTime().AddMonths(1);
519                         break;
520                     case AlarmRepeatIntervals.Year:
521                         firstAlarm = new DateTime(currentDateTime.Year, month, day, hour, minute, second, millisecond);
522                         intervalDateTime = new DateTime().AddYears(1);
523                         break;
524                     case AlarmRepeatIntervals.Disabled:
525                         ResetAlarm();
526                         return;
527                     default:
528                         throw new ArgumentException("Something's very wrong; this should never happen.");
529                 }
530 
531                 // Before this adjustment 'firstAlarm' can be in the past.
532                 if(firstAlarm < currentDateTime)
533                 {
534                     firstAlarm = firstAlarm.AddTicks(intervalDateTime.Ticks);
535                 }
536 
537                 alarmIntervalTicks = (ulong)intervalDateTime.Ticks / TimerTickToDateTimeTicks;
538                 nextAlarmValue = ValueFromDateTime(firstAlarm);
539                 Limit = nextAlarmValue;
540                 owner.Log(LogLevel.Debug, "First alarm set to: {0:o}, alarm repeat interval: {1}", firstAlarm, interval);
541             }
542 
543             public new ulong Value
544             {
545                 get
546                 {
547                     if(systemBus.TryGetCurrentCPU(out var cpu))
548                     {
549                         // being here means we are on the CPU thread
550                         cpu.SyncTime();
551                     }
552                     else
553                     {
554                         owner.Log(LogLevel.Noisy, "Couldn't synchronize time: returned value might lack precision");
555                     }
556                     return base.Value;
557                 }
558 
559                 set => base.Value = value;
560             }
561 
562             static private readonly ulong TimerTickToDateTimeTicks = (ulong)new DateTime().AddSeconds(1.0 / Frequency).Ticks;
563 
AlarmHandler()564             private void AlarmHandler()
565             {
566                 alarmAction();
567 
568                 // Value is automatically reset when the limit is reached.
569                 Value = nextAlarmValue;
570 
571                 nextAlarmValue += alarmIntervalTicks;
572                 Limit = nextAlarmValue;
573 
574                 owner.Log(LogLevel.Debug, "Alarm occurred at: {0:o}; next alarm: {1:o}", GetCurrentDateTime(), GetNextAlarmDateTime());
575             }
576 
ResetAlarm()577             private void ResetAlarm()
578             {
579                 Limit = ulong.MaxValue;
580                 alarmIntervalTicks = 0;
581                 nextAlarmValue = 0;
582             }
583 
ValueFromDateTime(DateTime dateTime, bool hushPrecisionWarning = false)584             private ulong ValueFromDateTime(DateTime dateTime, bool hushPrecisionWarning = false)
585             {
586                 var newDateTimeTicks = (ulong)(dateTime.Ticks - baseDateTimeTicks);
587                 if(!hushPrecisionWarning && (newDateTimeTicks % TimerTickToDateTimeTicks != 0))
588                 {
589                     owner.Log(LogLevel.Warning, "Requested time for RTC is more precise than it supports (0.01s): {0:o}", dateTime);
590                 }
591                 return newDateTimeTicks / TimerTickToDateTimeTicks;
592             }
593 
ValueToDateTime(ulong value)594             private DateTime ValueToDateTime(ulong value)
595             {
596                 var dateTimeTicksPassed = value * TimerTickToDateTimeTicks;
597                 return new DateTime(baseDateTimeTicks + (long)dateTimeTicksPassed);
598             }
599 
600             private readonly Action alarmAction;
601             private readonly long baseDateTimeTicks;
602             private readonly IPeripheral owner;
603             private readonly IBusController systemBus;
604 
605             private ulong alarmIntervalTicks;
606             private ulong nextAlarmValue;
607 
608             private new const long Frequency = 100;
609         }
610 
611         private enum AlarmRepeatIntervals
612         {
613             Disabled,
614             Year,
615             Month,
616             Week,
617             Day,
618             Hour,
619             Minute,
620             // Docs: "Interrupt every second/10th/100th".
621             Second,
622         }
623 
624         private enum Registers : long
625         {
626             Control = 0x0,
627             Status = 0x4,
628             CountersLower = 0x20,
629             CountersUpper = 0x24,
630             AlarmsLower = 0x30,
631             AlarmsUpper = 0x34,
632             InterruptEnable = 0x200,
633             InterruptStatus = 0x204,
634             InterruptClear = 0x208,
635             InterruptSet = 0x20C,
636         }
637     }
638 }
639