Skip to content

Commit fbf7df4

Browse files
Ctru14Connor TruonoCopilot
authored
Enable RTC-based Time Alarm functionality (#542)
Enables access to the RTC alarm registers to provide an interrupt-based time alarm on the RTC peripheral. This allows for a lower power floor as the OsTimer can be shut down for longer periods in favor of an RTC wake. Added the following to the RTC peripheral: - set_alarm() function - clear_alarm() function - RTC() interrupt handler on match - register_alarm_waker() function for use in Future::poll rtc-alarm.rs demo added to RT685 EVK examples showing the custom RtcAlarm struct/Future needed to implement the alarm based on the consumer's peripheral synchronization Demo results: ``` 269.753145 [INFO ] RTC set time: Datetime { data: UncheckedDatetime { year: 2026, month: January, day: 12, hour: 16, minute: 0, second: 0, nanosecond: 0 } } (rtc_alarm src/bin/rtc-alarm.rs:56) 269.753381 [INFO ] Current time before alarm: Datetime { data: UncheckedDatetime { year: 2026, month: January, day: 12, hour: 16, minute: 0, second: 0, nanosecond: 0 } } (rtc_alarm src/bin/rtc-alarm.rs:61) 269.753466 [INFO ] Waiting 5 seconds... (rtc_alarm src/bin/rtc-alarm.rs:63) 274.753764 [INFO ] Current time after waiting: Datetime { data: UncheckedDatetime { year: 2026, month: January, day: 12, hour: 16, minute: 0, second: 4, nanosecond: 0 } } (rtc_alarm src/bin/rtc-alarm.rs:66) 274.753879 [INFO ] Setting alarm to trigger in 10 seconds... (rtc_alarm src/bin/rtc-alarm.rs:72) 274.754072 [INFO ] Waiting for alarm... (rtc_alarm src/bin/rtc-alarm.rs:81) 274.754231 [INFO ] Alarm pending at time Datetime { data: UncheckedDatetime { year: 2026, month: January, day: 12, hour: 16, minute: 0, second: 4, nanosecond: 0 } }, expires at 1768233614 (rtc_alarm src/bin/rtc-alarm.rs:26) 283.918927 [INFO ] Alarm triggered! Wake time: Datetime { data: UncheckedDatetime { year: 2026, month: January, day: 12, hour: 16, minute: 0, second: 14, nanosecond: 0 } } (rtc_alarm src/bin/rtc-alarm.rs:86) 283.919091 [INFO ] Alarm cleared (rtc_alarm src/bin/rtc-alarm.rs:90) ``` --------- Co-authored-by: Connor Truono <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent a5aad0d commit fbf7df4

File tree

3 files changed

+262
-5
lines changed

3 files changed

+262
-5
lines changed

examples/rt685s-evk/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ These examples illustrates how to use the embassy-imxrt HAL.
88
Add uniquely named example to `src/bin` like `adc.rs`
99

1010
## Build
11-
`cd` to examples folder
11+
`cd` to examples/[device] folder
1212
`cargo build --bin <example_name>` for example, `cargo build --bin adc`
1313

1414
## Run
1515
Assuming RT685 is powered and connected to Jlink debug probe and the latest probe-rs is installed via
1616
`$ cargo install probe-rs-tools --git https://github.com/probe-rs/probe-rs --locked`
17-
`cd` to examples folder
17+
`cd` to examples/[device] folder
1818
`cargo run --bin <example_name>` for example, `cargo run --bin adc`
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#![no_std]
2+
#![no_main]
3+
4+
use core::task::Poll;
5+
6+
use defmt::info;
7+
use embassy_executor::Spawner;
8+
use embassy_imxrt::rtc::{Rtc, RtcDatetimeClock};
9+
use embassy_time::Timer;
10+
use embedded_mcu_hal::time::{Datetime, DatetimeClock, DatetimeClockError, Month, UncheckedDatetime};
11+
use {defmt_rtt as _, embassy_imxrt_examples as _, panic_probe as _};
12+
13+
/// RTC alarm struct to await the time alarm wakeup location
14+
/// This should be implemented by the user to handle RTC peripheral synchronization
15+
struct RtcAlarm<'r> {
16+
expires_at: u64,
17+
rtc: &'r RtcDatetimeClock<'r>,
18+
}
19+
20+
impl<'r> Future for RtcAlarm<'r> {
21+
type Output = Result<(), DatetimeClockError>;
22+
fn poll(self: core::pin::Pin<&mut Self>, cx: &mut core::task::Context<'_>) -> Poll<Self::Output> {
23+
match self.rtc.get_current_datetime() {
24+
Ok(now) => {
25+
if self.expires_at <= now.to_unix_time_seconds() {
26+
Poll::Ready(Ok(()))
27+
} else {
28+
info!("Alarm pending at time {}, expires at {}", now, self.expires_at);
29+
30+
// Register our waker to be called by the interrupt handler
31+
self.rtc.register_alarm_waker(cx.waker());
32+
33+
Poll::Pending
34+
}
35+
}
36+
Err(e) => Poll::Ready(Err(e)),
37+
}
38+
}
39+
}
40+
#[embassy_executor::main]
41+
async fn main(_spawner: Spawner) {
42+
const ALARM_SECONDS: u64 = 10;
43+
44+
let p = embassy_imxrt::init(Default::default());
45+
let mut r = Rtc::new(p.RTC);
46+
let (dt_clock, _rtc_nvram) = r.split();
47+
48+
// Initialize the system RTC
49+
let datetime = Datetime::new(UncheckedDatetime {
50+
year: 2026,
51+
month: Month::January,
52+
day: 12,
53+
hour: 16,
54+
..Default::default()
55+
})
56+
.unwrap();
57+
let ret = dt_clock.set_current_datetime(&datetime);
58+
info!("RTC set time: {:?}", datetime);
59+
assert!(ret.is_ok());
60+
61+
// Show RTC functioning: Display current time before setting alarm
62+
let current_time = dt_clock.get_current_datetime().unwrap();
63+
info!("Current time before alarm: {:?}", current_time);
64+
65+
info!("Waiting 5 seconds...");
66+
Timer::after_secs(5).await; // This timer uses the OsTimer peripheral
67+
68+
info!(
69+
"Current time after waiting: {:?}",
70+
dt_clock.get_current_datetime().unwrap()
71+
);
72+
73+
// Set an RTC alarm to trigger after ALARM_SECONDS
74+
info!("Setting alarm to trigger in {} seconds...", ALARM_SECONDS);
75+
let expires_at_secs = dt_clock.set_alarm_from_now(ALARM_SECONDS).expect("Failed to set alarm");
76+
77+
let alarm = RtcAlarm {
78+
expires_at: expires_at_secs,
79+
rtc: dt_clock,
80+
};
81+
82+
// Wait for the alarm to trigger
83+
info!("Waiting for alarm...");
84+
alarm.await.expect("Alarm failed");
85+
86+
// Display time after alarm triggered
87+
let wake_time = dt_clock.get_current_datetime().unwrap();
88+
info!("Alarm triggered! Wake time: {:?}", wake_time);
89+
90+
// Clear the alarm
91+
dt_clock.clear_alarm();
92+
info!("Alarm cleared");
93+
}

