blob: 3d9e69ebf2c7a17a5d0f2665fef8b97e3e04d90a [file] [log] [blame]
// Copyright 2021 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 "src/virtualization/bin/vmm/arch/x64/rtc_mc146818.h"
#include <errno.h>
#include <lib/syslog/cpp/macros.h>
#include <ctime>
namespace {
enum RegisterBFlags : uint8_t {
kRegisterBDaylightSavingsEnable = 1 << 0, // DSE
kRegisterB24HourFormat = 1 << 1, // 24/12
kRegisterBBinaryMode = 1 << 2, // DM
kRegisterBSquareWaveEnable = 1 << 3, // SQWE
kRegisterBUpdateInteruptEnable = 1 << 4, // UIE
kRegisterBAlarmInteruptEnable = 1 << 5, // AIE
kRegisterBPeriodicInteruptEnable = 1 << 6, // PIE
kRegisterBStopTicks = 1 << 7, // SET
};
// Alternate/extra RTC modes are unsupported by this emulated RTC, so we
// make them unwriteable
constexpr uint8_t kRegisterBUnwritableMask = kRegisterBDaylightSavingsEnable |
kRegisterB24HourFormat | kRegisterBBinaryMode |
kRegisterBSquareWaveEnable;
enum RegisterCFlags : uint8_t {
kRegisterCUpdateFlag = 1 << 4, // UF
kRegisterCAlarmFlag = 1 << 5, // AF
kRegisterCPeriodicFlag = 1 << 6, // PF
kRegisterCIRQFlag = 1 << 7, // IRQF
};
// Linux expects the RTC to be in BCD mode regardless of the binary mode flag
// on x86, so we have to convert registers back and forth
constexpr uint8_t ToBcd(uint8_t binary) {
return static_cast<uint8_t>(((binary / 10 % 10) << 4) | (binary % 10));
}
constexpr uint8_t FromBcd(uint8_t bcd) {
return ((bcd & 0xf0) >> 4) * 10 + (bcd & 0x0f);
}
} // namespace
RtcMc146818::RtcMc146818() {
registers_[Register::kA] = 0b00100000; // Tick rate: 1 second per second
registers_[Register::kB] = kRegisterB24HourFormat;
registers_[Register::kC] = 0;
registers_[Register::kSecondsAlarm] = 0;
registers_[Register::kMinutesAlarm] = 0;
registers_[Register::kHoursAlarm] = 0;
UpdateTime();
}
zx_status_t RtcMc146818::ReadRegister(Register reg, uint8_t* value) {
std::lock_guard<std::mutex> lock(mutex_);
if (registers_.count(reg) == 0) {
FX_LOGS(ERROR) << "Read from unsupported RTC register (0x" << std::hex
<< static_cast<uint32_t>(reg) << ")";
return ZX_ERR_NOT_SUPPORTED;
}
UpdateTime();
*value = registers_[reg];
if (reg == Register::kC) {
// Register C is cleared on read
registers_[Register::kC] = 0;
}
return ZX_OK;
}
zx_status_t RtcMc146818::WriteRegister(Register reg, uint8_t value) {
std::lock_guard<std::mutex> lock(mutex_);
UpdateTime();
switch (reg) {
case Register::kSeconds:
case Register::kMinutes:
case Register::kHours:
case Register::kDayOfWeek:
case Register::kDayOfMonth:
case Register::kMonth:
case Register::kYear:
case Register::kCentury:
registers_[reg] = value;
offset_ = GetOffset();
break;
case Register::kSecondsAlarm:
case Register::kMinutesAlarm:
case Register::kHoursAlarm:
// TODO: Implement alarms
return ZX_ERR_NOT_SUPPORTED;
case Register::kA:
// Changing the RTC speed is unsupported
FX_LOGS(DEBUG) << "Ignoring write to adjust RTC speed (0x" << std::hex
<< static_cast<uint32_t>(value) << ")";
break;
case Register::kB:
if (value & (kRegisterBUpdateInteruptEnable | kRegisterBAlarmInteruptEnable |
kRegisterBPeriodicInteruptEnable)) {
// Update, alarm, and periodic interrupts are unimplemented
// TODO: Implement alarms
return ZX_ERR_NOT_SUPPORTED;
}
registers_[Register::kB] = (value & ~kRegisterBUnwritableMask) |
(registers_[Register::kB] & kRegisterBUnwritableMask);
if (registers_[Register::kB] != value) {
FX_LOGS(INFO) << "Partially ignoring write to RTC operating mode (0x" << std::hex
<< static_cast<uint32_t>(value) << ")";
}
break;
case Register::kC:
FX_LOGS(INFO) << "Ignoring write to read-only RTC flags (0x" << std::hex
<< static_cast<uint32_t>(value) << ")";
break;
default:
FX_LOGS(ERROR) << "Write to unsupported RTC register (0x" << std::hex
<< static_cast<uint32_t>(reg) << ", 0x" << static_cast<uint32_t>(value) << ")";
return ZX_ERR_NOT_SUPPORTED;
}
return ZX_OK;
}
void RtcMc146818::UpdateTime() {
if (registers_[Register::kB] & kRegisterBStopTicks)
return;
time_t now = std::chrono::system_clock::to_time_t(Now() + offset_);
tm datetime;
gmtime_r(&now, &datetime);
registers_[Register::kSeconds] = ToBcd(datetime.tm_sec);
registers_[Register::kMinutes] = ToBcd(datetime.tm_min);
registers_[Register::kHours] = ToBcd(datetime.tm_hour);
registers_[Register::kDayOfWeek] = ToBcd(datetime.tm_wday + 1);
registers_[Register::kDayOfMonth] = ToBcd(datetime.tm_mday);
registers_[Register::kMonth] = ToBcd(datetime.tm_mon + 1);
int year = datetime.tm_year + 1900;
registers_[Register::kYear] = ToBcd(year % 100);
registers_[Register::kCentury] = ToBcd(year / 100);
}
std::chrono::seconds RtcMc146818::GetOffset() {
// Note: timegm() handles out-of-range values by just overflowing them to the
// next higher unit (e.g., 70 seconds turns into 1 minute and 10 seconds).
// Since the registers physically only allow for years 0000 to 9999, timegm()
// will never return its only error EOVERFLOW since they all fit into a
// 64-bit time_t
tm datetime{
.tm_sec = FromBcd(registers_[Register::kSeconds]),
.tm_min = FromBcd(registers_[Register::kMinutes]),
.tm_hour = FromBcd(registers_[Register::kHours]),
.tm_mday = FromBcd(registers_[Register::kDayOfMonth]),
.tm_mon = FromBcd(registers_[Register::kMonth]) - 1,
.tm_year = FromBcd(registers_[Register::kYear]) + 100 * FromBcd(registers_[Register::kCentury]) - 1900,
};
auto offset = std::chrono::system_clock::from_time_t(timegm(&datetime)) - Now();
return std::chrono::duration_cast<std::chrono::seconds>(offset);
}