From b99fb818c96062cc9d6d6ed1a08deca2ebffd219 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Sat, 22 Oct 2022 11:04:39 -0700 Subject: [PATCH] refac(hal-x86_64): move `pit` and `tsc` into `time` (#356) This commit moves the `pit` and `tsc` modules out of `interrupt` and `cpu`, and into a new `time` module. This feels like a better organization of things, IMO. --- hal-x86_64/src/cpu.rs | 4 +- hal-x86_64/src/cpu/entropy.rs | 2 +- hal-x86_64/src/interrupt.rs | 4 +- hal-x86_64/src/lib.rs | 1 + hal-x86_64/src/time.rs | 38 +++ hal-x86_64/src/{interrupt => time}/pit.rs | 382 ++++++++++++++-------- hal-x86_64/src/{cpu => time}/tsc.rs | 2 +- 7 files changed, 282 insertions(+), 151 deletions(-) create mode 100644 hal-x86_64/src/time.rs rename hal-x86_64/src/{interrupt => time}/pit.rs (67%) rename hal-x86_64/src/{cpu => time}/tsc.rs (93%) diff --git a/hal-x86_64/src/cpu.rs b/hal-x86_64/src/cpu.rs index 0f7466ef..314d52e2 100644 --- a/hal-x86_64/src/cpu.rs +++ b/hal-x86_64/src/cpu.rs @@ -4,9 +4,7 @@ use mycelium_util::bits; pub mod entropy; pub mod intrinsics; mod msr; -mod tsc; pub use self::msr::Msr; -pub use self::tsc::Rdtsc; #[repr(transparent)] pub struct Port { @@ -246,7 +244,7 @@ impl FeatureNotSupported { self.0 } - pub(in crate::cpu) fn new(feature_name: &'static str) -> Self { + pub(crate) fn new(feature_name: &'static str) -> Self { Self(feature_name) } } diff --git a/hal-x86_64/src/cpu/entropy.rs b/hal-x86_64/src/cpu/entropy.rs index 79401ab7..ebcea003 100644 --- a/hal-x86_64/src/cpu/entropy.rs +++ b/hal-x86_64/src/cpu/entropy.rs @@ -4,7 +4,7 @@ use mycelium_util::sync::spin; use raw_cpuid::CpuId; #[cfg(feature = "rand_core")] -use super::Rdtsc; +use crate::time::Rdtsc; #[cfg(feature = "rand_core")] use mycelium_util::sync::Lazy; #[cfg(feature = "rand_core")] diff --git a/hal-x86_64/src/interrupt.rs b/hal-x86_64/src/interrupt.rs index 3a158a14..8c383295 100644 --- a/hal-x86_64/src/interrupt.rs +++ b/hal-x86_64/src/interrupt.rs @@ -5,11 +5,9 @@ use mycelium_util::{bits, fmt}; pub mod idt; pub mod pic; -pub mod pit; pub use idt::Idt; pub use pic::CascadedPic; -pub use pit::PIT; pub type Control = &'static mut Idt; @@ -227,7 +225,7 @@ impl hal_core::interrupt::Control for Idt { use core::sync::atomic::Ordering; // if we weren't trying to do a PIT sleep, handle the timer tick // instead. - let was_sleeping = pit::SLEEPING + let was_sleeping = crate::time::pit::SLEEPING .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire) .is_ok(); if !was_sleeping { diff --git a/hal-x86_64/src/lib.rs b/hal-x86_64/src/lib.rs index ffcf2f42..a21a6de8 100644 --- a/hal-x86_64/src/lib.rs +++ b/hal-x86_64/src/lib.rs @@ -24,6 +24,7 @@ pub mod mm; pub mod segment; pub mod serial; pub mod task; +pub mod time; pub mod vga; pub const NAME: &str = "x86_64"; diff --git a/hal-x86_64/src/time.rs b/hal-x86_64/src/time.rs new file mode 100644 index 00000000..5a88db60 --- /dev/null +++ b/hal-x86_64/src/time.rs @@ -0,0 +1,38 @@ +//! x86 hardware timers and timekeeping functionality. +pub(crate) mod pit; +mod tsc; +pub use self::{ + pit::{Pit, PIT}, + tsc::Rdtsc, +}; +use core::fmt; +pub use core::time::Duration; + +/// Error indicating that a [`Duration`] was invalid for a particular use. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct InvalidDuration { + duration: Duration, + message: &'static str, +} + +// === impl InvalidDuration === + +impl fmt::Display for InvalidDuration { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { duration, message } = self; + write!(f, "invalid duration {duration:?}: {message}") + } +} + +impl InvalidDuration { + /// Returns the [`Duration`] that was invalid. + #[must_use] + pub fn duration(self) -> Duration { + self.duration + } + + #[must_use] + pub(crate) fn new(duration: Duration, message: &'static str) -> Self { + Self { duration, message } + } +} diff --git a/hal-x86_64/src/interrupt/pit.rs b/hal-x86_64/src/time/pit.rs similarity index 67% rename from hal-x86_64/src/interrupt/pit.rs rename to hal-x86_64/src/time/pit.rs index 0abd91da..0261e897 100644 --- a/hal-x86_64/src/interrupt/pit.rs +++ b/hal-x86_64/src/time/pit.rs @@ -1,3 +1,5 @@ +#![warn(missing_docs)] +use super::InvalidDuration; use crate::cpu::Port; use core::{ convert::{Infallible, TryFrom}, @@ -14,8 +16,8 @@ use mycelium_util::{ /// Intel 8253/8254 Programmable Interval Timer (PIT). /// /// The PIT is a simple timer, with three channels. The most interesting is -/// channel 0, which is capable of firing an interrupt to the [8259 PIC] or I/O -/// APIC on ISA interrupt vector 0. Channel 1 was used to time the DRAM refresh +/// channel 0, which is capable of firing an interrupt to the [8259 PIC] or [I/O +/// APIC] on ISA interrupt vector 0. Channel 1 was used to time the DRAM refresh /// rate on ancient IBM PCs and is now generally unused (and may not be /// implemented in hardware), and channel 2 was connected to the IBM PC speaker /// and could be used to play sounds. @@ -24,7 +26,8 @@ use mycelium_util::{ /// [extremely cool reasons][reasons], but a 16-bit divisor can be used to /// determine what multiple of this base frequency each channel fires at. /// -/// [8259 PIC]: super::pic +/// [8259 PIC]: crate::interrupt::pic +/// [I/O APIC]: crate::interrupt::apic::IoApic /// [base frequency]: Self::BASE_FREQUENCY_HZ /// [reasons]: https://en.wikipedia.org/wiki/Programmable_interval_timer#IBM_PC_compatible #[derive(Debug)] @@ -77,137 +80,15 @@ pub struct Pit { channel2: Port, /// PIT command port. command: Port, - channel0_frequency_hz: Option, -} - -pub static PIT: Mutex = Mutex::new(Pit::new()); -pub(crate) static SLEEPING: AtomicBool = AtomicBool::new(false); - -/// Sleep (by spinning) for `duration`. -pub fn sleep_blocking(duration: Duration) -> Result<(), InvalidDuration> { - SLEEPING - .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) - // TODO(eliza): make this return an error... - .expect("someone already started a sleep!"); - { - PIT.lock().interrupt_in(duration)?; - } - tracing::debug!(?duration, "started PIT sleep"); - - // spin until the sleep interrupt fires. - while SLEEPING.load(Ordering::Acquire) { - hint::spin_loop(); - } - - tracing::debug!(?duration, "PIT slept"); - Ok(()) -} - -impl Pit { - /// The PIT's base frequency runs at roughly 1.193182 MHz, for [extremely - /// cool reasons][reasons]. - /// - /// [reasons]: https://en.wikipedia.org/wiki/Programmable_interval_timer#IBM_PC_compatible - pub const BASE_FREQUENCY_HZ: usize = 1193180; - const TICKS_PER_MS: usize = Self::BASE_FREQUENCY_HZ / 1000; - - const fn new() -> Self { - const BASE: u16 = 0x40; - Self { - channel0: Port::at(BASE), - channel1: Port::at(BASE + 1), - channel2: Port::at(BASE + 2), - command: Port::at(BASE + 3), - channel0_frequency_hz: None, - } - } - - pub fn start_periodic_timer(&mut self, frequency_hz: usize) { - let divisor = Self::BASE_FREQUENCY_HZ / frequency_hz; - tracing::debug!( - frequency_hz, - divisor, - "setting PIT channel 0 frequency divisor" - ); - - let divisor = match u16::try_from(divisor) { - Ok(divisor) => divisor, - Err(_) => panic!( - "PIT frequency divisor {} for {} Hz frequency exceeds a 16-bit number", - divisor, frequency_hz - ), - }; - - // store the periodic timer frequency so we can reset later. - self.channel0_frequency_hz = Some(frequency_hz); - - // Send the PIT the following command: - let command = Command::new() - // use the binary counter - .with(Command::BCD_BINARY, false) - // generate a square wave (set the frequency divisor) - .with(Command::MODE, OperatingMode::SquareWave) - // we are sending both bytes of the divisor - .with(Command::ACCESS, AccessMode::Both) - // and we're configuring channel 0 - .with(Command::CHANNEL, ChannelSelect::Channel0); - self.send_command(command); - self.set_divisor(divisor); - } - - /// Configure the PIT to send an IRQ 0 interrupt in `duration`. - /// - /// This configures the PIT in mode 0 (oneshot mode). Once the interrupt has - /// fired, in order to use the periodic timer, the pit must be put back into - /// periodic mode by calling [`Pit::start_periodic_timer`]. - fn interrupt_in(&mut self, duration: Duration) -> Result<(), InvalidDuration> { - let duration_ms = usize::try_from(duration.as_millis()).map_err(|_| InvalidDuration { - duration, - message: "duration as milliseconds would exceed a usize", - })?; - let target_time = Self::TICKS_PER_MS * duration_ms; - let divisor = u16::try_from(target_time).map_err(|_| InvalidDuration { - duration, - message: "target tick count would exceed a u16", - })?; - - let command = Command::new() - // use the binary counter - .with(Command::BCD_BINARY, false) - // generate a square wave (set the frequency divisor) - .with(Command::MODE, OperatingMode::Interrupt) - // we are sending both bytes of the divisor - .with(Command::ACCESS, AccessMode::Both) - // and we're configuring channel 0 - .with(Command::CHANNEL, ChannelSelect::Channel0); - self.send_command(command); - self.set_divisor(divisor); - Ok(()) - } - - fn set_divisor(&mut self, divisor: u16) { - tracing::trace!(divisor = &fmt::hex(divisor), "Pit::set_divisor"); - let low = divisor as u8; - let high = (divisor >> 8) as u8; - unsafe { - self.channel0.writeb(low); // write the low byte - tracing::trace!(lo = &fmt::hex(low), "pit.channel0.writeb(lo)"); - self.channel0.writeb(high); // write the high byte - tracing::trace!(hi = &fmt::hex(high), "pit.channel0.writeb(hi)"); - } - } - - fn send_command(&self, command: Command) { - tracing::debug!(?command, "Pit::send_command"); - unsafe { - self.command.writeb(command.bits()); - } - } + /// If PIT channel 0 is configured in periodic mode, this stores the period + /// as a `Duration` so that we can reset to periodic mode after firing a + /// sleep interrupt. + channel0_interval: Option, } bitfield! { - pub struct Command { + struct Command { /// BCD/binary mode. /// /// The "BCD/Binary" bit determines if the PIT channel will operate in @@ -218,7 +99,7 @@ bitfield! { /// possible to use BCD mode, it may not work properly on some /// "compatible" chips. For the "read back" command and the "counter /// latch" command, this bit has different meanings. - pub const BCD_BINARY: bool; + const BCD_BINARY: bool; /// Operating mode. /// /// The operating mode bits specify which mode the selected PIT @@ -226,7 +107,7 @@ bitfield! { /// "counter latch" command, these bits have different meanings. /// There are 6 different operating modes. See the [`OperatingMode`] /// enum for details on the PIT operating modes. - pub const MODE: OperatingMode; + const MODE: OperatingMode; /// Access mode. /// /// The access mode bits tell the PIT what access mode you wish to use @@ -245,7 +126,7 @@ bitfield! { /// the lowest 8 bits followed by the highest 8 bits (both 8 bit /// transfers are to the same IO port, sequentially – a word transfer /// will not work). - pub const ACCESS: AccessMode; + const ACCESS: AccessMode; /// Channel select. /// /// The channel select bits select which channel is being configured, @@ -257,19 +138,13 @@ bitfield! { /// support it). /// /// ["read back"]: ChannelSelect::ReadBack - pub const CHANNEL: ChannelSelect; + const CHANNEL: ChannelSelect; } } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct InvalidDuration { - duration: Duration, - message: &'static str, -} - #[derive(Debug, Clone, Copy, Eq, PartialEq)] #[repr(u8)] -pub enum ChannelSelect { +enum ChannelSelect { Channel0 = 0b00, Channel1 = 0b01, Channel2 = 0b10, @@ -279,7 +154,7 @@ pub enum ChannelSelect { #[derive(Debug, Clone, Copy, Eq, PartialEq)] #[repr(u8)] -pub enum AccessMode { +enum AccessMode { /// Latch count value command LatchCount = 0b00, /// Access mode: low byte only @@ -292,7 +167,7 @@ pub enum AccessMode { #[derive(Debug, Clone, Copy, Eq, PartialEq)] #[repr(u8)] -pub enum OperatingMode { +enum OperatingMode { /// Mode 0 (interrupt on terminal count) Interrupt = 0b000, /// Mode 1 (hardware re-triggerable one-shot) @@ -316,6 +191,227 @@ pub enum OperatingMode { SquareWave2 = 0b111, } +/// The PIT. +/// +/// Since a system only has a single PIT, the `Pit` type cannot be constructed +/// publicly and is represented as a singleton. It's stored in a [`Mutex`] in +/// order to ensure that multiple CPU cores don't try to write conflicting +/// configurations to the PIT's configuration ports. +pub static PIT: Mutex = Mutex::new(Pit::new()); + +/// Are we currently sleeping on an interrupt? +pub(crate) static SLEEPING: AtomicBool = AtomicBool::new(false); + +impl Pit { + /// The PIT's base frequency runs at roughly 1.193182 MHz, for [extremely + /// cool reasons][reasons]. + /// + /// [reasons]: https://en.wikipedia.org/wiki/Programmable_interval_timer#IBM_PC_compatible + pub const BASE_FREQUENCY_HZ: usize = 1193180; + const TICKS_PER_MS: usize = Self::BASE_FREQUENCY_HZ / 1000; + + const fn new() -> Self { + const BASE: u16 = 0x40; + Self { + channel0: Port::at(BASE), + channel1: Port::at(BASE + 1), + channel2: Port::at(BASE + 2), + command: Port::at(BASE + 3), + channel0_interval: None, + } + } + + /// Sleep (by spinning) for `duration`. + /// + /// This function sets a flag indicating that a sleep is in progress, and + /// configures the PIT to fire an interrupt on channel 0 in `duration`. It then + /// spins until the flag is cleared by an interrupt handler. + /// + /// # Usage Notes + /// + /// This is a low-level way of sleeping, and is not recommended for use as a + /// system's primary method of sleeping for a duration. Instead, a timer wheel + /// or other way of tracking multiple sleepers should be constructed and + /// advanced based on a periodic timer. This function is provided primarily to + /// allow using the PIT to calibrate other timers as part of initialization + /// code, rather than for general purpose use in an operating system. + /// + /// In particular, using this function is subject to the following + /// considerations: + /// + /// - An interrupt handler for the PIT interrupt which clears the sleeping flag + /// must be installed. This is done automatically by the [`Controller::init`] + /// function in the [`interrupt`] module. If that interrupt handler is not + /// present, this function will spin forever! + /// - If the PIT is currently in periodic mode, it will be put in oneshot mode + /// when this function is called. This will temporarily disable the existing + /// periodic timer. + /// - This function returns an error if another CPU core is already sleeping. It + /// should generally be used only prior to the initialization of application + /// processors. + /// + /// # Returns + /// + /// - [`Ok`]`(())` after `duration` if a sleep was successfully completed. + /// - [`Err`]`(`[`InvalidDuration`]`)` if the provided duration was + /// too long. + /// + /// [`Controller::init`]: crate::interrupt::Controller::init + /// [`interrupt`]: crate::interrupt + #[tracing::instrument( + name = "Pit::sleep_blocking" + level = tracing::Level::DEBUG, + skip(self), + fields(?duration), + err, + )] + pub fn sleep_blocking(&mut self, duration: Duration) -> Result<(), InvalidDuration> { + SLEEPING + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .expect("if another CPU core is sleeping, it should be holding the lock on the PIT, preventing us from starting a sleep!"); + self.interrupt_in(duration)?; + tracing::debug!("started PIT sleep"); + + // spin until the sleep interrupt fires. + while SLEEPING.load(Ordering::Acquire) { + hint::spin_loop(); + } + + tracing::info!(?duration, "slept using PIT channel 0"); + + // if we were previously in periodic mode, re-enable it. + if let Some(interval) = self.channel0_interval { + tracing::debug!("restarting PIT periodic timer"); + self.start_periodic_timer(interval)?; + } + + Ok(()) + } + + /// Configures PIT channel 0 in periodic mode, to fire an interrupt every + /// time the provided `interval` elapses. + /// + /// # Returns + /// + /// - [`Ok`]`(())` if the timer was successfully configured in periodic + /// mode. + /// - [`Err`]`(`[`InvalidDuration`]`)` if the provided [`Duration`] was + /// too long. + #[tracing::instrument( + name = "Pit::start_periodic_timer" + level = tracing::Level::DEBUG, + skip(self), + fields(?interval), + err, + )] + pub fn start_periodic_timer(&mut self, interval: Duration) -> Result<(), InvalidDuration> { + debug_assert!( + !SLEEPING.load(Ordering::Acquire), + "tried to start a periodic timer while a sleep was in progress!" + ); + + let interval_ms = usize::try_from(interval.as_millis()).map_err(|_| { + InvalidDuration::new( + interval, + "PIT periodic timer interval as milliseconds would exceed a `usize`", + ) + })?; + let interval_ticks = Self::TICKS_PER_MS * interval_ms; + let divisor = u16::try_from(interval_ticks).map_err(|_| { + InvalidDuration::new(interval, "PIT channel 0 divisor would exceed a `u16`") + })?; + + // store the periodic timer interval so we can reset later. + self.channel0_interval = Some(interval); + + // Send the PIT the following command: + let command = Command::new() + // use the binary counter + .with(Command::BCD_BINARY, false) + // generate a square wave (set the frequency divisor) + .with(Command::MODE, OperatingMode::SquareWave) + // we are sending both bytes of the divisor + .with(Command::ACCESS, AccessMode::Both) + // and we're configuring channel 0 + .with(Command::CHANNEL, ChannelSelect::Channel0); + self.send_command(command); + self.set_divisor(divisor); + + tracing::info!( + ?interval, + interval_ms, + interval_ticks, + divisor, + "started PIT periodic timer" + ); + + Ok(()) + } + + /// Configure the PIT to send an IRQ 0 interrupt in `duration`. + /// + /// This configures the PIT in mode 0 (oneshot mode). Once the interrupt has + /// fired, in order to use the periodic timer, the pit must be put back into + /// periodic mode by calling [`Pit::start_periodic_timer`]. + #[tracing::instrument( + name = "Pit::interrupt_in" + level = tracing::Level::DEBUG, + skip(self), + fields(?duration), + err, + )] + pub fn interrupt_in(&mut self, duration: Duration) -> Result<(), InvalidDuration> { + let duration_ms = usize::try_from(duration.as_millis()).map_err(|_| { + InvalidDuration::new( + duration, + "PIT interrupt duration as milliseconds would exceed a `usize`", + ) + })?; + let target_time = Self::TICKS_PER_MS * duration_ms; + let divisor = u16::try_from(target_time).map_err(|_| { + InvalidDuration::new( + duration, + "PIT interrupt target tick count would exceed a `u16`", + ) + })?; + + tracing::trace!(?duration, duration_ms, target_time, "Pit::interrupt_in"); + + let command = Command::new() + // use the binary counter + .with(Command::BCD_BINARY, false) + // generate a square wave (set the frequency divisor) + .with(Command::MODE, OperatingMode::Interrupt) + // we are sending both bytes of the divisor + .with(Command::ACCESS, AccessMode::Both) + // and we're configuring channel 0 + .with(Command::CHANNEL, ChannelSelect::Channel0); + self.send_command(command); + self.set_divisor(divisor); + + Ok(()) + } + + fn set_divisor(&mut self, divisor: u16) { + tracing::trace!(divisor = &fmt::hex(divisor), "Pit::set_divisor"); + let low = divisor as u8; + let high = (divisor >> 8) as u8; + unsafe { + self.channel0.writeb(low); // write the low byte + tracing::trace!(lo = &fmt::hex(low), "pit.channel0.writeb(lo)"); + self.channel0.writeb(high); // write the high byte + tracing::trace!(hi = &fmt::hex(high), "pit.channel0.writeb(hi)"); + } + } + + fn send_command(&self, command: Command) { + tracing::debug!(?command, "Pit::send_command"); + unsafe { + self.command.writeb(command.bits()); + } + } +} + // === impl ChannelSelect === impl FromBits for ChannelSelect { diff --git a/hal-x86_64/src/cpu/tsc.rs b/hal-x86_64/src/time/tsc.rs similarity index 93% rename from hal-x86_64/src/cpu/tsc.rs rename to hal-x86_64/src/time/tsc.rs index 9ac9f8e7..a181a90d 100644 --- a/hal-x86_64/src/cpu/tsc.rs +++ b/hal-x86_64/src/time/tsc.rs @@ -1,4 +1,4 @@ -use super::{intrinsics, FeatureNotSupported}; +use crate::cpu::{intrinsics, FeatureNotSupported}; use raw_cpuid::CpuId; #[derive(Copy, Clone, Debug)]