From 7144ea1095a3974aa9bba27d15421b2b3be32e96 Mon Sep 17 00:00:00 2001 From: Changyuan Lyu Date: Fri, 6 Mar 2026 17:47:13 -0800 Subject: [PATCH] feat(cmos): add an emulated CMOS for x86 Signed-off-by: Changyuan Lyu --- Cargo.lock | 7 +- alioth/Cargo.toml | 1 + alioth/src/arch/x86_64/layout.rs | 3 + alioth/src/board/board_x86_64/board_x86_64.rs | 6 +- alioth/src/device/clock.rs | 34 ++++ alioth/src/device/clock_test.rs | 36 ++++ alioth/src/device/cmos.rs | 156 ++++++++++++++++++ alioth/src/device/cmos_test.rs | 82 +++++++++ alioth/src/device/device.rs | 2 + 9 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 alioth/src/device/clock.rs create mode 100644 alioth/src/device/clock_test.rs create mode 100644 alioth/src/device/cmos.rs create mode 100644 alioth/src/device/cmos_test.rs diff --git a/Cargo.lock b/Cargo.lock index acdfe601..3963fc54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,6 +25,7 @@ dependencies = [ "assert_matches", "bitfield", "bitflags", + "chrono", "ctor", "flexi_logger", "io-uring", @@ -188,12 +189,14 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", "windows-link", ] diff --git a/alioth/Cargo.toml b/alioth/Cargo.toml index abf6d904..d76fedec 100644 --- a/alioth/Cargo.toml +++ b/alioth/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true alioth-macros.workspace = true bitfield = "0.19.4" bitflags = "2.11.0" +chrono = "0.4.44" libc = "0.2.182" log = "0.4" mio = { version = "1", features = ["net", "os-ext", "os-poll"] } diff --git a/alioth/src/arch/x86_64/layout.rs b/alioth/src/arch/x86_64/layout.rs index 699ee1b2..ba6ee4d7 100644 --- a/alioth/src/arch/x86_64/layout.rs +++ b/alioth/src/arch/x86_64/layout.rs @@ -60,6 +60,9 @@ pub const MEM_64_START: u64 = 0x1_0000_0000; // 4GiB pub const PAGE_SIZE: u64 = 0x1000; // 4KiB +pub const PORT_CMOS_REG: u16 = 0x70; +pub const PORT_CMOS_DATA: u16 = 0x71; + pub const PORT_COM1: u16 = 0x3f8; pub const PORT_FW_CFG_SELECTOR: u16 = 0x510; diff --git a/alioth/src/board/board_x86_64/board_x86_64.rs b/alioth/src/board/board_x86_64/board_x86_64.rs index 5caf0f09..3c9b863a 100644 --- a/alioth/src/board/board_x86_64/board_x86_64.rs +++ b/alioth/src/board/board_x86_64/board_x86_64.rs @@ -29,10 +29,12 @@ use zerocopy::{FromZeros, IntoBytes}; use crate::arch::cpuid::CpuidIn; use crate::arch::layout::{ BIOS_DATA_END, EBDA_END, EBDA_START, IOAPIC_START, MEM_64_START, PORT_ACPI_RESET, - PORT_ACPI_SLEEP_CONTROL, PORT_ACPI_TIMER, RAM_32_SIZE, + PORT_ACPI_SLEEP_CONTROL, PORT_ACPI_TIMER, PORT_CMOS_REG, RAM_32_SIZE, }; use crate::arch::msr::{IA32_MISC_ENABLE, MiscEnable}; use crate::board::{Board, BoardConfig, CpuTopology, PCIE_MMIO_64_SIZE, Result, VcpuGuard, error}; +use crate::device::clock::SystemClock; +use crate::device::cmos::Cmos; use crate::device::ioapic::IoApic; use crate::firmware::acpi::bindings::{ AcpiTableFadt, AcpiTableHeader, AcpiTableRsdp, AcpiTableXsdt3, @@ -459,6 +461,8 @@ where pub fn arch_init(&self) -> Result<()> { let io_apic = self.arch.io_apic.clone(); self.mmio_devs.write().push((IOAPIC_START, io_apic)); + let mut io_devs = self.io_devs.write(); + io_devs.push((PORT_CMOS_REG, Arc::new(Cmos::new(SystemClock)))); Ok(()) } } diff --git a/alioth/src/device/clock.rs b/alioth/src/device/clock.rs new file mode 100644 index 00000000..0e84d720 --- /dev/null +++ b/alioth/src/device/clock.rs @@ -0,0 +1,34 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; + +use chrono::{DateTime, Utc}; + +pub trait Clock: Debug + Send + Sync + 'static { + fn now(&self) -> DateTime; +} + +#[derive(Debug)] +pub struct SystemClock; + +impl Clock for SystemClock { + fn now(&self) -> DateTime { + Utc::now() + } +} + +#[cfg(test)] +#[path = "clock_test.rs"] +pub mod tests; diff --git a/alioth/src/device/clock_test.rs b/alioth/src/device/clock_test.rs new file mode 100644 index 00000000..57ae635b --- /dev/null +++ b/alioth/src/device/clock_test.rs @@ -0,0 +1,36 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use chrono::{DateTime, Utc}; + +use crate::device::clock::{Clock, SystemClock}; + +#[derive(Debug)] +pub struct TestClock { + pub now: DateTime, +} + +impl Clock for TestClock { + fn now(&self) -> DateTime { + self.now + } +} + +#[test] +fn test_system_clock() { + let now = SystemClock.now(); + let utc_now = Utc::now(); + let diff = utc_now - now; + assert!(diff.num_seconds() < 1); +} diff --git a/alioth/src/device/cmos.rs b/alioth/src/device/cmos.rs new file mode 100644 index 00000000..15b18fa0 --- /dev/null +++ b/alioth/src/device/cmos.rs @@ -0,0 +1,156 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::atomic::{AtomicU8, Ordering}; + +use bitfield::bitfield; +use chrono::{Datelike, Timelike}; + +use crate::device::clock::Clock; +use crate::device::{MmioDev, Pause, Result}; +use crate::mem::emulated::{Action, Mmio}; +use crate::{bitflags, consts, mem}; + +bitfield! { + pub struct CmosReg(u8); + pub disable_nmi, set_disable_nmi: 7; + pub reg, set_reg: 6, 0; +} + +consts! { + pub struct CmosIntrFreq(u8) { + HZ_1024 = 0b0110; + } +} + +consts! { + pub struct CmosTimeBase(u8) { + HZ_32768 = 0b010; + } +} + +bitfield! { + pub struct CmosRegA(u8); + impl new; + pub u8, from into CmosIntrFreq, intr_freq, set_intr_freq: 3, 0; + pub u8, from into CmosTimeBase, time_base, set_time_base: 6, 4; + pub update_in_progress, set_update_in_progress: 7; +} + +impl Default for CmosRegA { + fn default() -> Self { + CmosRegA::new(CmosIntrFreq::HZ_1024, CmosTimeBase::HZ_32768, false) + } +} + +bitflags! { + pub struct CmosRegB(u8) { + HOUR_24 = 1 << 1; + BINARY_FORMAT = 1 << 2; + } +} + +bitflags! { + pub struct CmosRegD(u8) { + POWER = 1 << 7; + } +} + +/// CMOS RTC device. +/// +/// https://stanislavs.org/helppc/cmos_ram.html +/// https://wiki.osdev.org/CMOS +#[derive(Debug)] +pub struct Cmos { + reg: AtomicU8, + clock: C, +} + +impl Cmos { + pub fn new(clock: C) -> Self { + Self { + reg: AtomicU8::new(0), + clock, + } + } +} + +impl Mmio for Cmos { + fn size(&self) -> u64 { + 2 + } + + fn read(&self, offset: u64, _size: u8) -> mem::Result { + let reg = self.reg.load(Ordering::Relaxed); + if offset == 0 { + return Ok(reg as u64); + } + let now = self.clock.now(); + let ret = match CmosReg(reg).reg() { + 0x00 => now.second(), + 0x02 => now.minute(), + 0x04 => now.hour(), + 0x06 => now.weekday().number_from_sunday(), + 0x07 => now.day(), + 0x08 => now.month(), + 0x09 => now.year() as u32 % 100, + 0x32 => now.year() as u32 / 100 + 1, + 0x0a => { + // Assuming the hardware takes 8 crystal cycles to update + // the 8 registers above. + // 1 / 32768 Hz * 8 = 244140 ns + let mut r = CmosRegA::default(); + if now.nanosecond() < 244140 { + r.set_update_in_progress(true); + } + r.0 as u32 + } + 0x0b => (CmosRegB::HOUR_24 | CmosRegB::BINARY_FORMAT).bits() as u32, + 0x0d => CmosRegD::POWER.bits() as u32, + _ => { + log::debug!("cmos: read from reg {reg:#x}: ignored"); + 0 + } + }; + Ok(ret as u64) + } + + fn write(&self, offset: u64, _size: u8, val: u64) -> mem::Result { + if offset == 0 { + self.reg.store(val as u8, Ordering::Relaxed); + } else { + log::debug!( + "cmos: write {val:#x} to reg {:#x}: ignored", + self.reg.load(Ordering::Relaxed) + ); + } + Ok(Action::None) + } +} + +impl Pause for Cmos { + fn pause(&self) -> Result<()> { + todo!() + } + + fn resume(&self) -> Result<()> { + todo!() + } +} + +impl MmioDev for Cmos {} + +#[cfg(test)] +#[path = "cmos_test.rs"] +mod tests; diff --git a/alioth/src/device/cmos_test.rs b/alioth/src/device/cmos_test.rs new file mode 100644 index 00000000..7812dcee --- /dev/null +++ b/alioth/src/device/cmos_test.rs @@ -0,0 +1,82 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use assert_matches::assert_matches; +use chrono::DateTime; + +use crate::device::clock::tests::TestClock; +use crate::device::cmos::{Cmos, CmosRegA}; +use crate::mem::emulated::Mmio; + +#[test] +fn test_cmos() { + // Nov 7, 2025 at 15:44:58.01 GMT-08:00 + let now = DateTime::from_timestamp_nanos(1762559098010_000000); + let cmos = Cmos::new(TestClock { now }); + assert_eq!(cmos.size(), 2); + + assert_matches!(cmos.write(0x0, 1, 0xb), Ok(_)); + assert_matches!(cmos.read(0x0, 1), Ok(0xb)); + assert_matches!(cmos.read(0x1, 1), Ok(0b110)); + + assert_matches!(cmos.write(0x0, 1, 0xd), Ok(_)); + assert_matches!(cmos.read(0x1, 1), Ok(0x80)); + + assert_matches!(cmos.write(0x0, 1, 0xa), Ok(_)); + let reg_a = cmos.read(0x1, 1).unwrap(); + assert!( + !CmosRegA(reg_a as u8).update_in_progress(), + "CMOS update should be complete" + ); + + let tests = [ + (0x00, 58), + (0x02, 44), + (0x04, 23), + (0x06, 6), + (0x07, 7), + (0x08, 11), + (0x09, 25), + (0x32, 21), + ]; + for (reg, expected) in tests { + assert_matches!(cmos.write(0x0, 1, reg as u64), Ok(_)); + let value = cmos.read(0x1, 1).unwrap(); + assert_eq!( + value as u32, expected, + "CMOS register {reg:#02x} should match getter", + ); + } + + // Reads from unknown registers are ignored. + assert_matches!(cmos.write(0x0, 1, 0x01), Ok(_)); + assert_matches!(cmos.read(0x1, 1), Ok(0)); + + // Writes to all registers are ignored. + assert_matches!(cmos.write(0x1, 1, 0x0), Ok(_)); +} + +#[test] +fn test_cmos_upgrade_in_progress() { + // Nov 27, 2025 at 07:45:00.00 GMT-08:00 + let now = DateTime::from_timestamp_nanos(1764258300000_000000); + let cmos = Cmos::new(TestClock { now }); + + assert_matches!(cmos.write(0x0, 1, 0xa), Ok(_)); + let reg_a = cmos.read(0x1, 1).unwrap(); + assert!( + CmosRegA(reg_a as u8).update_in_progress(), + "CMOS update should be in progress" + ); +} diff --git a/alioth/src/device/device.rs b/alioth/src/device/device.rs index 048193a9..848b4baa 100644 --- a/alioth/src/device/device.rs +++ b/alioth/src/device/device.rs @@ -18,6 +18,8 @@ use snafu::Snafu; use crate::errors::DebugTrace; use crate::mem::emulated::Mmio; +pub mod clock; +pub mod cmos; pub mod console; #[cfg(target_arch = "x86_64")] #[path = "fw_cfg/fw_cfg.rs"]