aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDario Nieuwenhuis <[email protected]>2022-02-23 05:14:32 +0100
committerDario Nieuwenhuis <[email protected]>2022-02-23 05:16:30 +0100
commitfdb6e66b4b7aa5a6b72ec2b926ea9e5de728c657 (patch)
treeb1f41a260ad1148a825bce55f075e268fe127f88
parent78795d6f56a3c0d6c5a03e6453cd827821a42825 (diff)
time: better docs explaining overflow handling.
-rw-r--r--embassy-nrf/src/time_driver.rs63
-rw-r--r--embassy/src/time/driver.rs11
2 files changed, 54 insertions, 20 deletions
diff --git a/embassy-nrf/src/time_driver.rs b/embassy-nrf/src/time_driver.rs
index 4240b9ac4..a32a7bc7c 100644
--- a/embassy-nrf/src/time_driver.rs
+++ b/embassy-nrf/src/time_driver.rs
@@ -14,24 +14,51 @@ fn rtc() -> &'static pac::rtc0::RegisterBlock {
14 unsafe { &*pac::RTC1::ptr() } 14 unsafe { &*pac::RTC1::ptr() }
15} 15}
16 16
17// RTC timekeeping works with something we call "periods", which are time intervals 17/// Calculate the timestamp from the period count and the tick count.
18// of 2^23 ticks. The RTC counter value is 24 bits, so one "overflow cycle" is 2 periods. 18///
19// 19/// The RTC counter is 24 bit. Ticking at 32768hz, it overflows every ~8 minutes. This is
20// A `period` count is maintained in parallel to the RTC hardware `counter`, like this: 20/// too short. We must make it "never" overflow.
21// - `period` and `counter` start at 0 21///
22// - `period` is incremented on overflow (at counter value 0) 22/// The obvious way would be to count overflow periods. Every time the counter overflows,
23// - `period` is incremented "midway" between overflows (at counter value 0x800000) 23/// increase a `periods` variable. `now()` simply does `periods << 24 + counter`. So, the logic
24// 24/// around an overflow would look like this:
25// Therefore, when `period` is even, counter is in 0..0x7fffff. When odd, counter is in 0x800000..0xFFFFFF 25///
26// This allows for now() to return the correct value even if it races an overflow. 26/// ```not_rust
27// 27/// periods = 1, counter = 0xFF_FFFE --> now = 0x1FF_FFFE
28// To get `now()`, `period` is read first, then `counter` is read. If the counter value matches 28/// periods = 1, counter = 0xFF_FFFF --> now = 0x1FF_FFFF
29// the expected range for the `period` parity, we're done. If it doesn't, this means that 29/// **OVERFLOW**
30// a new period start has raced us between reading `period` and `counter`, so we assume the `counter` value 30/// periods = 2, counter = 0x00_0000 --> now = 0x200_0000
31// corresponds to the next period. 31/// periods = 2, counter = 0x00_0001 --> now = 0x200_0001
32// 32/// ```
33// `period` is a 32bit integer, so It overflows on 2^32 * 2^23 / 32768 seconds of uptime, which is 34865 years. 33///
34 34/// The problem is this is vulnerable to race conditions if `now()` runs at the exact time an
35/// overflow happens.
36///
37/// If `now()` reads `periods` first and `counter` later, and overflow happens between the reads,
38/// it would return a wrong value:
39///
40/// ```not_rust
41/// periods = 1 (OLD), counter = 0x00_0000 (NEW) --> now = 0x100_0000 -> WRONG
42/// ```
43///
44/// It fails similarly if it reads `counter` first and `periods` second.
45///
46/// To fix this, we define a "period" to be 2^23 ticks (instead of 2^24). One "overflow cycle" is 2 periods.
47///
48/// - `period` is incremented on overflow (at counter value 0)
49/// - `period` is incremented "midway" between overflows (at counter value 0x80_0000)
50///
51/// Therefore, when `period` is even, counter is in 0..0x7f_ffff. When odd, counter is in 0x80_0000..0xFF_FFFF
52/// This allows for now() to return the correct value even if it races an overflow.
53///
54/// To get `now()`, `period` is read first, then `counter` is read. If the counter value matches
55/// the expected range for the `period` parity, we're done. If it doesn't, this means that
56/// a new period start has raced us between reading `period` and `counter`, so we assume the `counter` value
57/// corresponds to the next period.
58///
59/// `period` is a 32bit integer, so It overflows on 2^32 * 2^23 / 32768 seconds of uptime, which is 34865
60/// years. For comparison, flash memory like the one containing your firmware is usually rated to retain
61/// data for only 10-20 years. 34865 years is long enough!
35fn calc_now(period: u32, counter: u32) -> u64 { 62fn calc_now(period: u32, counter: u32) -> u64 {
36 ((period as u64) << 23) + ((counter ^ ((period & 1) << 23)) as u64) 63 ((period as u64) << 23) + ((counter ^ ((period & 1) << 23)) as u64)
37} 64}
diff --git a/embassy/src/time/driver.rs b/embassy/src/time/driver.rs
index a21a29d46..29256aab5 100644
--- a/embassy/src/time/driver.rs
+++ b/embassy/src/time/driver.rs
@@ -80,8 +80,15 @@ impl AlarmHandle {
80/// Time driver 80/// Time driver
81pub trait Driver: Send + Sync + 'static { 81pub trait Driver: Send + Sync + 'static {
82 /// Return the current timestamp in ticks. 82 /// Return the current timestamp in ticks.
83 /// This is guaranteed to be monotonic, i.e. a call to now() will always return 83 ///
84 /// a greater or equal value than earler calls. 84 /// Implementations MUST ensure that:
85 /// - This is guaranteed to be monotonic, i.e. a call to now() will always return
86 /// a greater or equal value than earler calls. Time can't "roll backwards".
87 /// - It "never" overflows. It must not overflow in a sufficiently long time frame, say
88 /// in 10_000 years (Human civilization is likely to already have self-destructed
89 /// 10_000 years from now.). This means if your hardware only has 16bit/32bit timers
90 /// you MUST extend them to 64-bit, for example by counting overflows in software,
91 /// or chaining multiple timers together.
85 fn now(&self) -> u64; 92 fn now(&self) -> u64;
86 93
87 /// Try allocating an alarm handle. Returns None if no alarms left. 94 /// Try allocating an alarm handle. Returns None if no alarms left.