Fix mono delay (#843)

* rtic-time: Compenstate for timer uncertainty

* Update changelog and incorrect cargo.lock in an example

* Fix Monotonic impls

* Fix tests

* Fix other monotonics, again

* Update changelog

* Fix example

* Fix DelayUs and DelayMs impls

* Minor coding style fix in u64 conversions

* Fix all changelogs

* Fix changelog

* Fix blocking DelayUs

* Minor monotonic rework

* Add delay precision test

* Add more tests

* Add rust-version tags to Cargo.toml

* Fix imxrt, rp2040 and systick timer

* Fix more monotonics

* Fix systick monotonic

* Some reverts

* Fix imxrt

* Fix nrf

* Fix rp2040

* Fix stm32

* Fix systick

* Fix rtic-time tests

* Bump to e-h.rc2

* Apply e-h.rc2 fixes to rtic-time

* Apply fixes from arbiter

* Fix clippy warning

* Minor beautification

* Revert previous changes

* Fix variable name

* Add blocking tests, but disable them by default
This commit is contained in:
Finomnis 2023-12-01 08:59:22 +01:00 committed by GitHub
parent 9f5820da1d
commit 612a47ef4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 560 additions and 141 deletions

View file

@ -440,7 +440,7 @@ dependencies = [
[[package]] [[package]]
name = "rtic-monotonics" name = "rtic-monotonics"
version = "1.2.1" version = "1.3.0"
dependencies = [ dependencies = [
"atomic-polyfill", "atomic-polyfill",
"cfg-if", "cfg-if",

View file

@ -7,6 +7,10 @@ For each category, *Added*, *Changed*, *Fixed* add new entries at the top!
## Unreleased ## Unreleased
### Fixed
- **Soundness fix:** Monotonics did not wait long enough in `Duration` based delays.
## v1.3.0 - 2023-11-08 ## v1.3.0 - 2023-11-08
### Added ### Added

View file

@ -28,8 +28,8 @@ rustdoc-flags = ["--cfg", "docsrs"]
[dependencies] [dependencies]
rtic-time = { version = "1.0.0", path = "../rtic-time" } rtic-time = { version = "1.0.0", path = "../rtic-time" }
embedded-hal = { version = "1.0.0-rc.1" } embedded-hal = { version = "1.0.0-rc.2" }
embedded-hal-async = { version = "1.0.0-rc.1", optional = true } embedded-hal-async = { version = "1.0.0-rc.2", optional = true }
fugit = { version = "0.3.6" } fugit = { version = "0.3.6" }
atomic-polyfill = "1" atomic-polyfill = "1"
cfg-if = "1.0.0" cfg-if = "1.0.0"

View file

@ -31,7 +31,7 @@
use crate::{Monotonic, TimeoutError, TimerQueue}; use crate::{Monotonic, TimeoutError, TimerQueue};
use atomic_polyfill::{compiler_fence, AtomicU32, Ordering}; use atomic_polyfill::{compiler_fence, AtomicU32, Ordering};
pub use fugit::{self, ExtU64}; pub use fugit::{self, ExtU64, ExtU64Ceil};
use imxrt_ral as ral; use imxrt_ral as ral;
@ -216,31 +216,17 @@ macro_rules! make_timer {
} }
} }
rtic_time::embedded_hal_delay_impl_fugit64!($mono_name);
#[cfg(feature = "embedded-hal-async")] #[cfg(feature = "embedded-hal-async")]
impl embedded_hal_async::delay::DelayUs for $mono_name { rtic_time::embedded_hal_async_delay_impl_fugit64!($mono_name);
#[inline]
async fn delay_us(&mut self, us: u32) {
Self::delay((us as u64).micros()).await;
}
#[inline]
async fn delay_ms(&mut self, ms: u32) {
Self::delay((ms as u64).millis()).await;
}
}
impl embedded_hal::delay::DelayUs for $mono_name {
fn delay_us(&mut self, us: u32) {
let done = Self::now() + (us as u64).micros();
while Self::now() < done {}
}
}
impl Monotonic for $mono_name { impl Monotonic for $mono_name {
type Instant = fugit::TimerInstantU64<TIMER_HZ>; type Instant = fugit::TimerInstantU64<TIMER_HZ>;
type Duration = fugit::TimerDurationU64<TIMER_HZ>; type Duration = fugit::TimerDurationU64<TIMER_HZ>;
const ZERO: Self::Instant = Self::Instant::from_ticks(0); const ZERO: Self::Instant = Self::Instant::from_ticks(0);
const TICK_PERIOD: Self::Duration = Self::Duration::from_ticks(1);
fn now() -> Self::Instant { fn now() -> Self::Instant {
let gpt = unsafe{ $timer::instance() }; let gpt = unsafe{ $timer::instance() };

View file

@ -43,7 +43,7 @@ use nrf9160_pac::{self as pac, Interrupt, RTC0_NS as RTC0, RTC1_NS as RTC1};
use crate::{Monotonic, TimeoutError, TimerQueue}; use crate::{Monotonic, TimeoutError, TimerQueue};
use atomic_polyfill::{AtomicU32, Ordering}; use atomic_polyfill::{AtomicU32, Ordering};
use core::future::Future; use core::future::Future;
pub use fugit::{self, ExtU64}; pub use fugit::{self, ExtU64, ExtU64Ceil};
#[doc(hidden)] #[doc(hidden)]
#[macro_export] #[macro_export]
@ -167,28 +167,15 @@ macro_rules! make_rtc {
} }
} }
rtic_time::embedded_hal_delay_impl_fugit64!($mono_name);
#[cfg(feature = "embedded-hal-async")] #[cfg(feature = "embedded-hal-async")]
impl embedded_hal_async::delay::DelayUs for $mono_name { rtic_time::embedded_hal_async_delay_impl_fugit64!($mono_name);
#[inline]
async fn delay_us(&mut self, us: u32) {
Self::delay((us as u64).micros()).await;
}
#[inline]
async fn delay_ms(&mut self, ms: u32) {
Self::delay((ms as u64).millis()).await;
}
}
impl embedded_hal::delay::DelayUs for $mono_name {
fn delay_us(&mut self, us: u32) {
let done = Self::now() + u64::from(us).micros();
while Self::now() < done {}
}
}
impl Monotonic for $mono_name { impl Monotonic for $mono_name {
const ZERO: Self::Instant = Self::Instant::from_ticks(0); const ZERO: Self::Instant = Self::Instant::from_ticks(0);
const TICK_PERIOD: Self::Duration = Self::Duration::from_ticks(1);
type Instant = fugit::TimerInstantU64<32_768>; type Instant = fugit::TimerInstantU64<32_768>;
type Duration = fugit::TimerDurationU64<32_768>; type Duration = fugit::TimerDurationU64<32_768>;

View file

@ -29,7 +29,7 @@
use crate::{Monotonic, TimeoutError, TimerQueue}; use crate::{Monotonic, TimeoutError, TimerQueue};
use atomic_polyfill::{AtomicU32, Ordering}; use atomic_polyfill::{AtomicU32, Ordering};
use core::future::Future; use core::future::Future;
pub use fugit::{self, ExtU64}; pub use fugit::{self, ExtU64, ExtU64Ceil};
#[cfg(feature = "nrf52810")] #[cfg(feature = "nrf52810")]
use nrf52810_pac::{self as pac, Interrupt, TIMER0, TIMER1, TIMER2}; use nrf52810_pac::{self as pac, Interrupt, TIMER0, TIMER1, TIMER2};
@ -203,28 +203,14 @@ macro_rules! make_timer {
} }
} }
rtic_time::embedded_hal_delay_impl_fugit64!($mono_name);
#[cfg(feature = "embedded-hal-async")] #[cfg(feature = "embedded-hal-async")]
impl embedded_hal_async::delay::DelayUs for $mono_name { rtic_time::embedded_hal_async_delay_impl_fugit64!($mono_name);
#[inline]
async fn delay_us(&mut self, us: u32) {
Self::delay((us as u64).micros()).await;
}
#[inline]
async fn delay_ms(&mut self, ms: u32) {
Self::delay((ms as u64).millis()).await;
}
}
impl embedded_hal::delay::DelayUs for $mono_name {
fn delay_us(&mut self, us: u32) {
let done = Self::now() + (us as u64).micros();
while Self::now() < done {}
}
}
impl Monotonic for $mono_name { impl Monotonic for $mono_name {
const ZERO: Self::Instant = Self::Instant::from_ticks(0); const ZERO: Self::Instant = Self::Instant::from_ticks(0);
const TICK_PERIOD: Self::Duration = Self::Duration::from_ticks(1);
type Instant = fugit::TimerInstantU64<1_000_000>; type Instant = fugit::TimerInstantU64<1_000_000>;
type Duration = fugit::TimerDurationU64<1_000_000>; type Duration = fugit::TimerDurationU64<1_000_000>;

View file

@ -28,7 +28,7 @@ use super::Monotonic;
pub use super::{TimeoutError, TimerQueue}; pub use super::{TimeoutError, TimerQueue};
use core::future::Future; use core::future::Future;
pub use fugit::{self, ExtU64}; pub use fugit::{self, ExtU64, ExtU64Ceil};
use rp2040_pac::{timer, Interrupt, NVIC, RESETS, TIMER}; use rp2040_pac::{timer, Interrupt, NVIC, RESETS, TIMER};
/// Timer implementing [`Monotonic`] which runs at 1 MHz. /// Timer implementing [`Monotonic`] which runs at 1 MHz.
@ -104,6 +104,7 @@ impl Monotonic for Timer {
type Duration = fugit::TimerDurationU64<1_000_000>; type Duration = fugit::TimerDurationU64<1_000_000>;
const ZERO: Self::Instant = Self::Instant::from_ticks(0); const ZERO: Self::Instant = Self::Instant::from_ticks(0);
const TICK_PERIOD: Self::Duration = Self::Duration::from_ticks(1);
fn now() -> Self::Instant { fn now() -> Self::Instant {
let timer = Self::timer(); let timer = Self::timer();
@ -151,23 +152,10 @@ impl Monotonic for Timer {
fn disable_timer() {} fn disable_timer() {}
} }
rtic_time::embedded_hal_delay_impl_fugit64!(Timer);
#[cfg(feature = "embedded-hal-async")] #[cfg(feature = "embedded-hal-async")]
impl embedded_hal_async::delay::DelayUs for Timer { rtic_time::embedded_hal_async_delay_impl_fugit64!(Timer);
async fn delay_us(&mut self, us: u32) {
Self::delay((us as u64).micros()).await;
}
async fn delay_ms(&mut self, ms: u32) {
Self::delay((ms as u64).millis()).await;
}
}
impl embedded_hal::delay::DelayUs for Timer {
fn delay_us(&mut self, us: u32) {
let done = Self::now() + u64::from(us).micros();
while Self::now() < done {}
}
}
/// Register the Timer interrupt for the monotonic. /// Register the Timer interrupt for the monotonic.
#[macro_export] #[macro_export]

View file

@ -36,7 +36,7 @@
use crate::{Monotonic, TimeoutError, TimerQueue}; use crate::{Monotonic, TimeoutError, TimerQueue};
use atomic_polyfill::{compiler_fence, AtomicU64, Ordering}; use atomic_polyfill::{compiler_fence, AtomicU64, Ordering};
pub use fugit::{self, ExtU64}; pub use fugit::{self, ExtU64, ExtU64Ceil};
use stm32_metapac as pac; use stm32_metapac as pac;
mod _generated { mod _generated {
@ -218,31 +218,17 @@ macro_rules! make_timer {
} }
} }
rtic_time::embedded_hal_delay_impl_fugit64!($mono_name);
#[cfg(feature = "embedded-hal-async")] #[cfg(feature = "embedded-hal-async")]
impl embedded_hal_async::delay::DelayUs for $mono_name { rtic_time::embedded_hal_async_delay_impl_fugit64!($mono_name);
#[inline]
async fn delay_us(&mut self, us: u32) {
Self::delay((us as u64).micros()).await;
}
#[inline]
async fn delay_ms(&mut self, ms: u32) {
Self::delay((ms as u64).millis()).await;
}
}
impl embedded_hal::delay::DelayUs for $mono_name {
fn delay_us(&mut self, us: u32) {
let done = Self::now() + (us as u64).micros();
while Self::now() < done {}
}
}
impl Monotonic for $mono_name { impl Monotonic for $mono_name {
type Instant = fugit::TimerInstantU64<TIMER_HZ>; type Instant = fugit::TimerInstantU64<TIMER_HZ>;
type Duration = fugit::TimerDurationU64<TIMER_HZ>; type Duration = fugit::TimerDurationU64<TIMER_HZ>;
const ZERO: Self::Instant = Self::Instant::from_ticks(0); const ZERO: Self::Instant = Self::Instant::from_ticks(0);
const TICK_PERIOD: Self::Duration = Self::Duration::from_ticks(1);
fn now() -> Self::Instant { fn now() -> Self::Instant {
// Credits to the `time-driver` of `embassy-stm32`. // Credits to the `time-driver` of `embassy-stm32`.

View file

@ -40,11 +40,11 @@ use cortex_m::peripheral::SYST;
pub use fugit; pub use fugit;
cfg_if::cfg_if! { cfg_if::cfg_if! {
if #[cfg(feature = "systick-64bit")] { if #[cfg(feature = "systick-64bit")] {
pub use fugit::ExtU64; pub use fugit::{ExtU64, ExtU64Ceil};
use atomic_polyfill::AtomicU64; use atomic_polyfill::AtomicU64;
static SYSTICK_CNT: AtomicU64 = AtomicU64::new(0); static SYSTICK_CNT: AtomicU64 = AtomicU64::new(0);
} else { } else {
pub use fugit::ExtU32; pub use fugit::{ExtU32, ExtU32Ceil};
use atomic_polyfill::AtomicU32; use atomic_polyfill::AtomicU32;
static SYSTICK_CNT: AtomicU32 = AtomicU32::new(0); static SYSTICK_CNT: AtomicU32 = AtomicU32::new(0);
} }
@ -156,6 +156,7 @@ impl Monotonic for Systick {
} }
const ZERO: Self::Instant = Self::Instant::from_ticks(0); const ZERO: Self::Instant = Self::Instant::from_ticks(0);
const TICK_PERIOD: Self::Duration = Self::Duration::from_ticks(1);
fn now() -> Self::Instant { fn now() -> Self::Instant {
if Self::systick().has_wrapped() { if Self::systick().has_wrapped() {
@ -188,27 +189,17 @@ impl Monotonic for Systick {
fn disable_timer() {} fn disable_timer() {}
} }
#[cfg(feature = "embedded-hal-async")] cfg_if::cfg_if! {
impl embedded_hal_async::delay::DelayUs for Systick { if #[cfg(feature = "systick-64bit")] {
async fn delay_us(&mut self, us: u32) { rtic_time::embedded_hal_delay_impl_fugit64!(Systick);
#[cfg(feature = "systick-64bit")]
let us = u64::from(us);
Self::delay(us.micros()).await;
}
async fn delay_ms(&mut self, ms: u32) { #[cfg(feature = "embedded-hal-async")]
#[cfg(feature = "systick-64bit")] rtic_time::embedded_hal_async_delay_impl_fugit64!(Systick);
let ms = u64::from(ms); } else {
Self::delay(ms.millis()).await; rtic_time::embedded_hal_delay_impl_fugit32!(Systick);
}
}
impl embedded_hal::delay::DelayUs for Systick { #[cfg(feature = "embedded-hal-async")]
fn delay_us(&mut self, us: u32) { rtic_time::embedded_hal_async_delay_impl_fugit32!(Systick);
#[cfg(feature = "systick-64bit")]
let us = u64::from(us);
let done = Self::now() + us.micros();
while Self::now() < done {}
} }
} }

View file

@ -21,9 +21,9 @@ heapless = "0.7"
critical-section = "1" critical-section = "1"
rtic-common = { version = "1.0.0", path = "../rtic-common" } rtic-common = { version = "1.0.0", path = "../rtic-common" }
portable-atomic = { version = "1", default-features = false } portable-atomic = { version = "1", default-features = false }
embedded-hal = { version = "1.0.0-rc.1", optional = true } embedded-hal = { version = "1.0.0-rc.2", optional = true }
embedded-hal-async = { version = "1.0.0-rc.1", optional = true } embedded-hal-async = { version = "1.0.0-rc.2", optional = true }
embedded-hal-bus = { version = "0.1.0-rc.1", optional = true, features = ["async"] } embedded-hal-bus = { version = "0.1.0-rc.2", optional = true, features = ["async"] }
[dev-dependencies] [dev-dependencies]
tokio = { version = "1", features = ["rt", "macros", "time"] } tokio = { version = "1", features = ["rt", "macros", "time"] }

View file

@ -197,7 +197,7 @@ pub mod spi {
use super::Arbiter; use super::Arbiter;
use embedded_hal::digital::OutputPin; use embedded_hal::digital::OutputPin;
use embedded_hal_async::{ use embedded_hal_async::{
delay::DelayUs, delay::DelayNs,
spi::{ErrorType, Operation, SpiBus, SpiDevice}, spi::{ErrorType, Operation, SpiBus, SpiDevice},
}; };
use embedded_hal_bus::spi::DeviceError; use embedded_hal_bus::spi::DeviceError;
@ -229,7 +229,7 @@ pub mod spi {
Word: Copy + 'static, Word: Copy + 'static,
BUS: SpiBus<Word>, BUS: SpiBus<Word>,
CS: OutputPin, CS: OutputPin,
D: DelayUs, D: DelayNs,
{ {
async fn transaction( async fn transaction(
&mut self, &mut self,
@ -246,10 +246,10 @@ pub mod spi {
Operation::Write(buf) => bus.write(buf).await, Operation::Write(buf) => bus.write(buf).await,
Operation::Transfer(read, write) => bus.transfer(read, write).await, Operation::Transfer(read, write) => bus.transfer(read, write).await,
Operation::TransferInPlace(buf) => bus.transfer_in_place(buf).await, Operation::TransferInPlace(buf) => bus.transfer_in_place(buf).await,
Operation::DelayUs(us) => match bus.flush().await { Operation::DelayNs(ns) => match bus.flush().await {
Err(e) => Err(e), Err(e) => Err(e),
Ok(()) => { Ok(()) => {
self.delay.delay_us(*us).await; self.delay.delay_ns(*ns).await;
Ok(()) Ok(())
} }
}, },

View file

@ -15,6 +15,7 @@ For each category, *Added*, *Changed*, *Fixed* add new entries at the top!
### Fixed ### Fixed
- **Soundness fix:** `TimerQueue` did not wait long enough in `Duration` based delays. Fixing this sadly required adding a `const TICK_PERIOD` to the `Monotonic` trait, which requires updating all existing implementations.
- If the queue was non-empty and a new instant was added that was earlier than `head`, then the queue would no pend the monotonic handler. This would cause the new `head` to be dequeued at the wrong time. - If the queue was non-empty and a new instant was added that was earlier than `head`, then the queue would no pend the monotonic handler. This would cause the new `head` to be dequeued at the wrong time.
## [v1.0.0] - 2023-05-31 ## [v1.0.0] - 2023-05-31

View file

@ -22,5 +22,9 @@ futures-util = { version = "0.3.25", default-features = false }
rtic-common = { version = "1.0.0", path = "../rtic-common" } rtic-common = { version = "1.0.0", path = "../rtic-common" }
[dev-dependencies] [dev-dependencies]
embedded-hal = { version = "1.0.0-rc.2" }
embedded-hal-async = { version = "1.0.0-rc.2" }
fugit = "0.3.7"
parking_lot = "0.12" parking_lot = "0.12"
cassette = "0.2" cassette = "0.2"
cooked-waker = "5.0.0"

View file

@ -181,22 +181,36 @@ impl<Mono: Monotonic> TimerQueue<Mono> {
} }
} }
/// Timeout after a specific duration. /// Timeout after at least a specific duration.
#[inline] #[inline]
pub async fn timeout_after<F: Future>( pub async fn timeout_after<F: Future>(
&self, &self,
duration: Mono::Duration, duration: Mono::Duration,
future: F, future: F,
) -> Result<F::Output, TimeoutError> { ) -> Result<F::Output, TimeoutError> {
self.timeout_at(Mono::now() + duration, future).await let now = Mono::now();
let mut timeout = now + duration;
if now != timeout {
timeout = timeout + Mono::TICK_PERIOD;
}
// Wait for one period longer, because by definition timers have an uncertainty
// of one period, so waiting for 'at least' needs to compensate for that.
self.timeout_at(timeout, future).await
} }
/// Delay for some duration of time. /// Delay for at least some duration of time.
#[inline] #[inline]
pub async fn delay(&self, duration: Mono::Duration) { pub async fn delay(&self, duration: Mono::Duration) {
let now = Mono::now(); let now = Mono::now();
let mut timeout = now + duration;
if now != timeout {
timeout = timeout + Mono::TICK_PERIOD;
}
self.delay_until(now + duration).await; // Wait for one period longer, because by definition timers have an uncertainty
// of one period, so waiting for 'at least' needs to compensate for that.
self.delay_until(timeout).await;
} }
/// Delay to some specific time instant. /// Delay to some specific time instant.

View file

@ -10,6 +10,9 @@ pub trait Monotonic {
/// The time at time zero. /// The time at time zero.
const ZERO: Self::Instant; const ZERO: Self::Instant;
/// The duration between two timer ticks.
const TICK_PERIOD: Self::Duration;
/// The type for instant, defining an instant in time. /// The type for instant, defining an instant in time.
/// ///
/// **Note:** In all APIs in RTIC that use instants from this monotonic, this type will be used. /// **Note:** In all APIs in RTIC that use instants from this monotonic, this type will be used.
@ -65,3 +68,153 @@ pub trait Monotonic {
/// NOTE: This may be called more than once. /// NOTE: This may be called more than once.
fn disable_timer() {} fn disable_timer() {}
} }
/// Creates impl blocks for `embedded_hal::delay::DelayUs`,
/// based on `fugit::ExtU64Ceil`.
#[macro_export]
macro_rules! embedded_hal_delay_impl_fugit64 {
($t:ty) => {
impl ::embedded_hal::delay::DelayNs for $t {
fn delay_ns(&mut self, ns: u32) {
use ::fugit::ExtU64Ceil;
let now = Self::now();
let mut done = now + u64::from(ns).nanos_at_least();
if now != done {
// Compensate for sub-tick uncertainty
done += Self::TICK_PERIOD;
}
while Self::now() < done {}
}
fn delay_us(&mut self, us: u32) {
use ::fugit::ExtU64Ceil;
let now = Self::now();
let mut done = now + u64::from(us).micros_at_least();
if now != done {
// Compensate for sub-tick uncertainty
done += Self::TICK_PERIOD;
}
while Self::now() < done {}
}
fn delay_ms(&mut self, ms: u32) {
use ::fugit::ExtU64Ceil;
let now = Self::now();
let mut done = now + u64::from(ms).millis_at_least();
if now != done {
// Compensate for sub-tick uncertainty
done += Self::TICK_PERIOD;
}
while Self::now() < done {}
}
}
};
}
/// Creates impl blocks for `embedded_hal_async::delay::DelayUs`,
/// based on `fugit::ExtU64Ceil`.
#[macro_export]
macro_rules! embedded_hal_async_delay_impl_fugit64 {
($t:ty) => {
impl ::embedded_hal_async::delay::DelayNs for $t {
#[inline]
async fn delay_ns(&mut self, ns: u32) {
use ::fugit::ExtU64Ceil;
Self::delay(u64::from(ns).nanos_at_least()).await;
}
#[inline]
async fn delay_us(&mut self, us: u32) {
use ::fugit::ExtU64Ceil;
Self::delay(u64::from(us).micros_at_least()).await;
}
#[inline]
async fn delay_ms(&mut self, ms: u32) {
use ::fugit::ExtU64Ceil;
Self::delay(u64::from(ms).millis_at_least()).await;
}
}
};
}
/// Creates impl blocks for `embedded_hal::delay::DelayUs`,
/// based on `fugit::ExtU32Ceil`.
#[macro_export]
macro_rules! embedded_hal_delay_impl_fugit32 {
($t:ty) => {
impl ::embedded_hal::delay::DelayNs for $t {
fn delay_ns(&mut self, ns: u32) {
use ::fugit::ExtU32Ceil;
let now = Self::now();
let mut done = now + ns.nanos_at_least();
if now != done {
// Compensate for sub-tick uncertainty
done += Self::TICK_PERIOD;
}
while Self::now() < done {}
}
fn delay_us(&mut self, us: u32) {
use ::fugit::ExtU32Ceil;
let now = Self::now();
let mut done = now + us.micros_at_least();
if now != done {
// Compensate for sub-tick uncertainty
done += Self::TICK_PERIOD;
}
while Self::now() < done {}
}
fn delay_ms(&mut self, ms: u32) {
use ::fugit::ExtU32Ceil;
let now = Self::now();
let mut done = now + ms.millis_at_least();
if now != done {
// Compensate for sub-tick uncertainty
done += Self::TICK_PERIOD;
}
while Self::now() < done {}
}
}
};
}
/// Creates impl blocks for `embedded_hal_async::delay::DelayUs`,
/// based on `fugit::ExtU32Ceil`.
#[macro_export]
macro_rules! embedded_hal_async_delay_impl_fugit32 {
($t:ty) => {
impl ::embedded_hal_async::delay::DelayNs for $t {
#[inline]
async fn delay_ns(&mut self, ns: u32) {
use ::fugit::ExtU32Ceil;
Self::delay(ns.nanos_at_least()).await;
}
#[inline]
async fn delay_us(&mut self, us: u32) {
use ::fugit::ExtU32Ceil;
Self::delay(us.micros_at_least()).await;
}
#[inline]
async fn delay_ms(&mut self, ms: u32) {
use ::fugit::ExtU32Ceil;
Self::delay(ms.millis_at_least()).await;
}
}
};
}

View file

@ -0,0 +1,313 @@
//! A test that verifies the sub-tick correctness of the [`TimerQueue`]'s `delay` functionality.
//!
//! To run this test, you need to activate the `critical-section/std` feature.
use std::{
fmt::Debug,
future::Future,
pin::Pin,
sync::{
atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
Arc,
},
task::Context,
thread::sleep,
time::Duration,
};
use ::fugit::ExtU64Ceil;
use cooked_waker::{IntoWaker, WakeRef};
use parking_lot::Mutex;
use rtic_time::{Monotonic, TimeoutError, TimerQueue};
const SUBTICKS_PER_TICK: u32 = 10;
struct SubtickTestTimer;
static TIMER_QUEUE: TimerQueue<SubtickTestTimer> = TimerQueue::new();
static NOW_SUBTICKS: AtomicU64 = AtomicU64::new(0);
static COMPARE_TICKS: Mutex<Option<u64>> = Mutex::new(None);
impl Monotonic for SubtickTestTimer {
const ZERO: Self::Instant = Self::Instant::from_ticks(0);
const TICK_PERIOD: Self::Duration = Self::Duration::from_ticks(1);
type Instant = fugit::Instant<u64, SUBTICKS_PER_TICK, 1000>;
type Duration = fugit::Duration<u64, SUBTICKS_PER_TICK, 1000>;
fn now() -> Self::Instant {
Self::Instant::from_ticks(
NOW_SUBTICKS.load(Ordering::Relaxed) / u64::from(SUBTICKS_PER_TICK),
)
}
fn set_compare(instant: Self::Instant) {
*COMPARE_TICKS.lock() = Some(instant.ticks());
}
fn clear_compare_flag() {}
fn pend_interrupt() {
unsafe {
Self::__tq().on_monotonic_interrupt();
}
}
}
impl SubtickTestTimer {
pub fn init() {
Self::__tq().initialize(Self)
}
pub fn tick() -> u64 {
let now = NOW_SUBTICKS.fetch_add(1, Ordering::Relaxed) + 1;
let ticks = now / u64::from(SUBTICKS_PER_TICK);
let subticks = now % u64::from(SUBTICKS_PER_TICK);
let compare = COMPARE_TICKS.lock();
// println!(
// "ticks: {ticks}, subticks: {subticks}, compare: {:?}",
// *compare
// );
if subticks == 0 && Some(ticks) == *compare {
unsafe {
Self::__tq().on_monotonic_interrupt();
}
}
subticks
}
pub fn forward_to_subtick(subtick: u64) {
assert!(subtick < u64::from(SUBTICKS_PER_TICK));
while Self::tick() != subtick {}
}
pub fn now_subticks() -> u64 {
NOW_SUBTICKS.load(Ordering::Relaxed)
}
fn __tq() -> &'static TimerQueue<Self> {
&TIMER_QUEUE
}
/// Delay for some duration of time.
#[inline]
pub async fn delay(duration: <Self as Monotonic>::Duration) {
Self::__tq().delay(duration).await;
}
/// Timeout after a specific duration.
#[inline]
pub async fn timeout_after<F: core::future::Future>(
duration: <Self as Monotonic>::Duration,
future: F,
) -> Result<F::Output, TimeoutError> {
Self::__tq().timeout_after(duration, future).await
}
}
rtic_time::embedded_hal_delay_impl_fugit64!(SubtickTestTimer);
rtic_time::embedded_hal_async_delay_impl_fugit64!(SubtickTestTimer);
// A simple struct that counts the number of times it is awoken. Can't
// be awoken by value (because that would discard the counter), so we
// must instead wrap it in an Arc.
#[derive(Debug, Default)]
struct WakeCounter {
count: AtomicUsize,
}
impl WakeCounter {
fn get(&self) -> usize {
self.count.load(Ordering::SeqCst)
}
}
impl WakeRef for WakeCounter {
fn wake_by_ref(&self) {
let _prev = self.count.fetch_add(1, Ordering::SeqCst);
}
}
struct OnDrop<F: FnOnce()>(Option<F>);
impl<F: FnOnce()> OnDrop<F> {
pub fn new(f: F) -> Self {
Self(Some(f))
}
}
impl<F: FnOnce()> Drop for OnDrop<F> {
fn drop(&mut self) {
(self.0.take().unwrap())();
}
}
macro_rules! subtick_test {
(@run $start:expr, $actual_duration:expr, $delay_fn:expr) => {{
// forward clock to $start
SubtickTestTimer::forward_to_subtick($start);
// call wait function
let delay_fn = $delay_fn;
let mut future = std::pin::pin!(delay_fn);
let wakecounter = Arc::new(WakeCounter::default());
let waker = Arc::clone(&wakecounter).into_waker();
let mut context = Context::from_waker(&waker);
let mut finished_after: Option<u64> = None;
for i in 0..10 * u64::from(SUBTICKS_PER_TICK) {
if Future::poll(Pin::new(&mut future), &mut context).is_ready() {
if finished_after.is_none() {
finished_after = Some(i);
}
break;
};
assert_eq!(wakecounter.get(), 0);
SubtickTestTimer::tick();
}
let expected_wakeups = {
if $actual_duration == 0 {
0
} else {
1
}
};
assert_eq!(wakecounter.get(), expected_wakeups);
// Tick again to test that we don't get a second wake
SubtickTestTimer::tick();
assert_eq!(wakecounter.get(), expected_wakeups);
assert_eq!(
Some($actual_duration),
finished_after,
"Expected to wait {} ticks, but waited {:?} ticks.",
$actual_duration,
finished_after,
);
}};
(@run_blocking $start:expr, $actual_duration:expr, $delay_fn:expr) => {{
// forward clock to $start
SubtickTestTimer::forward_to_subtick($start);
let t_start = SubtickTestTimer::now_subticks();
let finished = AtomicBool::new(false);
std::thread::scope(|s|{
s.spawn(||{
let _finished_guard = OnDrop::new(|| finished.store(true, Ordering::Relaxed));
($delay_fn)();
});
s.spawn(||{
sleep(Duration::from_millis(10));
while !finished.load(Ordering::Relaxed) {
SubtickTestTimer::tick();
sleep(Duration::from_millis(10));
}
});
});
let t_end = SubtickTestTimer::now_subticks();
let measured_duration = t_end - t_start;
assert_eq!(
$actual_duration,
measured_duration,
"Expected to wait {} ticks, but waited {:?} ticks.",
$actual_duration,
measured_duration,
);
}};
($start:expr, $min_duration:expr, $actual_duration:expr) => {{
subtick_test!(@run $start, $actual_duration, async {
let mut timer = SubtickTestTimer;
embedded_hal_async::delay::DelayNs::delay_ms(&mut timer, $min_duration).await;
});
subtick_test!(@run $start, $actual_duration, async {
let mut timer = SubtickTestTimer;
embedded_hal_async::delay::DelayNs::delay_us(&mut timer, 1_000 * $min_duration).await;
});
subtick_test!(@run $start, $actual_duration, async {
let mut timer = SubtickTestTimer;
embedded_hal_async::delay::DelayNs::delay_ns(&mut timer, 1_000_000 * $min_duration).await;
});
subtick_test!(@run $start, $actual_duration, async {
SubtickTestTimer::delay($min_duration.millis_at_least()).await;
});
subtick_test!(@run $start, $actual_duration, async {
let _ = SubtickTestTimer::timeout_after($min_duration.millis_at_least(), std::future::pending::<()>()).await;
});
// Those are slow and unreliable; enable them when needed.
const ENABLE_BLOCKING_TESTS: bool = false;
if ENABLE_BLOCKING_TESTS {
subtick_test!(@run_blocking $start, $actual_duration, || {
let mut timer = SubtickTestTimer;
embedded_hal::delay::DelayNs::delay_ms(&mut timer, $min_duration);
});
subtick_test!(@run_blocking $start, $actual_duration, || {
let mut timer = SubtickTestTimer;
embedded_hal::delay::DelayNs::delay_us(&mut timer, 1_000 * $min_duration);
});
subtick_test!(@run_blocking $start, $actual_duration, || {
let mut timer = SubtickTestTimer;
embedded_hal::delay::DelayNs::delay_ns(&mut timer, 1_000_000 * $min_duration);
});
}
}};
}
#[test]
fn timer_queue_subtick_precision() {
SubtickTestTimer::init();
// subtick_test!(a, b, c) tests the following thing:
//
// If we start at subtick a and we need to wait b subticks,
// then we will actually wait c subticks.
// The important part is that c is never smaller than b,
// in all cases, as that would violate the contract of
// embedded-hal's DelayUs.
subtick_test!(0, 0, 0);
subtick_test!(0, 1, 20);
subtick_test!(0, 10, 20);
subtick_test!(0, 11, 30);
subtick_test!(0, 12, 30);
subtick_test!(1, 0, 0);
subtick_test!(1, 1, 19);
subtick_test!(1, 10, 19);
subtick_test!(1, 11, 29);
subtick_test!(1, 12, 29);
subtick_test!(2, 0, 0);
subtick_test!(2, 1, 18);
subtick_test!(2, 10, 18);
subtick_test!(2, 11, 28);
subtick_test!(2, 12, 28);
subtick_test!(3, 0, 0);
subtick_test!(3, 1, 17);
subtick_test!(3, 10, 17);
subtick_test!(3, 11, 27);
subtick_test!(3, 12, 27);
subtick_test!(8, 0, 0);
subtick_test!(8, 1, 12);
subtick_test!(8, 10, 12);
subtick_test!(8, 11, 22);
subtick_test!(8, 12, 22);
subtick_test!(9, 0, 0);
subtick_test!(9, 1, 11);
subtick_test!(9, 10, 11);
subtick_test!(9, 11, 21);
subtick_test!(9, 12, 21);
}

View file

@ -17,7 +17,7 @@ static NOW: Mutex<Option<Instant>> = Mutex::new(None);
pub struct Duration(u64); pub struct Duration(u64);
impl Duration { impl Duration {
pub fn from_ticks(millis: u64) -> Self { pub const fn from_ticks(millis: u64) -> Self {
Self(millis) Self(millis)
} }
@ -161,6 +161,7 @@ impl TestMono {
impl Monotonic for TestMono { impl Monotonic for TestMono {
const ZERO: Self::Instant = Instant::ZERO; const ZERO: Self::Instant = Instant::ZERO;
const TICK_PERIOD: Self::Duration = Duration::from_ticks(1);
type Instant = Instant; type Instant = Instant;
@ -210,7 +211,8 @@ fn timer_queue() {
let elapsed = start.elapsed().as_ticks(); let elapsed = start.elapsed().as_ticks();
println!("{total_millis} ticks delay reached after {elapsed} ticks"); println!("{total_millis} ticks delay reached after {elapsed} ticks");
if elapsed != total_millis { // Expect a delay of one longer, to compensate for timer uncertainty
if elapsed != total_millis + 1 {
panic!( panic!(
"{total_millis} ticks delay was not on time ({elapsed} ticks passed instead)" "{total_millis} ticks delay was not on time ({elapsed} ticks passed instead)"
); );
@ -263,25 +265,25 @@ fn timer_queue() {
if Instant::now() == 0.into() { if Instant::now() == 0.into() {
// First, we want to be waiting for our 300 tick delay // First, we want to be waiting for our 300 tick delay
assert_eq!(TestMono::compare(), Some(300.into())); assert_eq!(TestMono::compare(), Some(301.into()));
} }
if Instant::now() == 100.into() { if Instant::now() == 100.into() {
// After 100 ticks, we enqueue a new delay that is supposed to last // After 100 ticks, we enqueue a new delay that is supposed to last
// until the 200-tick-mark // until the 200-tick-mark
assert_eq!(TestMono::compare(), Some(200.into())); assert_eq!(TestMono::compare(), Some(201.into()));
} }
if Instant::now() == 200.into() { if Instant::now() == 201.into() {
// After 200 ticks, we dequeue the 200-tick-mark delay and // After 200 ticks, we dequeue the 200-tick-mark delay and
// requeue the 300 tick delay // requeue the 300 tick delay
assert_eq!(TestMono::compare(), Some(300.into())); assert_eq!(TestMono::compare(), Some(301.into()));
} }
if Instant::now() == 300.into() { if Instant::now() == 301.into() {
// After 300 ticks, we dequeue the 300-tick-mark delay and // After 300 ticks, we dequeue the 300-tick-mark delay and
// go to the 400 tick delay that is already enqueued // go to the 400 tick delay that is already enqueued
assert_eq!(TestMono::compare(), Some(400.into())); assert_eq!(TestMono::compare(), Some(401.into()));
} }
} }

View file

@ -8,10 +8,14 @@ For each category, *Added*, *Changed*, *Fixed* add new entries at the top!
## [Unreleased] ## [Unreleased]
### Added ### Added
- Unstable support for ESP32-C3 - Unstable support for ESP32-C3
### Fixed ### Fixed
- **Soundness fix:** Monotonics did not wait long enough in `Duration` based delays.
This is not directly a change for `rtic`, but required bumping the minimal version of `rtic-monotonics`.
### Changed ### Changed
## [v2.0.1] - 2023-07-25 ## [v2.0.1] - 2023-07-25

View file

@ -5,12 +5,12 @@ the hal takes a duration of Duration { ticks: 45 }
hal returned 5 hal returned 5
the hal takes a duration of Duration { ticks: 45 } the hal takes a duration of Duration { ticks: 45 }
hal returned 5 hal returned 5
now is Instant { ticks: 210 }, timeout at Instant { ticks: 260 } now is Instant { ticks: 213 }, timeout at Instant { ticks: 263 }
the hal takes a duration of Duration { ticks: 35 } the hal takes a duration of Duration { ticks: 35 }
hal returned 5 at time Instant { ticks: 245 } hal returned 5 at time Instant { ticks: 249 }
now is Instant { ticks: 310 }, timeout at Instant { ticks: 360 } now is Instant { ticks: 313 }, timeout at Instant { ticks: 363 }
the hal takes a duration of Duration { ticks: 45 } the hal takes a duration of Duration { ticks: 45 }
hal returned 5 at time Instant { ticks: 355 } hal returned 5 at time Instant { ticks: 359 }
now is Instant { ticks: 410 }, timeout at Instant { ticks: 460 } now is Instant { ticks: 413 }, timeout at Instant { ticks: 463 }
the hal takes a duration of Duration { ticks: 55 } the hal takes a duration of Duration { ticks: 55 }
timeout timeout