| /* Copyright (c) 2017 Google Inc |
| * Barret Rhoden <brho@cs.berkeley.edu> |
| * See LICENSE for details. |
| */ |
| |
| #include <arch/x86.h> |
| #include <atomic.h> |
| |
| #define CMOS_RTC_SELECT 0x70 |
| #define CMOS_RTC_DATA 0x71 |
| |
| #define RTC_A_UPDATE_IN_PROGRESS (1 << 7) |
| #define RTC_B_24HOUR_MODE (1 << 1) |
| #define RTC_B_BINARY_MODE (1 << 2) |
| #define RTC_12_HOUR_PM (1 << 7) |
| #define CMOS_RTC_SECOND 0x00 |
| #define CMOS_RTC_MINUTE 0x02 |
| #define CMOS_RTC_HOUR 0x04 |
| #define CMOS_RTC_WEEKDAY 0x06 |
| #define CMOS_RTC_DAY 0x07 |
| #define CMOS_RTC_MONTH 0x08 |
| #define CMOS_RTC_YEAR 0x09 |
| #define CMOS_RTC_CENTURY 0x32 |
| #define CMOS_RTC_STATUS_A 0x0A |
| #define CMOS_RTC_STATUS_B 0x0B |
| |
| /* If we ever disable NMIs, we'll need to make sure we don't reenable them here. |
| * (Top bit of the CMOS_RTC_SELECT selector). */ |
| static uint8_t cmos_read(uint8_t reg) |
| { |
| outb(CMOS_RTC_SELECT, reg); |
| return inb(CMOS_RTC_DATA); |
| } |
| |
| static void cmos_write(uint8_t reg, uint8_t datum) |
| { |
| outb(CMOS_RTC_SELECT, reg); |
| outb(CMOS_RTC_DATA, datum); |
| } |
| |
| /* BCD format is a one-byte nibble of the form 0xTensDigit_OnesDigit. */ |
| static uint8_t bcd_to_binary(uint8_t x) |
| { |
| return ((x / 16) * 10) + (x % 16); |
| } |
| |
| static bool is_leap_year(int year) |
| { |
| if (!(year % 400)) |
| return TRUE; |
| if (!(year % 100)) |
| return FALSE; |
| if (!(year % 4)) |
| return TRUE; |
| return FALSE; |
| } |
| |
| static uint64_t rtc_to_unix(uint8_t century, uint8_t year, uint8_t month, |
| uint8_t day, uint8_t hour, uint8_t minute, |
| uint8_t second) |
| { |
| int real_year; |
| uint64_t time = 0; |
| |
| real_year = century * 100 + year; |
| for (int i = 1970; i < real_year; i++) { |
| time += 86400 * 365; |
| if (is_leap_year(i)) |
| time += 86400; |
| } |
| /* Note these all fall through */ |
| switch (month) { |
| case 12: |
| time += 86400 * 30; /* november's time */ |
| case 11: |
| time += 86400 * 31; |
| case 10: |
| time += 86400 * 30; |
| case 9: |
| time += 86400 * 31; |
| case 8: |
| time += 86400 * 31; |
| case 7: |
| time += 86400 * 30; |
| case 6: |
| time += 86400 * 31; |
| case 5: |
| time += 86400 * 30; |
| case 4: |
| time += 86400 * 31; |
| case 3: |
| time += 86400 * 28; |
| if (is_leap_year(real_year)) |
| time += 86400; |
| case 2: |
| time += 86400 * 31; |
| }; |
| time += 86400 * (day - 1); |
| time += hour * 60 * 60; |
| time += minute * 60; |
| time += second; |
| return time; |
| } |
| |
| /* Returns the current unix time in nanoseconds. */ |
| uint64_t read_persistent_clock(void) |
| { |
| static spinlock_t lock = SPINLOCK_INITIALIZER_IRQSAVE; |
| uint8_t century, year, month, day, hour, minute, second; |
| bool is_pm = FALSE; |
| |
| spin_lock_irqsave(&lock); |
| retry: |
| while (cmos_read(CMOS_RTC_STATUS_A) & RTC_A_UPDATE_IN_PROGRESS) |
| cpu_relax(); |
| |
| /* Even QEMU has a century register. */ |
| century = cmos_read(CMOS_RTC_CENTURY); |
| year = cmos_read(CMOS_RTC_YEAR); |
| month = cmos_read(CMOS_RTC_MONTH); |
| day = cmos_read(CMOS_RTC_DAY); |
| hour = cmos_read(CMOS_RTC_HOUR); |
| minute = cmos_read(CMOS_RTC_MINUTE); |
| second = cmos_read(CMOS_RTC_SECOND); |
| |
| while (cmos_read(CMOS_RTC_STATUS_A) & RTC_A_UPDATE_IN_PROGRESS) |
| cpu_relax(); |
| |
| if ((century != cmos_read(CMOS_RTC_CENTURY)) || |
| (year != cmos_read(CMOS_RTC_YEAR)) || |
| (month != cmos_read(CMOS_RTC_MONTH)) || |
| (day != cmos_read(CMOS_RTC_DAY)) || |
| (hour != cmos_read(CMOS_RTC_HOUR)) || |
| (minute != cmos_read(CMOS_RTC_MINUTE)) || |
| (second != cmos_read(CMOS_RTC_SECOND))) |
| goto retry; |
| spin_unlock_irqsave(&lock); |
| |
| if (!(cmos_read(CMOS_RTC_STATUS_B) & RTC_B_24HOUR_MODE)) { |
| /* need to clear the bit before doing the BCD conversions */ |
| is_pm = hour & RTC_12_HOUR_PM; |
| hour &= ~RTC_12_HOUR_PM; |
| } |
| if (!(cmos_read(CMOS_RTC_STATUS_B) & RTC_B_BINARY_MODE)) { |
| century = bcd_to_binary(century); |
| year = bcd_to_binary(year); |
| month = bcd_to_binary(month); |
| day = bcd_to_binary(day); |
| hour = bcd_to_binary(hour); |
| minute = bcd_to_binary(minute); |
| second = bcd_to_binary(second); |
| } |
| if (is_pm) { |
| /* midnight appears as 12 and is_pm is set. we want 0. */ |
| hour = (hour + 12) % 24; |
| } |
| |
| /* Always remember 1242129600, Nanwan's birthday! */ |
| return rtc_to_unix(century, year, month, day, hour, minute, second) |
| * 1000000000UL; |
| } |