Merge pull request #634 from embassy-rs/time-docs
time: better docs explaining overflow handling.
This commit is contained in:
commit
4c6e61b3b1
2 changed files with 54 additions and 20 deletions
|
@ -14,24 +14,51 @@ fn rtc() -> &'static pac::rtc0::RegisterBlock {
|
|||
unsafe { &*pac::RTC1::ptr() }
|
||||
}
|
||||
|
||||
// RTC timekeeping works with something we call "periods", which are time intervals
|
||||
// of 2^23 ticks. The RTC counter value is 24 bits, so one "overflow cycle" is 2 periods.
|
||||
//
|
||||
// A `period` count is maintained in parallel to the RTC hardware `counter`, like this:
|
||||
// - `period` and `counter` start at 0
|
||||
// - `period` is incremented on overflow (at counter value 0)
|
||||
// - `period` is incremented "midway" between overflows (at counter value 0x800000)
|
||||
//
|
||||
// Therefore, when `period` is even, counter is in 0..0x7fffff. When odd, counter is in 0x800000..0xFFFFFF
|
||||
// This allows for now() to return the correct value even if it races an overflow.
|
||||
//
|
||||
// To get `now()`, `period` is read first, then `counter` is read. If the counter value matches
|
||||
// the expected range for the `period` parity, we're done. If it doesn't, this means that
|
||||
// a new period start has raced us between reading `period` and `counter`, so we assume the `counter` value
|
||||
// corresponds to the next period.
|
||||
//
|
||||
// `period` is a 32bit integer, so It overflows on 2^32 * 2^23 / 32768 seconds of uptime, which is 34865 years.
|
||||
|
||||
/// Calculate the timestamp from the period count and the tick count.
|
||||
///
|
||||
/// The RTC counter is 24 bit. Ticking at 32768hz, it overflows every ~8 minutes. This is
|
||||
/// too short. We must make it "never" overflow.
|
||||
///
|
||||
/// The obvious way would be to count overflow periods. Every time the counter overflows,
|
||||
/// increase a `periods` variable. `now()` simply does `periods << 24 + counter`. So, the logic
|
||||
/// around an overflow would look like this:
|
||||
///
|
||||
/// ```not_rust
|
||||
/// periods = 1, counter = 0xFF_FFFE --> now = 0x1FF_FFFE
|
||||
/// periods = 1, counter = 0xFF_FFFF --> now = 0x1FF_FFFF
|
||||
/// **OVERFLOW**
|
||||
/// periods = 2, counter = 0x00_0000 --> now = 0x200_0000
|
||||
/// periods = 2, counter = 0x00_0001 --> now = 0x200_0001
|
||||
/// ```
|
||||
///
|
||||
/// The problem is this is vulnerable to race conditions if `now()` runs at the exact time an
|
||||
/// overflow happens.
|
||||
///
|
||||
/// If `now()` reads `periods` first and `counter` later, and overflow happens between the reads,
|
||||
/// it would return a wrong value:
|
||||
///
|
||||
/// ```not_rust
|
||||
/// periods = 1 (OLD), counter = 0x00_0000 (NEW) --> now = 0x100_0000 -> WRONG
|
||||
/// ```
|
||||
///
|
||||
/// It fails similarly if it reads `counter` first and `periods` second.
|
||||
///
|
||||
/// To fix this, we define a "period" to be 2^23 ticks (instead of 2^24). One "overflow cycle" is 2 periods.
|
||||
///
|
||||
/// - `period` is incremented on overflow (at counter value 0)
|
||||
/// - `period` is incremented "midway" between overflows (at counter value 0x80_0000)
|
||||
///
|
||||
/// Therefore, when `period` is even, counter is in 0..0x7f_ffff. When odd, counter is in 0x80_0000..0xFF_FFFF
|
||||
/// This allows for now() to return the correct value even if it races an overflow.
|
||||
///
|
||||
/// To get `now()`, `period` is read first, then `counter` is read. If the counter value matches
|
||||
/// the expected range for the `period` parity, we're done. If it doesn't, this means that
|
||||
/// a new period start has raced us between reading `period` and `counter`, so we assume the `counter` value
|
||||
/// corresponds to the next period.
|
||||
///
|
||||
/// `period` is a 32bit integer, so It overflows on 2^32 * 2^23 / 32768 seconds of uptime, which is 34865
|
||||
/// years. For comparison, flash memory like the one containing your firmware is usually rated to retain
|
||||
/// data for only 10-20 years. 34865 years is long enough!
|
||||
fn calc_now(period: u32, counter: u32) -> u64 {
|
||||
((period as u64) << 23) + ((counter ^ ((period & 1) << 23)) as u64)
|
||||
}
|
||||
|
|
|
@ -80,8 +80,15 @@ impl AlarmHandle {
|
|||
/// Time driver
|
||||
pub trait Driver: Send + Sync + 'static {
|
||||
/// Return the current timestamp in ticks.
|
||||
/// This is guaranteed to be monotonic, i.e. a call to now() will always return
|
||||
/// a greater or equal value than earler calls.
|
||||
///
|
||||
/// Implementations MUST ensure that:
|
||||
/// - This is guaranteed to be monotonic, i.e. a call to now() will always return
|
||||
/// a greater or equal value than earler calls. Time can't "roll backwards".
|
||||
/// - It "never" overflows. It must not overflow in a sufficiently long time frame, say
|
||||
/// in 10_000 years (Human civilization is likely to already have self-destructed
|
||||
/// 10_000 years from now.). This means if your hardware only has 16bit/32bit timers
|
||||
/// you MUST extend them to 64-bit, for example by counting overflows in software,
|
||||
/// or chaining multiple timers together.
|
||||
fn now(&self) -> u64;
|
||||
|
||||
/// Try allocating an alarm handle. Returns None if no alarms left.
|
||||
|
|
Loading…
Reference in a new issue