| // Copyright 2016 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <ddk/binding.h> |
| #include <ddk/device.h> |
| #include <ddk/driver.h> |
| #include <zircon/device/rtc.h> |
| #include <hw/inout.h> |
| |
| #include <zircon/syscalls.h> |
| #include <zircon/types.h> |
| |
| #include <stdlib.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <threads.h> |
| |
| #define RTC_IO_BASE 0x70 |
| #define RTC_NUM_IO_REGISTERS 8 |
| |
| #define RTC_IDX_REG 0x70 |
| #define RTC_DATA_REG 0x71 |
| |
| #define RTC_HOUR_PM_BIT 0x80 |
| |
| // Leading 0 allows using the 1-indexed month values from rtc. |
| static const uint64_t days_in_month[] = { |
| 0, |
| 31, // January |
| 28, // February (not leap year) |
| 31, // March |
| 30, // April |
| 31, // May |
| 30, // June |
| 31, // July |
| 31, // August |
| 30, // September |
| 31, // October |
| 30, // November |
| 31, // December |
| }; |
| |
| // Start with seconds from the Unix epoch to 2016/1/1T00:00:00. |
| static const uint64_t local_epoc = 1451606400; |
| static const uint16_t local_epoc_year = 2016; |
| |
| static bool is_leap_year(uint16_t year) { |
| return ((year % 4) == 0 && (year % 100) != 0) || ((year % 400) == 0); |
| } |
| |
| // This is run on boot (after validation of the RTC) and whenever the |
| // RTC is adjusted. |
| static void set_utc_offset(rtc_t* rtc) { |
| // First add all of the prior years |
| uint64_t days_since_local_epoc = 0; |
| for (uint16_t year = local_epoc_year; year < rtc->year; year++) { |
| days_since_local_epoc += is_leap_year(year) ? 366 : 365; |
| } |
| |
| // Next add all the prior complete months this year. |
| for (size_t month = 1; month < rtc->month; month++) { |
| days_since_local_epoc += days_in_month[month]; |
| } |
| if (rtc->month > 2 && is_leap_year(rtc->year)) { |
| days_since_local_epoc++; |
| } |
| |
| // Add all the prior complete days. |
| days_since_local_epoc += rtc->day - 1; |
| |
| // Hours, minutes, and seconds are 0 indexed. |
| uint64_t hours_since_local_epoc = (days_since_local_epoc * 24) + rtc->hours; |
| uint64_t minutes_since_local_epoc = (hours_since_local_epoc * 60) + rtc->minutes; |
| uint64_t seconds_since_local_epoc = (minutes_since_local_epoc * 60) + rtc->seconds; |
| |
| uint64_t rtc_seconds = local_epoc + seconds_since_local_epoc; |
| uint64_t rtc_nanoseconds = rtc_seconds * 1000000000; |
| |
| uint64_t monotonic_nanoseconds = zx_clock_get(ZX_CLOCK_MONOTONIC); |
| int64_t offset = rtc_nanoseconds - monotonic_nanoseconds; |
| |
| zx_status_t status = zx_clock_adjust(get_root_resource(), ZX_CLOCK_UTC, offset); |
| if (status != ZX_OK) { |
| fprintf(stderr, "The RTC driver was unable to set the UTC clock!\n"); |
| } |
| } |
| |
| static mtx_t lock = MTX_INIT; |
| |
| enum intel_rtc_registers { |
| REG_SECONDS, |
| REG_SECONDS_ALARM, |
| REG_MINUTES, |
| REG_MINUTES_ALARM, |
| REG_HOURS, |
| REG_HOURS_ALARM, |
| REG_DAY_OF_WEEK, |
| REG_DAY_OF_MONTH, |
| REG_MONTH, |
| REG_YEAR, |
| REG_A, |
| REG_B, |
| REG_C, |
| REG_D, |
| }; |
| |
| enum intel_rtc_register_a { |
| REG_A_UPDATE_IN_PROGRESS_BIT = 1 << 7, |
| }; |
| |
| enum intel_rtc_register_b { |
| REG_B_DAYLIGHT_SAVINGS_ENABLE_BIT = 1 << 0, |
| REG_B_HOUR_FORMAT_BIT = 1 << 1, |
| REG_B_DATA_MODE_BIT = 1 << 2, |
| REG_B_SQUARE_WAVE_ENABLE_BIT = 1 << 3, |
| REG_B_UPDATE_ENDED_INTERRUPT_ENABLE_BIT = 1 << 4, |
| REB_B_ALARM_INTERRUPT_ENABLE_BIT = 1 << 5, |
| REG_B_PERIODIC_INTERRUPT_ENABLE_BIT = 1 << 6, |
| REG_B_UPDATE_CYCLE_INHIBIT_BIT = 1 << 7, |
| }; |
| |
| static uint8_t to_bcd(uint8_t binary) { |
| return ((binary / 10) << 4) | (binary % 10); |
| } |
| |
| static uint8_t from_bcd(uint8_t bcd) { |
| return ((bcd >> 4) * 10) + (bcd & 0xf); |
| } |
| |
| static uint8_t read_reg_raw(enum intel_rtc_registers reg) { |
| outp(RTC_IDX_REG, reg); |
| return inp(RTC_DATA_REG); |
| } |
| |
| static void write_reg_raw(enum intel_rtc_registers reg, uint8_t val) { |
| outp(RTC_IDX_REG, reg); |
| outp(RTC_DATA_REG, val); |
| } |
| |
| static uint8_t read_reg(enum intel_rtc_registers reg, bool reg_is_binary) { |
| uint8_t data = read_reg_raw(reg); |
| return reg_is_binary ? data : from_bcd(data); |
| } |
| |
| static void write_reg(enum intel_rtc_registers reg, uint8_t val, bool reg_is_binary) { |
| write_reg_raw(reg, reg_is_binary ? val : to_bcd(val)); |
| } |
| |
| // The high bit (RTC_HOUR_PM_BIT) is special for hours when not using |
| // the 24 hour time encoding. In that case, it is set for PM and unset |
| // for AM. This is true for both BCD and binary encodings of the |
| // value, so it has to be masked out first. |
| |
| static uint8_t read_reg_hour(bool reg_is_binary, bool reg_is_24_hour) { |
| uint8_t data = read_reg_raw(REG_HOURS); |
| |
| bool pm = data & RTC_HOUR_PM_BIT; |
| data &= ~RTC_HOUR_PM_BIT; |
| |
| uint8_t hour = reg_is_binary ? data : from_bcd(data); |
| |
| if (reg_is_24_hour) { |
| return hour; |
| } |
| |
| if (pm) { |
| hour += 12; |
| } |
| |
| // Adjust noon and midnight. |
| switch (hour) { |
| case 24: // 12 PM |
| return 12; |
| case 12: // 12 AM |
| return 0; |
| default: |
| return hour; |
| } |
| } |
| |
| static void write_reg_hour(uint8_t hour, bool reg_is_binary, bool reg_is_24_hour) { |
| bool pm = hour > 11; |
| |
| if (!reg_is_24_hour) { |
| if (pm) { |
| hour -= 12; |
| } |
| if (hour == 0) { |
| hour = 12; |
| } |
| } |
| |
| uint8_t data = reg_is_binary ? hour : to_bcd(hour); |
| |
| if (pm && !reg_is_24_hour) { |
| data |= RTC_HOUR_PM_BIT; |
| } |
| |
| write_reg_raw(REG_HOURS, data); |
| } |
| |
| // Retrieve the hour format and data mode bits. Note that on some |
| // platforms (including the acer) these bits can not be reliably |
| // written. So we must instead parse and provide the data in whatever |
| // format is given to us. |
| static void rtc_mode(bool* reg_is_24_hour, bool* reg_is_binary) { |
| uint8_t reg_b = read_reg_raw(REG_B); |
| *reg_is_24_hour = reg_b & REG_B_HOUR_FORMAT_BIT; |
| *reg_is_binary = reg_b & REG_B_DATA_MODE_BIT; |
| } |
| |
| static void read_time(rtc_t* rtc) { |
| mtx_lock(&lock); |
| bool reg_is_24_hour; |
| bool reg_is_binary; |
| rtc_mode(®_is_24_hour, ®_is_binary); |
| |
| rtc->seconds = read_reg(REG_SECONDS, reg_is_binary); |
| rtc->minutes = read_reg(REG_MINUTES, reg_is_binary); |
| rtc->hours = read_reg_hour(reg_is_binary, reg_is_24_hour); |
| |
| rtc->day = read_reg(REG_DAY_OF_MONTH, reg_is_binary); |
| rtc->month = read_reg(REG_MONTH, reg_is_binary); |
| rtc->year = read_reg(REG_YEAR, reg_is_binary) + 2000; |
| |
| mtx_unlock(&lock); |
| } |
| |
| static void write_time(const rtc_t* rtc) { |
| mtx_lock(&lock); |
| bool reg_is_24_hour; |
| bool reg_is_binary; |
| rtc_mode(®_is_24_hour, ®_is_binary); |
| |
| write_reg_raw(REG_B, read_reg_raw(REG_B) | REG_B_UPDATE_CYCLE_INHIBIT_BIT); |
| |
| write_reg(REG_SECONDS, rtc->seconds, reg_is_binary); |
| write_reg(REG_MINUTES, rtc->minutes, reg_is_binary); |
| write_reg_hour(rtc->hours, reg_is_binary, reg_is_24_hour); |
| |
| write_reg(REG_DAY_OF_MONTH, rtc->day, reg_is_binary); |
| write_reg(REG_MONTH, rtc->month, reg_is_binary); |
| write_reg(REG_YEAR, rtc->year - 2000, reg_is_binary); |
| |
| write_reg_raw(REG_B, read_reg_raw(REG_B) & ~REG_B_UPDATE_CYCLE_INHIBIT_BIT); |
| |
| mtx_unlock(&lock); |
| } |
| |
| static ssize_t intel_rtc_get(void* buf, size_t count) { |
| if (count < sizeof(rtc_t)) { |
| return ZX_ERR_BUFFER_TOO_SMALL; |
| } |
| |
| // Ensure we have a consistent time. |
| rtc_t rtc, prev; |
| do { |
| // Using memcpy, as we use memcmp to compare. |
| memcpy(&prev, &rtc, sizeof(rtc_t)); |
| read_time(&rtc); |
| } while (memcmp(&rtc, &prev, sizeof(rtc_t))); |
| |
| memcpy(buf, &rtc, sizeof(rtc_t)); |
| return sizeof(rtc_t); |
| } |
| |
| static bool rtc_is_invalid(const rtc_t* rtc) { |
| return rtc->seconds > 59 || |
| rtc->minutes > 59 || |
| rtc->hours > 23 || |
| rtc->day > 31 || |
| rtc->month > 12 || |
| rtc->year < 2000 || |
| rtc->year > 2099; |
| } |
| |
| static ssize_t intel_rtc_set(const void* buf, size_t count) { |
| if (count < sizeof(rtc_t)) { |
| return ZX_ERR_BUFFER_TOO_SMALL; |
| } |
| rtc_t rtc; |
| memcpy(&rtc, buf, sizeof(rtc_t)); |
| |
| // An invalid time was supplied. |
| if (rtc_is_invalid(&rtc)) { |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| |
| write_time(&rtc); |
| // TODO(kulakowski) This isn't the place for this long term. |
| set_utc_offset(&rtc); |
| return sizeof(rtc_t); |
| } |
| |
| // Validate that the RTC is set to a valid time, and to a relatively |
| // sane one. Report the validated or reset time back via rtc. |
| static void sanitize_rtc(rtc_t* rtc) { |
| // January 1, 2016 00:00:00 |
| static const rtc_t default_rtc = { |
| .day = 1, |
| .month = 1, |
| .year = 2016, |
| }; |
| |
| intel_rtc_get(rtc, sizeof(*rtc)); |
| if (rtc_is_invalid(rtc) || rtc->year < local_epoc_year) { |
| intel_rtc_set(&default_rtc, sizeof(&default_rtc)); |
| *rtc = default_rtc; |
| } |
| } |
| |
| // Implement ioctl protocol. |
| static zx_status_t intel_rtc_ioctl(void* ctx, uint32_t op, |
| const void* in_buf, size_t in_len, |
| void* out_buf, size_t out_len, size_t* out_actual) { |
| switch (op) { |
| case IOCTL_RTC_GET: { |
| ssize_t ret = intel_rtc_get(out_buf, out_len); |
| if (ret < 0) { |
| return ret; |
| } |
| *out_actual = ret; |
| return ZX_OK; |
| } |
| case IOCTL_RTC_SET: |
| return intel_rtc_set(in_buf, in_len); |
| } |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| static zx_protocol_device_t intel_rtc_device_proto __UNUSED = { |
| .version = DEVICE_OPS_VERSION, |
| .ioctl = intel_rtc_ioctl, |
| }; |
| |
| //TODO: bind against hw, not misc |
| static zx_status_t intel_rtc_bind(void* ctx, zx_device_t* parent) { |
| #if defined(__x86_64__) || defined(__i386__) |
| // TODO(teisenbe): This should be probed via the ACPI pseudo bus whenever it |
| // exists. |
| |
| zx_status_t status = zx_mmap_device_io(get_root_resource(), RTC_IO_BASE, RTC_NUM_IO_REGISTERS); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| device_add_args_t args = { |
| .version = DEVICE_ADD_ARGS_VERSION, |
| .name = "rtc", |
| .ops = &intel_rtc_device_proto, |
| }; |
| |
| zx_device_t* dev; |
| status = device_add(parent, &args, &dev); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| rtc_t rtc; |
| sanitize_rtc(&rtc); |
| set_utc_offset(&rtc); |
| |
| return ZX_OK; |
| #else |
| return ZX_ERR_NOT_SUPPORTED; |
| #endif |
| } |
| |
| static zx_driver_ops_t intel_rtc_driver_ops = { |
| .version = DRIVER_OPS_VERSION, |
| .bind = intel_rtc_bind, |
| }; |
| |
| ZIRCON_DRIVER_BEGIN(intel_rtc, intel_rtc_driver_ops, "zircon", "0.1", 6) |
| BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_ACPI), |
| BI_GOTO_IF(NE, BIND_ACPI_HID_0_3, 0x504e5030, 0), // PNP0B00\0 |
| BI_MATCH_IF(EQ, BIND_ACPI_HID_4_7, 0x42303000), |
| BI_LABEL(0), |
| BI_ABORT_IF(NE, BIND_ACPI_CID_0_3, 0x504e5030), // PNP0B00\0 |
| BI_MATCH_IF(EQ, BIND_ACPI_CID_4_7, 0x42303000), |
| ZIRCON_DRIVER_END(intel_rtc) |