Skip to main content

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}