diff --git a/docs/src/architecture/boot.md b/docs/src/architecture/boot.md index 6c0682df78e..6d42a32a9b0 100644 --- a/docs/src/architecture/boot.md +++ b/docs/src/architecture/boot.md @@ -27,7 +27,9 @@ (the TCG memory-overwrite request), so on firmware that defines and honours the request a reset leaves nothing of this boot in RAM for whoever boots next. -- The hardware clock keeps UTC. +- It reads the firmware's time once, and the kernel's wall clock counts on + from that reading: the kernel drives no real-time clock. The hardware clock + keeps UTC. ## Signed images diff --git a/issues/a-safe-port-read-reaches-any-declared-port.md b/issues/a-safe-port-read-reaches-any-declared-port.md index 974d8a84ccf..8254638441f 100644 --- a/issues/a-safe-port-read-reaches-any-declared-port.md +++ b/issues/a-safe-port-read-reaches-any-declared-port.md @@ -17,8 +17,8 @@ its own: a read of COM1's data register takes the received byte, and a read of its line status clears the error bits. `outb` and `outw` are `unsafe`, under a contract that the caller owns what the write commands; the reads carry none. -**Evidence:** the code. `inb`'s callers are `arch::console_uart`, `arch::rtc` -and `arch::acpi_mode`; `inw`'s are `arch::acpi_mode`, `arch::power` and +**Evidence:** the code. `inb`'s callers are `arch::console_uart` and +`arch::acpi_mode`; `inw`'s are `arch::acpi_mode`, `arch::power` and `arch::watchdog`. **Exit:** no safe call reads a port it was not handed. diff --git a/issues/chipset-counters-that-stall-together-read-as-a-tsc-step.md b/issues/chipset-counters-that-stall-together-read-as-a-tsc-step.md new file mode 100644 index 00000000000..75cbdd2b0e2 --- /dev/null +++ b/issues/chipset-counters-that-stall-together-read-as-a-tsc-step.md @@ -0,0 +1,59 @@ +--- +status: open +kind: defect +opened: 2026-11-11 +--- + +# Chipset counters that stall together read as a TSC step + +Where the TSC and the HPET disagree, x86-64's clock asks the ACPI PM timer +which moved (`kernel/src/arch/x86_64/clocksource.rs`). A PM timer that sides +with the HPET, or a machine with no counting PM timer, is taken as the TSC +having stepped. Nothing tells that from the chipset's counters standing +still together while a sound TSC counts on. The first forward step of a boot +is slewed out of the clock; a second, or one backward, demotes the TSC, and +every process's reading of now then asks the kernel, which counts by the +HPET. + +**Owner:** the clock. + +**What a sound TSC loses.** The disagreement is found within a second and +logged with every counter's span, and a demotion says `clock: DEMOTED`. What +is wrong is whose it is said to be: + +- (a) The HPET and the PM timer stall together twice in one boot. The first + stall is slewed away, so the clock stands the stall's length behind; the + second demotes the TSC to the HPET that stalls. +- (b) They stall together once, after the boot's one slew is spent. The AMD + bench spends it in every boot + (`issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md`), so there + one joint stall demotes. +- (c) The HPET alone stalls twice on a machine with no counting PM timer. + +**What the documents allow.** An invariant TSC runs at a constant rate in +every P-, C- and T-state (Intel SDM Vol. 3B, "Invariant TSC"). The HPET's +main counter may stop in S1 or S2, not in S0 (IA-PC HPET Specification 1.0a, +§2.3.3). The PM timer counts through S0 (ACPI 6.5 §4.8.3.3), and the same +section makes it optional. So (a) and (b) take the HPET and the PM timer both +out of specification, and (c) takes the HPET alone. + +**Evidence.** Until #1213 the RTC's update-ended edges, off a crystal of +their own, chose between a step and a joint stall, and a span they read as a +stall was left out. Across the 309 branches of the evidence repository on +2026-11-11 that verdict was written in no boot of either bench or of QEMU: +the edges confirmed the step or could not tell, and both took the action the +clock takes now +(https://github.com/ToyOSOrg/evidence/tree/3ae2cecf2b74a953ef15aab6238181433d7165e3/r1, +`rtc-verdicts-published.txt`). Both benches and QEMU have a PM timer that +counts. On the AMD bench the HPET and the PM timer run off one 14.318 MHz +clock by their periods, so their agreement against the TSC there is one +witness. + +**Not measured.** A machine whose chipset counters stall in S0. None is on a +bench. + +**Exit:** a boot of a supported machine that writes `clock: TSC STEP` or +`clock: DEMOTED` for a span its TSC did not step in reopens the choice, and +the clock then takes a reference outside the chipset's clock for that +machine; or the owner rules that the loss stands and this record becomes +`rejected`. diff --git a/issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md b/issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md index 6c297e7722e..b9a5b62b0ef 100644 --- a/issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md +++ b/issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md @@ -8,10 +8,12 @@ opened: 2026-10-31 On the AMD bench (Lenovo 82A2, BIOS DMCN38WW; Ryzen 7 4800U, CPUID signature 0x860f01, Family 17h Model 60h), the TSC jumps 1.69–1.73 ms ahead -once in every boot. The HPET, the ACPI PM timer and the RTC agree on it -within 37 µs. The RTC runs off its own 32.768 kHz crystal, and the other two -off the FCH's 14.318 MHz clock. Linux on the same machine sees the event in -5 of 30 boots. +once in every boot. The HPET and the ACPI PM timer agree on it, and so did +the RTC's update-ended edges, within 37 µs, while the kernel timed them +(#1104 to #1165's boots). The RTC runs off its own 32.768 kHz crystal, and the +other two off the FCH's 14.318 MHz clock, so it is the TSC that moves and not +the FCH's clock that stops. Linux on the same machine sees the event in 5 of +30 boots. **Owner:** the clock (`kernel/src/arch/x86_64/clocksource.rs`) and acpiserver, which hands over the idle state the step lands in. @@ -32,10 +34,9 @@ not known. **What the clock does meanwhile.** Readers see the forward step: the clock reads up to 1.7 ms ahead from the step until the sampler finds it, within a -second, and through the RTC's few seconds after. A forward step the HPET and -the PM timer agree on is then slewed away at no more than 500 ppm. This -happens whether the RTC confirms the step or cannot tell, and the correction -never steps back. A second step, or a backward one, demotes the TSC +second. A forward step the HPET and the PM timer agree on is then slewed away +at no more than 500 ppm, and the correction never steps back. A second step, +or a backward one, demotes the TSC (`issues/a-demoted-tsc-keeps-the-kernels-clock-apart-from-processes.md`). **Not read.** AMD's *Processor Programming Reference for Family 17h Model 60h* diff --git a/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md b/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md index 91e730f895c..87f50a577f5 100644 --- a/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md +++ b/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md @@ -95,11 +95,8 @@ and a compile-fail case. - metal `tls_rebase_window`: a thread's TLS block is never reachable before its rebase. - metal `kernel_heartbeat`: the kernel's heartbeat record arrives on its period; brings back `kernel/src/heartbeat.rs`. -- metal `wall_clock_utc`: the wall clock reads the RTC as UTC. - metal `spawn_cwd`: a spawn's working directory. - host `log_conservation_smp2`: every record is read or counted lost. -- host `wall_clock_rtc_dead`, `wall_clock_rtc_unstable`, `wall_clock_no_century`, - `wall_clock_century_register`: each RTC shape decodes or is refused. - unrepresentable `lock_across_switch_halts`: a lock guard cannot be held across a switch. - unrepresentable `hash_seed_precedes_every_map`: no kernel map is built before its seed. diff --git a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md index 012de414ccd..e5593f8bbc7 100644 --- a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md +++ b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md @@ -350,7 +350,7 @@ state. Nothing here is ruled, and the ranking is a proposal. | Scheduling: the core's driver, context switch, idle states, jobs, preemption | 20 | 3,160 | yes | | Firmware tables, ACPI mode, reset and power-off, the machine's stop, what the loader named | 12 | 3,099 | yes for the tables, the `acpi` row and power; the rest below | | Processes, threads, spawn | 8 | 2,736 | yes, less the userland text a spawn carries (below) | -| Clocks: TSC and generic timer, the wall clock, typed durations | 7 | 1,847 | the clock yes; the CMOS driver is a recorded defect (below) | +| Clocks: TSC and generic timer, the wall clock, typed durations | 7 | 1,847 | yes; the x86-64 CMOS driver and its IRQ8, 285 of these lines, went after this count | | Counters, the diary, IRQ census, mask windows, boot deadline, hard lockup, chipset watchdog | 14 | 1,844 | counters and diary yes; the three bounds are unnamed and inferred to be the kernel's, since they bound the kernel | | Paged objects and their pager calls | 2 | 1,325 | yes | | Syscall entry and argument decode | 3 | 806 | yes | @@ -399,9 +399,6 @@ Changes of the design, each inferred and none of them work until resolved: - **The quit's walk**: `issues/the-kernel-decides-who-is-asked-to-quit-and-who-is-killed.md`. -Already a defect with its exit: the x86-64 CMOS driver and its IRQ8 (285 -lines), `issues/x86-64-reads-the-cmos-for-a-wall-clock-the-loader-already-hands-it.md`. - ### The next removals, ranked (a proposal) By mechanisms eliminated: @@ -429,9 +426,6 @@ out, and a larger number needs one of the design changes above. - `SYS_STACK_INFO` is called by `tests/guest/rust-tests/src/bin/std_threads.rs` alone, and `SYS_VM_DECOMMIT` by two guest tests alone (`mmap_stress`, `starveling`). -- `KernelArgs::wall_clock_*` is read by no x86-64 code. (Its - `boot_partition_start_lba` and `boot_partition_blocks`, read by one log - line, went with #1210.) - `KernelStack::heap` (`kernel/src/loader/start.rs`) charges every thread two strings at 64 KiB and a page, "as a rename's paths". No rename is left: a spawn's path and arguments are the two strings today. After #1200 diff --git a/issues/the-loader-does-only-what-must-precede-the-handover.md b/issues/the-loader-does-only-what-must-precede-the-handover.md index daa9b711299..35aacb3fce3 100644 --- a/issues/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/the-loader-does-only-what-must-precede-the-handover.md @@ -356,7 +356,7 @@ Each stage lands on its own, in this order. 8. **The kernel arms the TCO before anything unbounded.** The loader's arm is the only bound today from `ExitBootServices` to `deadline::start` on `boot-deadline=` boots, and to `arch::watchdog::init` on the others: - through `mm::init`, ACPI, interrupts, HPET calibration, the RTC read, PCI + through `mm::init`, ACPI, interrupts, HPET calibration, PCI enumeration, `pcidev::publish` and `iommu::init`. - The kernel arms as its first act, before `mm::init`: `toyos_acpi::ecam_base` and the bus-0 scan `loader/src/watchdog.rs` does, through a boot map diff --git a/issues/x86-64-reads-the-cmos-for-a-wall-clock-the-loader-already-hands-it.md b/issues/x86-64-reads-the-cmos-for-a-wall-clock-the-loader-already-hands-it.md deleted file mode 100644 index 1f4cc83e719..00000000000 --- a/issues/x86-64-reads-the-cmos-for-a-wall-clock-the-loader-already-hands-it.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-10 ---- - -# x86-64 reads the CMOS for a wall clock the loader already hands it - -The loader asks firmware's `GetTime` before `ExitBootServices` on every -architecture and hands the instant, with the counter it was true at, in -`KernelArgs::wall_clock_*` (`loader/src/wallclock.rs`). AArch64 anchors -its wall clock on that (`kernel/src/arch/aarch64/rtc.rs`); x86-64 ignores it -and reads the CMOS itself (`kernel/src/arch/x86_64/rtc.rs`), with the FADT's -century register (`kernel/src/drivers/acpi.rs`), so the two architectures keep -two readers of one clock, and on x86-64 the field is written and never read. -Whether the T14's firmware answers `GetTime` from the same CMOS is not -measured. - -The wall clock is not the CMOS driver's only reader: x86-64's clock takes -the RTC's update-ended edges, on IRQ8, as the third reference that tells a -TSC step from a stall of the chipset's counters -(`kernel/src/arch/x86_64/clocksource.rs`, `rtc::last_edge`). So the exit -below deletes the wall-clock read and the century register, and -`arch/x86_64/rtc.rs` whole only if that reference goes or moves with it. - -Exit condition: x86-64 anchors on the loader's reading, and -`arch/x86_64/rtc.rs`, `acpi::rtc_century_register` and their callers are -deleted with it, against a metal row reading the T14's wall clock; or a -measurement on the T14 that its firmware's `GetTime` answers wrongly where the -CMOS answers right, recorded at `arch/x86_64/rtc.rs`, and this file deleted. diff --git a/kernel/pure/clock/mod.rs b/kernel/pure/clock/mod.rs index 2dac5aaf41e..ff72bcf8553 100644 --- a/kernel/pure/clock/mod.rs +++ b/kernel/pure/clock/mod.rs @@ -13,22 +13,16 @@ const NANOS_PER_SEC: u64 = 1_000_000_000; /// by the first scheduler pass after each doubling span. pub const TSC_REFINED: &str = "clock: over "; -/// What opens the kernel's report of a span in which the TSC moved apart from -/// the chipset's counters (`clock: the TSC reads ns from the chipset's -/// counters ...`): the clock's word on a step the machine made, written as -/// the sampler finds it. -pub const TSC_STEP_READ: &str = "clock: the TSC reads "; - -/// Unix seconds at boot, from a reading of `secs` true when the clock read -/// `at`, which is after boot where the kernel read the wall clock and before -/// it where the loader did; both are stamps, nanoseconds since the counter's -/// zero. Only the whole seconds between the two move the answer. +/// What opens the kernel's report of a span in which the TSC stepped against +/// the chipset's counters (`clock: TSC STEP: the TSC stepped ns ...`), +/// written as the sampler finds it. +pub const TSC_STEP: &str = "clock: TSC STEP: "; + +/// Unix seconds at boot, from the loader's reading of `secs`, true when the +/// clock read `at`, before boot; both are stamps, nanoseconds since the +/// counter's zero. Only the whole seconds between the two move the answer. pub const fn anchor(secs: u64, at: u64, boot: u64) -> u64 { - if at >= boot { - secs.saturating_sub((at - boot) / NANOS_PER_SEC) - } else { - secs.saturating_add((boot - at) / NANOS_PER_SEC) - } + secs.saturating_add(boot.saturating_sub(at) / NANOS_PER_SEC) } #[cfg(test)] @@ -38,11 +32,6 @@ mod tests { const SECS: u64 = 1_993_799_665; const BOOT: u64 = 50_000_000_000; - #[test] - fn a_reading_after_boot_is_carried_back_to_it() { - assert_eq!(anchor(SECS, BOOT + 3_700_000_000, BOOT), SECS - 3); - } - #[test] fn a_reading_before_boot_is_carried_forward_to_it() { assert_eq!(anchor(SECS, BOOT - 2_500_000_000, BOOT), SECS + 2); diff --git a/kernel/pure/clock/reference.rs b/kernel/pure/clock/reference.rs index 30dadc2e6d3..43e6ac80a2a 100644 --- a/kernel/pure/clock/reference.rs +++ b/kernel/pure/clock/reference.rs @@ -7,9 +7,7 @@ //! the counter readings either side of a reference reading bracket the moment //! it latched, and the reference's tick bounds where in that tick it was, so a //! bound that fails is a counter, a reference or a read that did not do what -//! it says. The one estimate is a third reference read by its interrupts -//! ([`Edges`]): their least latency, and its crystal's drift carried across a -//! span, which only choose between a step and none ([`third_says`]). +//! it says. use toyos_abi::clock::MULT_SHIFT; @@ -423,114 +421,6 @@ pub const fn arbitrate(first: Agreement, second: Agreement) -> Arbitration { } } -/// The RTC's edges the sampler has read, the latest [`EDGES_KEPT`]: each -/// edge's count and the counter as its interrupt was taken, which is the edge -/// plus the interrupt's latency, never earlier. -#[derive(Clone, Copy, Debug)] -pub struct Edges { - kept: [(u64, u64); EDGES_KEPT], - len: usize, -} - -pub const EDGES_KEPT: usize = 16; -/// The edges either side of a span its least-latency edge is taken from. -pub const EDGES_EACH_SIDE: usize = 3; - -impl Edges { - pub const fn new() -> Self { - Self { kept: [(0, 0); EDGES_KEPT], len: 0 } - } - - /// Keep the edge `seen` as the counter read `at`, where it is later than - /// the latest kept. - pub fn push(&mut self, seen: u64, at: u64) { - if self.len > 0 && self.kept[self.len - 1].0 >= seen { - return; - } - if self.len == EDGES_KEPT { - self.kept.copy_within(1.., 0); - self.len -= 1; - } - self.kept[self.len] = (seen, at); - self.len += 1; - } - - /// Drop the edges taken before the counter read `at`, where the counter - /// stepped: an edge from before a step and one from after it measure the - /// step, not the RTC crystal's drift, so neither a drift base nor a side - /// of a later span may reach across it. - pub fn forget_before(&mut self, at: u64) { - let kept = self.kept[..self.len].iter().position(|&(_, taken)| taken >= at).unwrap_or(self.len); - self.kept.copy_within(kept..self.len, 0); - self.len -= kept; - } - - /// Whether [`EDGES_EACH_SIDE`] edges have come after the counter read `to`. - pub fn complete_after(&self, to: u64) -> bool { - self.kept[..self.len].iter().filter(|&&(_, at)| at >= to).count() >= EDGES_EACH_SIDE - } - - /// The counter's span, at `mult`, less the RTC's seconds, from before - /// `from` to after `to`: from the least-latency of the [`EDGES_EACH_SIDE`] - /// edges before `from` to that of those after `to`. An edge reads late by - /// its interrupt's latency and never early, so the least on each side is - /// the nearest its edge. Less the RTC crystal's own drift from the - /// counter, where edges at least [`DRIFT_BASE_NS`] before give it: the - /// least-latency of the oldest kept to that before `from`, carried on - /// across the span. `None` without an edge either side. - pub fn apart_across(&self, from: u64, to: u64, mult: u64) -> Option { - let kept = &self.kept[..self.len]; - let &(seen_ref, at_ref) = kept.iter().rev().find(|&&(_, at)| at <= from)?; - let offset = |&(seen, at): &(u64, u64)| { - let ns = ((at as i128 - at_ref as i128) * mult as i128) >> MULT_SHIFT; - (ns - (seen as i128 - seen_ref as i128) * 1_000_000_000, at) - }; - let least = |edges: &mut dyn Iterator| edges.map(offset).min_by_key(|&(ns, _)| ns); - let before = least(&mut kept.iter().rev().filter(|&&(_, at)| at <= from).take(EDGES_EACH_SIDE))?; - let after = least(&mut kept.iter().filter(|&&(_, at)| at >= to).take(EDGES_EACH_SIDE))?; - let base_ticks = ((DRIFT_BASE_NS as u128) << MULT_SHIFT) / mult as u128; - let older = least(&mut kept.iter().filter(|&&(_, at)| (at as u128) + base_ticks <= before.1 as u128).take(EDGES_EACH_SIDE)); - let drift = older.map_or(0, |older| (before.0 - older.0) * (after.1 as i128 - before.1 as i128) / (before.1 as i128 - older.1 as i128)); - Some((after.0 - before.0 - drift) as i64) - } -} - -/// How far back edges must reach for [`Edges::apart_across`] to measure the -/// RTC crystal's drift from the counter: over this, a few microseconds of -/// latency are a part per million. -pub const DRIFT_BASE_NS: u64 = 4_000_000_000; - -impl Default for Edges { - fn default() -> Self { - Self::new() - } -} - -/// Which of the counter's step and no step at all a third reference, on -/// another oscillator, reads nearest: within a quarter of the step of -/// either, or of neither. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Third { - /// It reads the step: the counter moved. - Counter, - /// It reads no step: the two references moved together. - References, - /// It reads neither, or has no edge either side of the span yet. - Unsure, -} - -pub fn third_says(step_ns: i64, apart_ns: Option) -> Third { - let Some(apart) = apart_ns else { return Third::Unsure }; - let zone = step_ns.unsigned_abs() / 4; - if apart.abs_diff(step_ns) <= zone { - Third::Counter - } else if apart.unsigned_abs() <= zone { - Third::References - } else { - Third::Unsure - } -} - /// What the clock does about a step of its counter: the boot's first, forward, /// is slewed away; a second, or one back, leaves the counter for the /// reference. @@ -874,49 +764,6 @@ mod tests { assert_eq!(extend(0x0400_0010, 0x00ff_fff0, 24), 0x03ff_fff0); } - /// A 1 GHz counter, so ticks are nanoseconds. - const NS: u64 = 1 << MULT_SHIFT; - - /// Edges a second apart on the RTC, each read as late as its latency; - /// the counter steps by `step` at `step_at`. - fn edges(latency_ns: &[u64], step_at: u64, step: u64) -> Edges { - let mut edges = Edges::new(); - for (n, latency) in latency_ns.iter().enumerate() { - let edge = 10_000_000_000 + n as u64 * 1_000_000_000; - edges.push(n as u64 + 1, edge + latency + if edge >= step_at { step } else { 0 }); - } - edges - } - - /// The least-latency edge either side reads the step, however late the - /// edge nearest it was taken; and reads none where there was none. - #[test] - fn the_rtc_reads_a_step_through_its_interrupts_latency() { - let latency = [40_000, 700_000, 3_000, 650_000, 520_000, 9_000, 700_000, 2_000]; - let step = 1_700_000; - let stepped = edges(&latency, 14_500_000_000, step); - let (from, to) = (13_900_000_000, 14_900_000_000); - assert!(stepped.complete_after(to + step)); - let apart = stepped.apart_across(from, to + step, NS).expect("edges either side"); - assert!(apart.abs_diff(step as i64) <= 10_000, "read {apart} ns for a {step} ns step"); - assert_eq!(third_says(step as i64, Some(apart)), Third::Counter); - - let still = edges(&latency, u64::MAX, 0); - let apart = still.apart_across(from, to, NS).expect("edges either side"); - assert!(apart.unsigned_abs() <= 10_000, "read {apart} ns where nothing stepped"); - assert_eq!(third_says(step as i64, Some(apart)), Third::References); - } - - #[test] - fn the_rtc_is_unsure_without_an_edge_either_side_or_between_the_two() { - let few = edges(&[1_000, 1_000], u64::MAX, 0); - assert_eq!(few.apart_across(9_000_000_000, 10_500_000_000, NS), None); - assert!(!few.complete_after(10_500_000_000)); - assert_eq!(third_says(1_700_000, None), Third::Unsure); - assert_eq!(third_says(1_700_000, Some(850_000)), Third::Unsure); - assert_eq!(third_says(-1_700_000, Some(-1_600_000)), Third::Counter); - } - #[test] fn only_a_first_forward_step_is_slewed() { assert_eq!(respond(1_700_000, 0), Response::Slew); @@ -965,54 +812,4 @@ mod tests { assert!(later.debt_ns() < 0); assert!(later.mult(kept, period) > kept, "an overpaid slew runs the clock faster"); } - - /// An RTC crystal 60 ppm off the counter drifts 420 µs over the edges - /// either side of a span, about a quarter of a 1.7 ms step: corrected by - /// its own earlier edges, it reads the step and reads a stall as none. - #[test] - fn the_rtcs_crystal_drift_is_taken_out() { - let drifting = |step: u64| { - let mut edges = Edges::new(); - for n in 0..EDGES_KEPT as u64 { - let edge = 10_000_000_000 + n * 1_000_060_000; - let latency = [3_000, 650_000, 20_000][n as usize % 3]; - edges.push(n + 1, edge + latency + if n >= 11 { step } else { 0 }); - } - edges - }; - let (from, to) = (20_900_000_000, 21_900_000_000); - let apart = drifting(1_700_000).apart_across(from, to + 1_700_000, NS).expect("edges either side"); - assert_eq!(third_says(1_700_000, Some(apart)), Third::Counter, "read {apart} ns"); - let apart = drifting(0).apart_across(from, to, NS).expect("edges either side"); - assert!(apart.unsigned_abs() <= 30_000, "a stall read {apart} ns through the drift"); - assert_eq!(third_says(1_700_000, Some(apart)), Third::References); - } - - /// A 1.7 ms step after a 4 ms one reads as itself once the edges from - /// before the first are forgotten; with them, the drift base reaches - /// across the first step and takes a quarter of it for the crystal's drift. - #[test] - fn a_drift_base_never_reaches_across_a_step() { - let (first, second) = (4_000_000u64, 1_700_000u64); - let stepped = || { - let mut edges = Edges::new(); - for n in 0..EDGES_KEPT as u64 { - let edge = 10_000_000_000 + n * 1_000_000_000; - let latency = [3_000, 650_000, 20_000][n as usize % 3]; - let steps = if n >= 4 { first } else { 0 } + if n >= 13 { second } else { 0 }; - edges.push(n + 1, edge + latency + steps); - } - edges - }; - let first_taken_at = 14_000_000_000 + first; - let (from, to) = (22_500_000_000 + first, 22_900_000_000 + first + second); - - let mut forgotten = stepped(); - forgotten.forget_before(first_taken_at); - let apart = forgotten.apart_across(from, to, NS).expect("edges either side"); - assert_eq!(third_says(second as i64, Some(apart)), Third::Counter, "read {apart} ns"); - - let apart = stepped().apart_across(from, to, NS).expect("edges either side"); - assert_ne!(third_says(second as i64, Some(apart)), Third::Counter, "read {apart} ns across the first step"); - } } diff --git a/kernel/src/arch/aarch64/boot.rs b/kernel/src/arch/aarch64/boot.rs index 8b7d087ce53..114e7b71d7a 100644 --- a/kernel/src/arch/aarch64/boot.rs +++ b/kernel/src/arch/aarch64/boot.rs @@ -351,12 +351,11 @@ pub fn interrupts(rsdp_addr: u64) -> Platform { /// The clock: the generic timer's count, at the rate firmware states in /// `CNTFRQ_EL0`, which the Arm ARM makes firmware's to program and which is -/// what the counter counts at; and the wall clock the loader handed over. -pub fn clock(args: &KernelArgs) { +/// what the counter counts at. +pub fn clock(_rsdp_addr: u64) { let hz = super::cpu::stated_counter_hz().expect("clock: CNTFRQ_EL0 states no rate for the generic timer"); crate::clock::set_counter(super::cpu::counter(), toyos_abi::clock::Line::mult_of_hz(hz)); log!("clock: the generic timer counts at {hz} Hz"); - crate::clock::init_wall(super::rtc::read(args)); } /// Nothing: where the generic timer counts from is firmware's, and no diff --git a/kernel/src/arch/aarch64/mod.rs b/kernel/src/arch/aarch64/mod.rs index 9b46c11bbb0..c157df39df2 100644 --- a/kernel/src/arch/aarch64/mod.rs +++ b/kernel/src/arch/aarch64/mod.rs @@ -11,15 +11,6 @@ macro_rules! owed { }; } -/// The interrupt sources only this architecture counts: none past the -/// census's own (`irq_census`). -macro_rules! census_sources { - ($list:ident) => { - $list! {} - }; -} -pub(crate) use census_sources; - pub mod acpi_mode; pub mod barrier; pub mod boot; @@ -42,7 +33,6 @@ pub mod pio; pub mod pmu; pub mod power; pub mod psci; -pub mod rtc; pub mod smmu; pub mod smp; pub mod switch; diff --git a/kernel/src/arch/aarch64/rtc.rs b/kernel/src/arch/aarch64/rtc.rs deleted file mode 100644 index edfd5ddc2b3..00000000000 --- a/kernel/src/arch/aarch64/rtc.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! The wall clock at boot: firmware's `GetTime`, which the loader asked before -//! `ExitBootServices` and hands over in [`KernelArgs`], since the kernel calls -//! no UEFI service; on `virt` firmware reads it from the PL031, which only the -//! DSDT names. - -use core::fmt; - -use toyos_abi::boot::KernelArgs; -use toyos_wallclock::Civil; - -/// Why this machine did not say what time it is. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RtcFault { - /// The loader handed no reading; its own line says why. - NotHanded, -} - -impl fmt::Display for RtcFault { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NotHanded => write!(f, "the loader handed no reading of firmware's GetTime"), - } - } -} - -/// The instant firmware named, and the counter when it was true. -pub fn read(args: &KernelArgs) -> Result<(Civil, u64), RtcFault> { - match args.wall_clock_known { - 0 => Err(RtcFault::NotHanded), - 1 => Ok((Civil::from_unix_secs(args.wall_clock_secs), args.wall_clock_counter)), - other => panic!("the loader wrote {other} as wall_clock_known, which is 0 or 1"), - } -} diff --git a/kernel/src/arch/x86_64/boot.rs b/kernel/src/arch/x86_64/boot.rs index 62c853c6a5b..e6b24883f28 100644 --- a/kernel/src/arch/x86_64/boot.rs +++ b/kernel/src/arch/x86_64/boot.rs @@ -38,7 +38,7 @@ pub unsafe extern "C" fn _start(_kernel_args: &KernelArgs) -> ! { /// logs nothing: the read-back is `pat::check`, in [`after_console`], where a /// refusal has a channel to reach. /// The ACPI tables this architecture decodes: the MADT for its CPUs and I/O -/// APICs, the FADT for reset, the PM1a control block and the century register, the HPET for +/// APICs, the FADT for reset and the PM1a control block, the HPET for /// the clock, the MCFG for ECAM and the DMAR or IVRS for the IOMMU. pub const ACPI_TABLES: &[&[u8; 4]] = &[b"APIC", b"FACP", b"HPET", b"MCFG", b"DMAR", b"IVRS"]; @@ -114,18 +114,10 @@ pub fn interrupts(rsdp_addr: u64) -> Platform { Platform { madt, facs } } -/// The clock: the TSC, held to the HPET, and the CMOS wall clock. -pub fn clock(args: &KernelArgs) { - let hpet_base = acpi::find_hpet_base(args.rsdp_addr).expect("ACPI: HPET not found"); - super::clocksource::start(hpet_base, acpi::pm_timer(args.rsdp_addr)); - let century_reg = match acpi::rtc_century_register(args.rsdp_addr) { - Ok(reg) => reg, - Err(e) => { - log!("ACPI: the FADT is unreadable ({e:?}), so where the RTC keeps its century is unknown too"); - None - } - }; - crate::clock::init_wall(super::rtc::read(century_reg).map(|civil| (civil, super::cpu::counter()))); +/// The clock: the TSC, held to the HPET. +pub fn clock(rsdp_addr: u64) { + let hpet_base = acpi::find_hpet_base(rsdp_addr).expect("ACPI: HPET not found"); + super::clocksource::start(hpet_base, acpi::pm_timer(rsdp_addr)); } /// Where the TSC counts from: `IA32_TSC_ADJUST`, where CPUID says the CPU has @@ -149,9 +141,6 @@ pub fn platform_devices(platform: &Platform, rsdp_addr: u64) { super::pio::fill_i8042_row(); super::pio::fill_uart_row(); super::acpi_mode::init(rsdp_addr, platform.facs); - if let Err(why) = super::rtc::arm_update_edges() { - log!("clock: no RTC edges time the TSC: {why}"); - } } /// Every other CPU, running. diff --git a/kernel/src/arch/x86_64/clocksource.rs b/kernel/src/arch/x86_64/clocksource.rs index 382302e464c..13b14118435 100644 --- a/kernel/src/arch/x86_64/clocksource.rs +++ b/kernel/src/arch/x86_64/clocksource.rs @@ -5,16 +5,16 @@ //! The clock counts the TSC only where every CPU states it invariant, or under //! a hypervisor, whose TSC is its own to synthesise and whose CPUID need not //! say so. Every AP's TSC is bounded against the BSP's as it starts -//! ([`measure_offset`]). While it runs, one CPU a second samples the HPET, the -//! ACPI PM timer and the RTC's latest edge, and every CPU a second checks its -//! `IA32_TSC_ADJUST` and that its TSC is not behind another's ([`check`]). +//! ([`measure_offset`]). While it runs, one CPU a second samples the HPET and +//! the ACPI PM timer, and every CPU a second checks its `IA32_TSC_ADJUST` and +//! that its TSC is not behind another's ([`check`]). //! -//! **Where the TSC and the HPET disagree, the other two say which moved.** The -//! PM timer agreeing with the TSC is the HPET moving, and the span is left out. -//! The PM timer agreeing with the HPET leaves the chipset's 14.318 MHz clock -//! or the TSC; the RTC's edges, off its own 32.768 kHz crystal, choose: with -//! the TSC, the span is left out; with the chipset's counters, or saying -//! neither, the TSC stepped. The boot's first forward step is slewed away; +//! **Where the TSC and the HPET disagree, the PM timer says which moved.** It +//! agreeing with the TSC is the HPET moving, and the span is left out. It +//! agreeing with the HPET, or a machine with none, is taken as the TSC's +//! step, which the chipset's counters standing still together would read as +//! too (`issues/chipset-counters-that-stall-together-read-as-a-tsc-step.md`). +//! The boot's first forward step is slewed away; //! a second, one back, a check that finds a CPU's TSC written or behind, or //! an AP's start that proves its offset demotes the TSC ([`demote`]), and //! every process's reading of now asks the kernel, which counts by the HPET. @@ -22,7 +22,7 @@ use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use kernel::clock::reference::{self, Agreement, Arbitration, Calibration, Edges, Failure, Kept, Offset, Reference, Response, Sample, Slew, Third, Watch}; +use kernel::clock::reference::{self, Agreement, Arbitration, Calibration, Failure, Kept, Offset, Reference, Response, Sample, Slew, Watch}; use toyos_abi::clock::Line; use super::cpu; @@ -126,8 +126,8 @@ impl Hpet { /// The ACPI PM timer, the second reference: which of the TSC and the HPET /// moved, where the two disagree. **It and the HPET are both the chipset's -/// (the FCH's on AMD, the PCH's on Intel)**, and may share its clock, so the -/// RTC's edges judge a stall common to both. +/// (the FCH's on AMD, the PCH's on Intel)**, and may share its clock: a stall +/// common to both reads as the TSC's step. struct PmTimer { port: Declared, bits: u32, @@ -184,28 +184,13 @@ struct Held { hpet: Hpet, watch: Watch, pm: Option, - /// The RTC's edges, the third reference, on its own crystal. - edges: Edges, - /// A step the HPET and the PM timer read the TSC take, waiting for the - /// RTC's edges after it. - pending: Option, /// The steps this boot slewed away, and the slew under way. slewed: u32, slew: Option, - /// Spans left out: one reference moved, or the RTC said both did. + /// Spans left out: a reference moved, or the TSC stepped. left_out: u64, } -/// A step the chipset's counters agree the TSC took over `from..to`. -#[derive(Clone, Copy)] -struct Pending { - step_ns: i64, - from: Sample, - to: Sample, - /// When the sampler gives up waiting for the RTC's edges after it. - until_ns: u64, -} - /// A slew, and the line it runs on since the counter read `since`: at `ran` /// where the reference's time runs at `kept`. #[derive(Clone, Copy)] @@ -216,11 +201,6 @@ struct Slewing { kept: u64, } -/// How long a step waits for the RTC's edges after it before it is taken -/// unconfirmed: [`reference::EDGES_EACH_SIDE`] edges a second apart, and two -/// more for their interrupts. -const RTC_WAIT_NS: u64 = (reference::EDGES_EACH_SIDE as u64 + 2) * 1_000_000_000; - static HELD: Lock> = Lock::new(None); /// The last sample every check held, and the clock then: where a demoted @@ -321,7 +301,7 @@ pub fn start(hpet_base: u64, pm_timer: Result log!("clock: the ACPI PM timer at {:#x}, {} bits, tells the TSC from the HPET where they disagree", pm.port.port(0).number(), pm.bits), - Err(why) => log!("clock: no ACPI PM timer to tell the TSC from the HPET ({why}); the RTC alone tells them apart"), + Err(why) => log!("clock: no ACPI PM timer to tell the TSC from the HPET ({why}); where they disagree the TSC is taken to have stepped"), } LAST_HELD.publish([anchor.before, anchor.reference, crate::clock::stamp_at(anchor.before)]); *HPET_COPY.lock() = Some(hpet); @@ -330,8 +310,6 @@ pub fn start(hpet_base: u64, pm_timer: Result u64 { answered } -/// One sample of the HPET, of the PM timer beside it and of the RTC's -/// latest edge: each span the TSC and the HPET agree over is held; where -/// they do not, the PM timer and then the RTC say which moved. -fn sample(held: &mut Held, now_ns: u64) { +/// One sample of the HPET and of the PM timer beside it: each span the TSC +/// and the HPET agree over is held; where they do not, the PM timer says +/// which moved. +fn sample(held: &mut Held) { let kept = held.watch.kept(); let last = held.watch.last(); let hpet = held.hpet.sample(held.hpet.near(last, kept.mult), 1); let pm = held.pm.as_ref().map(|pm| (pm.last, pm.next(kept.mult))); - let edge = super::rtc::last_edge(); #[cfg(feature = "boot-actuators")] - let (hpet, pm, edge) = staged(hpet, pm, edge); - if let Some((seen, at)) = edge { - held.edges.push(seen, at); - } + let (hpet, pm) = staged(hpet, pm); let pm_agreement = held.pm.as_ref().zip(pm).map(|(timer, (from, to))| { let span = reference::Span::between(timer.reference, from, to); let tolerance = kept.bound_ppb + timer.reference.tolerance_over(timer.reference.span_ns(from, to)); @@ -721,18 +695,7 @@ fn sample(held: &mut Held, now_ns: u64) { Some(Arbitration::AllDisagree) => { log!("clock: the TSC, the HPET and the ACPI PM timer all disagree, so this span is left out: {spans}") } - Some(Arbitration::CounterMoved) | None => { - if let Some(earlier) = held.pending.take() { - settle(held, earlier); - } - log!( - "{}{} ns from the chipset's counters over the last span; the RTC's edges after it will \ - say which moved: {spans}", - kernel::clock::TSC_STEP_READ, - agreement.apart_ns, - ); - held.pending = Some(Pending { step_ns: agreement.apart_ns, from, to, until_ns: now_ns + RTC_WAIT_NS }); - } + Some(Arbitration::CounterMoved) | None => stepped(held, agreement.apart_ns, &spans), } } Err(Failure::Drift { since, to, agreement }) => panic!( @@ -744,53 +707,32 @@ fn sample(held: &mut Held, now_ns: u64) { agreement.allowed_ns, ), } - if let Some(pending) = held.pending.filter(|p| held.edges.complete_after(p.to.after) || now_ns >= p.until_ns) { - held.pending = None; - settle(held, pending); - } KEPT_MULT.store(held.watch.kept().mult, Ordering::Relaxed); slew(held); } -/// What the RTC says of a pending step, and what the clock does about it. -fn settle(held: &mut Held, pending: Pending) { +/// What the clock does about a step of `step` ns the TSC took against the +/// chipset's counters, whose readings of it are `spans`. +fn stepped(held: &mut Held, step: i64, spans: &str) { let kept = held.watch.kept(); - let apart = held.edges.apart_across(pending.from.before, pending.to.after, kept.mult); - let rtc = match apart { - Some(apart) => alloc::format!("the RTC's edges read {apart} ns"), - None => alloc::string::String::from("no RTC edge either side"), - }; - let step = pending.step_ns; - let confirmed = match reference::third_says(step, apart) { - Third::References => { - return log!( - "clock: the chipset's counters moved {step} ns from the TSC and {rtc}, with the TSC, so the span \ - was left out" - ); - } - Third::Counter => "confirmed by the RTC", - Third::Unsure => "unconfirmed by the RTC", - }; - held.edges.forget_before(pending.to.after); + let against = if held.pm.is_some() { "the HPET and the PM timer" } else { "the HPET" }; if IS_DEMOTED.load(Ordering::Acquire) { - return log!("clock: TSC STEP: the demoted TSC stepped {step} ns against the HPET and the PM timer, {confirmed} ({rtc}); not slewed"); + return log!("{}the demoted TSC stepped {step} ns against {against}; not slewed: {spans}", kernel::clock::TSC_STEP); } match reference::respond(step, held.slewed) { Response::Slew => { held.slewed += 1; held.slew = Some(Slewing { slew: Slew::new(step), since: cpu::counter(), ran: crate::clock::mult(), kept: kept.mult }); log!( - "clock: TSC STEP: the TSC stepped {step} ns forward against the HPET and the PM timer, {confirmed} \ - ({rtc}); the clock slews it away at most {} ppm, most of it in about {} ms", + "{}the TSC stepped {step} ns forward against {against}; the clock slews it away at most {} ppm, most \ + of it in about {} ms: {spans}", + kernel::clock::TSC_STEP, reference::SLEW_MOST_PPB / 1_000, u128::from(step.unsigned_abs()) * u128::from(reference::PPB) / reference::SLEW_MOST_PPB as u128 / 1_000_000, ); } Response::Demote => { - demote(format_args!( - "the TSC stepped {step} ns against the HPET and the PM timer, {confirmed} ({rtc}), after {} step(s) slewed", - held.slewed, - )); + demote(format_args!("the TSC stepped {step} ns against {against}, after {} step(s) slewed: {spans}", held.slewed)); } } } @@ -874,15 +816,13 @@ fn spans( ) } -/// The PM timer's span, and the RTC's latest edge, as one sample reads them. +/// The PM timer's span, as one sample reads it. #[cfg(feature = "boot-actuators")] type PmSpan = Option<(Sample, Sample)>; -#[cfg(feature = "boot-actuators")] -type Edge = Option<(u64, u64)>; /// `tsc-step` and `hpet-stall`: from the first report on, every reading reads -/// either the TSC 10 ms ahead of where it is, the HPET's, the PM timer's and -/// the RTC's edges alike, or the HPET 10 ms behind, as a TSC that stepped and +/// either the TSC 10 ms ahead of where it is, the HPET's and the PM timer's +/// alike, or the HPET 10 ms behind, as a TSC that stepped and /// an HPET that stood still; and the TSC 10 ms further once its first step is /// slewed away. A hypervisor's TSC may ignore a write (QEMU's TCG does), so /// each is staged in the readings the sampler takes, from the counter it @@ -904,12 +844,12 @@ mod stage { } #[cfg(feature = "boot-actuators")] -fn staged(hpet: Sample, pm: PmSpan, edge: Edge) -> (Sample, PmSpan, Edge) { +fn staged(hpet: Sample, pm: PmSpan) -> (Sample, PmSpan) { use stage::*; let forward = |s: Sample| Sample { before: ahead(s.before), after: ahead(s.after), ..s }; let stalled = if hpet.before >= STALLED_AT.load(Ordering::Relaxed) { STALL_TICKS.load(Ordering::Relaxed) } else { 0 }; let hpet = Sample { reference: hpet.reference - stalled, ..forward(hpet) }; - (hpet, pm.map(|(from, to)| (from, forward(to))), edge.map(|(seen, at)| (seen, ahead(at)))) + (hpet, pm.map(|(from, to)| (from, forward(to)))) } /// `tsc-step`'s and `hpet-stall`'s first, at the clock's first report. diff --git a/kernel/src/arch/x86_64/idt/mod.rs b/kernel/src/arch/x86_64/idt/mod.rs index cf6fdfa523a..99bb8b154f9 100644 --- a/kernel/src/arch/x86_64/idt/mod.rs +++ b/kernel/src/arch/x86_64/idt/mod.rs @@ -1,7 +1,6 @@ pub(crate) mod exceptions; mod device_irq; mod dma_fault; -mod rtc; mod isa; mod kick; mod nmi; @@ -37,9 +36,6 @@ pub const ISA_VECTORS: [u8; crate::isa::MAX_ROWS] = [Vector::Isa0 as u8, Vector: /// The vector an IOMMU writes into its own `FEDATA`. pub const DMA_FAULT_VECTOR: u8 = Vector::DmaFault as u8; -/// The vector the RTC's IRQ8 is routed to. -pub const RTC_VECTOR: u8 = Vector::Rtc as u8; - const PF_PRESENT: u64 = 1 << 0; const PF_WRITE: u64 = 1 << 1; const PF_INSTRUCTION_FETCH: u64 = 1 << 4; @@ -246,7 +242,6 @@ idt_vectors! { Nmi = 0x02, nmi::nmi_entry, ist 2; Timer = 0x20, timer::timer_entry; DmaFault = 0x25, dma_fault::dma_fault_entry; - Rtc = 0x26, rtc::rtc_entry; Isa0 = 0x2C, isa::isa0_entry; Isa1 = 0x2D, isa::isa1_entry; Isa2 = 0x2E, isa::isa2_entry; diff --git a/kernel/src/arch/x86_64/idt/rtc.rs b/kernel/src/arch/x86_64/idt/rtc.rs deleted file mode 100644 index a74244af2df..00000000000 --- a/kernel/src/arch/x86_64/idt/rtc.rs +++ /dev/null @@ -1,12 +0,0 @@ -use super::device_irq::device_irq_entry; - -extern "sysv64" fn rtc_handler() { - crate::arch::percpu::irq_took!(Rtc); - crate::arch::rtc::update_ended(); - crate::arch::apic::eoi(); -} - -device_irq_entry! { - /// The RTC's IRQ8 entry (see `device_irq_entry` for the asm contract). - pub(super) fn rtc_entry => rtc_handler -} diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs index a195f3bff0a..fda1932a0a7 100644 --- a/kernel/src/arch/x86_64/mod.rs +++ b/kernel/src/arch/x86_64/mod.rs @@ -5,22 +5,10 @@ //! [`percpu`] owns every `gs:` access; nothing outside this directory writes one. //! //! The PC platform's own devices live here too, because no other architecture -//! has them: the I/O APIC, the CMOS RTC, the chipset's watchdog, VT-d and +//! has them: the I/O APIC, the chipset's watchdog, VT-d and //! AMD-Vi. Generic code reaches each through the concept it serves //! (`watchdog`, `iommu_unit`, …), never by its name. -/// The interrupt sources only this architecture counts, handed to the -/// census's list (`irq_census`). -macro_rules! census_sources { - ($list:ident) => { - $list! { - /// Vector 0x26, the RTC's update-ended interrupt, once a second. - Rtc => "rtc", - } - }; -} -pub(crate) use census_sources; - pub mod acpi_mode; pub mod amdvi; pub mod apic; @@ -50,7 +38,6 @@ pub mod percpu; pub mod pio; pub mod pmu; pub mod power; -pub mod rtc; pub mod smi_cmd; pub mod smp; pub mod switch; diff --git a/kernel/src/arch/x86_64/pio.rs b/kernel/src/arch/x86_64/pio.rs index be5fc3065da..360c9d4d4f8 100644 --- a/kernel/src/arch/x86_64/pio.rs +++ b/kernel/src/arch/x86_64/pio.rs @@ -68,7 +68,6 @@ pub const PIC_PRIMARY: Declared = Declared::fixed(0x20, 2); pub const PIC_SECONDARY: Declared = Declared::fixed(0xA0, 2); /// POST codes, which nothing decodes: one bus cycle of delay. pub const POST: Declared = Declared::fixed(0x80, 1); -pub const CMOS: Declared = Declared::fixed(0x70, 2); /// The runs every machine has, and their holders. The PCI configuration /// mechanism is here though this kernel reaches configuration space through @@ -80,9 +79,9 @@ const FIXED: &[(&str, Declared, Mediated)] = &[ ("the 8259 pair", PIC_SECONDARY, Mediated::Kept), // A write nothing decodes costs the kernel nothing whoever makes it. ("the POST port", POST, Mediated::Open), - // Index and data: a holder's access between the kernel's two would move - // the index under it. - ("the CMOS RTC", CMOS, Mediated::Kept), + // Index and data, which nothing in ToyOS reads: a holder's write would + // set the clock firmware's `GetTime` answers the loader from. + ("the CMOS RTC", Declared::fixed(0x70, 2), Mediated::Kept), ("the PCI configuration mechanism", Declared::fixed(0xCF8, 1), Mediated::Kept), ("the PCI configuration mechanism", Declared::fixed(0xCFC, 4), Mediated::Kept), ]; diff --git a/kernel/src/arch/x86_64/rtc.rs b/kernel/src/arch/x86_64/rtc.rs deleted file mode 100644 index 5feb9256606..00000000000 --- a/kernel/src/arch/x86_64/rtc.rs +++ /dev/null @@ -1,273 +0,0 @@ -//! CMOS real-time clock: decoded once per boot into a [`Result`], never a -//! panic, because a read can be absent, wedged, or torn mid-update. -//! [`read`] takes the six-register set twice and accepts only two matching -//! reads, since a mid-update read is the only way two reads can disagree. - -use core::fmt; - -use crate::arch::cpu; -use crate::clock; -use crate::log; -use crate::time::{Bound, Duration}; -use toyos_wallclock::Civil; - -const CMOS_ADDR: super::pio::Port = super::pio::CMOS.port(0); -const CMOS_DATA: super::pio::Port = super::pio::CMOS.port(1); - -const SECONDS: u8 = 0x00; -const MINUTES: u8 = 0x02; -const HOURS: u8 = 0x04; -const DAY: u8 = 0x07; -const MONTH: u8 = 0x08; -const YEAR: u8 = 0x09; -const STATUS_A: u8 = 0x0A; -const STATUS_B: u8 = 0x0B; - -/// Register A bit 7: the clock registers are mid-update and must not be read. -const UPDATE_IN_PROGRESS: u8 = 1 << 7; -/// Register B bit 1: hours run 0..=23 rather than 1..=12 with a PM flag. -const HOUR_24: u8 = 1 << 1; -/// Register B bit 2: the registers hold binary rather than BCD. -const BINARY: u8 = 1 << 2; -/// Bit 7 of the hours register, in 12-hour mode only. -const PM: u8 = 1 << 7; - -/// Expiry is [`RtcFault::Updating`], not a retry or a panic. -/// 50x the hardware's own worst case: erring long only delays a boot whose -/// clock is already broken. -const MAX_UIP: Bound = Bound::from_spec( - Duration::from_millis(100), - "MC146818: the flag is raised 244us ahead of an update and cleared at most 1984us later", -); - -/// Four: three disagreements in a row means the registers never held one -/// instant. -const MAX_READ_ATTEMPTS: u32 = 4; - -/// Why this machine did not say what time it is. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RtcFault { - /// [`UPDATE_IN_PROGRESS`] never cleared inside [`MAX_UIP`]. - Updating, - /// No two of [`MAX_READ_ATTEMPTS`] reads agreed. - Unstable, - /// The registers agreed on a value that is not a valid date. - NotADate, -} - -impl fmt::Display for RtcFault { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Updating => write!( - f, - "its update flag never cleared in {} ms, which is what an absent one looks like", - MAX_UIP.duration().millis() - ), - Self::Unstable => write!( - f, - "no two of {MAX_READ_ATTEMPTS} reads agreed, so its registers never described one instant" - ), - Self::NotADate => write!(f, "its registers hold something that is not a date"), - } - } -} - -/// Current time; `century_reg` is `None` when firmware names no century -/// register. -pub fn read(century_reg: Option) -> Result { - // Requires the monotonic clock already running: this bound is a duration. - // An uncalibrated clock here is the kernel's own init order, not a - // hardware fault, so this fails fast rather than returning one. - assert!(clock::calibrated(), "rtc::read before the monotonic clock was calibrated"); - - let mut previous = read_registers(century_reg)?; - for _ in 1..MAX_READ_ATTEMPTS { - let current = read_registers(century_reg)?; - if current == previous { - return decode(current); - } - previous = current; - } - Err(RtcFault::Unstable) -} - -/// Equality between two reads is the evidence they describe one instant. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct Registers { - sec: u8, - min: u8, - hour: u8, - day: u8, - month: u8, - year: u8, - /// `None` means no century register, distinct from one holding zero. - century: Option, - status_b: u8, -} - -fn read_registers(century_reg: Option) -> Result { - wait_for_update()?; - Ok(Registers { - sec: cmos_read(SECONDS), - min: cmos_read(MINUTES), - hour: cmos_read(HOURS), - day: cmos_read(DAY), - month: cmos_read(MONTH), - year: cmos_read(YEAR), - century: century_reg.map(century_read), - status_b: cmos_read(STATUS_B), - }) -} - -/// Answers the *next* century, so a test can see the register's value reach -/// the target year. -fn century_read(reg: u8) -> u8 { - cmos_read(reg) -} - -fn wait_for_update() -> Result<(), RtcFault> { - let deadline = clock::nanos_since_boot() + MAX_UIP.nanos(); - while cmos_read(STATUS_A) & UPDATE_IN_PROGRESS != 0 { - if clock::nanos_since_boot() >= deadline { - return Err(RtcFault::Updating); - } - core::hint::spin_loop(); - } - Ok(()) -} - -/// Register B's format bits are read here; nothing past this point assumes -/// BCD or 12-hour. -fn decode(r: Registers) -> Result { - let binary = r.status_b & BINARY != 0; - let field = |raw: u8| { - if binary { Some(raw) } else { bcd_to_bin(raw) }.ok_or(RtcFault::NotADate) - }; - - let sec = field(r.sec)?; - let min = field(r.min)?; - let day = field(r.day)?; - let month = field(r.month)?; - let year_lo = field(r.year)?; - - let hour = if r.status_b & HOUR_24 != 0 { - field(r.hour)? - } else { - let afternoon = r.hour & PM != 0; - let twelve = field(r.hour & !PM)?; - if !(1..=12).contains(&twelve) { - return Err(RtcFault::NotADate); - } - twelve % 12 + if afternoon { 12 } else { 0 } - }; - - let year = match r.century { - Some(raw) => { - let century = field(raw)?; - // Below 1900 the register is unmaintained; refuse rather than - // fall back to the two-digit year. - if !(19..=99).contains(¢ury) { - return Err(RtcFault::NotADate); - } - century as u64 * 100 + year_lo as u64 - } - // 2000, not an older pivot: this kernel only boots UEFI machines. - None => 2000 + year_lo as u64, - }; - - let civil = Civil { - year, - month: month as u64, - day: day as u64, - hour: hour as u64, - min: min as u64, - sec: sec as u64, - }; - if !civil.is_valid() { - return Err(RtcFault::NotADate); - } - Ok(civil) -} - -/// `None` for a nibble above nine, which is not a digit and so not a time. -fn bcd_to_bin(bcd: u8) -> Option { - let (tens, ones) = (bcd >> 4, bcd & 0x0F); - (tens <= 9 && ones <= 9).then_some(tens * 10 + ones) -} - -fn port_read(reg: u8) -> u8 { - // SAFETY: `CMOS_ADDR`/`CMOS_DATA` are the fixed CMOS ports; `reg` is - // always this module's constants or the century index - // `acpi::rtc_century_register` already bounds below 0x80, so it never - // sets the index port's NMI-mask bit — no value makes the RTC do - // anything but answer a different byte. - unsafe { cpu::outb(CMOS_ADDR, reg) }; - cpu::inb(CMOS_DATA) -} - -/// The one substitution point for actuator-injected RTC faults; everything -/// downstream reads whatever this returns. -fn cmos_read(reg: u8) -> u8 { - port_read(reg) -} - -const STATUS_C: u8 = 0x0C; -/// Register B bit 4: raise IRQ8 as each update cycle ends. -const UPDATE_ENDED_INTERRUPT: u8 = 1 << 4; -/// Register C bit 4: an update cycle has ended since register C was last read. -const UPDATE_ENDED: u8 = 1 << 4; -/// ISA IRQ 8, which the RTC raises until register C is read (MC146818). -const RTC_IRQ: u8 = 8; - -/// The latest update-ended edge: how many the handler has seen, and the TSC -/// as it took the last. Published by IRQ8's handler on the one CPU it is routed to. -static EDGE: crate::seqlock::Published<2> = crate::seqlock::Published::new(); - -/// Raise IRQ8 at the end of each update cycle, once a second off the RTC's own -/// 32.768 kHz crystal, and take the TSC as each arrives: a reference outside -/// the chipset's 14.318 MHz domain, which the HPET and the ACPI PM timer share. -/// Called after the boot's [`read`], the only other CMOS access, so the index -/// port is the handler's alone from here. -pub fn arm_update_edges() -> Result<(), alloc::string::String> { - let line = super::ioapic::gsi_for_isa_irq(RTC_IRQ).ok_or("no I/O APIC carries IRQ8")?; - super::ioapic::route(line.gsi, super::idt::RTC_VECTOR, super::apic::MSG_DEST, line.trigger, line.polarity) - .map_err(|why| alloc::format!("{why:?}"))?; - let b = cmos_read(STATUS_B); - port_write(STATUS_B, b | UPDATE_ENDED_INTERRUPT); - let _ = cmos_read(STATUS_C); - super::ioapic::set_masked(line.gsi, false).map_err(|why| alloc::format!("{why:?}"))?; - // An update that ended while the pin was masked holds IRQ8 high with no - // edge the pin saw; reading C again lowers it, so the next update's edge - // arrives. Should the handler run first, it reads the same register. - let _ = cmos_read(STATUS_C); - log!( - "clock: the RTC's update-ended interrupt on gsi {} {} times the TSC against its 32.768 kHz crystal", - line.gsi.0, - super::ioapic::describe(line.trigger, line.polarity), - ); - Ok(()) -} - -/// IRQ8's handler: the TSC first, then register C, whose read lets the RTC -/// raise the next. -pub fn update_ended() { - let at = cpu::rdtsc_ordered(); - if cmos_read(STATUS_C) & UPDATE_ENDED != 0 { - let seen = EDGE.snapshot().map_or(0, |[seen, _]| seen); - EDGE.publish([seen + 1, at]); - } -} - -/// The edges seen and the TSC at the last, or `None` before the first. -pub fn last_edge() -> Option<(u64, u64)> { - EDGE.snapshot().filter(|&[seen, _]| seen > 0).map(|[seen, at]| (seen, at)) -} - -fn port_write(reg: u8, value: u8) { - // SAFETY: as `port_read`'s: `reg` is this module's constant, below 0x80, - // and the byte only enables the update-ended interrupt. - unsafe { - cpu::outb(CMOS_ADDR, reg); - cpu::outb(CMOS_DATA, value); - } -} diff --git a/kernel/src/clock.rs b/kernel/src/clock.rs index aa6903bfd49..b5890b601ac 100644 --- a/kernel/src/clock.rs +++ b/kernel/src/clock.rs @@ -1,10 +1,9 @@ //! The machine's clocks: monotonic since boot, read off the CPU's free-running //! counter along the line the architecture's boot starts in [`set_counter`] //! and may retune once it measures the counter's rate better ([`retune`]); -//! wall-clock, the architecture's one reading of the RTC — on x86-64 a CMOS -//! read, which can block for up to a second, and on AArch64 firmware's, which -//! the loader hands over — anchored in [`init_wall`] and answered after as -//! that reading plus [`nanos_since_boot`]; and the log's stamp ([`stamp`]), the +//! wall-clock, firmware's reading the loader hands over, anchored in +//! [`init_wall`] and answered after as that reading plus +//! [`nanos_since_boot`]; and the log's stamp ([`stamp`]), the //! same clock counted from the counter's zero, which is where every line of //! the log — the loader's, the kernel's and every program's — counts from. //! @@ -15,6 +14,7 @@ use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicU64, Ordering::{Acquire, Relaxed, Release}}; use alloc::sync::Arc; +use toyos_abi::boot::KernelArgs; use toyos_abi::clock::{Clock, ClockPage, Line}; use crate::arch::cpu; @@ -240,35 +240,37 @@ static BOOT_SECS: AtomicU64 = AtomicU64::new(0); /// Whether the above means anything; zero is a valid instant, not a sentinel. static WALL_KNOWN: AtomicBool = AtomicBool::new(false); -/// Anchors the wall clock, once, after [`set_counter`], to the architecture's -/// reading of the RTC, which keeps UTC: the instant, and the counter when it -/// was true, which on a machine whose loader read it is before boot. -pub fn init_wall(reading: Result<(Civil, u64), impl core::fmt::Display>) { - let (civil, at) = match reading { - Ok(reading) => reading, - Err(fault) => { - log!("clock: this machine will not say what time it is — {fault}"); +/// Anchors the wall clock, once, after [`set_counter`], to firmware's +/// `GetTime`, which the loader asked before `ExitBootServices` since the +/// kernel calls no UEFI service: the instant, UTC, and the counter when it was +/// true, which is before boot. +pub fn init_wall(args: &KernelArgs) { + let (secs, at) = match args.wall_clock_known { + 0 => { + log!("clock: this machine will not say what time it is: the loader handed no reading of firmware's GetTime"); return; } + 1 => (args.wall_clock_secs, args.wall_clock_counter), + other => panic!("the loader wrote {other} as wall_clock_known, which is 0 or 1"), }; let clock = clock().expect("clock: the wall clock anchored before the counter started"); - BOOT_SECS.store(anchor(civil.to_unix_secs(), clock.stamp_of(at), clock.stamp_at_boot), Relaxed); + BOOT_SECS.store(anchor(secs, clock.stamp_of(at), clock.stamp_at_boot), Relaxed); WALL_KNOWN.store(true, Release); - log!("clock: the RTC reads {civil} UTC"); + log!("clock: firmware's clock read {} UTC", Civil::from_unix_secs(secs)); } /// Unix seconds, now — what `SYS_CLOCK_EPOCH` serves: the whole seconds of -/// [`utc_nanos`]. `None` if the RTC never answered. +/// [`utc_nanos`]. `None` if firmware never answered. pub fn utc_secs() -> Option { utc_nanos().map(|nanos| nanos / NANOS_PER_SEC) } const NANOS_PER_SEC: u64 = 1_000_000_000; -/// Nanoseconds since the Unix epoch, UTC: the RTC's whole-second reading carried -/// on by the counter, so its resolution is the counter's and its accuracy the -/// RTC's second. +/// Nanoseconds since the Unix epoch, UTC: firmware's whole-second reading +/// carried on by the counter, so its resolution is the counter's and its +/// accuracy firmware's second. pub fn utc_nanos() -> Option { WALL_KNOWN .load(Acquire) diff --git a/kernel/src/drivers/acpi.rs b/kernel/src/drivers/acpi.rs index e4c89b5761d..92e7285c92f 100644 --- a/kernel/src/drivers/acpi.rs +++ b/kernel/src/drivers/acpi.rs @@ -13,7 +13,7 @@ use core::mem::size_of; use core::ptr::{read_unaligned, read_volatile}; use crate::log; use crate::DirectMap; -use toyos_acpi::{Century, MadtEntry, Mapped, Memory, CMOS_RAM, MADT_ENTRIES, SDT_HEADER_LEN, SDT_REVISION}; +use toyos_acpi::{MadtEntry, Mapped, Memory, MADT_ENTRIES, SDT_HEADER_LEN, SDT_REVISION}; pub use toyos_acpi::{IoApicEntry, SourceOverride, TableError}; @@ -167,30 +167,6 @@ pub fn ecam_windows(rsdp_addr: u64) -> Vec { windows } -/// Which CMOS register holds the RTC's century, as the FADT names it. -// `Ok(None)` is "no century register", distinct from `Err`, which the caller must not treat as one. -pub fn rtc_century_register(rsdp_addr: u64) -> Result, TableError> { - let named = toyos_acpi::rtc_century(direct_phys(), rsdp_addr)?; - match named { - Century::Absent => { - log!("ACPI: the FADT names no RTC century register"); - Ok(None) - } - Century::OutOfRange(index) => { - log!( - "ACPI: the FADT puts the RTC century register at CMOS {index:#04x}, outside {:#04x}..={:#04x} — ignoring it", - CMOS_RAM.start(), - CMOS_RAM.end() - ); - Ok(None) - } - Century::At(index) => { - log!("ACPI: the FADT puts the RTC century register at CMOS {index:#04x}"); - Ok(Some(index)) - } - } -} - /// Given the RSDP address, parse XSDT -> HPET table -> return HPET MMIO base address. pub fn find_hpet_base(rsdp_addr: u64) -> Option { let base = match toyos_acpi::hpet_base(direct_phys(), rsdp_addr) { diff --git a/kernel/src/irq_census.rs b/kernel/src/irq_census.rs index 42d80bb1027..b61ed7a8c3f 100644 --- a/kernel/src/irq_census.rs +++ b/kernel/src/irq_census.rs @@ -8,50 +8,41 @@ use core::sync::atomic::{AtomicU64, Ordering}; use crate::arch::percpu; -/// The census's sources: these, and between the device vectors and the IPIs, -/// those the architecture names (`arch::census_sources!`). -macro_rules! sources { - ($($(#[$meta:meta])* $arch:ident => $name:literal,)*) => { - /// One variant per interrupt source this kernel counts; order matches `arch::idt`'s vector table. - /// Adding a device vector means adding a variant here, or the architecture's, and one `irq_took!` at its handler. - /// A variant nothing counts does not compile: `irq_took!` is the only reference, so `-D dead-code` refuses an unconstructed one. - #[derive(Clone, Copy, PartialEq, Eq, Debug)] - #[repr(usize)] - pub enum Source { - /// Vector 0x20, this CPU's LAPIC one-shot: an expiry and nothing else. - Timer, - /// Vector 0xFC, `irqchip::kick_cpu`'s IPI. - Kick, - /// Vectors 0x28-0x2C: the MSI-X of a PCI function a process drives, and - /// the lines of an ISA function one does. One count for all five: which - /// claim an interrupt belonged to is the claim's own record, and the census - /// is about this machine's interrupt routing. - UserDev, - /// Vector 0x25, the remapping unit's fault event. - DmaFault, - $($(#[$meta])* $arch,)* - /// Vector 0xFE, the TLB shootdown IPI. - Tlb, - /// Vector 0x02, which `crate::hardlockup` samples a CPU with. - Nmi, - /// Vector 0xFF, the local APIC's spurious vector. - /// A non-zero count on a machine that staged nothing is an interrupt-routing defect. - Spurious, - /// Every vector no `idt_vectors!` row claims — `arch::trap::unclaimed`. - /// A non-zero count a boot staged nothing for is a routing defect the gate kept off `#DF`. - Unclaimed, - } - - impl Source { - /// Order `tests/common/irqcensus.rs` parses back; must match variant order. - pub const NAMES: &'static [&'static str] = - &["timer", "kick", "userdev", "dmafault", $($name,)* "tlb", "nmi", "spurious", "unclaimed"]; - pub const COUNT: usize = Self::NAMES.len(); - } - }; +/// One variant per interrupt source this kernel counts; order matches `arch::idt`'s vector table. +/// Adding a device vector means adding a variant here and one `irq_took!` at its handler. +/// A variant nothing counts does not compile: `irq_took!` is the only reference, so `-D dead-code` refuses an unconstructed one. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[repr(usize)] +pub enum Source { + /// Vector 0x20, this CPU's LAPIC one-shot: an expiry and nothing else. + Timer, + /// Vector 0xFC, `irqchip::kick_cpu`'s IPI. + Kick, + /// Vectors 0x28-0x2C: the MSI-X of a PCI function a process drives, and + /// the lines of an ISA function one does. One count for all five: which + /// claim an interrupt belonged to is the claim's own record, and the census + /// is about this machine's interrupt routing. + UserDev, + /// Vector 0x25, the remapping unit's fault event. + DmaFault, + /// Vector 0xFE, the TLB shootdown IPI. + Tlb, + /// Vector 0x02, which `crate::hardlockup` samples a CPU with. + Nmi, + /// Vector 0xFF, the local APIC's spurious vector. + /// A non-zero count on a machine that staged nothing is an interrupt-routing defect. + Spurious, + /// Every vector no `idt_vectors!` row claims — `arch::trap::unclaimed`. + /// A non-zero count a boot staged nothing for is a routing defect the gate kept off `#DF`. + Unclaimed, } -crate::arch::census_sources!(sources); +impl Source { + /// Order `tests/common/irqcensus.rs` parses back; must match variant order. + pub const NAMES: &'static [&'static str] = + &["timer", "kick", "userdev", "dmafault", "tlb", "nmi", "spurious", "unclaimed"]; + pub const COUNT: usize = Self::NAMES.len(); +} /// Every source's count summed, bar the NMI's. fn sum_bar_nmi(counts: &[u64; Source::COUNT]) -> u64 { diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 20dd724630c..58b548b66a8 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -374,7 +374,8 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { symbols::set_kernel_base(kernel_args.kernel_memory_addr); symbols::load_kernel(kernel_image.symbols, mm::PHYS_OFFSET + kernel_args.kernel_memory_addr); - arch::boot::clock(kernel_args); + arch::boot::clock(kernel_args.rsdp_addr); + clock::init_wall(kernel_args); trace::enable(); arch::boot::timer(); // After both halves of what it needs: a counter period to convert its bound diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index bd7b8d3d927..0d27da4113c 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -193,8 +193,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> SYS_SET_THREAD_POINTER => sys_set_thread_pointer(a1), SYS_THREAD_JOIN => sys_thread_join(a1), SYS_YIELD => sys_yield(), - // Answers from the boot-time anchor, never the CMOS: NotSupported beats a - // 1970-epoch number a caller cannot tell from real time. + // NotSupported beats a 1970-epoch number a caller cannot tell from real time. SYS_CLOCK_EPOCH => { crate::clock::utc_secs().unwrap_or(SyscallError::NotSupported.to_u64()) } diff --git a/kernel/src/time.rs b/kernel/src/time.rs index e82238b6824..8115d92b6f7 100644 --- a/kernel/src/time.rs +++ b/kernel/src/time.rs @@ -1,4 +1,4 @@ -//! A duration is typed by what its expiry means: [`Bound`], [`Cadence`], +//! A duration is typed by what its expiry means: [`Cadence`], //! [`Tripwire`], [`Deadline`], [`Floor`], [`Budget`], [`Delay`]. No kind is //! constructible from a bare magnitude; every constructor takes a //! `&'static str` justification beside the number. @@ -116,36 +116,6 @@ impl Deadline { } } -/// The contract that says the thing will happen: sourced from a device -/// register or a cited spec section, and its expiry means the device broke — -/// a named error, never a retry. -#[derive(Clone, Copy)] -pub struct Bound { - limit: Duration, - cite: &'static str, -} - -impl Bound { - /// A bound a specification states. `cite` is the section. - pub const fn from_spec(limit: Duration, cite: &'static str) -> Self { - Self { limit, cite } - } - - pub const fn duration(self) -> Duration { - self.limit - } - - pub const fn nanos(self) -> u64 { - self.limit.nanos() - } -} - -impl fmt::Display for Bound { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} ({})", self.limit, self.cite) - } -} - /// How often a thing may be re-done, and what makes that rate affordable. /// Nothing expires; a `Cadence` is a rate, not a register-poll limit. #[derive(Clone, Copy)] @@ -171,9 +141,9 @@ impl fmt::Display for Cadence { } /// A duration whose expiry is a **panic**: the machine is broken, fail fast. -/// The argument is how long is absurd, not where the number came from — a -/// tripwire that could be cited would be a [`Bound`]. A tripwire smaller than -/// the largest [`Bound`] on the path it covers panics a healthy machine. +/// The argument is how long is absurd, not where the number came from. A +/// tripwire smaller than the longest wait a specification allows on the path +/// it covers panics a healthy machine. #[derive(Clone, Copy)] pub struct Tripwire { limit: Duration, diff --git a/lib/acpi/src/fadt.rs b/lib/acpi/src/fadt.rs index b2c0b112236..1c9f1bb3d8e 100644 --- a/lib/acpi/src/fadt.rs +++ b/lib/acpi/src/fadt.rs @@ -2,13 +2,12 @@ use core::num::NonZeroU8; -use crate::{find_table, Phys, Table, TableError, SDT_REVISION}; +use crate::{Phys, Table, SDT_REVISION}; /// Table 5.9 offsets, from the start of the table. pub(crate) const FADT_FIRMWARE_CTRL: usize = 36; pub const FADT_DSDT: usize = 40; pub const FADT_PM1A_CNT_BLK: usize = 64; -const FADT_CENTURY: usize = 108; const FADT_FLAGS: usize = 112; /// A Generic Address Structure (§5.2.3.2): space at +0, bit width at +1, bit offset at +2, address at +4. const FADT_RESET_REG: usize = 116; @@ -28,37 +27,6 @@ const SPACE_SYSTEM_MEMORY: u8 = 0; const SPACE_SYSTEM_IO: u8 = 1; const SPACE_PCI_CONFIG: u8 = 2; -/// What the FADT says about the RTC's century register. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Century { - /// The century field is zero: this firmware names no register. - Absent, - /// Outside CMOS RAM, so it is not a century register whatever it is. - OutOfRange(u8), - At(u8), -} - -/// 0x80+ selects with NMI-mask bit 7 set; below 0x0E is the RTC's own clock/status regs, not a century register. -pub const CMOS_RAM: core::ops::RangeInclusive = 0x0E..=0x7F; - -/// Which CMOS register holds the RTC's century, as the FADT names it. -pub fn rtc_century(phys: P, rsdp_addr: u64) -> Result { - const NEEDED: usize = FADT_CENTURY + 1; - let fadt = find_table(phys, rsdp_addr, b"FACP", NEEDED)?; - let declared = fadt - .byte(FADT_CENTURY) - .ok_or(TableError::Length { declared: fadt.len() as u32, needed: NEEDED })?; - Ok(century_of(declared)) -} - -pub fn century_of(index: u8) -> Century { - match index { - 0 => Century::Absent, - i if CMOS_RAM.contains(&i) => Century::At(i), - i => Century::OutOfRange(i), - } -} - /// The 8-bit System I/O port the FADT names and the byte to write there, or the /// field that refused one — the alternative being a guessed port, and a guess /// writes a byte to whatever else lives there. diff --git a/lib/acpi/src/lib.rs b/lib/acpi/src/lib.rs index 6b320540b10..0debb9e6251 100644 --- a/lib/acpi/src/lib.rs +++ b/lib/acpi/src/lib.rs @@ -32,9 +32,8 @@ use toyos_bootmap::DirectMapEnd; pub use ecdt::{ecdt, Ec, EcRefused, EcRegister, ECDT_NEEDED}; pub use facs::{acquire, facs, release, Facs, FacsRefused, FACS_GLOBAL_LOCK, OWNED, PENDING}; pub use fadt::{ - century_of, dsdt_address, fixed_hardware, pm1a_control, pm_timer, psci, reset_register, - rtc_century, - Century, Field, FixedHardware, FixedRefused, LegacyMode, PmTimer, PowerButton, Psci, Reset, SmiCmd, CMOS_RAM, + dsdt_address, fixed_hardware, pm1a_control, pm_timer, psci, reset_register, + Field, FixedHardware, FixedRefused, LegacyMode, PmTimer, PowerButton, Psci, Reset, SmiCmd, FADT_FOR_FIXED_HARDWARE, FADT_PM1A_CNT_BLK, }; use fadt::FADT_DSDT; diff --git a/lib/acpi/tests/corpus.rs b/lib/acpi/tests/corpus.rs index 41825050963..12f4c07ca9c 100644 --- a/lib/acpi/tests/corpus.rs +++ b/lib/acpi/tests/corpus.rs @@ -12,7 +12,7 @@ use common::{declare_len, entry, madt, rsdp, sdt, xsdt, Machine}; use toyos_abi::acpi::Block; use toyos_acpi::{ definition_blocks, dsdt_address, ecam_allocations, find_by_id, find_table, fixed_hardware, hpet_base, isa_line, - madt_entries, pm1a_control, psci, reset_register, rtc_century, sci_line, Century, + madt_entries, pm1a_control, psci, reset_register, sci_line, Field, FixedRefused, LegacyMode, Line, MadtEntry, MadtHalt, Phys, Polarity, PowerButton, Psci, SmiCmd, Reset, SourceOverride, Table, TableError, Trigger, FADT_FOR_FIXED_HARDWARE, MADT_ENTRIES, MAX_TABLE_LEN, @@ -236,13 +236,12 @@ fn an_rsdp_address_the_reader_cannot_reach_is_refused() { assert_eq!(hpet_base(Machine { regions }, RSDP_AT).err(), Some(TableError::BadRsdp)); } -/// An ACPI 1.0-length FADT: 116 bytes, no `X_DSDT`, and the century byte still -/// inside it. The 32-bit `DSDT` is what such a table names. +/// An ACPI 1.0-length FADT: 116 bytes, no `X_DSDT`. The 32-bit `DSDT` is +/// what such a table names. #[test] fn a_fadt_of_the_acpi_1_0_length_is_read_without_its_x_fields() { let mut body = vec![0u8; 80]; body[40 - 36..44 - 36].copy_from_slice(&0x7ffb_9000u32.to_le_bytes()); - body[108 - 36] = 0x32; let t = sdt(b"FACP", 1, &body); assert_eq!(t.len(), 116); let head = rsdp(XSDT_AT, 2, 36); @@ -252,20 +251,6 @@ fn a_fadt_of_the_acpi_1_0_length_is_read_without_its_x_fields() { let fadt = find_table(m, RSDP_AT, b"FACP", 109).expect("FADT"); assert_eq!(dsdt_address(&fadt), 0x7ffb_9000); - assert_eq!(rtc_century(m, RSDP_AT), Ok(Century::At(0x32))); -} - -/// A FADT that ends with the century byte reads it. -#[test] -fn a_fadt_that_ends_at_the_century_byte_reads_it() { - let t = sdt(b"FACP", 1, &[0u8; 73]); - assert_eq!(t.len(), 109); - let head = rsdp(XSDT_AT, 2, 36); - let root = xsdt(&[TABLE_AT]); - let regions: &[(u64, &[u8])] = &[(RSDP_AT, &head), (XSDT_AT, &root), (TABLE_AT, &t)]; - let m = Machine { regions }; - // The century byte at 108 is the last one it holds. - assert_eq!(rtc_century(m, RSDP_AT), Ok(Century::Absent)); } #[test] @@ -308,26 +293,6 @@ fn a_revision_1_fadt_long_enough_to_hold_x_dsdt_is_still_read_at_its_32_bit_fiel assert_eq!(dsdt_address(&fadt), 0xdead_beef_0000); } -#[test] -fn a_century_register_outside_cmos_ram_is_told_apart_from_none() { - for (byte, want) in [ - (0u8, Century::Absent), - (0x0d, Century::OutOfRange(0x0d)), - (0x80, Century::OutOfRange(0x80)), - (0xff, Century::OutOfRange(0xff)), - (0x0e, Century::At(0x0e)), - (0x7f, Century::At(0x7f)), - ] { - let mut body = vec![0u8; 80]; - body[108 - 36] = byte; - let t = sdt(b"FACP", 1, &body); - let head = rsdp(XSDT_AT, 2, 36); - let root = xsdt(&[TABLE_AT]); - let regions: &[(u64, &[u8])] = &[(RSDP_AT, &head), (XSDT_AT, &root), (TABLE_AT, &t)]; - assert_eq!(rtc_century(Machine { regions }, RSDP_AT), Ok(want), "century byte {byte:#04x}"); - } -} - const FIXTURES: &[(&str, &[u8], u64)] = &[ ("rsdp", include_bytes!("../fixtures/qemu-11.1.1/rsdp.bin"), 0x7fb7_e014), ("xsdt", include_bytes!("../fixtures/qemu-11.1.1/xsdt.bin"), 0x7fb7_d0e8), @@ -341,7 +306,7 @@ const FIXTURES: &[(&str, &[u8], u64)] = &[ /// **No panic and no unbounded walk, over every byte of every table this crate /// decodes.** Each byte of each fixture takes each of its 255 other values in -/// turn, and the MCFG walk, `hpet_base`, `rtc_century` and the +/// turn, and the MCFG walk, `hpet_base` and the /// MADT walk to exhaustion all run over it. A panic anywhere — including the /// reader's own, which fires on a read no bound accepted — reds this. /// @@ -387,7 +352,6 @@ fn no_single_byte_mutation_of_a_real_table_panics_or_runs_away() { assert!(allocations.count() <= MAX_TABLE_LEN / 16 + 1, "{which}[{offset}]={value:#04x}: the MCFG walk is not ending"); } let _ = hpet_base(m, rsdp_at); - let _ = rtc_century(m, rsdp_at); if let Ok(blocks) = definition_blocks(m, rsdp_at) { // The DSDT, and at most an item an 8-byte entry of the largest XSDT. assert!(blocks.count() <= 1 + MAX_TABLE_LEN / 8, "{which}[{offset}]={value:#04x}: the block walk is not ending"); diff --git a/lib/acpi/tests/fixtures.rs b/lib/acpi/tests/fixtures.rs index 573bd9ca380..904a112b1ce 100644 --- a/lib/acpi/tests/fixtures.rs +++ b/lib/acpi/tests/fixtures.rs @@ -6,9 +6,9 @@ mod common; use common::Machine; use toyos_abi::acpi::Block; use toyos_acpi::{ - century_of, definition_blocks, dsdt_address, ecam_allocations, find_table, fixed_hardware, hpet_base, - isa_line, madt_entries, pm1a_control, pm_timer, psci, reset_register, rtc_century, sci_line, PmTimer, - Century, FixedHardware, IoApicEntry, Line, MadtEntry, Polarity, PowerButton, Psci, Reset, SmiCmd, + definition_blocks, dsdt_address, ecam_allocations, find_table, fixed_hardware, hpet_base, + isa_line, madt_entries, pm1a_control, pm_timer, psci, reset_register, sci_line, PmTimer, + FixedHardware, IoApicEntry, Line, MadtEntry, Polarity, PowerButton, Psci, Reset, SmiCmd, SourceOverride, TableError, Trigger, FADT_FOR_FIXED_HARDWARE, FADT_PM1A_CNT_BLK, MADT_ENTRIES, }; @@ -97,14 +97,6 @@ fn the_hpet_table_names_the_window_the_timer_driver_mapped() { assert_eq!(hpet_base(machine(), RSDP), Ok(0xfed0_0000)); } -/// `ACPI: the FADT puts the RTC century register at CMOS 0x32`. -#[test] -fn the_fadt_names_the_century_register_the_wall_clock_read() { - assert_eq!(rtc_century(machine(), RSDP), Ok(Century::At(0x32))); - // The actuator's override is applied to the raw byte, not to the verdict. - assert_eq!(century_of(0), Century::Absent); -} - /// `ACPI: PM1a=0x604`, and the DSDT this boot found `\_S5_` in. #[test] fn the_fadt_names_the_power_block_and_the_dsdt() { diff --git a/lib/fat32/src/time.rs b/lib/fat32/src/time.rs index 63d96087058..32cfce80ce0 100644 --- a/lib/fat32/src/time.rs +++ b/lib/fat32/src/time.rs @@ -4,7 +4,7 @@ use toyos_wallclock::Civil; /// /// This crate never reads a clock. Every call that stamps an entry takes one /// of these, so the only time that reaches the disk is time the caller chose — -/// the kernel's RTC in production, a fixed value in a test that wants a +/// the kernel's wall clock in production, a fixed value in a test that wants a /// reproducible image. /// /// FAT's encoding is lossy in three ways that no wrapper can hide, so they are diff --git a/lib/wallclock/src/lib.rs b/lib/wallclock/src/lib.rs index 33fe876339a..82d9ce2d208 100644 --- a/lib/wallclock/src/lib.rs +++ b/lib/wallclock/src/lib.rs @@ -12,7 +12,7 @@ const DAY: u64 = 86_400; /// A wall-clock instant in the fields a human reads. /// -/// The one calendar in the tree. The RTC decodes its registers into this, the +/// The one calendar in the tree. The loader reads firmware's time into this, the /// log's file names come out of it, the compositor's clock reads it and FAT's /// directory stamps are built from it — so there is one conversion between /// seconds and dates rather than one per caller. @@ -37,8 +37,8 @@ impl Civil { /// /// The Unix epoch is the floor because everything downstream counts /// unsigned seconds from it. A leap second lands on 60 and is rejected: the - /// RTC does not report one, and a clock that does is not one this kernel - /// understands. + /// firmware's clock does not report one, and a clock that does is not one + /// this kernel understands. pub fn is_valid(&self) -> bool { (1970..=9999).contains(&self.year) && (1..=12).contains(&self.month) @@ -59,8 +59,8 @@ impl Civil { /// needs: a timestamp is not load-bearing enough to fail a volume read /// over, and its `every_bit_pattern_decodes` asserts it over all 65,536 /// date encodings. [`Self::is_valid`] is the *other* caller's answer — the - /// RTC's — and refusing there is what keeps an impossible instant out of - /// the wall clock. + /// loader's, of firmware's reading — and refusing there is what keeps an + /// impossible instant out of the wall clock. pub fn to_unix_secs(&self) -> u64 { days_from_civil(self.year, self.month, self.day) * DAY + self.hour * 3_600 diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index a27d64aafb3..f33ce7547c5 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -765,7 +765,7 @@ pub struct Stat { /// When the file was last written: nanoseconds since the Unix epoch, UTC, /// off the wall clock at the write, to the resolution its mount keeps — /// the nanosecond on `/tmp` and DATA, two seconds at the flush on FAT. 0 is - /// undated: written on a machine whose RTC never answered, or shipped in + /// undated: written on a machine whose firmware gave no time, or shipped in /// ROOT's reproducible image. pub mtime: u64, /// The volume the file is on; 0 for an object that is no file of any @@ -1305,10 +1305,10 @@ pub fn random(buf: &mut [u8]) -> Result<(), SyscallError> { /// Seconds since the Unix epoch (1970-01-01 00:00:00 UTC). /// -/// `None` is a machine that never said what time it is — an RTC that is absent, -/// wedged, or answering with something that is not a date. It is `None` for the -/// whole of such a boot rather than intermittently, because the kernel reads -/// the clock once. +/// `None` is a machine that never said what time it is: firmware's `GetTime` +/// failed, or answered something that is not a date. It is `None` for the +/// whole of such a boot rather than intermittently, because the loader asks +/// once. /// Cheap: the kernel serves it from an anchor it took at boot plus the /// monotonic clock, so this is a syscall and not a device access. pub fn clock_epoch() -> Option { diff --git a/system/fileserver/src/volume.rs b/system/fileserver/src/volume.rs index 2b29716cbd3..4a18f7e4dd7 100644 --- a/system/fileserver/src/volume.rs +++ b/system/fileserver/src/volume.rs @@ -397,7 +397,7 @@ pub fn take_anchor() { /// What a file written now is stamped with (`toyos_abi::syscall::Stat::mtime`): /// nanoseconds since the Unix epoch, UTC, and 0 — undated — on a machine whose -/// RTC never answered. +/// firmware gave no time. pub fn now_nanos() -> u64 { let anchor = ANCHOR.get().expect("fileserver takes the anchor at its start"); anchor.map_or(0, |secs| secs.saturating_mul(NANOS_PER_SEC).saturating_add(nanos_since_boot())) diff --git a/system/logkeeper/src/main.rs b/system/logkeeper/src/main.rs index c4237b1953b..59f03b29ee3 100644 --- a/system/logkeeper/src/main.rs +++ b/system/logkeeper/src/main.rs @@ -152,7 +152,7 @@ fn main() { // The supervisor connected before it started anything, so this is already queued. let from_supervisor: Connection = acceptor.accept().expect("logkeeper: the supervisor's origins connection"); - // The wall clock, read once. The kernel reads the RTC once too, so a second + // The wall clock, read once. The kernel anchors it once too, so a second // reading later in the boot would answer out of the same anchor and tell // this program nothing new. let (stem, boot_secs, dated) = boot_stamp(); diff --git a/tests/checks.rs b/tests/checks.rs index fd4d2f856f8..a9967250663 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -368,7 +368,7 @@ mod checks { const PROGRAM: &str = "[2026-11-03 22:34:24 13.800 test-runner pid=7] counters_metal settle: the log holds this first line"; const BEFORE: &str = "[2026-11-03 22:34:23 12.094 cpu2 kernel] isa: a line before the second"; const REFINED: &str = "[2026-11-04 06:25:59 13.700 cpu5 kernel] clock: over 1 s the TSC counted 2419185510351 mHz against the HPET, within 11437 ppb; the clock keeps CPUID 15H's 2419200000000 mHz"; - const STEP: &str = "[2026-11-04 06:26:03 13.750 cpu12 kernel] clock: the TSC reads 1718866 ns from the chipset's counters over the last span; the RTC's edges after it will say which moved"; + const STEP: &str = "[2026-11-04 06:26:03 13.750 cpu12 kernel] clock: TSC STEP: the TSC stepped 1718866 ns forward against the HPET and the PM timer; the clock slews it away at most 500 ppm, most of it in about 3437 ms"; const SLEWED: &str = "[2026-11-04 06:26:03 13.760 cpu12 kernel] clock: the TSC's step is slewed away, 601 ns left"; let log = |lines: &[&str]| lines.join("\n"); @@ -646,6 +646,71 @@ mod checks { Ok(()) } + /// A line dated by the loader's reading carried on the log's clock + /// passes, to the second it lands in; an anchor ten seconds off is red, + /// whichever way; and a bench whose clock stands two hours east carries + /// its reading as well as any, and is red as the bench's. + #[test] + fn a_date_off_the_loaders_reading_is_red_and_a_bench_off_utc_is_the_benchs() { + const RATE: &str = "[10.300 cpu0 kernel] clock: the TSC counts at 1000000000 Hz, as CPUID 15H states, held to the HPET from a reading 90 cycles wide\n"; + // Read 10.000 s on the log's clock, so a line at 12.165 is dated two seconds on. + let loader = |read: &str| format!("[10.000 cpu0 loader] Wall clock: firmware's GetTime reads {read} UTC at counter 10000000000\n"); + let log = |dated: &str| format!("[{dated} 12.165 cpu0 kernel] Boot: complete (1165ms)\n"); + let carried = |read: &str, dated: &str| dates_carry_the_loaders_reading(&loader(read), RATE, &log(dated)); + let read = carried("2026-09-29 18:22:37", "2026-09-29 18:22:39").expect("a line dated by the reading"); + for dated in ["2026-09-29 18:22:37", "2026-09-29 18:22:40"] { + assert_eq!(carried("2026-09-29 18:22:37", dated), Ok(read), "{dated} is inside what the divisions drop"); + } + for dated in ["2026-09-29 18:22:49", "2026-09-29 18:22:29", "2026-09-29 18:22:36", "2026-09-29 18:22:41"] { + let refused = carried("2026-09-29 18:22:37", dated).expect_err(dated); + assert!(refused.contains("from the loader's reading"), "{refused}"); + } + assert!(dates_carry_the_loaders_reading(&loader("2026-09-29 18:22:37"), RATE, "[12.165 cpu0 kernel] undated\n").is_err()); + + let boot = metal::Readback::new("boot", String::new(), String::new(), &metal_checks::boot_file()).expect("a readback"); + assert_eq!(bench_clock_keeps_utc(read, boot.ran_within()), Ok(())); + let east = carried("2026-09-29 20:22:37", "2026-09-29 20:22:39").expect("a bench off UTC carries its reading too"); + for off in [east, read - 900] { + let refused = bench_clock_keeps_utc(off, boot.ran_within()).expect_err("a bench off UTC"); + assert!(refused.contains("this bench's hardware clock stands"), "{refused}"); + } + } + + /// The staged stall's row reads the staged span alone: left out with the + /// PM timer 10 ms from the HPET and a report after it, on a bench whose + /// TSC steps for real in another span or in that one; and red where the + /// span was read as the TSC's, where the PM timer stood with the HPET, or + /// never, or no report followed. + #[test] + fn a_staged_stall_is_judged_by_its_own_span() { + const FIRST: &str = "[11.370 cpu0 kernel] clock: over 1 s the TSC counted 2419185510351 mHz against the HPET, within 11437 ppb; the clock keeps CPUID 15H's 2419200000000 mHz; 0 spans left out; 0 TSC step(s) slewed\n"; + const STAGED: &str = "[11.370 cpu0 kernel] hpet-stall: the HPET reads 192000 ticks behind from here\n"; + const MOVED: &str = "[12.375 cpu3 kernel] clock: the HPET moved and the TSC did not, so this span is left out: the TSC counted 2431296000 ticks, the HPET 995001000 ns and reads 9999052 ns apart, 523256 allowed (a to b); the PM timer counted 1005000100 ns and reads 1487 ns apart, 516738 allowed (c to d)\n"; + const REPORT: &str = "[13.380 cpu1 kernel] clock: over 2 s the TSC counted 2419185510351 mHz against the HPET, within 5718 ppb; the clock keeps CPUID 15H's 2419200000000 mHz; 1 spans left out; 0 TSC step(s) slewed\n"; + const REAL_STEP: &str = "[ 9.120 cpu5 kernel] clock: TSC STEP: the TSC stepped 1718866 ns forward against the HPET and the PM timer; the clock slews it away at most 500 ppm, most of it in about 3437 ms: the TSC counted 1799000000 ticks, the HPET 998281134 ns and reads 1718866 ns apart, 520000 allowed (a to b); the PM timer counted 998281000 ns and reads 1718700 ns apart, 518000 allowed (c to d)\n"; + const AS_THE_TSCS: &str = "[12.375 cpu3 kernel] clock: TSC STEP: the TSC stepped 9999052 ns forward against the HPET; the clock slews it away at most 500 ppm, most of it in about 19998 ms: the TSC counted 2431296000 ticks, the HPET 995001000 ns and reads 9999052 ns apart, 523256 allowed (a to b); no PM timer\n"; + const ALL: &str = "[12.375 cpu3 kernel] clock: the TSC, the HPET and the ACPI PM timer all disagree, so this span is left out: the TSC counted 2431296000 ticks, the HPET 995001000 ns and reads 11717918 ns apart, 523256 allowed (a to b); the PM timer counted 1003281134 ns and reads 1718866 ns apart, 516738 allowed (c to d)\n"; + let judged = |lines: &[&str]| faults::stalled_hpet_left_out(&lines.concat()); + + assert_eq!(judged(&[FIRST, STAGED, MOVED, REPORT]), Ok(())); + // The AMD bench: its own step before the staging, or after the staged span was read. + assert_eq!(judged(&[REAL_STEP, FIRST, STAGED, MOVED, REPORT]), Ok(())); + assert_eq!(judged(&[FIRST, STAGED, MOVED, REAL_STEP, REPORT]), Ok(())); + // Its own step inside the staged span: every counter disagrees, and the PM timer still tells the stall. + assert_eq!(judged(&[FIRST, STAGED, ALL, REPORT]), Ok(())); + let untold = ALL.replace("reads 1718866 ns apart", "reads 9100000 ns apart"); + for (why, lines) in [ + ("read as the TSC's", &[FIRST, STAGED, AS_THE_TSCS, REPORT][..]), + ("a PM timer that stands with neither", &[FIRST, STAGED, untold.as_str(), REPORT]), + ("the bench's step read where the stall should be", &[FIRST, STAGED, REAL_STEP, MOVED, REPORT]), + ("no report after", &[FIRST, STAGED, MOVED]), + ("never read", &[FIRST, STAGED]), + ("never staged", &[FIRST, MOVED, REPORT]), + ] { + assert!(judged(lines).is_err(), "{why} judged green"); + } + } + /// A slewed step passes the clock's rows and a demotion fails them. #[test] fn a_demoted_clock_fails_its_rows() { diff --git a/tests/checks/metal.rs b/tests/checks/metal.rs index c6fdd638af0..e3c38ee2631 100644 --- a/tests/checks/metal.rs +++ b/tests/checks/metal.rs @@ -71,6 +71,10 @@ fn loader() -> String { const STARTED: &str = "[2026-09-29 18:22:38 0.050 cpu0 kernel] spawn: /system/bin/logkeeper pid=6\n\ [2026-09-29 18:22:38 0.050 supervisor] supervisor: started logkeeper\n"; +/// When [`boot_file`]'s boot came back, by its host's clock: 2026-09-29 +/// 18:23:19 UTC, 40 s after [`BOOTED`]'s date. +const BACK_AT: u64 = 1_790_706_199; + /// A T14 boot's `boot.txt` as the loop writes it. pub(super) fn boot_file() -> String { let phases = Phases { @@ -82,7 +86,7 @@ pub(super) fn boot_file() -> String { read_log: 1.4, raw_log: Some(9.9), }; - BootFile { back: 40, machine: t14(), phases }.text() + BootFile { back: 40, back_at: BACK_AT, machine: t14(), phases }.text() } /// One boot's readback as the loop writes it. diff --git a/tests/common/faults.rs b/tests/common/faults.rs index d1d87f4b217..b41698f7d43 100644 --- a/tests/common/faults.rs +++ b/tests/common/faults.rs @@ -150,9 +150,9 @@ const TSC_DEMOTED: &str = "clock: DEMOTED: "; const DEMOTED_READS: &str = "clock: a process's reading of now asked the kernel since the TSC was demoted"; /// A TSC `tsc-step` reads 10 ms ahead, and 10 ms further once the clock has -/// slewed the first step away: the clock reports the first -/// ([`kernel::clock::TSC_STEP_READ`]) and slews it, the second demotes the -/// TSC, and processes' readings of now reach the kernel after it. +/// slewed the first step away: the clock reports the first and slews it, the +/// second demotes the TSC, and processes' readings of now reach the kernel +/// after it. pub fn a_stepped_tsc_is_slewed_then_demoted(test_config: &Path) -> Result<(), String> { let mut qemu = QemuInstance::boot_with_options( test_config, @@ -161,13 +161,12 @@ pub fn a_stepped_tsc_is_slewed_then_demoted(test_config: &Path) -> Result<(), St BootOptions { profile: qemu::Profile::Metal, kernel_params: &["tsc-step"], ready_marker: TSC_STEP_SLEWED, ..Default::default() }, ); let mut serial = qemu.boot_log().to_string(); - // A 10 ms step at 500 ppm is 20 s of slewing; the rest is the RTC's edges either side of each step. + // A 10 ms step at 500 ppm is 20 s of slewing. serial.push_str(&qemu.drain_until(std::time::Duration::from_secs(120), |l| l.contains(DEMOTED_READS) || l.contains("PANIC"))); let lines: Vec<&str> = serial.lines().collect(); let at = |marker: &str| lines.iter().position(|l| l.contains(marker)).ok_or_else(|| format!("no {marker:?} line\n{serial}")); let order = [ at(TSC_STEP_STAGED)?, - at(kernel::clock::TSC_STEP_READ)?, at(TSC_STEP_SLEWED)?, at(TSC_SLEWED_AWAY)?, at(TSC_STEP_AGAIN)?, @@ -177,11 +176,11 @@ pub fn a_stepped_tsc_is_slewed_then_demoted(test_config: &Path) -> Result<(), St if !order.is_sorted() || serial.contains("PANIC") { return Err(format!("the steps, the slew and the demotion are out of order: {order:?}\n{serial}")); } - let stepped: Option = lines[order[2]].split(TSC_STEP_SLEWED).nth(1).and_then(|r| r.split(" ns").next()).and_then(|n| n.parse().ok()); + let stepped: Option = lines[order[1]].split(TSC_STEP_SLEWED).nth(1).and_then(|r| r.split(" ns").next()).and_then(|n| n.parse().ok()); if !stepped.is_some_and(|ns| (9_000_000..=11_000_000).contains(&ns)) { - return Err(format!("the slewed step is not the staged 10 ms: {}\n{serial}", lines[order[2]].trim())); + return Err(format!("the slewed step is not the staged 10 ms: {}\n{serial}", lines[order[1]].trim())); } - for i in [1, 2, 5, 6] { + for i in [1, 4, 5] { eprintln!(" [tsc_step] {}", lines[order[i]].trim()); } Ok(()) @@ -219,3 +218,35 @@ pub fn a_stalled_hpet_is_left_out(test_config: &Path) -> Result<(), String> { eprintln!(" [hpet_stall] {}", staged_then_moved.trim()); Ok(()) } + +/// The records `log` holds of the span `hpet-stall` staged, on a machine: the +/// first disagreement the clock wrote after the staging is that span's, since +/// the sample after the staging reads it. The span is left out, and the PM +/// timer reads the TSC 10 ms nearer than the HPET does, which is the staged +/// stall put on the HPET and not on the TSC; and the clock reported again +/// after it. A step the machine's own TSC took is no part of it, in another +/// span or in this one, where every counter then disagrees and the PM timer +/// still stands the 10 ms from the HPET, to within a millisecond: the two +/// counters' allowances over a second, about half a millisecond each. +pub fn stalled_hpet_left_out(log: &str) -> Result<(), String> { + const ALL_DISAGREE: &str = "all disagree, so this span is left out: "; + const DISAGREED: [&str; 4] = [HPET_MOVED, ALL_DISAGREE, kernel::clock::TSC_STEP, TSC_DEMOTED]; + let mut after = log.lines().skip_while(|line| !line.contains(HPET_STALL_STAGED)).skip(1); + let read = after + .by_ref() + .find(|line| DISAGREED.iter().any(|said| line.contains(said))) + .ok_or_else(|| format!("no record of the span {HPET_STALL_STAGED:?} staged: the boot was not armed, or ended before the next sample"))?; + let apart = |spans: &str| -> Option { spans.split_once(" and reads ")?.1.split_once(" ns apart")?.0.parse().ok() }; + let told = [HPET_MOVED, ALL_DISAGREE].iter().find_map(|said| read.split_once(said)).and_then(|(_, spans)| { + let (hpet, pm) = spans.split_once("; the PM timer counted ")?; + Some(apart(hpet)? - apart(pm)?) + }); + if !told.is_some_and(|ns| (9_000_000..=11_000_000).contains(&ns)) { + return Err(format!("the PM timer did not put the staged 10 ms on the HPET and leave the span out: {}", read.trim())); + } + if !after.any(|line| line.contains(CLOCK_REPORT)) { + return Err("the clock did not report again after it left the staged span out".to_string()); + } + eprintln!(" [hpet_stall] {}", read.trim()); + Ok(()) +} diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs index 20b038e198f..86e59f7d41c 100644 --- a/tests/common/irqcensus.rs +++ b/tests/common/irqcensus.rs @@ -11,7 +11,7 @@ use std::collections::BTreeMap; /// and [`census_sources`] refuses a line whose fields are not exactly these, so /// a source added on one side and not the other is a red rather than a silently /// dropped column. -pub const SOURCES: [&str; 9] = ["timer", "kick", "userdev", "dmafault", "rtc", "tlb", "nmi", "spurious", "unclaimed"]; +pub const SOURCES: [&str; 8] = ["timer", "kick", "userdev", "dmafault", "tlb", "nmi", "spurious", "unclaimed"]; /// Whether one line is a census line whose sources are exactly [`SOURCES`], /// or `None` for a line that is no census. diff --git a/tests/common/metal.rs b/tests/common/metal.rs index e792f31fdda..860c4b8aaeb 100644 --- a/tests/common/metal.rs +++ b/tests/common/metal.rs @@ -222,6 +222,8 @@ pub struct Readback { pub boot_ms: Option, /// What the machine spent getting back to Ubuntu's ssh server. pub back_secs: u64, + /// The host's clock as it got there, in Unix seconds. + back_at: u64, /// The machine the loop read before the flash. pub machine: Result, /// Where the loop's wall time went. @@ -238,6 +240,8 @@ impl Readback { let kernel = bootlog::kernel_records(&log); let back_secs = toyos_metal::back_secs(boot) .ok_or_else(|| format!("{label}'s boot file names no `back_secs`: {boot:?}"))?; + let back_at = toyos_metal::back_unix_secs(boot) + .ok_or_else(|| format!("{label}'s boot file names no `back_unix_secs`: {boot:?}"))?; let phases = toyos_metal::phases(boot) .map_err(|why| format!("{label}'s boot file {why}: {boot:?}"))?; Ok(Readback { @@ -247,6 +251,7 @@ impl Readback { kernel, log, back_secs, + back_at, machine: toyos_metal::machine(boot) .map_err(|why| format!("{label}'s boot file {why}")), phases, @@ -272,6 +277,14 @@ impl Readback { Serial::named(&format!("{}'s loader.log", self.label), self.loader.as_str()) } + /// The Unix seconds, by the host's clock, this boot ran within: from the + /// reboot that started it to the machine answering again, a second's + /// rounding either side. + pub fn ran_within(&self) -> std::ops::RangeInclusive { + let since = self.back_secs + self.phases.down.ceil() as u64 + 1; + self.back_at.saturating_sub(since)..=self.back_at + 1 + } + /// When the last record this boot left was written, on the log's clock. /// `None` on a log with no record at all. pub fn last_record_ms(&self) -> Option { diff --git a/tests/guest/rust-tests/src/bin/file_mtime.rs b/tests/guest/rust-tests/src/bin/file_mtime.rs index bb7d6983c4f..903cd048bd6 100644 --- a/tests/guest/rust-tests/src/bin/file_mtime.rs +++ b/tests/guest/rust-tests/src/bin/file_mtime.rs @@ -6,8 +6,8 @@ //! a create with truncation, a create of a missing file, a truncation — and a //! later write's stamp is later, which a clock of whole seconds cannot say. //! `write ` makes the first judgement on `path` and `read ` only -//! prints what it finds there. `undated` runs on a machine whose RTC never -//! answered and never asks the time: a file written there, on `/tmp` or on +//! prints what it finds there. `undated` runs on a machine whose firmware +//! gave no time and never asks the time: a file written there, on `/tmp` or on //! `/home`, is undated, which std reports as an error and not as 1970. use std::fs::{self, OpenOptions}; diff --git a/tests/guest/rust-tests/src/bin/hpet_stall_read.rs b/tests/guest/rust-tests/src/bin/hpet_stall_read.rs new file mode 100644 index 00000000000..51577bcfb07 --- /dev/null +++ b/tests/guest/rust-tests/src/bin/hpet_stall_read.rs @@ -0,0 +1,38 @@ +//! Wait until the kernel's clock has read the span `hpet-stall` staged and +//! reported once more, and exit non-zero where the runner's own bound comes +//! near first. +//! +//! The `a_stalled_hpet_is_told_by_the_pm_timer` metal row runs it on the boot +//! armed with `hpet-stall`: the kernel stages the stall at its clock's first +//! report, the next sample reads the staged span, and the report after that +//! says the clock went on. The row's judge reads those records, so this holds +//! the boot until they exist and judges nothing. + +use std::time::Duration; + +#[path = "../served_log.rs"] +mod served_log; + +/// The kernel's records, as `kernel/src/arch/x86_64/clocksource.rs` writes +/// them: the stall staged, and a report of the clock's. +const STAGED: &str = "hpet-stall: the HPET reads "; +const REPORT: &str = "clock: over "; + +/// How long after boot the wait gives up: the runner's bound less a tenth. +const UNTIL_MS: u64 = toyos_tco::JOB_BOUND_MS - toyos_tco::JOB_BOUND_MS / 10; + +fn main() { + let since_boot = toyos_abi::clock::nanos_since_boot() / 1_000_000; + let bound = Duration::from_millis(UNTIL_MS.saturating_sub(since_boot)); + let mut staged = false; + served_log::Log::open().until("the clock's report after the span hpet-stall staged", bound, |line| { + let kernel = toyos_logstream::parse(line).filter(|said| said.source == toyos_logstream::Source::Kernel); + let Some(said) = kernel else { return false }; + if said.text.starts_with(STAGED) { + staged = true; + return false; + } + staged && said.text.starts_with(REPORT) + }); + println!("hpet_stall_read: the clock read the staged span and reported after it"); +} diff --git a/tests/metal/src/lib.rs b/tests/metal/src/lib.rs index a80f7a138fa..b8879ed7bbc 100644 --- a/tests/metal/src/lib.rs +++ b/tests/metal/src/lib.rs @@ -704,6 +704,10 @@ pub const FLASHABLE: &[&str] = &[ // kernel path reads or writes a port for that entry, so it reaches no // device; what it changes is that a claim on the i8042's row is refused. "isa-port-declared", + // From the clock's first report on, the sampler's readings of the HPET are + // 10 ms behind what the counter read. It writes no register; the clock + // leaves one span out and the boot goes on. + "hpet-stall", // Three nested `scheduler::Operation`s with known deadlines, in both homes, // each printing what it asked for and what it observed. It establishes and // drops them and reaches nothing else. @@ -1690,6 +1694,10 @@ pub fn run(args: &Args) -> Result, Refusal> { } let (down, back) = driver.ride_the_reboot(wait_secs)?; + let back_at = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("this host's clock reads after 1970") + .as_secs(); println!("the machine answered ssh again after {back} s"); let began = std::time::Instant::now(); let (loader, log_text, stopped) = driver.read_log()?; @@ -1716,7 +1724,7 @@ pub fn run(args: &Args) -> Result, Refusal> { let raw_log = args.fat32_check.then(|| began.elapsed().as_secs_f64()); let phases = Phases { preamble, flash, dd_secs: installed.dd, entry, down, read_log, raw_log }; println!("phases: {phases}"); - let boot = BootFile { back, machine, phases }.text(); + let boot = BootFile { back, back_at, machine, phases }.text(); let verdict = judge_and_write_readback(&armed, &loader, &log_text, stopped, args.readback.as_deref(), &boot); // After the readback, so a boot that moved the order is still judged; the // order's refusal is the louder answer, since the next failure loops. @@ -1950,6 +1958,9 @@ pub const READBACK_VOLUME: &str = "log-partition.img"; /// The keys [`READBACK_BOOT`] carries, one ` ` per line. pub const BACK_SECS: &str = "back_secs"; +/// This host's clock as the machine answered `ssh` again, in Unix seconds: +/// the boot ran in the [`BACK_SECS`] and the going down before it. +pub const BACK_UNIX_SECS: &str = "back_unix_secs"; /// The machine the boot ran on, as [`Machine::QUERY`] answered before the flash. pub const VENDOR_KEY: &str = "machine_vendor"; @@ -2014,16 +2025,18 @@ impl fmt::Display for Phases { /// machine it ran on. pub struct BootFile { pub back: u64, + pub back_at: u64, pub machine: Machine, pub phases: Phases, } impl BootFile { pub fn text(&self) -> String { - let Self { back, machine, phases } = self; + let Self { back, back_at, machine, phases } = self; let secs = |s: f64| format!("{s:.3}"); let rows = [ (BACK_SECS, back.to_string()), + (BACK_UNIX_SECS, back_at.to_string()), (VENDOR_KEY, machine.vendor.clone()), (PRODUCT_KEY, machine.product.clone()), (BIOS_KEY, machine.bios.clone()), @@ -2119,6 +2132,10 @@ pub fn back_secs(text: &str) -> Option { key(text, BACK_SECS) } +pub fn back_unix_secs(text: &str) -> Option { + key(text, BACK_UNIX_SECS) +} + /// The machine a readback names, or why it names none. pub fn machine(text: &str) -> Result { match (word(text, VENDOR_KEY), word(text, PRODUCT_KEY), word(text, BIOS_KEY)) { @@ -2363,9 +2380,10 @@ mod tests { #[test] fn the_machine_crosses_in_the_boot_file() { - let boot = BootFile { back: 47, machine: t14(), phases: phased(None) }.text(); + let boot = BootFile { back: 47, back_at: 1_993_799_665, machine: t14(), phases: phased(None) }.text(); assert_eq!(machine(&boot), Ok(t14())); assert_eq!(back_secs(&boot), Some(47)); + assert_eq!(back_unix_secs(&boot), Some(1_993_799_665)); assert!(machine("back_secs 47\nmachine_vendor LENOVO\n").is_err()); } @@ -2374,11 +2392,11 @@ mod tests { #[test] fn the_phases_cross_in_the_boot_file() { for raw_log in [Some(9.875), None] { - let boot = BootFile { back: 141, machine: t14(), phases: phased(raw_log) }.text(); + let boot = BootFile { back: 141, back_at: 0, machine: t14(), phases: phased(raw_log) }.text(); assert_eq!(phases(&boot), Ok(phased(raw_log)), "{boot}"); assert_eq!(back_secs(&boot), Some(141)); for line in boot.lines().filter(|line| line.contains("_secs ")) { - if line.starts_with(BACK_SECS) { + if line.starts_with(BACK_SECS) || line.starts_with(BACK_UNIX_SECS) { continue; } let short = boot.replace(&format!("{line}\n"), ""); @@ -2386,7 +2404,7 @@ mod tests { } } assert!(phases("back_secs 47\n").is_err()); - let garbled = BootFile { back: 1, machine: t14(), phases: phased(None) } + let garbled = BootFile { back: 1, back_at: 0, machine: t14(), phases: phased(None) } .text() .replace("flash_secs 121.500", "flash_secs later"); assert!(phases(&garbled).is_err()); diff --git a/tests/toyos.rs b/tests/toyos.rs index 23851775012..c4292f79f7c 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -80,6 +80,9 @@ const RUST_SKIP: &[&str] = &[ // The log-write load `mask_windows` reads windows under, on the boot // whose kernel reports them. "log_write", + // It waits for records only a boot armed with `hpet-stall` writes: the + // `a_stalled_hpet_is_told_by_the_pm_timer` metal row runs it there. + "hpet_stall_read", // It opens a window, which needs a compositor the shared boot does not // run: `a_window_is_drawn_at_the_scale_and_relaid_at_the_next` runs it on // tests/images/scalecase.toml, and the `gop_edid_scale` metal row on @@ -1159,7 +1162,7 @@ const METAL: &[(&str, metal::Metal)] = &[ ), // ---- the boot facts, riding the same tests/images/testcases.toml image ---- // - // **Seven names and no extra minute of the machine.** Every needle below is a + // **Eight names and no extra minute of the machine.** Every needle below is a // record the *shipping* kernel writes on any boot it takes, so each rides // whatever image is already going on the bench; the two rows after them are // the two things this suite measures rather than reads, and those cost a @@ -1202,6 +1205,14 @@ const METAL: &[(&str, metal::Metal)] = &[ }, }, ), + ( + // Every dated line is the loader's reading of firmware's `GetTime` + // carried on by the log's clock, and that reading is this host's + // time: which answer each bench's firmware gives is the machine's, + // and QEMU's is OVMF's. + "wall_clock_is_firmwares", + metal::Metal { arms: TESTCASES, judge: |b| wall_clock_is_firmwares(b[0]) }, + ), ( "pci_inventory", metal::Metal { arms: TESTCASES, judge: |b| pci_inventory(b[0].kernel().text()) }, @@ -1435,6 +1446,22 @@ const METAL: &[(&str, metal::Metal)] = &[ "lapic_spurious_vector", metal::Metal { arms: SELFTESTS, judge: |b| lapic_vectors(b[0].kernel().text()) }, ), + ( + // The HPET an actuator stalls, told from the TSC by each bench's own + // ACPI PM timer, which is all that tells them apart: its readings and + // their brackets are the machine's. + "a_stalled_hpet_is_told_by_the_pm_timer", + metal::Metal { + arms: &[metal::Arm { + features: ACTUATOR_KERNEL, + ..metal::once("shared-debug", "tests/images/testcases.toml", SELFTEST_PARAMS, &["test_rs_hpet_stall_read"]) + }], + judge: |b| { + b[0].job_passed("test_rs_hpet_stall_read")?; + faults::stalled_hpet_left_out(b[0].kernel().text()) + }, + }, + ), ( // No drain on this side: the whole boot's records are on the log, so // the probe's line is either in them or it never ran. @@ -2139,12 +2166,15 @@ const PROCTREECASE: &[metal::Arm] = &[metal::once( /// what the machine *is*: each stages inputs the hardware cannot produce — a /// crafted capability list, a malformed descriptor, a vector nothing claims — /// runs a check over them and prints a count. So they arm the boot -/// [`ACTUATOR_TESTS`] ride, which is the same kernel. +/// [`ACTUATOR_TESTS`] ride, which is the same kernel. `hpet-stall` rides it +/// too: it changes one reading the clock's sampler takes, which leaves one +/// span out. const SELFTEST_PARAMS: &[&str] = &[ "pci-cap-selftest", "lapic-spurious-selftest", "unclaimed-vector-selftest", "sysret-ss-probe", + "hpet-stall", ]; const SELFTESTS: &[metal::Arm] = &[metal::Arm { @@ -7074,7 +7104,7 @@ const ACPI_FIRST_INTERRUPT: &str = "isa: the ACPI fixed hardware took its first /// once; /// - the clock's TSC refinement ([`kernel::clock::TSC_REFINED`]), written on a /// scheduler pass and arming no timer; -/// - its step report ([`kernel::clock::TSC_STEP_READ`]), which the AMD bench's +/// - its step report ([`kernel::clock::TSC_STEP`]), which the AMD bench's /// deep idle provokes once a boot /// (`issues/the-amd-benchs-tsc-steps-1-7-ms-forward-once-a-boot.md`). fn idle_second_lines(log: &str, from_ms: u64, to_ms: u64) -> Result, Vec<&str>> { @@ -7089,7 +7119,7 @@ fn idle_second_lines(log: &str, from_ms: u64, to_ms: u64) -> Result, V .is_some_and(|p| p.source == toyos_logstream::Source::Kernel && p.text == ACPI_FIRST_INTERRUPT); let clock = toyos_logstream::parse(line).is_some_and(|p| { p.source == toyos_logstream::Source::Kernel - && (p.text.starts_with(kernel::clock::TSC_REFINED) || p.text.starts_with(kernel::clock::TSC_STEP_READ)) + && (p.text.starts_with(kernel::clock::TSC_REFINED) || p.text.starts_with(kernel::clock::TSC_STEP)) }); named && once(line) || clock }; @@ -7918,6 +7948,86 @@ fn timer_calibration(log: &str) -> Result<(), String> { Ok(()) } +/// How far a bench's hardware clock may stand from this host's: a third of +/// the smallest step between two zones, 15 minutes, so a clock kept in any +/// zone but UTC is outside it, and far past what the bench's other system +/// leaves a clock it disciplines. A bound on the bench, not on ToyOS. +const BENCH_CLOCK_APART_SECS: u64 = 300; + +/// `YYYY-MM-DD HH:MM:SS` where `text` starts, as Unix seconds. +fn unix_secs_at(text: &str) -> Option { + let mut fields = text.get(..19)?.split(['-', ' ', ':']).map(|field| field.parse::().ok()); + let mut next = || fields.next().flatten(); + let civil = toyos_wallclock::Civil { year: next()?, month: next()?, day: next()?, hour: next()?, min: next()?, sec: next()? }; + civil.is_valid().then(|| civil.to_unix_secs()) +} + +/// **Every dated line is the loader's reading carried on by the log's own +/// clock.** The loader says the instant firmware's `GetTime` answered and the +/// counter then; the counter over the rate the clock started at is that +/// reading's time on the log's clock, which counts from the counter's zero. +/// A line stamped `ms` is dated the reading plus the seconds between, less up +/// to three and plus up to one: the kernel's anchor, `SYS_CLOCK_EPOCH`, and +/// `logkeeper`'s two divisions each drop a part of a second, three one way and +/// one the other. A tenth of a second more either side is for the rate the +/// clock retunes to and a step it slews, which move a boot's stamps far less. +/// No host's clock is read. +fn dates_carry_the_loaders_reading(loader: &str, kernel: &str, log: &str) -> Result { + const READ: &str = "Wall clock: firmware's GetTime reads "; + const AT: &str = " UTC at counter "; + let said = loader.lines().find(|line| line.contains(READ)).ok_or_else(|| format!("the loader never said {READ:?}"))?; + let (read, counter) = said + .split_once(READ) + .and_then(|(_, rest)| Some((unix_secs_at(rest)?, rest.split_once(AT)?.1.trim().parse::().ok()?))) + .ok_or_else(|| format!("no instant and counter in {said:?}"))?; + let read_ms = u128::from(counter) * 1_000_000 / u128::from(boot_rate(kernel)?.millihertz); + let mut dated = 0; + for line in log.lines() { + let Some(date) = line.strip_prefix('[').and_then(unix_secs_at) else { continue }; + let Some(ms) = toyos_logstream::parse(line).and_then(|said| said.ms) else { continue }; + let carried = i128::from(read) * 1_000 + i128::from(ms) - read_ms as i128; + let ahead = i128::from(date) * 1_000 - carried; + if !(-3_100..=1_100).contains(&ahead) { + return Err(format!( + "a line is dated {ahead} ms from the loader's reading of {read} carried {} ms on by the log's clock: {}", + i128::from(ms) - read_ms as i128, + line.trim() + )); + } + dated += 1; + } + if dated == 0 { + return Err("no line of the log carries a date and a time".to_string()); + } + eprintln!(" [wall] {dated} dated lines carry the loader's reading of {read}, taken {read_ms} ms on the log's clock"); + Ok(read) +} + +/// **The bench's part: its hardware clock keeps UTC.** Firmware's reading +/// lies in the span this host's clock saw the boot run in +/// ([`metal::Readback::ran_within`]), [`BENCH_CLOCK_APART_SECS`] either side. +/// With the bench's clock set, it is the independent reading of the loader's +/// decode of `EFI_TIME`. +fn bench_clock_keeps_utc(read: u64, ran: std::ops::RangeInclusive) -> Result<(), String> { + let apart = if read < *ran.start() { ran.start() - read } else { read.saturating_sub(*ran.end()) }; + if apart > BENCH_CLOCK_APART_SECS { + return Err(format!( + "this bench's hardware clock stands {apart} s from this host's: firmware read {read} in Unix seconds and the \ + host saw the boot run in {ran:?}. The design requires the hardware clock to keep UTC; this is the bench's \ + setting and not ToyOS's reading of it" + )); + } + eprintln!(" [wall] firmware read {read}, {apart} s outside the span the host saw the boot run in"); + Ok(()) +} + +/// The wall clock on a bench: ToyOS's carrying of firmware's reading, then +/// the bench's own clock. +fn wall_clock_is_firmwares(boot: &metal::Readback) -> Result<(), String> { + let read = dates_carry_the_loaders_reading(boot.loader().text(), boot.kernel().text(), boot.log().text())?; + bench_clock_keeps_utc(read, boot.ran_within()) +} + /// The rate the boot started the clock at: CPUID 15H's, or one the HPET /// measured with its bound, which on the bench is a rate the clock can keep at /// all. Its bound is held against minutes by [`clock_holds_over_minutes`]. @@ -7973,11 +8083,11 @@ fn clock_report(line: &str) -> Option { }) } -/// Whether the boot's clock kept the TSC: every step it slewed, every span it -/// left out and every step it waited on the RTC for is printed, and a -/// demotion fails the row, since a demoted boot's processes read the HPET. +/// Whether the boot's clock kept the TSC: every step it slewed and every span +/// it left out is printed, and a demotion fails the row, since a demoted +/// boot's processes read the HPET. fn clock_kept_its_counter(log: &str) -> Result<(), String> { - const SAID: [&str; 4] = ["clock: TSC STEP: ", "so this span is left out", "so the span was left out", "the RTC's edges after it will say which moved"]; + const SAID: [&str; 2] = [kernel::clock::TSC_STEP, "so this span is left out"]; for line in log.lines().filter(|line| SAID.iter().any(|said| line.contains(said))) { eprintln!(" [clock] {}", line.trim()); }