diff --git a/ci.sh b/ci.sh index f08c38b7c..edc9bd617 100755 --- a/ci.sh +++ b/ci.sh @@ -53,8 +53,8 @@ cargo batch \ --- build --release --manifest-path embassy-net/Cargo.toml --target thumbv7em-none-eabi --features defmt,tcp,udp,dns,proto-ipv4,proto-ipv6,medium-ip \ --- build --release --manifest-path embassy-net/Cargo.toml --target thumbv7em-none-eabi --features defmt,tcp,udp,dns,proto-ipv4,proto-ipv6,medium-ip,medium-ethernet \ --- build --release --manifest-path embassy-net/Cargo.toml --target thumbv7em-none-eabi --features defmt,tcp,udp,dns,proto-ipv4,proto-ipv6,medium-ip,medium-ethernet,medium-ieee802154 \ - --- build --release --manifest-path embassy-imxrt/Cargo.toml --target thumbv8m.main-none-eabihf --features mimxrt633s,defmt,unstable-pac \ - --- build --release --manifest-path embassy-imxrt/Cargo.toml --target thumbv8m.main-none-eabihf --features mimxrt685s,defmt,unstable-pac \ + --- build --release --manifest-path embassy-imxrt/Cargo.toml --target thumbv8m.main-none-eabihf --features mimxrt633s,defmt,unstable-pac,time,time-driver-rtc \ + --- build --release --manifest-path embassy-imxrt/Cargo.toml --target thumbv8m.main-none-eabihf --features mimxrt685s,defmt,unstable-pac,time,time-driver-rtc \ --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv6m-none-eabi --features nrf51,gpiote,time,time-driver-rtc1 \ --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52805,gpiote,time,time-driver-rtc1 \ --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52810,gpiote,time,time-driver-rtc1 \ diff --git a/embassy-imxrt/Cargo.toml b/embassy-imxrt/Cargo.toml index 38087bf77..d58de6353 100644 --- a/embassy-imxrt/Cargo.toml +++ b/embassy-imxrt/Cargo.toml @@ -12,13 +12,13 @@ documentation = "https://docs.embassy.dev/embassy-imxrt" [package.metadata.embassy_docs] src_base = "https://github.com/embassy-rs/embassy/blob/embassy-imxrt-v$VERSION/embassy-imxrt/src/" src_base_git = "https://github.com/embassy-rs/embassy/blob/$COMMIT/embassy-imxrt/src/" -features = ["defmt", "unstable-pac"] +features = ["defmt", "unstable-pac", "time", "time-driver"] flavors = [ { regex_feature = "mimxrt6.*", target = "thumbv8m.main-none-eabihf" } ] [package.metadata.docs.rs] -features = ["mimxrt685s", "defmt", "unstable-pac"] +features = ["mimxrt685s", "defmt", "unstable-pac", "time", "time-driver"] rustdoc-args = ["--cfg", "docsrs"] [features] @@ -33,6 +33,14 @@ rt = [ ## Enable defmt defmt = ["dep:defmt", "embassy-hal-internal/defmt", "embassy-sync/defmt", "mimxrt685s-pac?/defmt", "mimxrt633s-pac?/defmt"] +## Enable features requiring `embassy-time` +time = ["dep:embassy-time", "embassy-embedded-hal/time"] + +## Enable custom embassy time-driver implementation, using 32KHz RTC +time-driver-rtc = ["_time-driver"] + +_time-driver = ["dep:embassy-time-driver", "embassy-time-driver?/tick-hz-1_000", "dep:embassy-time-queue-utils", "embassy-embedded-hal/time"] + ## Reexport the PAC for the currently enabled chip at `embassy_imxrt::pac` (unstable) unstable-pac = [] @@ -53,6 +61,9 @@ mimxrt633s = ["mimxrt633s-pac", "_mimxrt633s"] [dependencies] embassy-sync = { version = "0.6.2", path = "../embassy-sync" } +embassy-time-driver = { version = "0.2", path = "../embassy-time-driver", optional = true } +embassy-time-queue-utils = { version = "0.1", path = "../embassy-time-queue-utils", optional = true } +embassy-time = { version = "0.4", path = "../embassy-time", optional = true } embassy-hal-internal = { version = "0.2.0", path = "../embassy-hal-internal", features = ["cortex-m", "prio-bits-3"] } embassy-embedded-hal = { version = "0.3.0", path = "../embassy-embedded-hal", default-features = false } embassy-futures = { version = "0.1.1", path = "../embassy-futures" } diff --git a/embassy-imxrt/src/lib.rs b/embassy-imxrt/src/lib.rs index d56d993c3..5fbf3244b 100644 --- a/embassy-imxrt/src/lib.rs +++ b/embassy-imxrt/src/lib.rs @@ -21,6 +21,9 @@ pub mod clocks; pub mod gpio; pub mod iopctl; +#[cfg(feature = "_time-driver")] +pub mod rtc; + // This mod MUST go last, so that it sees all the `impl_foo!' macros #[cfg_attr(feature = "mimxrt633s", path = "chips/mimxrt633s.rs")] #[cfg_attr(feature = "mimxrt685s", path = "chips/mimxrt685s.rs")] @@ -86,12 +89,18 @@ pub mod config { pub struct Config { /// Clock configuration. pub clocks: ClockConfig, + + /// RTC Time driver interrupt priority. + #[cfg(feature = "_time-driver")] + pub time_interrupt_priority: crate::interrupt::Priority, } impl Default for Config { fn default() -> Self { Self { clocks: ClockConfig::crystal(), + #[cfg(feature = "_time-driver")] + time_interrupt_priority: crate::interrupt::Priority::P0, } } } @@ -99,7 +108,11 @@ pub mod config { impl Config { /// Create a new configuration with the provided clock config. pub fn new(clocks: ClockConfig) -> Self { - Self { clocks } + Self { + clocks, + #[cfg(feature = "_time-driver")] + time_interrupt_priority: crate::interrupt::Priority::P0, + } } } } @@ -122,6 +135,10 @@ pub fn init(config: config::Config) -> Peripherals { gpio::init(); } + // init RTC time driver + #[cfg(feature = "_time-driver")] + rtc::init(config.time_interrupt_priority); + peripherals } diff --git a/embassy-imxrt/src/rtc.rs b/embassy-imxrt/src/rtc.rs new file mode 100644 index 000000000..56a8f7397 --- /dev/null +++ b/embassy-imxrt/src/rtc.rs @@ -0,0 +1,254 @@ +//! RTC Time Driver. +use core::cell::{Cell, RefCell}; +use core::sync::atomic::{compiler_fence, AtomicU32, Ordering}; + +use critical_section::CriticalSection; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::blocking_mutex::Mutex; +use embassy_time_driver::Driver; +use embassy_time_queue_utils::Queue; + +use crate::interrupt::InterruptExt; +use crate::{interrupt, pac}; + +fn rtc() -> &'static pac::rtc::RegisterBlock { + unsafe { &*pac::Rtc::ptr() } +} + +/// Calculate the timestamp from the period count and the tick count. +/// +/// 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. +/// +/// the 1kHz RTC counter is 16 bits and RTC doesn't have separate compare channels, +/// so using a 32 bit GPREG0-2 as counter, compare, and int_en +/// `period` is a 32bit integer, gpreg 'counter' is 31 bits plus the parity bit for overflow detection +fn calc_now(period: u32, counter: u32) -> u64 { + ((period as u64) << 31) + ((counter ^ ((period & 1) << 31)) as u64) +} + +struct AlarmState { + timestamp: Cell, +} + +unsafe impl Send for AlarmState {} + +impl AlarmState { + const fn new() -> Self { + Self { + timestamp: Cell::new(u64::MAX), + } + } +} + +struct Rtc { + /// Number of 2^31 periods elapsed since boot. + period: AtomicU32, + /// Timestamp at which to fire alarm. u64::MAX if no alarm is scheduled. + alarms: Mutex, + queue: Mutex>, +} + +embassy_time_driver::time_driver_impl!(static DRIVER: Rtc = Rtc { + period: AtomicU32::new(0), + alarms: Mutex::const_new(CriticalSectionRawMutex::new(), AlarmState::new()), + queue: Mutex::new(RefCell::new(Queue::new())), +}); + +impl Rtc { + /// Access the GPREG0 register to use it as a 31-bit counter. + #[inline] + fn counter_reg(&self) -> &pac::rtc::Gpreg { + rtc().gpreg(0) + } + + /// Access the GPREG1 register to use it as a compare register for triggering alarms. + #[inline] + fn compare_reg(&self) -> &pac::rtc::Gpreg { + rtc().gpreg(1) + } + + /// Access the GPREG2 register to use it to enable or disable interrupts (int_en). + #[inline] + fn int_en_reg(&self) -> &pac::rtc::Gpreg { + rtc().gpreg(2) + } + + fn init(&'static self, irq_prio: crate::interrupt::Priority) { + let r = rtc(); + // enable RTC int (1kHz since subsecond doesn't generate an int) + r.ctrl().modify(|_r, w| w.rtc1khz_en().set_bit()); + // TODO: low power support. line above is leaving out write to .wakedpd_en().set_bit()) + // which enables wake from deep power down + + // safety: Writing to the gregs is always considered unsafe, gpreg1 is used + // as a compare register for triggering an alarm so to avoid unnecessary triggers + // after initialization, this is set to 0x:FFFF_FFFF + self.compare_reg().write(|w| unsafe { w.gpdata().bits(u32::MAX) }); + // safety: writing a value to the 1kHz RTC wake counter is always considered unsafe. + // The following loads 10 into the count-down timer. + r.wake().write(|w| unsafe { w.bits(0xA) }); + interrupt::RTC.set_priority(irq_prio); + unsafe { interrupt::RTC.enable() }; + } + + #[cfg(feature = "rt")] + fn on_interrupt(&self) { + let r = rtc(); + // This interrupt fires every 10 ticks of the 1kHz RTC high res clk and adds + // 10 to the 31 bit counter gpreg0. The 32nd bit is used for parity detection + // This is done to avoid needing to calculate # of ticks spent on interrupt + // handlers to recalibrate the clock between interrupts + // + // TODO: this is admittedly not great for power that we're generating this + // many interrupts, will probably get updated in future iterations. + if r.ctrl().read().wake1khz().bit_is_set() { + r.ctrl().modify(|_r, w| w.wake1khz().set_bit()); + // safety: writing a value to the 1kHz RTC wake counter is always considered unsafe. + // The following reloads 10 into the count-down timer after it triggers an int. + // The countdown begins anew after the write so time can continue to be measured. + r.wake().write(|w| unsafe { w.bits(0xA) }); + if (self.counter_reg().read().bits() + 0xA) > 0x8000_0000 { + // if we're going to "overflow", increase the period + self.next_period(); + let rollover_diff = 0x8000_0000 - (self.counter_reg().read().bits() + 0xA); + // safety: writing to gpregs is always considered unsafe. In order to + // not "lose" time when incrementing the period, gpreg0, the extended + // counter, is restarted at the # of ticks it would overflow by + self.counter_reg().write(|w| unsafe { w.bits(rollover_diff) }); + } else { + self.counter_reg().modify(|r, w| unsafe { w.bits(r.bits() + 0xA) }); + } + } + + critical_section::with(|cs| { + // gpreg2 as an "int_en" set by next_period(). This is + // 1 when the timestamp for the alarm deadline expires + // before the counter register overflows again. + if self.int_en_reg().read().gpdata().bits() == 1 { + // gpreg0 is our extended counter register, check if + // our counter is larger than the compare value + if self.counter_reg().read().bits() > self.compare_reg().read().bits() { + self.trigger_alarm(cs); + } + } + }) + } + + #[cfg(feature = "rt")] + fn next_period(&self) { + critical_section::with(|cs| { + let period = self + .period + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |p| Some(p + 1)) + .unwrap_or_else(|p| { + trace!("Unable to increment period. Time is now inaccurate"); + // TODO: additional error handling beyond logging + + p + }); + let t = (period as u64) << 31; + + let alarm = &self.alarms.borrow(cs); + let at = alarm.timestamp.get(); + if at < t + 0xc000_0000 { + // safety: writing to gpregs is always unsafe, gpreg2 is an alarm + // enable. If the alarm must trigger within the next period, then + // just enable it. `set_alarm` has already set the correct CC val. + self.int_en_reg().write(|w| unsafe { w.gpdata().bits(1) }); + } + }) + } + + #[must_use] + fn set_alarm(&self, cs: CriticalSection, timestamp: u64) -> bool { + let alarm = self.alarms.borrow(cs); + alarm.timestamp.set(timestamp); + + let t = self.now(); + if timestamp <= t { + // safety: Writing to the gpregs is always unsafe, gpreg2 is + // always just used as the alarm enable for the timer driver. + // If alarm timestamp has passed the alarm will not fire. + // Disarm the alarm and return `false` to indicate that. + self.int_en_reg().write(|w| unsafe { w.gpdata().bits(0) }); + + alarm.timestamp.set(u64::MAX); + + return false; + } + + // If it hasn't triggered yet, setup it by writing to the compare field + // An alarm can be delayed, but this is allowed by the Alarm trait contract. + // What's not allowed is triggering alarms *before* their scheduled time, + let safe_timestamp = timestamp.max(t + 10); //t+3 was done for nrf chip, choosing 10 + + // safety: writing to the gregs is always unsafe. When a new alarm is set, + // the compare register, gpreg1, is set to the last 31 bits of the timestamp + // as the 32nd and final bit is used for the parity check in `next_period` + // `period` will be used for the upper bits in a timestamp comparison. + self.compare_reg() + .modify(|_r, w| unsafe { w.bits(safe_timestamp as u32 & 0x7FFF_FFFF) }); + + // The following checks that the difference in timestamp is less than the overflow period + let diff = timestamp - t; + if diff < 0xc000_0000 { + // this is 0b11 << (30). NRF chip used 23 bit periods and checked against 0b11<<22 + + // safety: writing to the gpregs is always unsafe. If the alarm + // must trigger within the next period, set the "int enable" + self.int_en_reg().write(|w| unsafe { w.gpdata().bits(1) }); + } else { + // safety: writing to the gpregs is always unsafe. If alarm must trigger + // some time after the current period, too far in the future, don't setup + // the alarm enable, gpreg2, yet. It will be setup later by `next_period`. + self.int_en_reg().write(|w| unsafe { w.gpdata().bits(0) }); + } + + true + } + + #[cfg(feature = "rt")] + fn trigger_alarm(&self, cs: CriticalSection) { + let mut next = self.queue.borrow(cs).borrow_mut().next_expiration(self.now()); + while !self.set_alarm(cs, next) { + next = self.queue.borrow(cs).borrow_mut().next_expiration(self.now()); + } + } +} + +impl Driver for Rtc { + fn now(&self) -> u64 { + // `period` MUST be read before `counter`, see comment at the top for details. + let period = self.period.load(Ordering::Acquire); + compiler_fence(Ordering::Acquire); + let counter = self.counter_reg().read().bits(); + calc_now(period, counter) + } + + fn schedule_wake(&self, at: u64, waker: &core::task::Waker) { + critical_section::with(|cs| { + let mut queue = self.queue.borrow(cs).borrow_mut(); + + if queue.schedule_wake(at, waker) { + let mut next = queue.next_expiration(self.now()); + while !self.set_alarm(cs, next) { + next = queue.next_expiration(self.now()); + } + } + }) + } +} + +#[cfg(feature = "rt")] +#[allow(non_snake_case)] +#[interrupt] +fn RTC() { + DRIVER.on_interrupt() +} + +pub(crate) fn init(irq_prio: crate::interrupt::Priority) { + DRIVER.init(irq_prio) +} diff --git a/examples/mimxrt6/Cargo.toml b/examples/mimxrt6/Cargo.toml index 894ce174c..0e4a1ee36 100644 --- a/examples/mimxrt6/Cargo.toml +++ b/examples/mimxrt6/Cargo.toml @@ -12,7 +12,8 @@ defmt-rtt = "1.0" embassy-executor = { version = "0.7.0", path = "../../embassy-executor", features = ["arch-cortex-m", "executor-thread", "executor-interrupt", "defmt"] } embassy-futures = { version = "0.1.1", path = "../../embassy-futures" } -embassy-imxrt = { version = "0.1.0", path = "../../embassy-imxrt", features = ["defmt", "mimxrt685s", "unstable-pac"] } +embassy-imxrt = { version = "0.1.0", path = "../../embassy-imxrt", features = ["defmt", "mimxrt685s", "unstable-pac", "time", "time-driver-rtc"] } +embassy-time = { version = "0.4", path = "../../embassy-time" } embassy-sync = { version = "0.6.2", path = "../../embassy-sync", features = ["defmt"] } embedded-hal-1 = { package = "embedded-hal", version = "1.0" } embedded-hal-async = "1.0.0" diff --git a/examples/mimxrt6/src/bin/blinky.rs b/examples/mimxrt6/src/bin/blinky.rs index e40e71e6f..de079d505 100644 --- a/examples/mimxrt6/src/bin/blinky.rs +++ b/examples/mimxrt6/src/bin/blinky.rs @@ -6,6 +6,7 @@ extern crate embassy_imxrt_examples; use defmt::info; use embassy_executor::Spawner; use embassy_imxrt::gpio; +use embassy_time::Timer; #[embassy_executor::main] async fn main(_spawner: Spawner) { @@ -24,6 +25,6 @@ async fn main(_spawner: Spawner) { loop { info!("Toggling LED"); led.toggle(); - cortex_m::asm::delay(5_000_000); + Timer::after_secs(1).await; } }