src/rtc.rs

Lines changed: 167 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
33
use core::marker::PhantomData;
44

5+
use embassy_hal_internal::interrupt::InterruptExt;
6+
use embassy_sync::waitqueue::AtomicWaker;
57
use embedded_mcu_hal::time::{Datetime, DatetimeClock, DatetimeClockError};
68
use embedded_mcu_hal::{Nvram, NvramStorage};
79

8-
use crate::{Peri, pac, peripherals};
10+
use crate::{Peri, interrupt, pac, peripherals};
911

1012
/// Number of general-purpose registers in the RTC NVRAM
1113
// If you need to consume some of these registers for internal HAL use, write a feature flag that reduces this count,
@@ -19,6 +21,27 @@ unsafe fn rtc() -> &'static pac::rtc::RegisterBlock {
1921
unsafe { &*pac::Rtc::ptr() }
2022
}
2123

24+
/// Static waker for RTC alarm interrupts.
25+
///
26+
/// This is a **global** `AtomicWaker` used by the RTC alarm interrupt handler to
27+
/// wake an asynchronous task waiting for the next alarm.
28+
///
29+
/// # Concurrency and usage constraints
30+
///
31+
/// - Only **one** alarm can be pending at a time when using this waker. Registering
32+
/// a new waker (e.g. by polling a different future that also uses this waker)
33+
/// will replace the previously registered waker.
34+
/// - This value is a low-level primitive and does **not** itself represent a
35+
/// future. Consumers are expected to implement their own `Future` wrapper
36+
/// (for example, a type like `RtcAlarm`) that:
37+
/// - registers its task's waker with `RTC_ALARM_WAKER`,
38+
/// - programs/configures the hardware alarm, and
39+
/// - completes when the alarm interrupt fires and calls `wake()`.
40+
///
41+
/// Using this waker directly from multiple independent alarm abstractions at the
42+
/// same time is not supported and may lead to lost wakeups.
43+
static RTC_ALARM_WAKER: AtomicWaker = AtomicWaker::new();
44+
2245
/// Represents the real-time clock (RTC) peripheral and provides access to its datetime clock and NVRAM functionality.
2346
pub struct Rtc<'r> {
2447
_p: Peri<'r, peripherals::RTC>,
@@ -49,10 +72,10 @@ pub struct RtcDatetimeClock<'r> {
4972
_phantom: PhantomData<&'r Peri<'r, peripherals::RTC>>,
5073
}
5174

