fast_clock/generic_timer.rs
1use crate::{
2 Clock,
3 wrapping_u64::{U64Calibration, WrappingU64Duration, WrappingU64Instant, WrappingU64Time},
4};
5
6/// The aarch64 generic timer virtual counter (`CNTVCT_EL0`).
7///
8/// The architecture requires a constant frequency, consistent across cores.
9/// Its period must be at least 40 years.
10/// Access to this counter may be disabled by the kernel.
11/// It is almost always available on Linux.
12///
13/// ### Frequency
14/// The frequency of the counter can be read from `CNTFRQ_EL0`, so it does not need calibration.
15/// This value is typically populated by firmware.
16///
17/// From Armv8.6 and Armv9.1 on, the frequency must be exactly 1GHz.
18/// Before those, the counter frequency was typically between 1MHz and 50MHz.
19/// The clock frequency is represented in 32-bits, which means the frequency will be at most [`u32::MAX`] Hz ≈ 4.3GHz.
20///
21/// Note that even on newer systems the resolution may be lower than 1GHz.
22/// For example, the counter may atomically increase by 40 every 40ns.
23///
24/// ### Bit Size
25/// The hardware counter has 56-64 bits (implementation dependent).
26/// From Armv8.6 and Armv9.1 on, the counter must be 64 bits.
27/// `BITS` is the counter width assumed by this clock and must be in `56..=64`.
28/// A mismatch with the hardware in either direction has downsides.
29/// If the width on the target machine is not known, a tradeoff has to be made.
30///
31/// If `BITS` is greater than the hardware size, calculations involving instants between which the counter rolled over will be incorrect.
32/// Typically, `CNTVCT_EL0` is reset to 0 at boot, so rollover will occur if a machine stays running for longer than the rollover time.
33/// Arm requires the rollover time to be at least 40 years.
34/// It is recommended that most applications go with `BITS=64`.
35///
36/// If `BITS` is less than the hardware size, the top bits of the value read from `CNTVCT_EL0` are discarded.
37/// This makes the clock function correctly, but reduces rollover time.
38/// In the worst case, rollover time is slightly over 6 months.
39/// Keep in mind that instant comparison only works reliably if the two instants are less than half of a rollover time from each other.
40
41// The below python function computes the worst case rollover time in years
42// def rollover_years(hardware_bits,software_bits):
43// assert 56 <= software_bits <= hardware_bits <= 64
44// min_rollover_seconds = (40*365.25*24*3600)
45// max_freq = (min(2**32-1,2**hardware_bits/min_rollover_seconds))
46// rollover_seconds = 2**software_bits/max_freq
47// return rollover_seconds/3600/24/365.25
48
49#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
50pub struct GenericTimer<const BITS: u32 = 64>(());
51
52impl<const BITS: u32> Clock for GenericTimer<BITS> {
53 type Time = WrappingU64Time<BITS>;
54 type Calibration = U64Calibration<BITS>;
55
56 #[inline(always)]
57 fn now(&self) -> WrappingU64Instant<BITS> {
58 let ticks: u64;
59 unsafe {
60 core::arch::asm!("mrs {}, cntvct_el0", out(reg) ticks, options(nomem, nostack));
61 }
62 WrappingU64Instant::wrapping_new(ticks)
63 }
64}
65
66/// Error returned when the generic timer is not available on the current system.
67#[derive(Debug)]
68#[non_exhaustive]
69pub struct GenericTimerUnavailable;
70
71impl core::fmt::Display for GenericTimerUnavailable {
72 fn fmt(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
73 formatter.write_str("No generic timer available")
74 }
75}
76
77#[cfg(feature = "std")]
78impl std::error::Error for GenericTimerUnavailable {}
79
80impl<const BITS: u32> GenericTimer<BITS> {
81 const CHECK_BITS: () = assert!(BITS >= 56 && BITS <= 64, "BITS must be in 56..=64");
82
83 /// Returns a [`GenericTimer`], assuming `CNTVCT_EL0` is readable from EL0.
84 ///
85 /// Operating systems that expose a userspace clock without a syscall enable this, which
86 /// covers Linux and macOS. If it is disabled, [`Clock::now`] traps with `SIGILL`. There is no
87 /// way to query this from EL0, so prefer [`GenericTimer::try_new_linux_sys`] on Linux.
88 pub fn new_assume_accessible() -> Self {
89 const { Self::CHECK_BITS };
90 GenericTimer(())
91 }
92
93 /// Returns `Ok(`[`GenericTimer`]`)` if the Linux kernel reports `arch_sys_counter` as an available clocksource.
94 #[cfg(all(target_os = "linux", feature = "std"))]
95 pub fn try_new_linux_sys() -> Result<Self, GenericTimerUnavailable> {
96 const { Self::CHECK_BITS };
97 let counter_detected = std::fs::read_to_string(
98 "/sys/devices/system/clocksource/clocksource0/available_clocksource",
99 )
100 .is_ok_and(|x| x.split_whitespace().any(|x| x == "arch_sys_counter"));
101 if counter_detected {
102 Ok(GenericTimer(()))
103 } else {
104 Err(GenericTimerUnavailable)
105 }
106 }
107
108 /// Returns the counter frequency in Hz as reported by `CNTFRQ_EL0`.
109 pub fn read_cntfrq(&self) -> u32 {
110 let frequency: u32;
111 unsafe {
112 core::arch::asm!("mrs {:w}, cntfrq_el0", out(reg) frequency, options(pure, nomem, nostack));
113 }
114 frequency
115 }
116
117 /// Returns a calibration derived from `CNTFRQ_EL0`.
118 ///
119 /// # Panics
120 ///
121 /// Panics if `CNTFRQ_EL0` is zero, which means firmware did not populate it.
122 pub fn calibration_from_cntfrq(&self) -> U64Calibration<BITS> {
123 let frequency = self.read_cntfrq() as u64;
124 if frequency == 0 {
125 panic!("cntfrq_el0 = 0");
126 }
127 U64Calibration::new(WrappingU64Duration::new(frequency), 1_000_000_000)
128 }
129}