From f68f148d129e0f98a975f6851def60f03b7f3d26 Mon Sep 17 00:00:00 2001 From: Trangar Date: Mon, 29 Nov 2021 11:15:20 +0100 Subject: [PATCH] Added RtcClock DateTime and alarms (#213) * Added RealTimeClock, DateTime and RTC alarms * Improved documentation on weird behaviors in the RealTimeClock * Fixed incorrect leap_year_check in RealTimeClock * Fixed rtc-datetime PR feedback --- .github/workflows/check.yml | 6 +- rp2040-hal/Cargo.toml | 1 + rp2040-hal/src/rtc.rs | 3 - rp2040-hal/src/rtc/datetime_chrono.rs | 67 ++++++++ rp2040-hal/src/rtc/datetime_no_deps.rs | 133 ++++++++++++++++ rp2040-hal/src/rtc/filter.rs | 120 ++++++++++++++ rp2040-hal/src/rtc/mod.rs | 207 +++++++++++++++++++++++++ 7 files changed, 533 insertions(+), 4 deletions(-) delete mode 100644 rp2040-hal/src/rtc.rs create mode 100644 rp2040-hal/src/rtc/datetime_chrono.rs create mode 100644 rp2040-hal/src/rtc/datetime_no_deps.rs create mode 100644 rp2040-hal/src/rtc/filter.rs create mode 100644 rp2040-hal/src/rtc/mod.rs diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1c45d10..9c21c27 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -21,4 +21,8 @@ jobs: - uses: actions-rs/cargo@v1 with: command: test - args: --doc --target x86_64-unknown-linux-gnu \ No newline at end of file + args: --doc --target x86_64-unknown-linux-gnu + - uses: actions-rs/cargo@v1 + with: + command: test + args: --doc --target x86_64-unknown-linux-gnu --features chrono diff --git a/rp2040-hal/Cargo.toml b/rp2040-hal/Cargo.toml index 6c1d68e..7400477 100644 --- a/rp2040-hal/Cargo.toml +++ b/rp2040-hal/Cargo.toml @@ -25,6 +25,7 @@ void = { version = "1.0.2", default-features = false } rand_core = "0.6.3" futures = { version = "0.3", default-features = false, optional = true } +chrono = { version = "0.4", default-features = false, optional = true } # namespaced features will let use use "dep:embassy-traits" in the features rather than using this # trick of renaming the crate. diff --git a/rp2040-hal/src/rtc.rs b/rp2040-hal/src/rtc.rs deleted file mode 100644 index 15b7b5f..0000000 --- a/rp2040-hal/src/rtc.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Real Time Clock (RTC) -// See [Chapter 4 Section 8](https://datasheets.raspberrypi.org/rp2040/rp2040_datasheet.pdf) for more details -// TODO diff --git a/rp2040-hal/src/rtc/datetime_chrono.rs b/rp2040-hal/src/rtc/datetime_chrono.rs new file mode 100644 index 0000000..09cd214 --- /dev/null +++ b/rp2040-hal/src/rtc/datetime_chrono.rs @@ -0,0 +1,67 @@ +use chrono::{Datelike, Timelike}; +use rp2040_pac::rtc::{rtc_0, rtc_1, setup_0, setup_1}; + +/// Alias for [`chrono::NaiveDateTime`] +pub type DateTime = chrono::NaiveDateTime; +/// Alias for [`chrono::Weekday`] +pub type DayOfWeek = chrono::Weekday; + +/// Errors regarding the [`DateTime`] and [`DateTimeFilter`] structs. +/// +/// [`DateTimeFilter`]: struct.DateTimeFilter.html +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Error { + /// The [DateTime] has an invalid year. The year must be between 0 and 4095. + InvalidYear, + /// The [DateTime] contains an invalid date. + InvalidDate, + /// The [DateTime] contains an invalid time. + InvalidTime, +} + +pub(super) fn day_of_week_to_u8(dotw: DayOfWeek) -> u8 { + dotw.num_days_from_sunday() as u8 +} + +pub(crate) fn validate_datetime(dt: &DateTime) -> Result<(), Error> { + if dt.year() < 0 || dt.year() > 4095 { + // rp2040 can't hold these years + Err(Error::InvalidYear) + } else { + // The rest of the chrono date is assumed to be valid + Ok(()) + } +} + +pub(super) fn write_setup_0(dt: &DateTime, w: &mut setup_0::W) { + // Safety: the `.bits()` fields are marked `unsafe` but all bit values are valid + unsafe { + w.year().bits(dt.year() as u16); + w.month().bits(dt.month() as u8); + w.day().bits(dt.day() as u8); + } +} + +pub(super) fn write_setup_1(dt: &DateTime, w: &mut setup_1::W) { + // Safety: the `.bits()` fields are marked `unsafe` but all bit values are valid + unsafe { + w.dotw().bits(dt.weekday().num_days_from_sunday() as u8); + w.hour().bits(dt.hour() as u8); + w.min().bits(dt.minute() as u8); + w.sec().bits(dt.second() as u8); + } +} + +pub(super) fn datetime_from_registers(rtc_0: rtc_0::R, rtc_1: rtc_1::R) -> Result { + let year = rtc_1.year().bits() as i32; + let month = rtc_1.month().bits() as u32; + let day = rtc_1.day().bits() as u32; + + let hour = rtc_0.hour().bits() as u32; + let minute = rtc_0.min().bits() as u32; + let second = rtc_0.sec().bits() as u32; + + let date = chrono::NaiveDate::from_ymd_opt(year, month, day).ok_or(Error::InvalidDate)?; + let time = chrono::NaiveTime::from_hms_opt(hour, minute, second).ok_or(Error::InvalidTime)?; + Ok(DateTime::new(date, time)) +} diff --git a/rp2040-hal/src/rtc/datetime_no_deps.rs b/rp2040-hal/src/rtc/datetime_no_deps.rs new file mode 100644 index 0000000..4a1dc24 --- /dev/null +++ b/rp2040-hal/src/rtc/datetime_no_deps.rs @@ -0,0 +1,133 @@ +use rp2040_pac::rtc::{rtc_0, rtc_1, setup_0, setup_1}; + +/// Errors regarding the [`DateTime`] and [`DateTimeFilter`] structs. +/// +/// [`DateTimeFilter`]: struct.DateTimeFilter.html +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Error { + /// The [DateTime] contains an invalid year value. Must be between `0..=4095`. + InvalidYear, + /// The [DateTime] contains an invalid month value. Must be between `1..=12`. + InvalidMonth, + /// The [DateTime] contains an invalid day value. Must be between `1..=31`. + InvalidDay, + /// The [DateTime] contains an invalid day of week. Must be between `0..=6` where 0 is Sunday. + InvalidDayOfWeek( + /// The value of the DayOfWeek that was given. + u8, + ), + /// The [DateTime] contains an invalid hour value. Must be between `0..=23`. + InvalidHour, + /// The [DateTime] contains an invalid minute value. Must be between `0..=59`. + InvalidMinute, + /// The [DateTime] contains an invalid second value. Must be between `0..=59`. + InvalidSecond, +} + +/// Structure containing date and time information +pub struct DateTime { + /// 0..4095 + pub year: u16, + /// 1..12, 1 is January + pub month: u8, + /// 1..28,29,30,31 depending on month + pub day: u8, + /// + pub day_of_week: DayOfWeek, + /// 0..23 + pub hour: u8, + /// 0..59 + pub minute: u8, + /// 0..59 + pub second: u8, +} + +/// A day of the week +#[repr(u8)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +#[allow(missing_docs)] +pub enum DayOfWeek { + Sunday = 0, + Monday = 1, + Tuesday = 2, + Wednesday = 3, + Thursday = 4, + Friday = 5, + Saturday = 6, +} + +fn day_of_week_from_u8(v: u8) -> Result { + Ok(match v { + 0 => DayOfWeek::Sunday, + 1 => DayOfWeek::Monday, + 2 => DayOfWeek::Tuesday, + 3 => DayOfWeek::Wednesday, + 4 => DayOfWeek::Thursday, + 5 => DayOfWeek::Friday, + 6 => DayOfWeek::Saturday, + x => return Err(Error::InvalidDayOfWeek(x)), + }) +} + +pub(super) fn day_of_week_to_u8(dotw: DayOfWeek) -> u8 { + dotw as u8 +} + +pub(super) fn validate_datetime(dt: &DateTime) -> Result<(), Error> { + if dt.year > 4095 { + Err(Error::InvalidYear) + } else if dt.month < 1 || dt.month > 12 { + Err(Error::InvalidMonth) + } else if dt.day < 1 || dt.day > 31 { + Err(Error::InvalidDay) + } else if dt.hour > 23 { + Err(Error::InvalidHour) + } else if dt.minute > 59 { + Err(Error::InvalidMinute) + } else if dt.second > 59 { + Err(Error::InvalidSecond) + } else { + Ok(()) + } +} + +pub(super) fn write_setup_0(dt: &DateTime, w: &mut setup_0::W) { + // Safety: the `.bits()` fields are marked `unsafe` but all bit values are valid + unsafe { + w.year().bits(dt.year); + w.month().bits(dt.month); + w.day().bits(dt.day); + } +} + +pub(super) fn write_setup_1(dt: &DateTime, w: &mut setup_1::W) { + // Safety: the `.bits()` fields are marked `unsafe` but all bit values are valid + unsafe { + w.dotw().bits(dt.day_of_week as u8); + w.hour().bits(dt.hour); + w.min().bits(dt.minute); + w.sec().bits(dt.second); + } +} + +pub(super) fn datetime_from_registers(rtc_0: rtc_0::R, rtc_1: rtc_1::R) -> Result { + let year = rtc_1.year().bits(); + let month = rtc_1.month().bits(); + let day = rtc_1.day().bits(); + + let day_of_week = rtc_0.dotw().bits(); + let hour = rtc_0.hour().bits(); + let minute = rtc_0.min().bits(); + let second = rtc_0.sec().bits(); + + let day_of_week = day_of_week_from_u8(day_of_week)?; + Ok(DateTime { + year, + month, + day, + day_of_week, + hour, + minute, + second, + }) +} diff --git a/rp2040-hal/src/rtc/filter.rs b/rp2040-hal/src/rtc/filter.rs new file mode 100644 index 0000000..5212d4a --- /dev/null +++ b/rp2040-hal/src/rtc/filter.rs @@ -0,0 +1,120 @@ +use super::DayOfWeek; +use rp2040_pac::rtc::{irq_setup_0, irq_setup_1}; + +/// A filter used for [`RealTimeClock::schedule_alarm`]. +/// +/// [`RealTimeClock::schedule_alarm`]: struct.RealTimeClock.html#method.schedule_alarm +#[derive(Default)] +pub struct DateTimeFilter { + /// The year that this alarm should trigger on, `None` if the RTC alarm should not trigger on a year value. + pub year: Option, + /// The month that this alarm should trigger on, `None` if the RTC alarm should not trigger on a month value. + pub month: Option, + /// The day that this alarm should trigger on, `None` if the RTC alarm should not trigger on a day value. + pub day: Option, + /// The day of week that this alarm should trigger on, `None` if the RTC alarm should not trigger on a day of week value. + pub day_of_week: Option, + /// The hour that this alarm should trigger on, `None` if the RTC alarm should not trigger on a hour value. + pub hour: Option, + /// The minute that this alarm should trigger on, `None` if the RTC alarm should not trigger on a minute value. + pub minute: Option, + /// The second that this alarm should trigger on, `None` if the RTC alarm should not trigger on a second value. + pub second: Option, +} + +impl DateTimeFilter { + /// Set a filter on the given year + pub fn year(mut self, year: u16) -> Self { + self.year = Some(year); + self + } + /// Set a filter on the given month + pub fn month(mut self, month: u8) -> Self { + self.month = Some(month); + self + } + /// Set a filter on the given day + pub fn day(mut self, day: u8) -> Self { + self.day = Some(day); + self + } + /// Set a filter on the given day of the week + pub fn day_of_week(mut self, day_of_week: DayOfWeek) -> Self { + self.day_of_week = Some(day_of_week); + self + } + /// Set a filter on the given hour + pub fn hour(mut self, hour: u8) -> Self { + self.hour = Some(hour); + self + } + /// Set a filter on the given minute + pub fn minute(mut self, minute: u8) -> Self { + self.minute = Some(minute); + self + } + /// Set a filter on the given second + pub fn second(mut self, second: u8) -> Self { + self.second = Some(second); + self + } +} + +// register helper functions +impl DateTimeFilter { + pub(super) fn write_setup_0(&self, w: &mut irq_setup_0::W) { + // Safety: setting .bits() is considered unsafe because + // svd2rust doesn't know what the valid values are. + // But all values in these bitmasks are safe + if let Some(year) = self.year { + w.year_ena().set_bit(); + + unsafe { + w.year().bits(year); + } + } + if let Some(month) = self.month { + w.month_ena().set_bit(); + unsafe { + w.month().bits(month); + } + } + if let Some(day) = self.day { + w.day_ena().set_bit(); + unsafe { + w.day().bits(day); + } + } + } + pub(super) fn write_setup_1(&self, w: &mut irq_setup_1::W) { + // Safety: setting .bits() is considered unsafe because + // svd2rust doesn't know what the valid values are. + // But all values in these bitmasks are safe + if let Some(day_of_week) = self.day_of_week { + w.dotw_ena().set_bit(); + let bits = super::datetime::day_of_week_to_u8(day_of_week); + + unsafe { + w.dotw().bits(bits); + } + } + if let Some(hour) = self.hour { + w.hour_ena().set_bit(); + unsafe { + w.hour().bits(hour); + } + } + if let Some(minute) = self.minute { + w.min_ena().set_bit(); + unsafe { + w.min().bits(minute); + } + } + if let Some(second) = self.second { + w.sec_ena().set_bit(); + unsafe { + w.sec().bits(second); + } + } + } +} diff --git a/rp2040-hal/src/rtc/mod.rs b/rp2040-hal/src/rtc/mod.rs new file mode 100644 index 0000000..c28964e --- /dev/null +++ b/rp2040-hal/src/rtc/mod.rs @@ -0,0 +1,207 @@ +//! Real time clock functionality +//! +//! A [`RealTimeClock`] can be configured with an initial [`DateTime`]. Afterwards the clock will track time automatically. The current `DateTime` can be retrieved by [`RealTimeClock::now()`]. +//! +//! With the **chrono** feature enabled, the following types will be alias for chrono types: +//! - `DateTime`: `chrono::NaiveDateTime` +//! - `DayOfWeek`: `chrono::Weekday` +//! +//! # Notes +//! +//! There are some things to take into account. As per the datasheet: +//! +//! - **Day of week**: The RTC will not compute the correct day of the week; it will only increment the existing value. +//! - With the `chrono` feature, the day of week is calculated by chrono and should be correct. The value from the rp2040 itself is not used. +//! - **Leap year**: If the current year is evenly divisible by 4, a leap year is detected, then Feb 28th is followed by Feb 29th instead of March 1st. +//! - There are cases where this is incorrect, e.g. century years have no leap day, but the chip will still add a Feb 29th. +//! - To disable leap year checking and never have a Feb 29th, call `RealTimeClock::set_leap_year_check(false)`. +//! +//! Other limitations: +//! +//! - **Leap seconds**: The rp2040 will not take leap seconds into account +//! - With the `chrono` feature, leap seconds will be silently handled by `chrono`. This means there might be a slight difference between the value of [`RealTimeClock::now()`] and adding 2 times together in code. + +use crate::clocks::Clock; +use crate::clocks::RtcClock; +use embedded_time::fixed_point::FixedPoint; +use rp2040_pac::{RESETS, RTC}; + +mod filter; + +pub use self::filter::DateTimeFilter; + +#[cfg_attr(feature = "chrono", path = "datetime_chrono.rs")] +#[cfg_attr(not(feature = "chrono"), path = "datetime_no_deps.rs")] +mod datetime; + +pub use self::datetime::{DateTime, DayOfWeek, Error as DateTimeError}; + +/// A reference to the real time clock of the system +pub struct RealTimeClock { + rtc: RTC, +} + +impl RealTimeClock { + /// Create a new instance of the real time clock, with the given date as an initial value. + /// + /// Note that the [`ClocksManager`] should be enabled first. See the [`clocks`] module for more information. + /// + /// # Errors + /// + /// Will return `RtcError::InvalidDateTime` if the datetime is not a valid range. + /// + /// [`ClocksManager`]: ../clocks/struct.ClocksManager.html + /// [`clocks`]: ../clocks/index.html + pub fn new( + rtc: RTC, + clock: RtcClock, + resets: &mut RESETS, + initial_date: DateTime, + ) -> Result { + // Toggle the RTC reset + resets.reset.modify(|_, w| w.rtc().set_bit()); + resets.reset.modify(|_, w| w.rtc().clear_bit()); + while resets.reset_done.read().rtc().bit_is_clear() { + core::hint::spin_loop(); + } + + // Set the RTC divider + let freq = clock.freq().integer() - 1; + rtc.clkdiv_m1.write(|w| unsafe { w.bits(freq) }); + + let mut result = Self { rtc }; + result.set_leap_year_check(true); // should be on by default, make sure this is the case. + result.set_datetime(initial_date)?; + Ok(result) + } + + /// Enable or disable the leap year check. The rp2040 chip will always add a Feb 29th on every year that is divisable by 4, but this may be incorrect (e.g. on century years). This function allows you to disable this check. + /// + /// Leap year checking is enabled by default. + pub fn set_leap_year_check(&mut self, leap_year_check_enabled: bool) { + self.rtc + .ctrl + .modify(|_, w| w.force_notleapyear().bit(!leap_year_check_enabled)); + } + + /// Checks to see if this RealTimeClock is running + pub fn is_running(&self) -> bool { + self.rtc.ctrl.read().rtc_active().bit_is_set() + } + + /// Set the datetime to a new value. + /// + /// # Errors + /// + /// Will return `RtcError::InvalidDateTime` if the datetime is not a valid range. + pub fn set_datetime(&mut self, t: DateTime) -> Result<(), RtcError> { + self::datetime::validate_datetime(&t).map_err(RtcError::InvalidDateTime)?; + + // disable RTC while we configure it + self.rtc.ctrl.modify(|_, w| w.rtc_enable().clear_bit()); + while self.rtc.ctrl.read().rtc_active().bit_is_set() { + core::hint::spin_loop(); + } + + self.rtc.setup_0.write(|w| { + self::datetime::write_setup_0(&t, w); + w + }); + self.rtc.setup_1.write(|w| { + self::datetime::write_setup_1(&t, w); + w + }); + + // Load the new datetime and re-enable RTC + self.rtc.ctrl.write(|w| w.load().set_bit()); + self.rtc.ctrl.write(|w| w.rtc_enable().set_bit()); + while self.rtc.ctrl.read().rtc_active().bit_is_clear() { + core::hint::spin_loop(); + } + + Ok(()) + } + + /// Return the current datetime. + /// + /// # Errors + /// + /// Will return an `RtcError::InvalidDateTime` if the stored value in the system is not a valid [`DayOfWeek`]. + pub fn now(&self) -> Result { + if !self.is_running() { + return Err(RtcError::NotRunning); + } + + let rtc_0 = self.rtc.rtc_0.read(); + let rtc_1 = self.rtc.rtc_1.read(); + + self::datetime::datetime_from_registers(rtc_0, rtc_1).map_err(RtcError::InvalidDateTime) + } + + /// Disable the alarm that was scheduled with [`schedule_alarm`]. + /// + /// [`schedule_alarm`]: #method.schedule_alarm + pub fn disable_alarm(&mut self) { + self.rtc + .irq_setup_0 + .modify(|_, s| s.match_ena().clear_bit()); + + while self.rtc.irq_setup_0.read().match_active().bit() { + core::hint::spin_loop(); + } + } + + /// Schedule an alarm. The `filter` determines at which point in time this alarm is set. + /// + /// Keep in mind that the filter only triggers on the specified time. If you want to schedule this alarm every minute, you have to call: + /// ```no_run + /// # #[cfg(feature = "chrono")] + /// # fn main() { } + /// # #[cfg(not(feature = "chrono"))] + /// # fn main() { + /// # use rp2040_hal::rtc::{RealTimeClock, DateTimeFilter}; + /// # let mut real_time_clock: RealTimeClock = unsafe { core::mem::zeroed() }; + /// let now = real_time_clock.now().unwrap(); + /// real_time_clock.schedule_alarm( + /// DateTimeFilter::default() + /// .minute(if now.minute == 59 { 0 } else { now.minute + 1 }) + /// ); + /// # } + /// ``` + pub fn schedule_alarm(&mut self, filter: DateTimeFilter) { + self.disable_alarm(); + + self.rtc.irq_setup_0.write(|w| { + filter.write_setup_0(w); + w + }); + self.rtc.irq_setup_1.write(|w| { + filter.write_setup_1(w); + w + }); + + // Set the enable bit and check if it is set + self.rtc.irq_setup_0.modify(|_, w| w.match_ena().set_bit()); + while self.rtc.irq_setup_0.read().match_active().bit_is_clear() { + core::hint::spin_loop(); + } + } + + /// Clear the interrupt. This should be called every time the `RTC_IRQ` interrupt is triggered, + /// or the next [`schedule_alarm`] will never fire. + /// + /// [`schedule_alarm`]: #method.schedule_alarm + pub fn clear_interrupt(&mut self) { + self.disable_alarm(); + } +} + +/// Errors that can occur on methods on [RtcClock] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RtcError { + /// An invalid DateTime was given or stored on the hardware. + InvalidDateTime(DateTimeError), + + /// The RTC clock is not running + NotRunning, +}