52-
/// Implementation for `RtcDatetime`.
75+
/// Implementation for `RtcDatetimeClock`.
5376
impl<'r> RtcDatetimeClock<'r> {
5477
/// Set the datetime in seconds since the Unix time epoch (January 1, 1970).
55-
fn set_datetime_in_secs(&self, secs: u64) -> Result<(), DatetimeClockError> {
78+
fn set_datetime_in_secs(&mut self, secs: u64) -> Result<(), DatetimeClockError> {
5679
// SAFETY: We have sole ownership of the RTC peripheral and we enforce that there is only one instance of RtcDatetime,
5780
// so we can safely access it as long as it's always from an object that has the handle-to-RTC.
5881
let r = unsafe { rtc() };
@@ -87,6 +110,119 @@ impl<'r> RtcDatetimeClock<'r> {
87110

88111
Ok(secs.into())
89112
}
113+
114+
/// Sets the RTC wake alarm via the match register to wake after the given time in seconds.
115+
///
116+
/// WARNING:
117+
/// * Only one RTC alarm can be present in the system at a time.
118+
/// Setting a new alarm will cancel an existing one.
119+
/// * After an alarm is set, changing the RTC time will NOT adjust the alarm accordingly.
120+
/// The alarm will still fire at the originally configured absolute time value
121+
/// in the hardware register.
122+
///
123+
/// # Parameters
124+
///
125+
/// * `secs_from_now` - A relative offset in seconds from the current RTC time
126+
/// after which the alarm should fire.
127+
///
128+
/// # Returns
129+
///
130+
/// `u64` - The absolute RTC time in seconds at which the alarm is scheduled to fire.
131+
pub fn set_alarm_from_now(&mut self, secs_from_now: u64) -> Result<u64, DatetimeClockError> {
132+
let secs = self
133+
.get_datetime_in_secs()?
134+
.checked_add(secs_from_now)
135+
.ok_or(DatetimeClockError::UnsupportedDatetime)?;
136+
self.set_alarm_at(secs)?;
137+
Ok(secs)
138+
}
139+
140+
/// Sets the RTC wake alarm via the match register to wake at the given Datetime.
141+
///
142+
/// WARNING:
143+
/// * Only one RTC alarm can be present in the system at a time.
144+
/// Setting a new alarm will cancel an existing one.
145+
/// * After an alarm is set, changing the RTC time will NOT adjust the alarm accordingly.
146+
/// The alarm will still fire at the originally configured absolute time value
147+
/// in the hardware register.
148+
///
149+
/// # Parameters
150+
///
151+
/// * `unix_time_secs` - An absolute RTC time in seconds at which the alarm should fire.
152+
pub fn set_alarm_at(&mut self, unix_time_secs: u64) -> Result<(), DatetimeClockError> {
153+
// Check that the alarm end time is not in the past
154+
if self.get_datetime_in_secs()? > unix_time_secs {
155+
return Err(DatetimeClockError::UnsupportedDatetime);
156+
}
157+
158+
// Convert seconds to u32 to interface with 32 bit hardware RTC
159+
let secs: u32 = unix_time_secs
160+
.try_into()
161+
.map_err(|_| DatetimeClockError::UnsupportedDatetime)?;
162+
163+
// SAFETY: We have sole ownership of the RTC peripheral and we enforce that there is only one instance of RtcDatetime,
164+
// so we can safely access it as long as it's always from an object that has the handle-to-RTC.
165+
// The handle was retrieved in get_datetime_in_secs(), which has since completed and gone out of scope.
166+
let r = unsafe { rtc() };
167+
168+
critical_section::with(|_cs| {
169+
// Clear any pending alarm interrupt
170+
r.ctrl().modify(|_r, w| w.alarm1hz().set_bit());
171+
172+
// Set the match register with the new time
173+
r.match_().write(|w| unsafe { w.bits(secs) });
174+
175+
// Enable the 1Hz timer alarm for deep power down
176+
r.ctrl().modify(|_r, w| w.alarmdpd_en().set_bit());
177+
178+
// Enable RTC interrupt
179+
interrupt::RTC.unpend();
180+
unsafe {
181+
interrupt::RTC.enable();
182+
}
183+
});
184+
185+
Ok(())
186+
}
187+
188+
/// Clears the RTC 1Hz alarm by resetting related registers
189+
pub fn clear_alarm(&mut self) {
190+
// SAFETY: We have sole ownership of the RTC peripheral and we enforce that there is only one instance of RtcDatetime,
191+
// so we can safely access it as long as it's always from an object that has the handle-to-RTC.
192+
let r = unsafe { rtc() };
193+
194+
critical_section::with(|_cs| {
195+
// Disable the 1Hz timer alarm for deep power down
196+
r.ctrl().modify(|_r, w| w.alarmdpd_en().clear_bit());
197+
198+
// Clear the Alarm1Hz match status register
199+
// Note: "Writing a 1 clears this bit"
200+
r.ctrl().modify(|_r, w| w.alarm1hz().set_bit());
201+
202+
// Resets the match register to its default
203+
r.match_().write(|w| unsafe { w.bits(u32::MAX) });
204+
205+
// Disable RTC interrupt
206+
interrupt::RTC.disable();
207+
});
208+
}
209+
210+
/// Registers a waker to be notified when the RTC alarm fires.
211+
///
212+
/// This method forwards the waker to the internal static [`AtomicWaker`] used
213+
/// by the RTC interrupt handler. When the alarm interrupt fires, the registered
214+
/// waker will be called.
215+
///
216+
/// This is typically called from a `Future::poll` implementation when waiting
217+
/// for an RTC alarm to expire. Only one waker can be registered at a time;
218+
/// calling this method replaces any previously registered waker.
219+
///
220+
/// # Parameters
221+
///
222+
/// * `waker` - The waker to be notified when the alarm fires
223+
pub fn register_alarm_waker(&self, waker: &core::task::Waker) {
224+
RTC_ALARM_WAKER.register(waker);
225+
}
90226
}
91227

92228
impl DatetimeClock for RtcDatetimeClock<'_> {
@@ -186,3 +322,31 @@ impl<'r> Nvram<'r, RtcNvramStorage<'r>, u32, IMXRT_GPREG_COUNT> for RtcNvram<'r>
186322
self.storage.each_ref().map(|storage| storage.read())
187323
}
188324
}
325+
326+
/// RTC interrupt handler
327+
/// This is called when the RTC alarm fires
328+
#[cfg(feature = "rt")]
329+
#[interrupt]
330+
fn RTC() {
331+
// SAFETY: This is called from an interrupt context, but we only read/write the CTRL register
332+
// which is safe to access from multiple contexts with proper atomic operations
333+
let r = unsafe { rtc() };
334+
335+
// Check if this is an alarm interrupt
336+
if r.ctrl().read().alarm1hz().bit_is_set() {
337+
// Clear any pending RTC interrupt before waking tasks to avoid spurious retriggers
338+
interrupt::RTC.unpend();
339+
340+
// Disable RTC interrupt
341+
interrupt::RTC.disable();
342+
343+
// Disable the 1Hz timer alarm for deep power down
344+
r.ctrl().modify(|_r, w| w.alarmdpd_en().clear_bit());
345+
346+
// Clear the alarm interrupt flag by writing 1 to it
347+
r.ctrl().modify(|_r, w| w.alarm1hz().set_bit());
348+
349+
// Wake any task waiting on the alarm
350+
RTC_ALARM_WAKER.wake();
351+
}
352+
}

0 commit comments

Comments
 (0)