diff --git a/CHANGELOG.md b/CHANGELOG.md index 11fc3a1..602f454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,18 @@ ### Added -- Add fallibility to `Clock` methods +- A `Timer` type supporting one-shot and periodic software timers utilizing a `Clock` implementation +- `Timer` unit tests +- Fallibility to `Clock` methods - `Instant::duration_until()` with order checking - Order checking to `Instant::duration_since()` - Bounds checking on `Instant` impls of Add/Sub - Changelog back to v0.5.0 release +- [`crossbeam-utils`](https://crates.io/crates/crossbeam-utils) dev-dependency for scoped threads in tests ### Changed -- Add `&mut self` to `Clock` functions (make stateful, or at least allow stateful implementations) +- Add `&self` to `Clock` functions (make stateful, or at least allow stateful implementations) - All time-type inner types from signed to unsigned - `Instant::duration_since()` return type to `Result` - Refactor `examples/nrf52_dk` diff --git a/Cargo.lock b/Cargo.lock index 050557d..0d34995 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cortex-m" version = "0.6.2" @@ -62,6 +68,17 @@ dependencies = [ "syn", ] +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + [[package]] name = "embedded-hal" version = "0.2.4" @@ -78,6 +95,7 @@ version = "0.5.2" dependencies = [ "cortex-m", "cortex-m-rt", + "crossbeam-utils", "mutex-trait", "nrf52832-hal", "num", @@ -100,6 +118,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f6af40154f64bd7095b5245d6933fef99b0a28c432b38a676cfafaa0aedfc2" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "mutex-trait" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 91319f3..437bcce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,5 +22,8 @@ panic_rtt = "0.3.0" nrf52832-hal = { version = "0.10.0", default-features = false, features = ["rt", "xxAA-package"] } mutex-trait = "0.2.0" +[target.'cfg(not(target_arch = "arm"))'.dev-dependencies] +crossbeam-utils = "0.7.2" + [patch.crates-io] cortex-m = { git = "https://github.com/rust-embedded/cortex-m", branch = "mutex_add" } diff --git a/examples/nrf52_dk/main.rs b/examples/nrf52_dk/main.rs index a450522..e85144e 100644 --- a/examples/nrf52_dk/main.rs +++ b/examples/nrf52_dk/main.rs @@ -152,12 +152,20 @@ where led2.set_high()?; led3.set_high()?; led4.set_low()?; - clock.delay(250_u32.milliseconds()).unwrap(); + clock + .new_timer() + .set_duration(250_u32.milliseconds()) + .start() + .wait(); led1.set_high()?; led2.set_low()?; led3.set_low()?; led4.set_high()?; - clock.delay(250_u32.milliseconds()).unwrap(); + clock + .new_timer() + .set_duration(250_u32.milliseconds()) + .start() + .wait(); } } diff --git a/src/clock.rs b/src/clock.rs index 4fcc263..fd3e5e1 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -1,7 +1,7 @@ //! The `Clock` trait can be implemented over hardware timers or other time-keeping device -use crate::{time_int::TimeInt, Duration, Instant, Period}; -use core::convert::TryFrom; +use crate::timer::param; +use crate::{time_int::TimeInt, Duration, Instant, Period, Timer}; /// Potential `Clock` errors #[non_exhaustive] @@ -28,17 +28,8 @@ pub trait Clock: Sized { /// Implementation-specific error returned through [`Error::Other(ImplError)`] fn now(&self) -> Result, Error>; - /// Blocking delay - /// - /// # Errors - /// Implementation-specific error returned through [`Error::Other(ImplError)`] - fn delay(&self, dur: Dur) -> Result<(), Error> - where - Self::Rep: TryFrom, - { - let start = self.now()?; - let end = start + dur; - while self.now()? < end {} - Ok(()) + /// Spawn a new, `OneShot` [`Timer`] from this clock + fn new_timer(&self) -> Timer { + Timer::::new(&self) } } diff --git a/src/lib.rs b/src/lib.rs index e8c9f1e..83c2e5f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,8 @@ //! **Wrapping Clock**: A clock that when at its maximum value, the next count is the minimum //! value. //! +//! **Timer**: An entity that counts toward an expiration. +//! //! **Instant**: A specific instant in time ("time-point") read from a clock. //! //! **Duration**: The difference of two instances. The time that has elapsed since an instant. A @@ -77,6 +79,7 @@ mod frequency; mod instant; mod period; mod time_int; +mod timer; pub use clock::Clock; use core::{convert::Infallible, fmt}; @@ -84,6 +87,7 @@ pub use duration::Duration; pub use instant::Instant; pub use period::Period; pub use time_int::TimeInt; +pub use timer::Timer; /// Public _traits_ /// diff --git a/src/timer.rs b/src/timer.rs new file mode 100644 index 0000000..196acae --- /dev/null +++ b/src/timer.rs @@ -0,0 +1,344 @@ +use crate::{duration::TryConvertFrom, timer::param::*, traits::*, units::*, Duration, Instant}; +use core::{convert::TryFrom, marker::PhantomData, ops::Add, prelude::v1::*}; + +pub(crate) mod param { + #[derive(Debug)] + pub struct None; + + #[derive(Debug)] + pub struct Disarmed; + + #[derive(Debug)] + pub struct Armed; + + #[derive(Debug)] + pub struct Running; + + #[derive(Debug)] + pub struct Periodic; + + #[derive(Debug)] + pub struct OneShot; +} + +/// A `Timer` counts toward an expiration, can be polled for elapsed and remaining time, as +/// well as optionally execute a task upon expiration. +#[derive(Debug)] +pub struct Timer<'a, Type, State, Clock: crate::Clock, Dur: Duration> { + clock: &'a Clock, + duration: Option, + expiration: Option>, + _type: PhantomData, + _state: PhantomData, +} + +impl<'a, Clock: crate::Clock, Dur: Duration> Timer<'_, param::None, param::None, Clock, Dur> { + /// Construct a new, `OneShot` `Timer` + #[allow(clippy::new_ret_no_self)] + pub fn new(clock: &Clock) -> Timer { + Timer:: { + clock, + duration: Option::None, + expiration: Option::None, + _type: PhantomData, + _state: PhantomData, + } + } +} + +impl<'a, Type, State, Clock: crate::Clock, Dur: Duration> Timer<'a, Type, State, Clock, Dur> { + /// Change timer type to one-shot + pub fn into_oneshot(self) -> Timer<'a, OneShot, State, Clock, Dur> { + Timer:: { + clock: self.clock, + duration: self.duration, + expiration: self.expiration, + _type: PhantomData, + _state: PhantomData, + } + } + + /// Change timer type into periodic + pub fn into_periodic(self) -> Timer<'a, Periodic, State, Clock, Dur> { + Timer:: { + clock: self.clock, + duration: self.duration, + expiration: self.expiration, + _type: PhantomData, + _state: PhantomData, + } + } +} + +impl<'a, Type, Clock: crate::Clock, Dur: Duration> Timer<'a, Type, Disarmed, Clock, Dur> { + /// Set the [`Duration`](trait.Duration.html) of the timer + /// + /// This _arms_ the timer (makes it ready to run). + pub fn set_duration(self, duration: Dur) -> Timer<'a, Type, Armed, Clock, Dur> { + Timer:: { + clock: self.clock, + duration: Some(duration), + expiration: Option::None, + _type: PhantomData, + _state: PhantomData, + } + } +} +impl<'a, Type, Clock: crate::Clock, Dur: Duration> Timer<'a, Type, Armed, Clock, Dur> { + /// Start the _armed_ timer from this instant + pub fn start(self) -> Timer<'a, Type, Running, Clock, Dur> + where + Instant: Add>, + { + Timer:: { + clock: self.clock, + duration: self.duration, + expiration: Some(self.clock.now().unwrap() + self.duration.unwrap()), + _type: PhantomData, + _state: PhantomData, + } + } +} + +impl Timer<'_, Type, Running, Clock, Dur> { + fn _is_expired(&self) -> bool { + self.clock.now().unwrap() >= self.expiration.unwrap() + } + + /// Returns the [`Duration`](trait.Duration.html) of time elapsed since it was started + /// + /// The units of the [`Duration`](trait.Duration.html) are the same as that used with + /// [`set_duration()`](struct.Timer.html#method.set_duration). + pub fn elapsed(&self) -> Dur + where + Dur::Rep: TryFrom, + Clock::Rep: TryFrom, + { + self.clock + .now() + .unwrap() + .duration_since(&(self.expiration.unwrap() - self.duration.unwrap())) + .unwrap() + } + + /// Returns the [`Duration`](trait.Duration.html) until the expiration of the timer + /// + /// The units of the [`Duration`](trait.Duration.html) are the same as that used with + /// [`set_duration()`](struct.Timer.html#method.set_duration). + pub fn remaining(&self) -> Dur + where + Dur::Rep: TryFrom, + Clock::Rep: TryFrom, + Dur: TryConvertFrom>, + { + if let Ok(duration) = self + .expiration + .unwrap() + .duration_since(&self.clock.now().unwrap()) + { + duration + } else { + 0.seconds().try_convert_into().unwrap() + } + } +} + +impl<'a, Clock: crate::Clock, Dur: Duration> Timer<'a, OneShot, Running, Clock, Dur> { + /// Block until the timer has expired + pub fn wait(self) -> Timer<'a, OneShot, Armed, Clock, Dur> { + // since the timer is running, _is_expired() will return a value + while !self._is_expired() {} + + Timer::::new(self.clock) + .set_duration(self.duration.unwrap()) + } + + /// Check whether the timer has expired + /// + /// The timer is not restarted + pub fn is_expired(&self) -> bool { + self._is_expired() + } +} + +impl Timer<'_, Periodic, Running, Clock, Dur> { + /// Block until the timer has expired + /// + /// The timer is restarted + pub fn wait(self) -> Self + where + Instant: Add>, + { + // since the timer is running, _is_expired() will return a value + while !self._is_expired() {} + + Self { + clock: self.clock, + duration: self.duration, + expiration: self + .expiration + .map(|expiration| expiration + self.duration.unwrap()), + _type: PhantomData, + _state: PhantomData, + } + } + + /// Check whether a _periodic_ timer has elapsed + /// + /// The timer is restarted if it has elapsed. + pub fn period_complete(&mut self) -> bool + where + Instant: Add>, + { + // since the timer is running, _is_expired() will return a value + if self._is_expired() { + self.expiration = Some(self.expiration.unwrap() + self.duration.unwrap()); + + true + } else { + false + } + } +} + +#[cfg(test)] +#[allow(unused_imports)] +#[allow(unsafe_code)] +mod test { + use crate::{traits::*, units::*, Duration, Error, Instant, Period}; + use core::convert::{Infallible, TryFrom}; + use crossbeam_utils::thread; + use std::sync::atomic::{AtomicU64, Ordering}; + + #[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] + struct Clock; + + static TICKS: AtomicU64 = AtomicU64::new(0); + + impl crate::Clock for Clock { + type Rep = u64; + const PERIOD: Period = ::new(1, 1_000); + type ImplError = Infallible; + + fn now(&self) -> Result, crate::clock::Error> { + Ok(Instant::new(TICKS.load(Ordering::Acquire))) + } + } + + #[test] + fn oneshot_wait() { + init_ticks(); + let clock = Clock; + + let timer = clock.new_timer().set_duration(1_u32.seconds()).start(); + + thread::scope(|s| { + let timer_handle = s.spawn(|_| timer.wait()); + + add_to_ticks(1_u32.seconds()); + + let result = timer_handle.join(); + + assert!(result.is_ok()); + + add_to_ticks(1_u32.seconds()); + + let timer = result.unwrap().start(); + assert!(!timer.is_expired()); + + let timer_handle = s.spawn(|_| timer.wait()); + add_to_ticks(1_u32.seconds()); + + assert!(timer_handle.join().is_ok()); + }) + .unwrap(); + } + + #[test] + fn periodic_wait() { + init_ticks(); + let clock = Clock; + + let timer = clock + .new_timer() + .into_periodic() + .set_duration(1_u32.seconds()) + .start(); + + thread::scope(|s| { + let timer_handle = s.spawn(|_| timer.wait()); + + add_to_ticks(1_u32.seconds()); + + let result = timer_handle.join(); + + assert!(result.is_ok()); + + let timer = result.unwrap(); + + // WHEN blocking on a timer + let timer_handle = s.spawn(|_| timer.wait()); + + add_to_ticks(1_u32.seconds()); + + assert!(timer_handle.join().is_ok()); + }) + .unwrap(); + } + + #[test] + fn periodic_expiration() { + init_ticks(); + let clock = Clock; + + let mut timer = clock + .new_timer() + .into_periodic() + .set_duration(1_u32.seconds()) + .start(); + + add_to_ticks(2_u32.seconds()); + + assert!(timer.period_complete()); + assert!(timer.period_complete()); + } + + #[test] + fn read_timer() { + init_ticks(); + let clock = Clock; + + let timer = clock.new_timer().set_duration(2_u32.seconds()).start(); + + add_to_ticks(1_u32.milliseconds()); + + assert_eq!(timer.elapsed(), 0_u32.seconds()); + assert_eq!(timer.remaining(), 1_u32.seconds()); + + add_to_ticks(1_u32.seconds()); + + assert_eq!(timer.elapsed(), 1_u32.seconds()); + assert_eq!(timer.remaining(), 0_u32.seconds()); + + add_to_ticks(1_u32.seconds()); + + assert_eq!(timer.elapsed(), 2_u32.seconds()); + assert_eq!(timer.remaining(), 0_u32.seconds()); + + add_to_ticks(1_u32.seconds()); + + assert_eq!(timer.elapsed(), 3_u32.seconds()); + assert_eq!(timer.remaining(), 0_u32.seconds()); + } + + fn init_ticks() {} + + fn add_to_ticks(duration: Dur) { + let ticks = TICKS.load(Ordering::Acquire); + let ticks = ticks + + duration + .into_ticks::<::Rep>(Clock::PERIOD) + .unwrap(); + TICKS.store(ticks, Ordering::Release); + } +}